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 ? ( + <> + +