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:
76
apps/web/src/components/NotificationPanel.tsx
Normal file
76
apps/web/src/components/NotificationPanel.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Badge, List, Popover, Button, Empty, Typography, Space } from 'antd';
|
||||
import { BellOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useMessageStore } from '../stores/message';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export default function NotificationPanel() {
|
||||
const navigate = useNavigate();
|
||||
const { unreadCount, recentMessages, fetchUnreadCount, fetchRecentMessages, markAsRead } =
|
||||
useMessageStore();
|
||||
|
||||
useEffect(() => {
|
||||
fetchUnreadCount();
|
||||
fetchRecentMessages();
|
||||
// 每 60 秒刷新一次
|
||||
const interval = setInterval(() => {
|
||||
fetchUnreadCount();
|
||||
fetchRecentMessages();
|
||||
}, 60000);
|
||||
return () => clearInterval(interval);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const content = (
|
||||
<div style={{ width: 360 }}>
|
||||
{recentMessages.length === 0 ? (
|
||||
<Empty description="暂无消息" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
) : (
|
||||
<List
|
||||
dataSource={recentMessages}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
style={{ padding: '8px 0', cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
if (!item.is_read) {
|
||||
markAsRead(item.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={
|
||||
<Space>
|
||||
<Text strong={!item.is_read} ellipsis style={{ maxWidth: 260 }}>
|
||||
{item.title}
|
||||
</Text>
|
||||
{!item.is_read && <span style={{ display: 'inline-block', width: 6, height: 6, borderRadius: '50%', background: '#1677ff' }} />}
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<Text type="secondary" ellipsis style={{ maxWidth: 300 }}>
|
||||
{item.body}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div style={{ textAlign: 'center', paddingTop: 8, borderTop: '1px solid #f0f0f0' }}>
|
||||
<Button type="link" onClick={() => navigate('/messages')}>
|
||||
查看全部消息
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={content} trigger="click" placement="bottomRight">
|
||||
<Badge count={unreadCount} size="small" offset={[4, -4]}>
|
||||
<BellOutlined style={{ fontSize: 18, cursor: 'pointer' }} />
|
||||
</Badge>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user