- 底部新增"待办事项"区域,显示最近 3 条 pending 行动项 - 角标数字改为 unreadCount + pendingActionCount - 点击待办项跳转 /health/action-inbox
249 lines
7.4 KiB
TypeScript
249 lines
7.4 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { Badge, Divider, List, Popover, Button, Empty, Typography } from 'antd';
|
|
import { BellOutlined } from '@ant-design/icons';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useMessageStore } from '../stores/message';
|
|
import { useThemeMode } from '../hooks/useThemeMode';
|
|
import { actionInboxApi, type ActionItem } from '../api/health/actionInbox';
|
|
|
|
const { Text } = Typography;
|
|
|
|
export default function NotificationPanel() {
|
|
const navigate = useNavigate();
|
|
const unreadCount = useMessageStore((s) => s.unreadCount);
|
|
const recentMessages = useMessageStore((s) => s.recentMessages);
|
|
const markAsRead = useMessageStore((s) => s.markAsRead);
|
|
const isDark = useThemeMode();
|
|
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(() => {
|
|
if (initializedRef.current) return;
|
|
initializedRef.current = true;
|
|
|
|
const { fetchUnreadCount, fetchRecentMessages, connectSSE } = useMessageStore.getState();
|
|
fetchUnreadCount();
|
|
fetchRecentMessages();
|
|
fetchPendingActions();
|
|
|
|
const disconnectSSE = connectSSE();
|
|
|
|
const interval = setInterval(() => {
|
|
fetchUnreadCount();
|
|
fetchRecentMessages();
|
|
fetchPendingActions();
|
|
}, 60000);
|
|
|
|
return () => {
|
|
clearInterval(interval);
|
|
disconnectSSE();
|
|
initializedRef.current = false;
|
|
};
|
|
}, [fetchPendingActions]);
|
|
|
|
const totalBadge = unreadCount + pendingActions.length;
|
|
|
|
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: '#2563eb' }}
|
|
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 ? '#0f172a' : '#eff6ff') : 'transparent',
|
|
}}
|
|
onClick={() => {
|
|
if (!item.is_read) {
|
|
markAsRead(item.id);
|
|
}
|
|
navigate('/messages');
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
if (item.is_read) {
|
|
e.currentTarget.style.background = isDark ? '#0f172a' : '#f1f5f9';
|
|
}
|
|
}}
|
|
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: '#2563eb',
|
|
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 ? '#0f172a' : '#f8fafc'}`,
|
|
}}>
|
|
<Button
|
|
type="text"
|
|
onClick={() => navigate('/messages')}
|
|
style={{ fontSize: 13, color: '#2563eb', fontWeight: 500 }}
|
|
>
|
|
查看全部消息
|
|
</Button>
|
|
</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>
|
|
);
|
|
|
|
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 ? '#0f172a' : '#f8fafc';
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.background = 'transparent';
|
|
}}
|
|
>
|
|
<Badge count={totalBadge} size="small" offset={[4, -4]}>
|
|
<BellOutlined style={{
|
|
fontSize: 16,
|
|
color: isDark ? '#94a3b8' : '#475569',
|
|
}} />
|
|
</Badge>
|
|
</div>
|
|
</Popover>
|
|
);
|
|
}
|