"use client"; import { useMemo, useRef, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { ArrowLeft, FileText } from "lucide-react"; import { apiAdminCreateSitePage, apiAdminUpdateSitePage, type SitePage, type SitePageInput, } from "@/lib/api"; import { toast } from "@/lib/toast"; import MarkdownEditor from "@/components/MarkdownEditor"; import { AdminField, AdminPageHeader, AdminSegmented, AdminSettingsActions, AdminSettingsGroup, AdminSettingsPage, AdminSettingsRow, AdminSwitch, } from "@/components/admin"; import ConfirmDialog from "@/components/ConfirmDialog"; import useUnsavedGuard from "@/hooks/useUnsavedGuard"; type PublishKey = "published" | "draft"; type FormState = { slug: string; title: string; content: string; excerpt: string; published: boolean; show_in_sidebar: boolean; sort_order: number; }; function toForm(p?: SitePage | null): FormState { return { slug: p?.slug ?? "", title: p?.title ?? "", content: p?.content ?? "", excerpt: p?.excerpt ?? "", published: p?.published ?? true, show_in_sidebar: p?.show_in_sidebar ?? false, sort_order: p?.sort_order ?? 0, }; } function sameForm(a: FormState, b: FormState): boolean { return ( a.slug === b.slug && a.title === b.title && a.content === b.content && a.excerpt === b.excerpt && a.published === b.published && a.show_in_sidebar === b.show_in_sidebar && a.sort_order === b.sort_order ); } /** slug 仅允许小写字母数字与连字符 */ function normalizeSlug(raw: string): string { return raw .trim() .toLowerCase() .replace(/[^a-z0-9-]+/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, ""); } export default function PageComposeClient({ mode, initial, }: { mode: "create" | "edit"; initial?: SitePage | null; }) { const router = useRouter(); const baseline = useMemo(() => toForm(initial), [initial]); const [form, setForm] = useState(baseline); const formRef = useRef(form); const [saving, setSaving] = useState(false); const isEdit = mode === "edit" && initial != null; const dirty = !sameForm(form, baseline); const { leaveOpen, confirmLeave, cancelLeave } = useUnsavedGuard(dirty && !saving); const patchForm = (patch: Partial | ((prev: FormState) => FormState)) => { setForm((prev) => { const next = typeof patch === "function" ? patch(prev) : { ...prev, ...patch }; formRef.current = next; return next; }); }; async function save() { if (saving) return; const current = formRef.current; const slug = normalizeSlug(current.slug); const payload: SitePageInput = { slug, title: current.title.trim(), content: current.content.trim(), excerpt: current.excerpt.trim(), published: current.published, show_in_sidebar: current.show_in_sidebar, sort_order: current.sort_order, }; if (!payload.slug || !payload.title || !payload.content) { toast("slug、标题和内容不能为空"); return; } setSaving(true); try { if (isEdit) { await apiAdminUpdateSitePage(initial.id, payload); toast("页面已更新", "ok"); } else { await apiAdminCreateSitePage(payload); toast(payload.published ? "页面已发布" : "草稿已保存", "ok"); } router.push("/admin/pages"); router.refresh(); } catch (e) { toast(e instanceof Error ? e.message : "保存失败"); } finally { setSaving(false); } } const saveLabel = isEdit ? "保存修改" : form.published ? "发布页面" : "保存草稿"; return ( 返回列表 } />
/p/
patchForm({ slug: e.target.value })} onBlur={() => patchForm((f) => ({ ...f, slug: normalizeSlug(f.slug) }))} placeholder="about" />
patchForm({ excerpt })} placeholder="一句话介绍本页…" /> patchForm({ sort_order: parseInt(e.target.value, 10) || 0 })} /> role="group" ariaLabel="发布状态" value={form.published ? "published" : "draft"} onChange={(key) => patchForm({ published: key === "published" })} options={[ { key: "published", label: "立即发布" }, { key: "draft", label: "存为草稿" }, ]} /> patchForm((f) => ({ ...f, show_in_sidebar: !f.show_in_sidebar }))} label="显示在首页侧栏" />
void save()} onDiscard={() => { formRef.current = baseline; setForm(baseline); }} /> } > patchForm({ title })} placeholder="页面标题" />
patchForm({ content })} allowHide={false} placeholder="请输入页面正文,支持 Markdown…" rows={18} onSubmitShortcut={() => void save()} />
); }