import { App, Button, Form, Input, Modal, Select, Segmented, Switch, Tag } from "antd"; import type { FormInstance } from "antd"; import type { ColumnsType } from "antd/es/table"; import { Eye, Pencil, Pin, Plus, Search, Upload, X } from "lucide-react"; import { useCallback, useEffect, useRef, useState, type ChangeEvent } from "react"; import { PaginationBar } from "@/components/layout/workspace-page"; import { AnnouncementContent } from "@/components/ui/announcement-content"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { announcementImageUrl, closeAdminAnnouncement, createAdminAnnouncement, discardAdminAnnouncementImage, listAdminAnnouncements, uploadAdminAnnouncementImage, updateAdminAnnouncement, type AnnouncementLevel, type AnnouncementStatus, type SystemAnnouncement, } from "@/services/api/announcements"; import { resourceFileUrl } from "@/services/api/resources"; import { AdminDataTable, AdminFilterChip, AdminRowActions, AdminStatusBadge, AdminTableEmpty } from "./admin-ui"; type AnnouncementFormValues = { title: string; content: string; imageResourceId?: string; level: AnnouncementLevel; pinned: boolean; }; const levelOptions: Array<{ value: AnnouncementLevel; label: string }> = [ { value: "info", label: "平台通知" }, { value: "success", label: "状态恢复" }, { value: "warning", label: "服务提醒" }, { value: "critical", label: "重要通知" }, ]; const levelMeta: Record = { info: { label: "平台通知", tone: "info" }, success: { label: "状态恢复", tone: "success" }, warning: { label: "服务提醒", tone: "warning" }, critical: { label: "重要通知", tone: "error" }, }; export default function AdminAnnouncementsPanel() { const { message } = App.useApp(); const [form] = Form.useForm(); const [announcements, setAnnouncements] = useState([]); const [keyword, setKeyword] = useState(""); const debouncedKeyword = useDebouncedValue(keyword); const [status, setStatus] = useState<"all" | AnnouncementStatus>("all"); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); const [modalOpen, setModalOpen] = useState(false); const [editingAnnouncement, setEditingAnnouncement] = useState(null); const [publishing, setPublishing] = useState(false); const [closingId, setClosingId] = useState(""); const [imagePreviewUrl, setImagePreviewUrl] = useState(""); const [draftImageResourceId, setDraftImageResourceId] = useState(""); const [imageUploading, setImageUploading] = useState(false); const [editorMode, setEditorMode] = useState<"edit" | "preview">("edit"); const imageInputRef = useRef(null); const reload = useCallback(async () => { setLoading(true); try { const data = await listAdminAnnouncements({ keyword: debouncedKeyword || undefined, status: status === "all" ? undefined : status, page, limit: pageSize }); setAnnouncements(data.announcements || []); setTotal(data.total || 0); } catch (error) { message.error(error instanceof Error ? error.message : "读取公告列表失败"); } finally { setLoading(false); } }, [debouncedKeyword, message, page, pageSize, status]); useEffect(() => { void reload(); }, [reload]); const openPublishModal = () => { setEditingAnnouncement(null); form.setFieldsValue({ title: "", content: "", imageResourceId: "", level: "info", pinned: false }); setImagePreviewUrl(""); setDraftImageResourceId(""); setEditorMode("edit"); setModalOpen(true); }; const openEditModal = (announcement: SystemAnnouncement) => { setEditingAnnouncement(announcement); form.setFieldsValue({ title: announcement.title, content: announcement.content, imageResourceId: announcement.imageResourceId || "", level: announcement.level, pinned: announcement.pinned }); setImagePreviewUrl(announcementImageUrl(announcement)); setDraftImageResourceId(""); setEditorMode("edit"); setModalOpen(true); }; const discardDraftImage = async () => { if (!draftImageResourceId) return; await discardAdminAnnouncementImage(draftImageResourceId); setDraftImageResourceId(""); }; const uploadAnnouncementImage = async (event: ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ""; if (!file) return; if (!file.type.startsWith("image/")) { message.warning("公告配图必须是图片文件"); return; } if (file.size > 10 * 1024 * 1024) { message.warning("公告配图不能超过 10MB"); return; } setImageUploading(true); try { if (draftImageResourceId) await discardDraftImage(); const { resource } = await uploadAdminAnnouncementImage(file); form.setFieldValue("imageResourceId", resource.id); setDraftImageResourceId(resource.id); setImagePreviewUrl(resourceFileUrl(resource.id)); message.success("公告配图已上传"); } catch (error) { message.error(error instanceof Error ? error.message : "公告配图上传失败"); } finally { setImageUploading(false); } }; const clearAnnouncementImage = async () => { setImageUploading(true); try { await discardDraftImage(); form.setFieldValue("imageResourceId", ""); setImagePreviewUrl(""); } catch (error) { message.error(error instanceof Error ? error.message : "公告配图清理失败"); } finally { setImageUploading(false); } }; const closeEditor = async () => { setImageUploading(true); try { await discardDraftImage(); setModalOpen(false); setEditingAnnouncement(null); setImagePreviewUrl(""); form.resetFields(); } catch (error) { message.error(error instanceof Error ? error.message : "公告配图草稿清理失败,请重试"); } finally { setImageUploading(false); } }; const publish = async () => { const values = await form.validateFields(); setPublishing(true); try { const input = { title: values.title.trim(), content: values.content.trim(), imageResourceId: values.imageResourceId?.trim() || "", level: values.level, pinned: Boolean(values.pinned) }; if (editingAnnouncement) await updateAdminAnnouncement(editingAnnouncement.id, input); else await createAdminAnnouncement(input); setDraftImageResourceId(""); setModalOpen(false); setEditingAnnouncement(null); setPage(1); await reload(); message.success(editingAnnouncement ? "公告已更新并重新发布" : "公告已发布"); } catch (error) { message.error(error instanceof Error ? error.message : editingAnnouncement ? "更新公告失败" : "发布公告失败"); } finally { setPublishing(false); } }; const closeAnnouncement = async (announcement: SystemAnnouncement) => { setClosingId(announcement.id); try { await closeAdminAnnouncement(announcement.id); await reload(); message.success("公告已关闭"); } catch (error) { message.error(error instanceof Error ? error.message : "关闭公告失败"); } finally { setClosingId(""); } }; const columns: ColumnsType = [ { title: "公告内容", dataIndex: "title", minWidth: 360, render: (_, announcement) => (
{announcement.imageUrl ? : null}
{announcement.title}
{announcement.content}
), }, { title: "置顶", dataIndex: "pinned", width: 90, render: (pinned: boolean) => pinned ? }>置顶 : --, }, { title: "级别", dataIndex: "level", width: 120, render: (level: AnnouncementLevel) => { const meta = levelMeta[level] || levelMeta.info; return ; }, }, { title: "状态", dataIndex: "status", width: 100, render: (value: AnnouncementStatus) => , }, { title: "发布时间", dataIndex: "publishedAt", width: 170, render: formatDateTime, }, { title: "关闭时间", dataIndex: "closedAt", width: 170, render: (value?: string) => value ? formatDateTime(value) : "--", }, { title: "操作", key: "actions", width: 160, render: (_, announcement) => openEditModal(announcement) }} actions={announcement.status === "active" ? [{ key: "close", label: "关闭", danger: true, disabled: closingId === announcement.id, onClick: () => void closeAnnouncement(announcement), confirm: { title: "关闭这条公告?", description: "关闭后用户公告中心将不再展示,历史记录会保留。", okText: "关闭公告" } }] : []} />, }, ]; return ( <> } value={keyword} placeholder="搜索公告标题或正文" onChange={(event) => { setKeyword(event.target.value); setPage(1); }} />} toolbarActiveFilters={<>{keyword ? { setKeyword(""); setPage(1); }} /> : null}{status !== "all" ? { setStatus("all"); setPage(1); }} /> : null}} toolbarActive={Boolean(keyword || status !== "all")} toolbarFilters={
{imagePreviewUrl ? (
公告配图预览
) :
暂未添加配图
} void uploadAnnouncementImage(event)} />
); } function AnnouncementDraftPreview({ form, imagePreviewUrl }: { form: FormInstance; imagePreviewUrl: string }) { const title = Form.useWatch("title", form) || "未填写标题"; const content = Form.useWatch("content", form) || "暂未填写公告正文"; const level = Form.useWatch("level", form) || "info"; const pinned = Form.useWatch("pinned", form); const meta = levelMeta[level as AnnouncementLevel] || levelMeta.info; return (
{pinned ? 置顶 : null}
预览效果

{title}

{imagePreviewUrl ? 公告配图预览 : null}
); } function formatDateTime(value: string) { const date = new Date(value); if (Number.isNaN(date.getTime())) return "--"; return new Intl.DateTimeFormat("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false }).format(date).replaceAll("/", "-"); }