feat: 添加全员公告发布页面,支持撰写和广播系统通知并查看历史记录。

This commit is contained in:
2026-01-29 13:54:13 +08:00
parent a8dc4d2f06
commit 9c901447af

View File

@@ -1,15 +1,44 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Form, Input, Select, Button, message, Card, Alert, Row, Col } from 'antd'; import { Form, Input, Select, Button, message, Card, Alert, Row, Col, Empty, Spin } from 'antd';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import request from '../../utils/request'; import request from '../../utils/request';
const { TextArea } = Input; const { TextArea } = Input;
const { Option } = Select; const { Option } = Select;
interface Announcement {
id: number;
title: string;
content: string;
type: string;
sent_count: number;
created_at: string;
}
const NotificationPublish: React.FC = () => { const NotificationPublish: React.FC = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [historyLoading, setHistoryLoading] = useState(false);
const [history, setHistory] = useState<Announcement[]>([]);
const [form] = Form.useForm(); const [form] = Form.useForm();
const fetchHistory = async () => {
setHistoryLoading(true);
try {
const res: any = await request.get('/notifications/announcements?limit=5');
if (res && res.announcements) {
setHistory(res.announcements);
}
} catch (error) {
console.error('获取历史记录失败:', error);
} finally {
setHistoryLoading(false);
}
};
useEffect(() => {
fetchHistory();
}, []);
const onFinish = async (values: any) => { const onFinish = async (values: any) => {
setLoading(true); setLoading(true);
try { try {
@@ -19,6 +48,7 @@ const NotificationPublish: React.FC = () => {
style: { marginTop: '10vh' } style: { marginTop: '10vh' }
}); });
form.resetFields(); form.resetFields();
fetchHistory(); // Refresh history after publish
} catch (error) { } catch (error) {
console.error('发布失败:', error); console.error('发布失败:', error);
} finally { } finally {
@@ -26,6 +56,16 @@ const NotificationPublish: React.FC = () => {
} }
}; };
const formatTime = (timeStr: string) => {
const date = new Date(timeStr);
return date.toLocaleString('zh-CN', {
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
return ( return (
<div style={{ maxWidth: 1000, margin: '0 auto' }}> <div style={{ maxWidth: 1000, margin: '0 auto' }}>
<div style={{ marginBottom: 32 }}> <div style={{ marginBottom: 32 }}>
@@ -113,7 +153,7 @@ const NotificationPublish: React.FC = () => {
<Col lg={10}> <Col lg={10}>
<Alert <Alert
title="安全提示" title="安全提示"
description="全员公告将穿透所有用户的勿扰模式(如果适用),发送前请仔细检查文案拼写和逻辑,发布后无法撤回。" description="全员公告将穿透所有用户的勿扰模式,发布后无法撤回,请反复通过侧边栏历史核对文案。"
type="info" type="info"
showIcon showIcon
icon={<Icon icon="solar:info-circle-bold-duotone" width="24" />} icon={<Icon icon="solar:info-circle-bold-duotone" width="24" />}
@@ -127,36 +167,43 @@ const NotificationPublish: React.FC = () => {
<span></span> <span></span>
</div> </div>
} }
extra={<Button type="link" size="small" onClick={fetchHistory} loading={historyLoading}></Button>}
variant="borderless" variant="borderless"
> >
{[ <Spin spinning={historyLoading}>
{ title: '系统性能优化完成', time: '1小时前', type: 'info' }, {history.length === 0 ? (
{ title: '夜间数据库升级', time: '昨天', type: 'alert' }, <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无发布记录" />
{ title: '欢迎新用户加入', time: '3天前', type: 'system' } ) : (
].map((item, idx) => ( history.map((item, idx) => (
<div key={idx} style={{ <div key={item.id} style={{
padding: '12px 0', padding: '12px 0',
borderBottom: idx === 2 ? 'none' : '1px solid #f1f5f9', borderBottom: idx === history.length - 1 ? 'none' : '1px solid #f1f5f9',
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center' alignItems: 'center'
}}> }}>
<div> <div style={{ overflow: 'hidden', marginRight: 12 }}>
<div style={{ fontWeight: 500, fontSize: 14 }}>{item.title}</div> <div style={{ fontWeight: 500, fontSize: 14, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<div style={{ fontSize: 12, color: '#94a3b8' }}>{item.time}</div> {item.title}
</div> </div>
<div style={{ <div style={{ fontSize: 12, color: '#94a3b8' }}>
padding: '2px 8px', {formatTime(item.created_at)} · {item.sent_count}
borderRadius: 4, </div>
fontSize: 11, </div>
background: item.type === 'alert' ? '#fee2e2' : item.type === 'info' ? '#dcfce7' : '#e0e7ff', <div style={{
color: item.type === 'alert' ? '#ef4444' : item.type === 'info' ? '#10b981' : '#6366f1' padding: '2px 8px',
}}> borderRadius: 4,
{item.type.toUpperCase()} fontSize: 10,
</div> flexShrink: 0,
</div> background: item.type === 'alert' ? '#fee2e2' : item.type === 'info' ? '#dcfce7' : '#e0e7ff',
))} color: item.type === 'alert' ? '#ef4444' : item.type === 'info' ? '#10b981' : '#6366f1'
<Button type="link" block style={{ marginTop: 12 }}></Button> }}>
{item.type.toUpperCase()}
</div>
</div>
))
)}
</Spin>
</Card> </Card>
</Col> </Col>
</Row> </Row>