diff --git a/src/api.ts b/src/api.ts
index e7741b9..e34dd97 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -23,6 +23,28 @@ export type Destination = {
_count?: { products: number };
};
+export type DestinationHero = {
+ id: string;
+ title: string;
+ kicker: string | null;
+ image: string | null;
+ isActive: boolean;
+ sortOrder: number;
+ createdAt?: string;
+ updatedAt?: string;
+};
+
+export type DestinationRegion = {
+ id: string;
+ label: string;
+ keyword?: string | null;
+ spots?: string | null;
+ isActive: boolean;
+ sortOrder: number;
+ createdAt?: string;
+ updatedAt?: string;
+};
+
export type Product = {
id: string;
sourceId?: number | null;
@@ -144,6 +166,8 @@ export type VehicleOption = {
export type SiteConfig = {
heroSlides: Array<{ id: string; title: string; kicker: string | null; image: string | null; isActive: boolean; sortOrder: number; createdAt?: string; updatedAt?: string }>;
destinations: Destination[];
+ destinationHero: DestinationHero[];
+ destinationRegions: DestinationRegion[];
map: Array<{ id: string; image: string | null; isActive: boolean; createdAt?: string; updatedAt?: string }>;
themes: Array<{ id: string; label: string; image: string; targetType?: string | null; targetValue?: string | null; isActive: boolean }>;
campaigns: Campaign[];
@@ -153,18 +177,20 @@ export type SiteConfig = {
ctaBanners: Array<{ id: string; alt: string; image: string; targetType: string; targetValue?: string | null; isActive: boolean; sortOrder: number; createdAt?: string; updatedAt?: string }>;
};
-export type SiteModule = "heroSlides" | "destinations" | "map" | "themes" | "campaigns" | "routeSections" | "hotelGroups" | "vehicleOptions" | "ctaBanners";
+export type SiteModule = "heroSlides" | "destinations" | "destinationHero" | "destinationRegions" | "map" | "themes" | "campaigns" | "routeSections" | "hotelGroups" | "vehicleOptions" | "ctaBanners";
export type SiteConfigItem = SiteConfig[SiteModule][number];
type SiteConfigItemResponse = SiteConfigItem | { item: SiteConfigItem };
export type SiteItemPatch = {
title?: string;
subtitle?: string | null;
- kicker?: string;
+ kicker?: string | null;
name?: string;
slug?: string;
region?: string | null;
label?: string;
+ keyword?: string | null;
+ spots?: string | null;
alt?: string;
image?: string | null;
description?: string | null;
diff --git a/src/components/admin/AdminShell.tsx b/src/components/admin/AdminShell.tsx
index c919896..39f615e 100644
--- a/src/components/admin/AdminShell.tsx
+++ b/src/components/admin/AdminShell.tsx
@@ -21,7 +21,7 @@ const sidebarGroups: { id: string; label: string; items: { id: Tab; label: strin
label: "前台页面",
items: [
{ id: "home", label: "小程序首页", icon: Home, description: "轮播、主题、CTA" },
- { id: "destinations", label: "目的地页", icon: MapPin, description: "字典、搜索、商品" },
+ { id: "destinations", label: "目的地页", icon: MapPin, description: "主视觉、热门、省内" },
{ id: "products", label: "商品维护", icon: Package, description: "列表、卡片、详情" },
],
},
diff --git a/src/lib/admin-utils.ts b/src/lib/admin-utils.ts
index cb429d1..ecbb315 100644
--- a/src/lib/admin-utils.ts
+++ b/src/lib/admin-utils.ts
@@ -129,6 +129,8 @@ export function compactProductPayload(draft: ProductInput): ProductInput {
export function siteItemPrimaryKey(moduleId: SiteModule): keyof SiteItemPatch {
if (moduleId === "heroSlides") return "title";
if (moduleId === "destinations") return "name";
+ if (moduleId === "destinationHero") return "title";
+ if (moduleId === "destinationRegions") return "label";
if (moduleId === "map") return "image";
if (moduleId === "themes") return "label";
if (moduleId === "campaigns") return "title";
@@ -157,6 +159,12 @@ export function createEmptySiteItemDraft(moduleId: SiteModule, sortOrder: number
if (moduleId === "destinations") {
return { name: "", image: "", isActive: false, sortOrder };
}
+ if (moduleId === "destinationHero") {
+ return { title: "", kicker: "", image: "", isActive: true, sortOrder };
+ }
+ if (moduleId === "destinationRegions") {
+ return { label: "", keyword: "", spots: "", isActive: true, sortOrder };
+ }
if (moduleId === "map") {
return { image: "", isActive: false };
}
@@ -218,6 +226,26 @@ export function compactSiteItemPayload(moduleId: SiteModule, draft: SiteItemPatc
payload.region = draft.region?.trim() || null;
payload.isHot = Boolean(draft.isHot);
}
+ if (moduleId === "destinationHero") {
+ payload.title = draft.title?.trim();
+ payload.kicker = draft.kicker?.trim() || null;
+ payload.image = draft.image?.trim() || null;
+ payload.productIds = undefined;
+ payload.subtitle = undefined;
+ payload.targetType = undefined;
+ payload.targetValue = undefined;
+ }
+ if (moduleId === "destinationRegions") {
+ payload.label = draft.label?.trim();
+ payload.keyword = draft.keyword?.trim() || null;
+ payload.spots = draft.spots?.trim() || null;
+ payload.image = undefined;
+ payload.productIds = undefined;
+ payload.kicker = undefined;
+ payload.subtitle = undefined;
+ payload.targetType = undefined;
+ payload.targetValue = undefined;
+ }
if (moduleId === "map") {
payload.sortOrder = undefined;
}
@@ -311,6 +339,11 @@ export function itemName(item: EditableSiteItem) {
export function itemMeta(item: EditableSiteItem): string {
if ("kicker" in item) return item.kicker || "轮播";
+ if ("spots" in item || "keyword" in item) {
+ const spots = "spots" in item ? item.spots : "";
+ const keyword = "keyword" in item ? item.keyword : "";
+ return [spots, keyword].filter(Boolean).join(" / ") || "热门区域";
+ }
if ("aliases" in item) return item.region || item.aliases?.map((alias) => alias.alias).join(" / ") || "目的地";
if ("priceAmount" in item) {
const priceUnit = "priceUnit" in item && typeof item.priceUnit === "string" ? item.priceUnit : "";
@@ -349,5 +382,5 @@ export function moduleItems(config: SiteConfig, moduleId: SiteModule): EditableS
}
export function moduleEditable(moduleId: ModuleId): moduleId is SiteModule {
- return moduleId === "heroSlides" || moduleId === "destinations" || moduleId === "map" || moduleId === "themes" || moduleId === "campaigns" || moduleId === "routeSections" || moduleId === "hotelGroups" || moduleId === "vehicleOptions" || moduleId === "ctaBanners";
+ return moduleId === "heroSlides" || moduleId === "destinations" || moduleId === "destinationHero" || moduleId === "destinationRegions" || moduleId === "map" || moduleId === "themes" || moduleId === "campaigns" || moduleId === "routeSections" || moduleId === "hotelGroups" || moduleId === "vehicleOptions" || moduleId === "ctaBanners";
}
diff --git a/src/pages/structure/StructurePage.tsx b/src/pages/structure/StructurePage.tsx
index e34ddef..a14e6ef 100644
--- a/src/pages/structure/StructurePage.tsx
+++ b/src/pages/structure/StructurePage.tsx
@@ -139,20 +139,26 @@ const pageSpecs: {
{
id: "destination",
title: "目的地页",
- subtitle: "维护目的地、别名和与商品的关联,影响搜索和目的地 tab。",
+ subtitle: "按前台目的地页模块核对数据来源和维护入口。",
frontPath: "前台:bottom tab「目的地」/ 搜索弹层",
modules: [
{
- id: "destinations",
- label: "目的地字典",
- hint: "维护名称、封面、热门状态和启停。",
- frontPosition: "目的地首页和搜索列表",
+ id: "destinationHero",
+ label: "顶部主视觉",
+ hint: "对应目的地页顶部大图和“贵州小包团目的地”文案。",
+ frontPosition: "目的地页顶部主视觉",
},
{
- id: "routeProducts",
- label: "目的地商品",
- hint: "商品绑定目的地后会进入对应结果页。",
- frontPosition: "搜索结果页产品列表",
+ id: "destinationRegions",
+ label: "热门区域",
+ hint: "对应目的地页“热门区域”快捷入口。",
+ frontPosition: "目的地页「热门区域」",
+ },
+ {
+ id: "destinations",
+ label: "省内目的地",
+ hint: "维护目的地卡片名称、封面、热门状态、启停和展示顺序。",
+ frontPosition: "目的地页「省内目的地」",
},
],
},
@@ -299,6 +305,8 @@ export function StructurePage({
slug: "slug" in item ? item.slug : undefined,
region: "region" in item ? item.region || "" : undefined,
label: "label" in item ? item.label : undefined,
+ keyword: "keyword" in item ? item.keyword || "" : undefined,
+ spots: "spots" in item ? item.spots || "" : undefined,
alt: "alt" in item ? item.alt : undefined,
image: imageValue,
description: "description" in item ? item.description || "" : undefined,
@@ -948,6 +956,8 @@ function SiteItemEditor({
const primaryLabel = (() => {
if (moduleId === "heroSlides") return "轮播标题";
if (moduleId === "destinations") return "目的地名称";
+ if (moduleId === "destinationHero") return "主视觉标题";
+ if (moduleId === "destinationRegions") return "区域名称";
if (moduleId === "map") return "地图图片";
if (moduleId === "themes") return "主题名称";
if (moduleId === "campaigns") return "活动标题";
@@ -960,6 +970,9 @@ function SiteItemEditor({
const isImageOnlyModule = moduleId === "map";
const isCampaignModule = moduleId === "campaigns";
const isHotelModule = moduleId === "hotelGroups";
+ const isDestinationHeroModule = moduleId === "destinationHero";
+ const isDestinationRegionModule = moduleId === "destinationRegions";
+ const isTextOnlyModule = isDestinationRegionModule;
const usesOfferConfig = isCampaignModule || isHotelModule;
const isMoreServicesModule = moduleId === "ctaBanners";
const isCardContentModule = moduleId === "vehicleOptions";
@@ -1035,9 +1048,9 @@ function SiteItemEditor({
}
/>
- {moduleId === "heroSlides" ? (
+ {moduleId === "heroSlides" || isDestinationHeroModule ? (
) : null}
+ {isDestinationRegionModule ? (
+ <>
+
+
+ >
+ ) : null}
{isCardContentModule ? (