关联 Issue: P25-2, P25-3, P25-4, P26-2, P26-4, P28-1, P28-2, P28-3, P28-4, P24-2, P29-3
85 行
4.2 KiB
TypeScript
85 行
4.2 KiB
TypeScript
import { useState } from "react";
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { archiveAnnouncement, createAnnouncement, fetchAnnouncements, updateAnnouncement } from "../api/announcements";
|
|
import { PageHeader } from "../components/common/PageHeader";
|
|
import { Button } from "../components/ui/button";
|
|
import { Input } from "../components/ui/input";
|
|
import { Label } from "../components/ui/label";
|
|
import { toast } from "sonner";
|
|
|
|
export function PortalAnnouncementsPage() {
|
|
const queryClient = useQueryClient();
|
|
const [title, setTitle] = useState("");
|
|
const [summary, setSummary] = useState("");
|
|
const [linkUrl, setLinkUrl] = useState("");
|
|
const [priority, setPriority] = useState("100");
|
|
|
|
const listQuery = useQuery({ queryKey: ["announcements", "manage"], queryFn: () => fetchAnnouncements(true) });
|
|
const refresh = () => queryClient.invalidateQueries({ queryKey: ["announcements"] });
|
|
|
|
const createMutation = useMutation({
|
|
mutationFn: () => createAnnouncement({ title, summary, linkUrl: linkUrl || null, priority: Number(priority || 100) }),
|
|
onSuccess: async () => {
|
|
setTitle("");
|
|
setSummary("");
|
|
setLinkUrl("");
|
|
setPriority("100");
|
|
await refresh();
|
|
toast.success("公告已创建");
|
|
},
|
|
});
|
|
|
|
const archiveMutation = useMutation({
|
|
mutationFn: archiveAnnouncement,
|
|
onSuccess: async () => {
|
|
await refresh();
|
|
toast.success("公告已归档");
|
|
},
|
|
});
|
|
|
|
const toggleMutation = useMutation({
|
|
mutationFn: ({ id, enabled }: { id: string; enabled: boolean }) => updateAnnouncement(id, { enabled }),
|
|
onSuccess: async () => {
|
|
await refresh();
|
|
toast.success("公告状态已更新");
|
|
},
|
|
});
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<PageHeader title="公告管理" subtitle="轻量发布公告、制度链接和 FAQ 入口。" />
|
|
<div className="grid gap-6 xl:grid-cols-[360px,1fr]">
|
|
<div className="card-std space-y-4 p-6">
|
|
<h3 className="text-base font-semibold text-zinc-900">新建公告</h3>
|
|
<div className="space-y-2"><Label>标题</Label><Input value={title} onChange={(event) => setTitle(event.target.value)} /></div>
|
|
<div className="space-y-2"><Label>摘要</Label><textarea className="min-h-28 w-full rounded-lg border border-zinc-300 px-3 py-2 text-sm" value={summary} onChange={(event) => setSummary(event.target.value)} /></div>
|
|
<div className="space-y-2"><Label>链接</Label><Input value={linkUrl} onChange={(event) => setLinkUrl(event.target.value)} /></div>
|
|
<div className="space-y-2"><Label>优先级</Label><Input value={priority} onChange={(event) => setPriority(event.target.value)} /></div>
|
|
<Button type="button" disabled={createMutation.isPending} onClick={() => createMutation.mutate()}>{createMutation.isPending ? "创建中..." : "创建公告"}</Button>
|
|
</div>
|
|
<div className="card-std space-y-4 p-6">
|
|
<h3 className="text-base font-semibold text-zinc-900">公告列表</h3>
|
|
<div className="space-y-3">
|
|
{(listQuery.data?.items ?? []).map((item) => (
|
|
<div key={item.id} className="rounded-lg border border-zinc-200 p-4">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div>
|
|
<div className="font-medium text-zinc-900">{item.title}</div>
|
|
<div className="mt-1 text-sm text-zinc-600">{item.summary}</div>
|
|
<div className="mt-1 text-xs text-zinc-500">priority={item.priority} · {item.enabled ? "enabled" : "disabled"}</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button type="button" variant="secondary" onClick={() => toggleMutation.mutate({ id: item.id, enabled: !item.enabled })}>{item.enabled ? "停用" : "启用"}</Button>
|
|
<Button type="button" variant="secondary" onClick={() => archiveMutation.mutate(item.id)}>归档</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{!listQuery.data?.items?.length ? <div className="text-sm text-zinc-500">暂无公告</div> : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|