feat: add support for map module in site configuration and update related APIs
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user