feat(message): add message center module (Phase 5)
Implement the complete message center with: - Database migrations for message_templates, messages, message_subscriptions tables - erp-message crate with entities, DTOs, services, handlers - Message CRUD, send, read/unread tracking, soft delete - Template management with variable interpolation - Subscription preferences with DND support - Frontend: messages page, notification panel, unread count badge - Server integration with module registration and routing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
166
apps/web/src/pages/messages/NotificationList.tsx
Normal file
166
apps/web/src/pages/messages/NotificationList.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Table, Button, Tag, Space, Modal, Typography, message } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { listMessages, markRead, markAllRead, deleteMessage, type MessageInfo, type MessageQuery } from '../../api/messages';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
interface Props {
|
||||
queryFilter?: MessageQuery;
|
||||
}
|
||||
|
||||
export default function NotificationList({ queryFilter }: Props) {
|
||||
const [data, setData] = useState<MessageInfo[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchData = async (p = page) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await listMessages({ page: p, page_size: 20, ...queryFilter });
|
||||
setData(result.data);
|
||||
setTotal(result.total);
|
||||
} catch {
|
||||
message.error('加载消息列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(1);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [queryFilter]);
|
||||
|
||||
const handleMarkRead = async (id: string) => {
|
||||
try {
|
||||
await markRead(id);
|
||||
fetchData();
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleMarkAllRead = async () => {
|
||||
try {
|
||||
await markAllRead();
|
||||
fetchData();
|
||||
message.success('已全部标记为已读');
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteMessage(id);
|
||||
fetchData();
|
||||
message.success('已删除');
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const showDetail = (record: MessageInfo) => {
|
||||
Modal.info({
|
||||
title: record.title,
|
||||
width: 520,
|
||||
content: (
|
||||
<div>
|
||||
<Paragraph>{record.body}</Paragraph>
|
||||
<div style={{ marginTop: 8, color: '#999', fontSize: 12 }}>
|
||||
{record.created_at}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
if (!record.is_read) {
|
||||
handleMarkRead(record.id);
|
||||
}
|
||||
};
|
||||
|
||||
const priorityColor: Record<string, string> = {
|
||||
urgent: 'red',
|
||||
important: 'orange',
|
||||
normal: 'blue',
|
||||
};
|
||||
|
||||
const columns: ColumnsType<MessageInfo> = [
|
||||
{
|
||||
title: '标题',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
render: (text: string, record) => (
|
||||
<span style={{ fontWeight: record.is_read ? 400 : 700, cursor: 'pointer' }} onClick={() => showDetail(record)}>
|
||||
{text}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '优先级',
|
||||
dataIndex: 'priority',
|
||||
key: 'priority',
|
||||
width: 100,
|
||||
render: (p: string) => <Tag color={priorityColor[p] || 'blue'}>{p}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '发送者',
|
||||
dataIndex: 'sender_type',
|
||||
key: 'sender_type',
|
||||
width: 80,
|
||||
render: (s: string) => (s === 'system' ? '系统' : '用户'),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'is_read',
|
||||
key: 'is_read',
|
||||
width: 80,
|
||||
render: (r: boolean) => (r ? <Tag>已读</Tag> : <Tag color="processing">未读</Tag>),
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 120,
|
||||
render: (_: unknown, record) => (
|
||||
<Space>
|
||||
{!record.is_read && (
|
||||
<Button type="link" size="small" onClick={() => handleMarkRead(record.id)}>
|
||||
标记已读
|
||||
</Button>
|
||||
)}
|
||||
<Button type="link" size="small" danger onClick={() => handleDelete(record.id)}>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span>共 {total} 条消息</span>
|
||||
<Button onClick={handleMarkAllRead}>全部标记已读</Button>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
total,
|
||||
pageSize: 20,
|
||||
onChange: (p) => { setPage(p); fetchData(p); },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user