- 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)
185 lines
5.5 KiB
TypeScript
185 lines
5.5 KiB
TypeScript
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>
|
||
);
|
||
}
|