import { useCallback, useEffect, useState } from 'react' import { Alert, Button, Card, Checkbox, Empty, Form, Input, InputNumber, Modal, Popconfirm, Radio, Select, Space, Table, Tag, Typography, message, } from 'antd' import { BellOutlined, DeleteOutlined, EditOutlined, PlusOutlined, PushpinOutlined, ReloadOutlined, WarningOutlined, } from '@ant-design/icons' import { createSiteAnnouncement, deleteSiteAnnouncement, updateSiteAnnouncement, updateSiteAnnouncementStatus, updateSiteBranding, listSiteAnnouncements, listSites, getMe, type Site, type SiteAnnouncement, type SiteAnnouncementPayload, } from '../api' import { useMember } from '../MemberContext' const CATEGORY_OPTIONS = [ { value: 'notice', label: '通知公告' }, { value: 'tutorial', label: '使用教程' }, { value: 'rule', label: '规则说明' }, { value: 'faq', label: '常见问题' }, ] const STATUS_OPTIONS = [ { value: '', label: '全部状态' }, { value: 'draft', label: '草稿' }, { value: 'published', label: '已发布' }, { value: 'archived', label: '已归档' }, ] const STATUS_META: Record = { draft: { label: '草稿', color: 'default' }, published: { label: '已发布', color: 'green' }, archived: { label: '已归档', color: 'orange' }, } const EMPTY_FORM: SiteAnnouncementPayload = { title: '', content: '', category: 'notice', priority: 0, is_pinned: false, is_important: false, } function formatTime(value?: string | null) { if (!value) return '-' const date = new Date(value) if (Number.isNaN(date.getTime())) return value const pad = (n: number) => String(n).padStart(2, '0') return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}` } /** * 分站公告中心:站长自助维护本站公告。 * * 展示来源由本站 announcement_mode 控制: * inherit(默认)= 主站公告 + 本站公告;own_only = 仅本站公告。 */ export default function Announcements() { const member = useMember() const [siteID, setSiteID] = useState(member?.site_id) const [sites, setSites] = useState([]) const [announcementMode, setAnnouncementMode] = useState<'inherit' | 'own_only'>('inherit') const [modeSaving, setModeSaving] = useState(false) const [items, setItems] = useState([]) const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) const [status, setStatus] = useState('') const [loading, setLoading] = useState(false) const [modalOpen, setModalOpen] = useState(false) const [editing, setEditing] = useState(null) const [saving, setSaving] = useState(false) const [form] = Form.useForm() const load = useCallback( async (targetSiteID?: number, nextPage = page, nextPageSize = pageSize, nextStatus = status) => { if (!targetSiteID) return setLoading(true) try { const result = await listSiteAnnouncements(targetSiteID, { status: nextStatus || undefined, page: nextPage, page_size: nextPageSize, }) setItems(result.items) setTotal(result.total) } catch { message.error('公告加载失败') } finally { setLoading(false) } }, [page, pageSize, status], ) useEffect(() => { async function init() { let currentMember = member if (!currentMember) { try { currentMember = await getMe() } catch { // ignore } } const activeSiteID = currentMember?.site_id if (!activeSiteID) return setSiteID(activeSiteID) try { const rows = await listSites() setSites(rows) const current = rows.find(row => row.id === activeSiteID) setAnnouncementMode(current?.announcement_mode === 'own_only' ? 'own_only' : 'inherit') } catch { // 站点信息读取失败时保留默认口径 } void load(activeSiteID, 1, pageSize, '') } void init() // eslint-disable-next-line react-hooks/exhaustive-deps }, [member]) async function handleModeChange(next: 'inherit' | 'own_only') { if (!siteID || next === announcementMode) return setModeSaving(true) try { await updateSiteBranding(siteID, { announcement_mode: next }) setAnnouncementMode(next) message.success(next === 'own_only' ? '已切换为「仅本站公告」' : '已切换为「继承主站 + 本站公告」') } catch { message.error('公告来源切换失败') } finally { setModeSaving(false) } } function openCreate() { setEditing(null) form.setFieldsValue({ ...EMPTY_FORM }) setModalOpen(true) } function openEdit(row: SiteAnnouncement) { setEditing(row) form.setFieldsValue({ title: row.title, content: row.content, category: row.category, priority: row.priority, is_pinned: row.is_pinned, is_important: row.is_important, }) setModalOpen(true) } async function handleSubmit() { if (!siteID) return const values = await form.validateFields() setSaving(true) try { if (editing) { await updateSiteAnnouncement(siteID, editing.id, values) message.success('公告已保存') } else { await createSiteAnnouncement(siteID, values) message.success('公告已创建为草稿,发布后才会在前台展示') } setModalOpen(false) void load(siteID) } catch { message.error('公告保存失败') } finally { setSaving(false) } } async function handleStatusAction(row: SiteAnnouncement, action: 'publish' | 'archive' | 'unarchive') { if (!siteID) return try { await updateSiteAnnouncementStatus(siteID, row.id, action) message.success(action === 'publish' ? '公告已发布' : action === 'archive' ? '公告已归档' : '公告已取消归档') void load(siteID) } catch { message.error('操作失败') } } async function handleDelete(row: SiteAnnouncement) { if (!siteID) return try { await deleteSiteAnnouncement(siteID, row.id) message.success('公告已删除') void load(siteID) } catch { message.error('删除失败') } } return ( 公告中心 维护本站公告(列表 / 详情页展示)。公告发布后立即在本站前台生效。 公告来源 void handleModeChange(e.target.value)} disabled={modeSaving} > 继承主站 + 本站公告 仅本站公告 「继承」会同时展示主站公告与本站公告;「仅本站」只展示本站公告,主站公告不再下发(本站未发布公告时公告中心为空)。