Files
erp/apps/web/src/components/NotificationPanel.tsx
iven 9568dd7875 chore: apply cargo fmt across workspace and update docs
- Run cargo fmt on all Rust crates for consistent formatting
- Update CLAUDE.md with WASM plugin commands and dev.ps1 instructions
- Update wiki: add WASM plugin architecture, rewrite dev environment docs
- Minor frontend cleanup (unused imports)
2026-04-15 00:49:20 +08:00

185 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useRef } from 'react';
import { Badge, List, Popover, Button, Empty, Typography, theme } 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();
// 使用独立 selector数据订阅和函数引用分离避免 effect 重复触发
const unreadCount = useMessageStore((s) => s.unreadCount);
const recentMessages = useMessageStore((s) => s.recentMessages);
const markAsRead = useMessageStore((s) => s.markAsRead);
const { token } = theme.useToken();
const isDark = token.colorBgContainer === '#111827' || token.colorBgContainer === 'rgb(17, 24, 39)';
const initializedRef = useRef(false);
useEffect(() => {
// 防止 StrictMode 双重 mount 和路由切换导致的重复初始化
if (initializedRef.current) return;
initializedRef.current = true;
const { fetchUnreadCount, fetchRecentMessages } = useMessageStore.getState();
fetchUnreadCount();
fetchRecentMessages();
const interval = setInterval(() => {
fetchUnreadCount();
fetchRecentMessages();
}, 60000);
return () => {
clearInterval(interval);
initializedRef.current = false;
};
}, []);
const content = (
<div style={{ width: 360 }}>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 12,
padding: '4px 0',
}}>
<span style={{ fontWeight: 600, fontSize: 14 }}></span>
{unreadCount > 0 && (
<Button
type="text"
size="small"
style={{ fontSize: 12, color: '#4F46E5' }}
onClick={() => navigate('/messages')}
>
</Button>
)}
</div>
{recentMessages.length === 0 ? (
<Empty
description="暂无消息"
image={Empty.PRESENTED_IMAGE_SIMPLE}
style={{ padding: '24px 0' }}
/>
) : (
<List
dataSource={recentMessages.slice(0, 5)}
renderItem={(item) => (
<List.Item
style={{
padding: '10px 12px',
margin: '2px 0',
borderRadius: 8,
cursor: 'pointer',
transition: 'background 0.15s ease',
border: 'none',
background: !item.is_read ? (isDark ? '#1E293B' : '#F5F3FF') : 'transparent',
}}
onClick={() => {
if (!item.is_read) {
markAsRead(item.id);
}
}}
onMouseEnter={(e) => {
if (item.is_read) {
e.currentTarget.style.background = isDark ? '#1E293B' : '#F8FAFC';
}
}}
onMouseLeave={(e) => {
if (item.is_read) {
e.currentTarget.style.background = 'transparent';
}
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Text
strong={!item.is_read}
ellipsis
style={{ maxWidth: 260, fontSize: 13 }}
>
{item.title}
</Text>
{!item.is_read && (
<span style={{
display: 'inline-block',
width: 6,
height: 6,
borderRadius: '50%',
background: '#4F46E5',
flexShrink: 0,
}} />
)}
</div>
<Text
type="secondary"
ellipsis
style={{ maxWidth: 300, fontSize: 12, display: 'block', marginTop: 2 }}
>
{item.body}
</Text>
</div>
</List.Item>
)}
/>
)}
{recentMessages.length > 0 && (
<div style={{
textAlign: 'center',
paddingTop: 8,
marginTop: 4,
borderTop: `1px solid ${isDark ? '#1E293B' : '#F1F5F9'}`,
}}>
<Button
type="text"
onClick={() => navigate('/messages')}
style={{ fontSize: 13, color: '#4F46E5', fontWeight: 500 }}
>
</Button>
</div>
)}
</div>
);
return (
<Popover
content={content}
trigger="click"
placement="bottomRight"
overlayStyle={{ padding: 0 }}
>
<div
style={{
width: 36,
height: 36,
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
transition: 'all 0.15s ease',
position: 'relative',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = isDark ? '#1E293B' : '#F1F5F9';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
}}
>
<Badge count={unreadCount} size="small" offset={[4, -4]}>
<BellOutlined style={{
fontSize: 16,
color: isDark ? '#94A3B8' : '#64748B',
}} />
</Badge>
</div>
</Popover>
);
}