From 3b3d8d0aa19f2d7883e6d665f2682a296a549a2b Mon Sep 17 00:00:00 2001 From: duanshuwen Date: Tue, 7 Jul 2026 21:24:38 +0800 Subject: [PATCH] feat(admin): add destination hero and region admin modules Add new API type definitions for DestinationHero and DestinationRegion Extend SiteConfig and SiteModule to include the new site modules Update destination page sidebar label and structure page configuration Add admin editor UI, payload handling, and empty draft generators for the new modules Adjust item metadata and form fields to support the new site item types --- src/api.ts | 30 ++++++++- src/components/admin/AdminShell.tsx | 2 +- src/lib/admin-utils.ts | 35 ++++++++++- src/pages/structure/StructurePage.tsx | 91 +++++++++++++++++++-------- src/types/admin.ts | 2 + 5 files changed, 130 insertions(+), 30 deletions(-) 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 ? ( + <> + +