import { useEffect, useState } from "react"; import { ArrowDown, ArrowUp, ChevronRight, Eye, Plus, RefreshCcw, Save, Send, Trash2, X, } from "lucide-react"; import { createSiteConfigItem, deleteSiteConfigItem, getProducts, getSiteConfig, publishSite, reorderSiteConfigItems, resetGuizhouContent, updateSiteConfigItem, } from "@/api"; import type { Product, SiteConfig, SiteItemPatch, SiteModule } from "@/api"; import { AdminDisclosure } from "@/components/admin/AdminDisclosure"; import { EmptyState } from "@/components/admin/EmptyState"; import { SingleImageUploader } from "@/components/admin/SingleImageUploader"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { compactSiteItemPayload, createEmptySiteItemDraft, itemImage, itemMeta, itemName, itemSortOrder, moduleEditable, moduleItems, siteItemPrimaryKey, } from "@/lib/admin-utils"; import type { DirtyChangeHandler, EditableSiteItem, ModuleId, Notify, PageId, Tab, } from "@/types/admin"; const NEW_SITE_ITEM_ID = "__new_site_item__"; const pageSpecs: { id: PageId; title: string; subtitle: string; frontPath: string; modules: { id: ModuleId; label: string; hint: string; frontPosition: string; }[]; }[] = [ { id: "home", title: "万趣首页", subtitle: "维护用户进入小程序后第一屏到页尾的内容顺序。", frontPath: "前台:首页 / bottom tab「万趣」", modules: [ { id: "heroSlides", label: "顶部轮播", hint: "控制首屏大图、活动入口和主视觉顺序。", frontPosition: "首页第一屏", }, { id: "destinations", label: "探索贵州", hint: "控制省内目的地宫格、热门标记和搜索入口。", frontPosition: "首页「探索贵州」", }, { id: "map", label: "贵州地图", hint: "展示贵州省内的地图信息和地点。", frontPosition: "首页「探索贵州」", }, { id: "themes", label: "主题甄选", hint: "控制横向主题卡和搜索跳转。", frontPosition: "首页「主题甄选」", }, { id: "routeProducts", label: "精选线路", hint: "从商品库选择上架线路,决定首页线路池。", frontPosition: "首页精选线路", }, { id: "ctaBanners", label: "底部运营入口", hint: "控制权益卡、服务管家入口、目的地搜索、提交需求入口。", frontPosition: "首页页尾 CTA", }, ], }, { id: "destination", title: "目的地页", subtitle: "维护目的地、别名和与商品的关联,影响搜索和目的地 tab。", frontPath: "前台:bottom tab「目的地」/ 搜索弹层", modules: [ { id: "destinations", label: "目的地字典", hint: "维护名称、封面、热门状态和启停。", frontPosition: "目的地首页和搜索列表", }, { id: "routeProducts", label: "目的地商品", hint: "商品绑定目的地后会进入对应结果页。", frontPosition: "搜索结果页产品列表", }, ], }, { id: "detail", title: "商品维护", subtitle: "维护用户点击商品卡后的详情页内容和售卖状态。", frontPath: "前台:产品卡点击后详情页", modules: [ { id: "routeProducts", label: "商品库", hint: "编辑标题、价格、封面、标签、详情模块和上下架。", frontPosition: "详情页、搜索页、首页商品卡", }, ], }, { id: "campaign", title: "活动专题", subtitle: "维护轮播、主题卡和 CTA 指向的活动内容。", frontPath: "前台:轮播 / 主题卡 / CTA 跳转", modules: [ { id: "heroSlides", label: "轮播活动入口", hint: "首屏主视觉上的活动入口。", frontPosition: "首页轮播", }, { id: "themes", label: "主题活动入口", hint: "主题卡片进入对应活动专题。", frontPosition: "主题甄选横滑区", }, { id: "ctaBanners", label: "营销 CTA", hint: "页尾活动和需求入口。", frontPosition: "首页底部运营区", }, { id: "campaignProducts", label: "活动商品池", hint: "活动专题内展示的商品来自商品库。", frontPosition: "活动专题产品列表", }, ], }, { id: "demand", title: "需求线索", subtitle: "维护用户提交出行需求后的分配和跟进。", frontPath: "前台:提交出行需求 / 立即咨询", modules: [ { id: "leadFlow", label: "线索跟进", hint: "查看联系方式、目的地、来源商品并推进状态。", frontPosition: "需求表单提交后", }, ], }, ]; export function StructurePage({ fixedPage, onJump, onDirtyChange, notify, }: { fixedPage?: PageId; onJump: (tab: Tab) => void; onDirtyChange: DirtyChangeHandler; notify: Notify; }) { const [config, setConfig] = useState(null); const [products, setProducts] = useState([]); const [pageId, setPageId] = useState(fixedPage ?? "home"); const [moduleId, setModuleId] = useState("heroSlides"); const [selectedId, setSelectedId] = useState(""); const [draft, setDraft] = useState({}); const [message, setMessage] = useState(""); const [editorOpen, setEditorOpen] = useState(false); const [saving, setSaving] = useState(false); const activePage = pageSpecs.find((page) => page.id === (fixedPage ?? pageId)) ?? pageSpecs[0]; const activeModule = activePage.modules.find((module) => module.id === moduleId) ?? activePage.modules[0]; const editableItems = config && moduleEditable(moduleId) ? moduleItems(config, moduleId) : []; const isMapModule = moduleId === "map"; const canCreateSiteItem = moduleEditable(moduleId) && (!isMapModule || editableItems.length === 0); const isCreatingSiteItem = selectedId === NEW_SITE_ITEM_ID; const selectedItem = isCreatingSiteItem ? undefined : (editableItems.find((item) => item.id === selectedId) ?? editableItems[0]); const showEditorDrawer = editorOpen && moduleEditable(moduleId) && (isCreatingSiteItem || Boolean(selectedItem)); const gridClassName = [ "maintenance-grid", fixedPage ? "focused-page-grid" : "", ] .filter(Boolean) .join(" "); const setDraftFromItem = (item: EditableSiteItem) => { setDraft({ title: "title" in item ? item.title : undefined, kicker: "kicker" in item ? item.kicker || "" : undefined, name: "name" in item ? item.name : undefined, slug: "slug" in item ? item.slug : undefined, region: "region" in item ? item.region || "" : undefined, label: "label" in item ? item.label : undefined, alt: "alt" in item ? item.alt : undefined, image: "image" in item ? item.image || "" : "", isHot: "isHot" in item ? item.isHot : undefined, isActive: item.isActive, sortOrder: itemSortOrder(item), }); }; const load = async () => { const [site, productResult] = await Promise.all([ getSiteConfig(), getProducts(), ]); setConfig(site); setProducts(productResult.items); const firstModule = activePage.modules[0]?.id ?? "heroSlides"; setModuleId((current) => activePage.modules.some((module) => module.id === current) ? current : firstModule, ); if (moduleEditable(firstModule)) { const first = moduleItems(site, firstModule)[0]; if (first && !selectedId) { setSelectedId(first.id); setDraftFromItem(first); } } }; useEffect(() => { load().catch((err) => setMessage(err.message)); }, []); useEffect(() => { if (!fixedPage) return; setPageId(fixedPage); const next = pageSpecs.find((page) => page.id === fixedPage)?.modules[0]?.id ?? "heroSlides"; setModuleId(next); setSelectedId(""); setEditorOpen(false); }, [fixedPage]); useEffect(() => { if (!config || !moduleEditable(moduleId)) { setSelectedId(""); setDraft({}); setEditorOpen(false); return; } const first = moduleItems(config, moduleId)[0]; if (!first) { setSelectedId(""); setDraft({}); setEditorOpen(false); return; } setSelectedId(first.id); setDraftFromItem(first); setEditorOpen(false); }, [moduleId, config]); const selectPage = (next: PageId) => { setPageId(next); const page = pageSpecs.find((item) => item.id === next); setModuleId(page?.modules[0]?.id ?? "heroSlides"); setSelectedId(""); setMessage(""); setEditorOpen(false); onDirtyChange(false); }; const startCreateSiteItem = () => { if (!moduleEditable(moduleId)) return; setSelectedId(NEW_SITE_ITEM_ID); setDraft(createEmptySiteItemDraft(moduleId, editableItems.length)); setMessage(""); setEditorOpen(true); onDirtyChange(true); }; const selectItem = (item: EditableSiteItem) => { setSelectedId(item.id); setDraftFromItem(item); setMessage(""); setEditorOpen(true); onDirtyChange(false); }; const closeEditorDrawer = () => { setEditorOpen(false); setMessage(""); if (isCreatingSiteItem) { const first = editableItems[0]; if (first) { setSelectedId(first.id); setDraftFromItem(first); } else { setSelectedId(""); setDraft({}); } onDirtyChange(false); } }; const save = async () => { if ( saving || !config || !moduleEditable(moduleId) || (!selectedId && !isCreatingSiteItem) ) return; const payload = compactSiteItemPayload(moduleId, draft); const primaryKey = siteItemPrimaryKey(moduleId); const primaryValue = String(payload[primaryKey] ?? "").trim(); if (!primaryValue) { const messageText = moduleId === "map" ? "请先上传贵州地图图片。" : "请先填写当前模块的主标题/名称。"; notify({ tone: "warning", title: "内容未保存", message: messageText }); return; } setSaving(true); setMessage(""); try { let saved: EditableSiteItem; const existingMapItem = moduleId === "map" ? moduleItems(config, "map")[0] : undefined; try { saved = moduleId === "map" && existingMapItem ? ((await updateSiteConfigItem( moduleId, existingMapItem.id, payload, )) as EditableSiteItem) : isCreatingSiteItem ? ((await createSiteConfigItem(moduleId, payload)) as EditableSiteItem) : ((await updateSiteConfigItem( moduleId, selectedId, payload, )) as EditableSiteItem); } catch (err) { if (moduleId !== "map" || !isCreatingSiteItem) throw err; const latestConfig = await getSiteConfig().catch(() => null); const latestMapItem = latestConfig ? moduleItems(latestConfig, "map")[0] : undefined; if (!latestMapItem) throw err; saved = (await updateSiteConfigItem( moduleId, latestMapItem.id, payload, )) as EditableSiteItem; } setSelectedId(saved.id); setDraftFromItem(saved); setMessage(""); onDirtyChange(false); notify({ tone: "success", title: isCreatingSiteItem ? "内容已新增" : "内容已保存", message: `${activeModule.label} 已同步到维护数据。`, }); await load(); } catch (err) { const messageText = err instanceof Error ? err.message : "保存失败"; setMessage(messageText); notify({ tone: "danger", title: "内容保存失败", message: messageText }); } finally { setSaving(false); } }; const removeSiteItem = async (item: EditableSiteItem) => { if (!moduleEditable(moduleId)) return; const confirmed = window.confirm( `确认删除「${itemName(item)}」吗?删除后需要后端同步移除对应模块配置。`, ); if (!confirmed) return; try { await deleteSiteConfigItem(moduleId, item.id); setSelectedId(""); setDraft({}); setMessage(""); onDirtyChange(false); notify({ tone: "success", title: "内容已删除", message: `${activeModule.label} 已移除一项配置。`, }); await load(); } catch (err) { const messageText = err instanceof Error ? err.message : "删除失败"; setMessage(messageText); notify({ tone: "danger", title: "内容删除失败", message: messageText }); } }; const moveSiteItem = async (item: EditableSiteItem, direction: -1 | 1) => { if (!moduleEditable(moduleId)) return; const currentIndex = editableItems.findIndex( (candidate) => candidate.id === item.id, ); const nextIndex = currentIndex + direction; if (currentIndex < 0 || nextIndex < 0 || nextIndex >= editableItems.length) return; const nextIds = editableItems.map((candidate) => candidate.id); [nextIds[currentIndex], nextIds[nextIndex]] = [ nextIds[nextIndex], nextIds[currentIndex], ]; try { await reorderSiteConfigItems(moduleId, nextIds); setMessage(""); onDirtyChange(false); notify({ tone: "success", title: "顺序已更新", message: `${activeModule.label} 已按新顺序保存。`, }); await load(); } catch (err) { const messageText = err instanceof Error ? err.message : "排序失败"; setMessage(messageText); notify({ tone: "danger", title: "排序保存失败", message: messageText }); } }; const publish = async () => { const result = await publishSite(); setMessage(""); notify({ tone: "success", title: "已生成发布版本", message: result.title }); }; const resetToGuizhou = async () => { const result = await resetGuizhouContent(); setMessage(""); notify({ tone: "success", title: "贵州内容已重置", message: "后台配置、商品库和素材引用已切换为贵州省内定制游。", }); setSelectedId(""); setDraft({}); onDirtyChange(false); await load(); }; if (!config) return ; return (

{fixedPage ? activePage.title : "小程序维护地图"}

{fixedPage ? activePage.subtitle : "按用户看到的前台页面组织后台,逐层展开到可维护内容。"}

{!fixedPage ? ( ) : (
当前维护页面

{activePage.title}

{activePage.frontPath}

{activePage.subtitle}
)}
{activePage.frontPath} {activePage.subtitle}

页面模块

{activePage.modules.map((module, index) => ( ))}

{activeModule.label}

{activeModule.hint}

{canCreateSiteItem ? ( ) : null}
{moduleEditable(moduleId) ? (
{editableItems.map((item, index) => (
{!isMapModule ? ( <> ) : null}
))} {!editableItems.length ? (

{isMapModule ? "暂无地图图片,点击上传地图。" : "暂无配置数据,点击新增创建第一项。"}

) : null}
) : ( )}
{showEditorDrawer && moduleEditable(moduleId) ? (
) : null}
); } function ReferencePanel({ moduleId, products, onJump, }: { moduleId: ModuleId; products: Product[]; onJump: (tab: Tab) => void; }) { if (moduleId === "leadFlow") { return (

这个模块由客户需求列表维护,重点是状态流转、服务管家跟进和来源追踪。

); } return (

这个模块引用商品库。编辑商品后,首页商品卡、搜索结果和详情页会同步使用同一份数据。

{products.slice(0, 4).map((product) => ( {product.title} ))}
); } function SiteItemEditor({ moduleId, moduleLabel, draft, isCreating, saving, onDraft, onSave, onClose, }: { moduleId: SiteModule; moduleLabel: string; draft: SiteItemPatch; isCreating: boolean; saving: boolean; onDraft: (draft: SiteItemPatch) => void; onSave: () => void; onClose: () => void; }) { const primaryKey = siteItemPrimaryKey(moduleId); const primaryLabel = moduleId === "heroSlides" ? "轮播标题" : moduleId === "destinations" ? "目的地名称" : moduleId === "map" ? "地图图片" : moduleId === "themes" ? "主题名称" : "入口文案"; const editorTitle = `${isCreating ? "新增" : "编辑"}${moduleLabel}`; const isImageOnlyModule = moduleId === "map"; const editorHint = isImageOnlyModule ? isCreating ? `上传后会作为「${moduleLabel}」唯一展示图片` : `保存后会替换「${moduleLabel}」前台展示图片` : isCreating ? `创建后会进入「${moduleLabel}」的数据列表` : `保存后会影响「${moduleLabel}」前台模块`; return (

{editorTitle}

{editorHint}
{!isImageOnlyModule ? ( {moduleId === "heroSlides" ? ( ) : null} ) : null} onDraft({ ...draft, image })} group={moduleId} /> {!isImageOnlyModule ? ( ) : null} {moduleId === "destinations" ? ( ) : null}
); }