引入 Notion 风格的 DESIGN.md 设计系统文件,并全面重构前端 UI: - 主色从 Indigo (#4F46E5) 迁移到 Notion Blue (#0075de) - 页面背景从冷灰 (#F1F5F9) 迁移到暖白 (#f6f5f4) - 侧边栏从深色 (#0F172A) 迁移到白色,活跃项用蓝色指示 - 文字从 Slate 冷色迁移到暖灰系列 (Warm Gray 500/300) - 圆角从 8px 缩小到 4px(按钮/输入),8px(卡片) - 阴影改为多层超轻 Notion 风格(最大 opacity 0.05) - 字体优先使用 Inter,保留中文回退 - 暗色模式适配暖黑色调 (#191918) - 更新 27 个前端文件的硬编码颜色值
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: '#0075de' }}
|
||
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 ? '#1e1e1d' : '#f2f9ff') : 'transparent',
|
||
}}
|
||
onClick={() => {
|
||
if (!item.is_read) {
|
||
markAsRead(item.id);
|
||
}
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
if (item.is_read) {
|
||
e.currentTarget.style.background = isDark ? '#1e1e1d' : '#fafaf9';
|
||
}
|
||
}}
|
||
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: '#0075de',
|
||
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 ? '#1e1e1d' : '#f6f5f4'}`,
|
||
}}>
|
||
<Button
|
||
type="text"
|
||
onClick={() => navigate('/messages')}
|
||
style={{ fontSize: 13, color: '#0075de', 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 ? '#1e1e1d' : '#f6f5f4';
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
e.currentTarget.style.background = 'transparent';
|
||
}}
|
||
>
|
||
<Badge count={unreadCount} size="small" offset={[4, -4]}>
|
||
<BellOutlined style={{
|
||
fontSize: 16,
|
||
color: isDark ? '#a39e98' : '#615d59',
|
||
}} />
|
||
</Badge>
|
||
</div>
|
||
</Popover>
|
||
);
|
||
}
|