首页右栏接入单页入口与公告置顶/全部页;Markdown 编辑器支持可视化时间线与 Git 导入;隐藏块改为 [hide]/[timeline] 命名闭合,并修复单页/公告 GORM 更新只改 updated_at 的问题。 Co-authored-by: Cursor <cursoragent@cursor.com>
236 lines
7.8 KiB
TypeScript
236 lines
7.8 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useState } from "react";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
import { ArrowLeft, Loader2, Save } 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 { AdminSegmented, AdminSwitch } from "@/components/admin";
|
|
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,
|
|
};
|
|
}
|
|
|
|
export default function AnnouncementComposeClient({
|
|
mode,
|
|
initial,
|
|
}: {
|
|
mode: "create" | "edit";
|
|
initial?: Announcement | null;
|
|
}) {
|
|
const router = useRouter();
|
|
const [form, setForm] = useState<FormState>(() => toForm(initial));
|
|
const formRef = useRef(form);
|
|
const [saving, setSaving] = useState(false);
|
|
const isEdit = mode === "edit" && initial != null;
|
|
|
|
const patchForm = (patch: Partial<FormState> | ((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);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="j13-compose-page w-full">
|
|
<div className="j13-compose-stage">
|
|
<header className="j13-compose-toolbar">
|
|
<h1 className="j13-compose-toolbar-title">
|
|
{isEdit ? "编辑公告" : "新建公告"}
|
|
</h1>
|
|
<div className="j13-compose-toolbar-actions">
|
|
<Link href="/admin/announcements" className="btn btn-line btn-sm">
|
|
<ArrowLeft size={14} /> 返回列表
|
|
</Link>
|
|
<button
|
|
type="button"
|
|
className="btn btn-primary btn-sm min-w-[4.5rem]"
|
|
disabled={saving}
|
|
onClick={() => void save()}
|
|
>
|
|
{saving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
|
|
保存
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="j13-compose-body">
|
|
<section className="j13-compose-section" aria-label="发布设置">
|
|
<h2 className="j13-compose-section-title">发布设置</h2>
|
|
<div className="j13-compose-typepanel space-y-4">
|
|
<div>
|
|
<label className="field-label" htmlFor="anno-tag">
|
|
标签
|
|
</label>
|
|
<input
|
|
id="anno-tag"
|
|
className="j13-compose-field w-full max-w-[12rem]"
|
|
maxLength={32}
|
|
value={form.tag}
|
|
onChange={(e) => patchForm({ tag: e.target.value })}
|
|
placeholder="公告"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="field-label">标签颜色</p>
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
{ANNOUNCEMENT_COLORS.map((c) => (
|
|
<button
|
|
key={c.value}
|
|
type="button"
|
|
data-tip={c.label}
|
|
aria-label={c.label}
|
|
aria-pressed={form.tag_color === c.value}
|
|
className={`j13-anno-dot is-${c.value}`}
|
|
onClick={() => patchForm({ tag_color: c.value })}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="field-label">发布状态</p>
|
|
<AdminSegmented<PublishKey>
|
|
role="group"
|
|
ariaLabel="发布状态"
|
|
value={form.published ? "published" : "draft"}
|
|
onChange={(key) => patchForm({ published: key === "published" })}
|
|
options={[
|
|
{ key: "published", label: "立即发布" },
|
|
{ key: "draft", label: "存为草稿" },
|
|
]}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between gap-3 flex-wrap">
|
|
<div>
|
|
<p className="text-[14px] font-medium" style={{ color: "var(--ink)" }}>
|
|
置顶公告
|
|
</p>
|
|
<p className="meta text-[12px] mt-0.5">置顶后优先显示在首页右栏</p>
|
|
</div>
|
|
<AdminSwitch
|
|
checked={form.pinned}
|
|
onChange={() => patchForm((f) => ({ ...f, pinned: !f.pinned }))}
|
|
label="置顶公告"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="j13-compose-section j13-compose-section-writing" aria-label="标题与正文">
|
|
<div className="flex items-center justify-between gap-2 mb-3">
|
|
<h2 className="j13-compose-section-title !mb-0">标题与正文</h2>
|
|
</div>
|
|
<input
|
|
className="j13-compose-title"
|
|
maxLength={200}
|
|
value={form.title}
|
|
onChange={(e) => patchForm({ title: e.target.value })}
|
|
placeholder="请输入公告标题"
|
|
aria-label="公告标题"
|
|
/>
|
|
<div className="j13-compose-canvas">
|
|
<MarkdownEditor
|
|
value={form.content}
|
|
onChange={(content) => patchForm({ content })}
|
|
allowHide
|
|
allowReplyHide={false}
|
|
placeholder="请输入公告正文,支持 Markdown…"
|
|
rows={18}
|
|
onSubmitShortcut={() => void save()}
|
|
/>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<div className="j13-compose-actions">
|
|
<div className="j13-compose-actions-end">
|
|
<Link href="/admin/announcements" className="btn btn-line">
|
|
取消
|
|
</Link>
|
|
<button
|
|
type="button"
|
|
className="btn btn-primary"
|
|
disabled={saving}
|
|
onClick={() => void save()}
|
|
>
|
|
{saving ? (
|
|
<>
|
|
<Loader2 size={15} className="animate-spin" /> 保存中…
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save size={15} /> {isEdit ? "保存修改" : form.published ? "发布公告" : "保存草稿"}
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|