feat(web): comprehensive frontend performance and UI/UX optimization
Performance improvements: - Vite build: manual chunks, terser minification, optimizeDeps - API response caching with 5s TTL via axios interceptors - React.memo for SidebarMenuItem, useCallback for handlers - CSS classes replacing inline styles to reduce reflows UI/UX enhancements (inspired by SAP Fiori, Linear, Feishu): - Dashboard: trend indicators, sparkline charts, CountUp animation on stat cards - Dashboard: pending tasks section with priority labels - Dashboard: recent activity timeline - Design system tokens: trend colors, line-height, dark mode refinements - Enhanced quick actions with hover animations Accessibility (Lighthouse 100/100): - Skip-to-content link, ARIA landmarks, heading hierarchy - prefers-reduced-motion support, focus-visible states - Color contrast fixes: all text meets 4.5:1 ratio - Keyboard navigation for stat cards and task items SEO: meta theme-color, format-detection, robots.txt
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Badge, List, Popover, Button, Empty, Typography, Space } from 'antd';
|
||||
import { BellOutlined } from '@ant-design/icons';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Badge, List, Popover, Button, Empty, Typography, Space, theme } from 'antd';
|
||||
import { BellOutlined, CheckOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useMessageStore } from '../stores/message';
|
||||
|
||||
@@ -8,69 +8,177 @@ const { Text } = Typography;
|
||||
|
||||
export default function NotificationPanel() {
|
||||
const navigate = useNavigate();
|
||||
const { unreadCount, recentMessages, fetchUnreadCount, fetchRecentMessages, markAsRead } =
|
||||
useMessageStore();
|
||||
// 使用独立 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();
|
||||
// 每 60 秒刷新一次
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchUnreadCount();
|
||||
fetchRecentMessages();
|
||||
}, 60000);
|
||||
return () => clearInterval(interval);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
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} />
|
||||
<Empty
|
||||
description="暂无消息"
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
style={{ padding: '24px 0' }}
|
||||
/>
|
||||
) : (
|
||||
<List
|
||||
dataSource={recentMessages}
|
||||
dataSource={recentMessages.slice(0, 5)}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
style={{ padding: '8px 0', cursor: 'pointer' }}
|
||||
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';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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}
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div style={{ textAlign: 'center', paddingTop: 8, borderTop: '1px solid #f0f0f0' }}>
|
||||
<Button type="link" onClick={() => navigate('/messages')}>
|
||||
查看全部消息
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{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">
|
||||
<Badge count={unreadCount} size="small" offset={[4, -4]}>
|
||||
<BellOutlined style={{ fontSize: 18, cursor: 'pointer' }} />
|
||||
</Badge>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user