feat: add support for map module in site configuration and update related APIs

This commit is contained in:
duanshuwen
2026-07-01 21:29:38 +08:00
parent 462498660e
commit 710d020504
8 changed files with 293 additions and 103 deletions

View File

@@ -81,6 +81,12 @@ const pageSpecs: {
hint: "控制省内目的地宫格、热门标记和搜索入口。",
frontPosition: "首页「探索贵州」",
},
{
id: "map",
label: "贵州地图",
hint: "展示贵州省内的地图信息和地点。",
frontPosition: "首页「探索贵州」",
},
{
id: "themes",
label: "主题甄选",
@@ -202,6 +208,7 @@ export function StructurePage({
const [draft, setDraft] = useState<SiteItemPatch>({});
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];
@@ -210,6 +217,9 @@ export function StructurePage({
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
@@ -327,6 +337,7 @@ export function StructurePage({
const closeEditorDrawer = () => {
setEditorOpen(false);
setMessage("");
if (isCreatingSiteItem) {
const first = editableItems[0];
if (first) {
@@ -342,6 +353,7 @@ export function StructurePage({
const save = async () => {
if (
saving ||
!config ||
!moduleEditable(moduleId) ||
(!selectedId && !isCreatingSiteItem)
@@ -352,17 +364,55 @@ export function StructurePage({
const primaryValue = String(payload[primaryKey] ?? "").trim();
if (!primaryValue) {
const messageText = "请先填写当前模块的主标题/名称。";
const messageText =
moduleId === "map"
? "请先上传贵州地图图片。"
: "请先填写当前模块的主标题/名称。";
notify({ tone: "warning", title: "内容未保存", message: messageText });
return;
}
setSaving(true);
setMessage("");
try {
const saved = isCreatingSiteItem
? await createSiteConfigItem(moduleId, payload)
: await updateSiteConfigItem(moduleId, selectedId, payload);
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 as EditableSiteItem);
setDraftFromItem(saved);
setMessage("");
onDirtyChange(false);
notify({
@@ -375,6 +425,8 @@ export function StructurePage({
const messageText = err instanceof Error ? err.message : "保存失败";
setMessage(messageText);
notify({ tone: "danger", title: "内容保存失败", message: messageText });
} finally {
setSaving(false);
}
};
@@ -541,13 +593,13 @@ export function StructurePage({
<h3>{activeModule.label}</h3>
<p>{activeModule.hint}</p>
</span>
{moduleEditable(moduleId) ? (
{canCreateSiteItem ? (
<Button
className="primary-action compact"
onClick={startCreateSiteItem}
>
<Plus size={16} />
{isMapModule ? "上传地图" : "新增"}
</Button>
) : null}
</header>
@@ -569,7 +621,7 @@ export function StructurePage({
<b>{itemName(item)}</b>
<small>
{itemMeta(item)}
{typeof itemSortOrder(item) === "number"
{!isMapModule && typeof itemSortOrder(item) === "number"
? ` · 排序 ${itemSortOrder(item)}`
: ""}
</small>
@@ -579,24 +631,28 @@ export function StructurePage({
className="mapped-list-actions"
aria-label={`${itemName(item)} 操作`}
>
<Button
variant="ghost"
size="icon"
disabled={index === 0}
onClick={() => moveSiteItem(item, -1)}
aria-label={`上移 ${itemName(item)}`}
>
<ArrowUp size={15} />
</Button>
<Button
variant="ghost"
size="icon"
disabled={index === editableItems.length - 1}
onClick={() => moveSiteItem(item, 1)}
aria-label={`下移 ${itemName(item)}`}
>
<ArrowDown size={15} />
</Button>
{!isMapModule ? (
<>
<Button
variant="ghost"
size="icon"
disabled={index === 0}
onClick={() => moveSiteItem(item, -1)}
aria-label={`上移 ${itemName(item)}`}
>
<ArrowUp size={15} />
</Button>
<Button
variant="ghost"
size="icon"
disabled={index === editableItems.length - 1}
onClick={() => moveSiteItem(item, 1)}
aria-label={`下移 ${itemName(item)}`}
>
<ArrowDown size={15} />
</Button>
</>
) : null}
<Button
variant="ghost"
size="icon"
@@ -610,7 +666,9 @@ export function StructurePage({
))}
{!editableItems.length ? (
<p className="mapped-empty">
{isMapModule
? "暂无地图图片,点击上传地图。"
: "暂无配置数据,点击新增创建第一项。"}
</p>
) : null}
</div>
@@ -642,6 +700,7 @@ export function StructurePage({
moduleLabel={activeModule.label}
draft={draft}
isCreating={isCreatingSiteItem}
saving={saving}
onDraft={(nextDraft) => {
setDraft(nextDraft);
onDirtyChange(true);
@@ -709,6 +768,7 @@ function SiteItemEditor({
moduleLabel,
draft,
isCreating,
saving,
onDraft,
onSave,
onClose,
@@ -717,6 +777,7 @@ function SiteItemEditor({
moduleLabel: string;
draft: SiteItemPatch;
isCreating: boolean;
saving: boolean;
onDraft: (draft: SiteItemPatch) => void;
onSave: () => void;
onClose: () => void;
@@ -727,21 +788,27 @@ function SiteItemEditor({
? "轮播标题"
: moduleId === "destinations"
? "目的地名称"
: moduleId === "themes"
? "主题名称"
: "入口文案";
: moduleId === "map"
? "地图图片"
: moduleId === "themes"
? "主题名称"
: "入口文案";
const editorTitle = `${isCreating ? "新增" : "编辑"}${moduleLabel}`;
const isImageOnlyModule = moduleId === "map";
const editorHint = isImageOnlyModule
? isCreating
? `上传后会作为「${moduleLabel}」唯一展示图片`
: `保存后会替换「${moduleLabel}」前台展示图片`
: isCreating
? `创建后会进入「${moduleLabel}」的数据列表`
: `保存后会影响「${moduleLabel}」前台模块`;
return (
<div className="mapped-editor">
<header className="editor-head">
<span>
<h3 id="site-item-editor-title">{editorTitle}</h3>
<small>
{isCreating
? `创建后会进入「${moduleLabel}」的数据列表`
: `保存后会影响「${moduleLabel}」前台模块`}
</small>
<small>{editorHint}</small>
</span>
<Button
variant="ghost"
@@ -753,68 +820,50 @@ function SiteItemEditor({
</Button>
</header>
<div className="admin-disclosure-stack">
<AdminDisclosure title="展示内容">
<label>
{primaryLabel}
<Input
value={String(draft[primaryKey] ?? "")}
onChange={(event) =>
onDraft({ ...draft, [primaryKey]: event.target.value })
}
/>
</label>
{moduleId === "heroSlides" ? (
{!isImageOnlyModule ? (
<AdminDisclosure title="展示内容">
<label>
{primaryLabel}
<Input
value={draft.kicker ?? ""}
value={String(draft[primaryKey] ?? "")}
onChange={(event) =>
onDraft({ ...draft, kicker: event.target.value })
onDraft({ ...draft, [primaryKey]: event.target.value })
}
/>
</label>
) : null}
{moduleId === "destinations" ? (
<div className="form-grid compact-grid">
{moduleId === "heroSlides" ? (
<label>
<Input
value={draft.slug ?? ""}
value={draft.kicker ?? ""}
onChange={(event) =>
onDraft({ ...draft, slug: event.target.value })
onDraft({ ...draft, kicker: event.target.value })
}
/>
</label>
<label>
<Input
value={draft.region ?? ""}
onChange={(event) =>
onDraft({ ...draft, region: event.target.value })
}
/>
</label>
</div>
) : null}
</AdminDisclosure>
<AdminDisclosure title="资源图片">
) : null}
</AdminDisclosure>
) : null}
<AdminDisclosure title={isImageOnlyModule ? primaryLabel : "资源图片"}>
<SingleImageUploader
value={draft.image}
onChange={(image) => onDraft({ ...draft, image })}
group={moduleId}
/>
</AdminDisclosure>
<AdminDisclosure title="排序与状态">
<label>
<Input
type="number"
value={draft.sortOrder ?? 0}
onChange={(event) =>
onDraft({ ...draft, sortOrder: Number(event.target.value) })
}
/>
</label>
<AdminDisclosure title={isImageOnlyModule ? "显示状态" : "排序与状态"}>
{!isImageOnlyModule ? (
<label>
<Input
type="number"
value={draft.sortOrder ?? 0}
onChange={(event) =>
onDraft({ ...draft, sortOrder: Number(event.target.value) })
}
/>
</label>
) : null}
{moduleId === "destinations" ? (
<label className="switch-line">
<Switch
@@ -838,12 +887,16 @@ function SiteItemEditor({
</AdminDisclosure>
</div>
<footer className="drawer-editor-footer">
<Button variant="outline" onClick={onClose}>
<Button variant="outline" onClick={onClose} disabled={saving}>
</Button>
<Button className="primary-action compact" onClick={onSave}>
<Button
className="primary-action compact"
onClick={onSave}
disabled={saving}
>
<Save size={17} />
{isCreating ? "创建" : "保存"}
{saving ? "保存中" : isCreating ? "创建" : "保存"}
</Button>
</footer>
</div>