feat(web): NotificationPanel 增加待办预览区域
- 底部新增"待办事项"区域,显示最近 3 条 pending 行动项 - 角标数字改为 unreadCount + pendingActionCount - 点击待办项跳转 /health/action-inbox
This commit is contained in:
@@ -1,37 +1,47 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Badge, List, Popover, Button, Empty, Typography } from 'antd';
|
import { Badge, Divider, List, Popover, Button, Empty, Typography } from 'antd';
|
||||||
import { BellOutlined } from '@ant-design/icons';
|
import { BellOutlined } from '@ant-design/icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useMessageStore } from '../stores/message';
|
import { useMessageStore } from '../stores/message';
|
||||||
import { useThemeMode } from '../hooks/useThemeMode';
|
import { useThemeMode } from '../hooks/useThemeMode';
|
||||||
|
import { actionInboxApi, type ActionItem } from '../api/health/actionInbox';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
export default function NotificationPanel() {
|
export default function NotificationPanel() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
// 使用独立 selector:数据订阅和函数引用分离,避免 effect 重复触发
|
|
||||||
const unreadCount = useMessageStore((s) => s.unreadCount);
|
const unreadCount = useMessageStore((s) => s.unreadCount);
|
||||||
const recentMessages = useMessageStore((s) => s.recentMessages);
|
const recentMessages = useMessageStore((s) => s.recentMessages);
|
||||||
const markAsRead = useMessageStore((s) => s.markAsRead);
|
const markAsRead = useMessageStore((s) => s.markAsRead);
|
||||||
const isDark = useThemeMode();
|
const isDark = useThemeMode();
|
||||||
const initializedRef = useRef(false);
|
const initializedRef = useRef(false);
|
||||||
|
|
||||||
|
const [pendingActions, setPendingActions] = useState<ActionItem[]>([]);
|
||||||
|
|
||||||
|
const fetchPendingActions = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const resp = await actionInboxApi.list({ status: 'pending', page_size: 3 });
|
||||||
|
setPendingActions(resp.data);
|
||||||
|
} catch {
|
||||||
|
// 静默失败,不影响通知面板
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 防止 StrictMode 双重 mount 和路由切换导致的重复初始化
|
|
||||||
if (initializedRef.current) return;
|
if (initializedRef.current) return;
|
||||||
initializedRef.current = true;
|
initializedRef.current = true;
|
||||||
|
|
||||||
const { fetchUnreadCount, fetchRecentMessages, connectSSE } = useMessageStore.getState();
|
const { fetchUnreadCount, fetchRecentMessages, connectSSE } = useMessageStore.getState();
|
||||||
fetchUnreadCount();
|
fetchUnreadCount();
|
||||||
fetchRecentMessages();
|
fetchRecentMessages();
|
||||||
|
fetchPendingActions();
|
||||||
|
|
||||||
// SSE 实时推送,收到消息即刷新
|
|
||||||
const disconnectSSE = connectSSE();
|
const disconnectSSE = connectSSE();
|
||||||
|
|
||||||
// 降级轮询(SSE 断开时兜底)
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
fetchUnreadCount();
|
fetchUnreadCount();
|
||||||
fetchRecentMessages();
|
fetchRecentMessages();
|
||||||
|
fetchPendingActions();
|
||||||
}, 60000);
|
}, 60000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -39,7 +49,9 @@ export default function NotificationPanel() {
|
|||||||
disconnectSSE();
|
disconnectSSE();
|
||||||
initializedRef.current = false;
|
initializedRef.current = false;
|
||||||
};
|
};
|
||||||
}, []);
|
}, [fetchPendingActions]);
|
||||||
|
|
||||||
|
const totalBadge = unreadCount + pendingActions.length;
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div style={{ width: 360 }}>
|
<div style={{ width: 360 }}>
|
||||||
@@ -149,6 +161,52 @@ export default function NotificationPanel() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 待办预览区域 */}
|
||||||
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
|
<div style={{ padding: '4px 0' }}>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 4,
|
||||||
|
}}>
|
||||||
|
<Text strong style={{ fontSize: 13 }}>待办事项</Text>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
onClick={() => navigate('/health/action-inbox')}
|
||||||
|
style={{ fontSize: 12, padding: 0 }}
|
||||||
|
>
|
||||||
|
查看全部
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{pendingActions.length === 0 ? (
|
||||||
|
<Text type="secondary" style={{ fontSize: 12, display: 'block', textAlign: 'center', padding: '8px 0' }}>
|
||||||
|
暂无待办
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<List
|
||||||
|
size="small"
|
||||||
|
dataSource={pendingActions}
|
||||||
|
renderItem={(item) => (
|
||||||
|
<List.Item
|
||||||
|
style={{ padding: '4px 0', cursor: 'pointer', border: 'none' }}
|
||||||
|
onClick={() => navigate('/health/action-inbox')}
|
||||||
|
>
|
||||||
|
<List.Item.Meta
|
||||||
|
title={<Text style={{ fontSize: 12 }}>{item.title}</Text>}
|
||||||
|
description={
|
||||||
|
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||||
|
{item.patient_name}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -178,7 +236,7 @@ export default function NotificationPanel() {
|
|||||||
e.currentTarget.style.background = 'transparent';
|
e.currentTarget.style.background = 'transparent';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Badge count={unreadCount} size="small" offset={[4, -4]}>
|
<Badge count={totalBadge} size="small" offset={[4, -4]}>
|
||||||
<BellOutlined style={{
|
<BellOutlined style={{
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: isDark ? '#94a3b8' : '#475569',
|
color: isDark ? '#94a3b8' : '#475569',
|
||||||
|
|||||||
Reference in New Issue
Block a user