"use client"; import { useMemo, useRef, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { ArrowLeft, Megaphone } from "lucide-react"; import { apiAdminCreateAnnouncement, apiAdminUpdateAnnouncement, type Announcement, type AnnouncementColor, type AnnouncementInput, } 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"; import { ANNOUNCEMENT_COLORS } from "./announcementColors"; type PublishKey = "published" | "draft"; type FormState = { title: string; content: string; tag: string; tag_color: AnnouncementColor; published: boolean; pinned: boolean; }; function toForm(a?: Announcement | null): FormState { return { title: a?.title ?? "", content: a?.content ?? "", tag: a?.tag ?? "公告", tag_color: a?.tag_color ?? "blue", published: a?.published ?? true, pinned: a?.pinned ?? false, }; } function sameForm(a: FormState, b: FormState): boolean { return ( a.title === b.title && a.content === b.content && a.tag === b.tag && a.tag_color === b.tag_color && a.published === b.published && a.pinned === b.pinned ); } export default function AnnouncementComposeClient({ mode, initial, }: { mode: "create" | "edit"; initial?: Announcement | 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 payload: AnnouncementInput = { title: current.title.trim(), content: current.content.trim(), tag: current.tag.trim() || "公告", tag_color: current.tag_color, published: current.published, pinned: current.pinned, }; if (!payload.title || !payload.content) { toast("标题和内容不能为空"); return; } setSaving(true); try { if (isEdit) { await apiAdminUpdateAnnouncement(initial.id, payload); toast("公告已更新", "ok"); } else { await apiAdminCreateAnnouncement(payload); toast(payload.published ? "公告已发布" : "草稿已保存", "ok"); } router.push("/admin/announcements"); router.refresh(); } catch (e) { toast(e instanceof Error ? e.message : "保存失败"); } finally { setSaving(false); } } const saveLabel = isEdit ? "保存修改" : form.published ? "发布公告" : "保存草稿"; return ( 返回列表 } />
patchForm({ tag })} placeholder="公告" />
{ANNOUNCEMENT_COLORS.map((c) => (
role="group" ariaLabel="发布状态" value={form.published ? "published" : "draft"} onChange={(key) => patchForm({ published: key === "published" })} options={[ { key: "published", label: "立即发布" }, { key: "draft", label: "存为草稿" }, ]} /> patchForm((f) => ({ ...f, pinned: !f.pinned }))} label="置顶公告" />
void save()} onDiscard={() => { formRef.current = baseline; setForm(baseline); }} /> } > patchForm({ title })} placeholder="请输入公告标题" />
patchForm({ content })} allowHide allowReplyHide={false} placeholder="请输入公告正文,支持 Markdown…" rows={18} onSubmitShortcut={() => void save()} />
); }