1. 待办列表 UNION ALL 聚合:list_action_items 现从 ai_suggestion + alerts + follow_up_task 三表查询, ActionType 扩展为 AiSuggestion/Alert/Followup/DataAnomaly 四种类型, get_action_thread 按类型构建不同线程时间线(AI 建议/告警/随访) 2. 真实团队概览:get_team_overview 从 doctor_profile + follow_up_task + alerts 聚合成员统计和风险分布 3. 统计卡片较昨日描述:PersonalStatsResp 新增 6 个 yesterday_* 字段, Home.tsx 统计卡片底部渲染"较昨日+N"绿色/红色描述 4. 前端 ActionDetailDrawer 改用 item.id(action_type:uuid 格式)调用线程 API
439 lines
21 KiB
TypeScript
439 lines
21 KiB
TypeScript
import { useEffect, useState, useCallback } from 'react';
|
||
import { Row, Col, Spin, Empty } from 'antd';
|
||
import {
|
||
UserOutlined,
|
||
TeamOutlined,
|
||
CalendarOutlined,
|
||
HeartOutlined,
|
||
MedicineBoxOutlined,
|
||
SafetyCertificateOutlined,
|
||
MessageOutlined,
|
||
BellOutlined,
|
||
AlertOutlined,
|
||
TrophyOutlined,
|
||
ShoppingOutlined,
|
||
FileTextOutlined,
|
||
RightOutlined,
|
||
PartitionOutlined,
|
||
ClockCircleOutlined,
|
||
CheckCircleOutlined,
|
||
ThunderboltOutlined,
|
||
SettingOutlined,
|
||
ApartmentOutlined,
|
||
} from '@ant-design/icons';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { useThemeMode } from '../hooks/useThemeMode';
|
||
import { useDashboardRole, type DashboardRole } from '../hooks/useDashboardRole';
|
||
import { useMessageStore } from '../stores/message';
|
||
import { listAuditLogs, type AuditLogItem } from '../api/auditLogs';
|
||
import { listPendingTasks, type TaskInfo } from '../api/workflowTasks';
|
||
import { pointsApi, type PersonalStats } from '../api/health/points';
|
||
import { useStatsData } from './health/StatisticsDashboard/useStatsData';
|
||
import { useCountUp } from '../hooks/useCountUp';
|
||
import TodoList from './health/components/workbench/TodoList';
|
||
import AiInsightPanel from './health/components/workbench/AiInsightPanel';
|
||
import TeamOverviewPanel from './health/components/workbench/TeamOverviewPanel';
|
||
import ActionDetailDrawer from './health/components/workbench/ActionDetailDrawer';
|
||
import type { ActionItem } from '../api/health/actionInbox';
|
||
|
||
// --- Shared utilities ---
|
||
|
||
function formatTimeAgo(dateStr: string): string {
|
||
const diff = Date.now() - new Date(dateStr).getTime();
|
||
const minutes = Math.floor(diff / 60000);
|
||
if (minutes < 1) return '刚刚';
|
||
if (minutes < 60) return `${minutes} 分钟前`;
|
||
const hours = Math.floor(minutes / 60);
|
||
if (hours < 24) return `${hours} 小时前`;
|
||
const days = Math.floor(hours / 24);
|
||
return `${days} 天前`;
|
||
}
|
||
|
||
const ACTION_LABELS: Record<string, string> = {
|
||
create: '创建', created: '创建', update: '更新', updated: '更新', delete: '删除', deleted: '删除',
|
||
login: '登录', 'user.create': '创建', 'user.update': '更新', 'user.delete': '删除',
|
||
'patient.create': '创建', 'patient.update': '更新', 'appointment.create': '创建',
|
||
};
|
||
const RESOURCE_LABELS: Record<string, string> = {
|
||
user: '用户', role: '角色', patient: '患者', doctor: '医护', appointment: '预约',
|
||
follow_up_task: '随访', consultation_session: '咨询', message: '消息', plugin: '插件',
|
||
process_instance: '流程实例', organization: '组织',
|
||
};
|
||
const RESOURCE_ICONS: Record<string, React.ReactNode> = {
|
||
user: <UserOutlined />, role: <SafetyCertificateOutlined />,
|
||
patient: <UserOutlined />, organization: <ApartmentOutlined />,
|
||
process_instance: <FileTextOutlined />, message: <BellOutlined />,
|
||
};
|
||
|
||
function formatActionLabel(action: string): string {
|
||
return ACTION_LABELS[action] || ACTION_LABELS[action.split('.').pop() || ''] || action;
|
||
}
|
||
function formatResourceLabel(resource: string): string {
|
||
return RESOURCE_LABELS[resource] || RESOURCE_LABELS[resource.split('.').pop() || ''] || resource;
|
||
}
|
||
|
||
// --- Role configs ---
|
||
|
||
interface StatCardDef {
|
||
key: string;
|
||
title: string;
|
||
getValue: (p: PersonalStats | null, s: ReturnType<typeof useStatsData>) => number;
|
||
getDiff?: (p: PersonalStats | null) => number | undefined;
|
||
icon: React.ReactNode;
|
||
suffix?: string;
|
||
path: string;
|
||
}
|
||
|
||
interface QuickActionDef {
|
||
icon: React.ReactNode;
|
||
label: string;
|
||
path: string;
|
||
}
|
||
|
||
const ROLE_WELCOME: Record<DashboardRole, { title: string; subtitle: string }> = {
|
||
doctor: { title: '今日工作台', subtitle: '患者概览与待办事项' },
|
||
nurse: { title: '随访监控台', subtitle: '今日随访与体征上报' },
|
||
admin: { title: '管理中心', subtitle: '平台运营数据概览' },
|
||
operator: { title: '运营中心', subtitle: '积分、内容与活动' },
|
||
};
|
||
|
||
const STAT_BAR_COLORS: string[] = [
|
||
'linear-gradient(90deg, #2563EB, #60A5FA)',
|
||
'linear-gradient(90deg, #7C3AED, #A78BFA)',
|
||
'linear-gradient(90deg, #DC2626, #F87171)',
|
||
'linear-gradient(90deg, #D97706, #FBBF24)',
|
||
];
|
||
const STAT_TEXT_COLORS: string[] = ['#2563EB', '#7C3AED', '#DC2626', '#D97706'];
|
||
|
||
const ROLE_STATS: Record<DashboardRole, StatCardDef[]> = {
|
||
doctor: [
|
||
{ key: 'my-patients', title: '我的患者', getValue: (p) => p?.my_patients ?? 0, getDiff: (p) => { const c = p?.my_patients, y = p?.yesterday_my_patients; return c != null && y != null ? c - y : undefined; }, icon: <TeamOutlined />, path: '/health/patients' },
|
||
{ key: 'today-appointments', title: '今日预约', getValue: (p) => p?.today_appointments ?? 0, getDiff: (p) => { const c = p?.today_appointments, y = p?.yesterday_today_appointments; return c != null && y != null ? c - y : undefined; }, icon: <CalendarOutlined />, path: '/health/appointments' },
|
||
{ key: 'consultations', title: '本月咨询', getValue: (p) => p?.consultations_this_month ?? 0, getDiff: (p) => { const c = p?.consultations_this_month, y = p?.yesterday_consultations_this_month; return c != null && y != null ? c - y : undefined; }, icon: <MessageOutlined />, path: '/health/consultations' },
|
||
{ key: 'followup-rate', title: '随访完成率', getValue: (p) => p?.follow_up_rate ?? 0, icon: <HeartOutlined />, suffix: '%', path: '/health/follow-ups' },
|
||
],
|
||
nurse: [
|
||
{ key: 'today-appointments', title: '今日预约', getValue: (p) => p?.today_appointments ?? 0, getDiff: (p) => { const c = p?.today_appointments, y = p?.yesterday_today_appointments; return c != null && y != null ? c - y : undefined; }, icon: <CalendarOutlined />, path: '/health/appointments' },
|
||
{ key: 'today-followups', title: '今日随访', getValue: (p) => p?.today_follow_ups ?? 0, getDiff: (p) => { const c = p?.today_follow_ups, y = p?.yesterday_today_follow_ups; return c != null && y != null ? c - y : undefined; }, icon: <HeartOutlined />, path: '/health/follow-ups' },
|
||
{ key: 'overdue', title: '逾期随访', getValue: (p) => p?.overdue_follow_ups ?? 0, getDiff: (p) => { const c = p?.overdue_follow_ups, y = p?.yesterday_overdue_follow_ups; return c != null && y != null ? c - y : undefined; }, icon: <AlertOutlined />, path: '/health/follow-ups' },
|
||
{ key: 'vital-rate', title: '体征上报率', getValue: (p) => p?.vital_signs_report_rate ?? 0, icon: <MedicineBoxOutlined />, suffix: '%', path: '/health/vital-signs' },
|
||
],
|
||
admin: [
|
||
{ key: 'patients', title: '患者总数', getValue: (_p, s) => s.patientStats?.total_patients ?? 0, icon: <TeamOutlined />, path: '/health/patients' },
|
||
{ key: 'appointments', title: '本月预约', getValue: (_p, s) => s.healthDataStats?.appointments?.this_month ?? 0, icon: <CalendarOutlined />, path: '/health/appointments' },
|
||
{ key: 'followup-rate', title: '随访完成率', getValue: (_p, s) => s.followUpStats?.completion_rate ?? 0, icon: <SafetyCertificateOutlined />, suffix: '%', path: '/health/follow-ups' },
|
||
{ key: 'vital-rate', title: '体征上报率', getValue: (_p, s) => s.healthDataStats?.vital_signs_report_rate?.report_rate ?? 0, icon: <MedicineBoxOutlined />, suffix: '%', path: '/health/vital-signs' },
|
||
],
|
||
operator: [
|
||
{ key: 'issued', title: '积分发放', getValue: (_p, s) => s.pointsStats?.total_issued ?? 0, icon: <TrophyOutlined />, path: '/health/points' },
|
||
{ key: 'spent', title: '积分消费', getValue: (_p, s) => s.pointsStats?.total_spent ?? 0, icon: <ShoppingOutlined />, path: '/health/mall' },
|
||
{ key: 'active', title: '活跃账户', getValue: (_p, s) => s.pointsStats?.active_accounts ?? 0, icon: <TeamOutlined />, path: '/health/points' },
|
||
{ key: 'articles', title: '内容发布', getValue: () => 0, icon: <FileTextOutlined />, path: '/health/content' },
|
||
],
|
||
};
|
||
|
||
const ROLE_ACTIONS: Record<DashboardRole, QuickActionDef[]> = {
|
||
doctor: [
|
||
{ icon: <TeamOutlined />, label: '患者管理', path: '/health/patients' },
|
||
{ icon: <CalendarOutlined />, label: '预约管理', path: '/health/appointments' },
|
||
{ icon: <HeartOutlined />, label: '随访管理', path: '/health/follow-ups' },
|
||
{ icon: <MessageOutlined />, label: '咨询管理', path: '/health/consultations' },
|
||
{ icon: <AlertOutlined />, label: '告警中心', path: '/health/alert-dashboard' },
|
||
{ icon: <MedicineBoxOutlined />, label: '健康数据', path: '/health/statistics' },
|
||
],
|
||
nurse: [
|
||
{ icon: <HeartOutlined />, label: '随访管理', path: '/health/follow-ups' },
|
||
{ icon: <MedicineBoxOutlined />, label: '健康数据', path: '/health/vital-signs' },
|
||
{ icon: <CalendarOutlined />, label: '预约管理', path: '/health/appointments' },
|
||
{ icon: <AlertOutlined />, label: '告警中心', path: '/health/alert-dashboard' },
|
||
{ icon: <TeamOutlined />, label: '患者管理', path: '/health/patients' },
|
||
{ icon: <SafetyCertificateOutlined />, label: '健康统计', path: '/health/statistics' },
|
||
],
|
||
admin: [
|
||
{ icon: <TeamOutlined />, label: '患者管理', path: '/health/patients' },
|
||
{ icon: <CalendarOutlined />, label: '预约管理', path: '/health/appointments' },
|
||
{ icon: <HeartOutlined />, label: '随访管理', path: '/health/follow-ups' },
|
||
{ icon: <MedicineBoxOutlined />, label: '健康数据', path: '/health/vital-signs' },
|
||
{ icon: <TrophyOutlined />, label: '积分商城', path: '/health/points' },
|
||
{ icon: <SettingOutlined />, label: '系统设置', path: '/settings' },
|
||
],
|
||
operator: [
|
||
{ icon: <TrophyOutlined />, label: '积分管理', path: '/health/points' },
|
||
{ icon: <FileTextOutlined />, label: '内容管理', path: '/health/content' },
|
||
{ icon: <CalendarOutlined />, label: '线下活动', path: '/health/events' },
|
||
{ icon: <TeamOutlined />, label: '患者管理', path: '/health/patients' },
|
||
{ icon: <SafetyCertificateOutlined />, label: '健康统计', path: '/health/statistics' },
|
||
{ icon: <SettingOutlined />, label: '系统设置', path: '/settings' },
|
||
],
|
||
};
|
||
|
||
// --- Components ---
|
||
|
||
function StatValue({ value, loading }: { value: number; loading: boolean }) {
|
||
const animatedValue = useCountUp(value);
|
||
if (loading) return <Spin size="small" />;
|
||
return <span className="erp-count-up">{animatedValue.toLocaleString()}</span>;
|
||
}
|
||
|
||
export default function Home() {
|
||
const navigate = useNavigate();
|
||
const role = useDashboardRole();
|
||
const isDark = useThemeMode();
|
||
const fetchUnreadCount = useMessageStore((s) => s.fetchUnreadCount);
|
||
|
||
const [personalStats, setPersonalStats] = useState<PersonalStats | null>(null);
|
||
const [personalLoading, setPersonalLoading] = useState(true);
|
||
const [pendingTasks, setPendingTasks] = useState<TaskInfo[]>([]);
|
||
const [recentActivities, setRecentActivities] = useState<AuditLogItem[]>([]);
|
||
const [activitiesLoading, setActivitiesLoading] = useState(true);
|
||
const [drawerItem, setDrawerItem] = useState<ActionItem | null>(null);
|
||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||
|
||
const statsData = useStatsData();
|
||
const loading = personalLoading || statsData.loading;
|
||
|
||
const welcome = ROLE_WELCOME[role];
|
||
const statDefs = ROLE_STATS[role];
|
||
const quickActions = ROLE_ACTIONS[role];
|
||
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
fetchUnreadCount();
|
||
|
||
if (role === 'doctor' || role === 'nurse') {
|
||
pointsApi.getPersonalStats()
|
||
.then((data) => { if (!cancelled) setPersonalStats(data); })
|
||
.catch(() => {})
|
||
.finally(() => { if (!cancelled) setPersonalLoading(false); });
|
||
} else {
|
||
setPersonalLoading(false);
|
||
}
|
||
|
||
listPendingTasks(1, 5)
|
||
.then((result) => { if (!cancelled) setPendingTasks(result.data); })
|
||
.catch(() => {});
|
||
|
||
listAuditLogs({ page: 1, page_size: 5 })
|
||
.then((result) => {
|
||
if (!cancelled) setRecentActivities(result.data.filter((a) => a.action !== 'login_failed'));
|
||
})
|
||
.catch(() => {})
|
||
.finally(() => { if (!cancelled) setActivitiesLoading(false); });
|
||
|
||
return () => { cancelled = true; };
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [role]);
|
||
|
||
const handleNavigate = useCallback((path: string) => {
|
||
navigate(path);
|
||
}, [navigate]);
|
||
|
||
return (
|
||
<div>
|
||
{/* 欢迎语 */}
|
||
<div className="erp-fade-in" style={{ marginBottom: 24 }}>
|
||
<h2 style={{
|
||
fontSize: 24,
|
||
fontWeight: 700,
|
||
color: isDark ? '#f8fafc' : 'var(--erp-text-primary)',
|
||
margin: '0 0 4px',
|
||
letterSpacing: '-0.5px',
|
||
}}>
|
||
{welcome.title}
|
||
</h2>
|
||
<p style={{ fontSize: 14, color: 'var(--erp-text-secondary)', margin: 0 }}>
|
||
{welcome.subtitle}
|
||
</p>
|
||
</div>
|
||
|
||
{/* 统计卡片行 */}
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16, marginBottom: 24 }}>
|
||
{statDefs.map((def, i) => {
|
||
const value = def.getValue(personalStats, statsData);
|
||
const diff = def.getDiff?.(personalStats);
|
||
return (
|
||
<div
|
||
key={def.key}
|
||
className={`erp-fade-in erp-fade-in-delay-${i + 1}`}
|
||
style={{
|
||
background: 'var(--erp-bg-card, white)',
|
||
borderRadius: 12,
|
||
border: '1px solid var(--erp-border, #E2E8F0)',
|
||
overflow: 'hidden',
|
||
cursor: 'pointer',
|
||
transition: 'all 0.2s',
|
||
}}
|
||
onClick={() => handleNavigate(def.path)}
|
||
role="button"
|
||
tabIndex={0}
|
||
onKeyDown={(e) => { if (e.key === 'Enter') handleNavigate(def.path); }}
|
||
onMouseEnter={(e) => { e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.06)'; e.currentTarget.style.transform = 'translateY(-1px)'; }}
|
||
onMouseLeave={(e) => { e.currentTarget.style.boxShadow = 'none'; e.currentTarget.style.transform = 'none'; }}
|
||
>
|
||
<div style={{ height: 3, background: STAT_BAR_COLORS[i] || STAT_BAR_COLORS[0] }} />
|
||
<div style={{ padding: '16px 20px' }}>
|
||
<div style={{ fontSize: 12, color: '#94A3B8', marginBottom: 6 }}>{def.title}</div>
|
||
<div style={{ fontSize: 28, fontWeight: 700, lineHeight: 1.2, color: STAT_TEXT_COLORS[i] || STAT_TEXT_COLORS[0] }}>
|
||
<StatValue value={value} loading={loading} />
|
||
{def.suffix && <span style={{ fontSize: 14, marginLeft: 2 }}>{def.suffix}</span>}
|
||
</div>
|
||
{diff != null && (
|
||
<div style={{ fontSize: 11, marginTop: 4, color: diff > 0 ? '#16A34A' : diff < 0 ? '#DC2626' : '#94A3B8' }}>
|
||
{diff === 0 ? '与昨日持平' : `较昨日 ${diff > 0 ? '+' : ''}${diff}`}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* 双栏布局 */}
|
||
{(role === 'doctor' || role === 'nurse') ? (
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 340px', gap: 20, marginBottom: 24 }}>
|
||
{/* 左:待办列表 */}
|
||
<div style={{
|
||
background: 'var(--erp-bg-card, white)',
|
||
borderRadius: 12,
|
||
border: '1px solid var(--erp-border, #E2E8F0)',
|
||
overflow: 'hidden',
|
||
}}>
|
||
<div style={{
|
||
padding: '16px 20px',
|
||
borderBottom: '1px solid #F1F5F9',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
}}>
|
||
<span style={{ fontSize: 15, fontWeight: 600 }}>待办事项</span>
|
||
</div>
|
||
<TodoList onItemClick={(item) => { setDrawerItem(item); setDrawerOpen(true); }} />
|
||
</div>
|
||
|
||
{/* 右:AI 洞察 */}
|
||
<AiInsightPanel />
|
||
</div>
|
||
) : (
|
||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||
<Col xs={24} lg={14}>
|
||
<div className="erp-content-card erp-fade-in erp-fade-in-delay-2">
|
||
<div className="erp-section-header">
|
||
<CheckCircleOutlined className="erp-section-icon" />
|
||
<span className="erp-section-title">待办任务</span>
|
||
<span style={{ marginLeft: 'auto', fontSize: 12, color: 'var(--erp-text-secondary)' }}>
|
||
{pendingTasks.length} 项待处理
|
||
</span>
|
||
</div>
|
||
<div className="erp-task-list">
|
||
{pendingTasks.length === 0 ? (
|
||
<Empty description="暂无待办任务" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||
) : (
|
||
pendingTasks.map((task) => (
|
||
<div
|
||
key={task.id}
|
||
className="erp-task-item"
|
||
style={{ '--task-color': 'var(--erp-primary)' } as React.CSSProperties}
|
||
onClick={() => handleNavigate('/workflow')}
|
||
role="button"
|
||
tabIndex={0}
|
||
onKeyDown={(e) => { if (e.key === 'Enter') handleNavigate('/workflow'); }}
|
||
>
|
||
<div className="erp-task-item-icon"><PartitionOutlined /></div>
|
||
<div className="erp-task-item-content">
|
||
<div className="erp-task-item-title">{task.node_name || task.definition_name || '流程任务'}</div>
|
||
<div className="erp-task-item-meta">
|
||
<span>{task.definition_name || '工作流'}</span>
|
||
<span>{task.status === 'pending' ? '待处理' : task.status}</span>
|
||
</div>
|
||
</div>
|
||
<span className="erp-task-priority erp-task-priority-medium">一般</span>
|
||
<RightOutlined style={{ color: 'var(--erp-text-tertiary)', fontSize: 12 }} />
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
</Col>
|
||
|
||
<Col xs={24} lg={10}>
|
||
<div className="erp-content-card erp-fade-in erp-fade-in-delay-3" style={{ height: '100%' }}>
|
||
<div className="erp-section-header">
|
||
<ClockCircleOutlined className="erp-section-icon" />
|
||
<span className="erp-section-title">最近动态</span>
|
||
</div>
|
||
<div className="erp-activity-list">
|
||
{activitiesLoading ? (
|
||
<div style={{ textAlign: 'center', padding: 24 }}><Spin /></div>
|
||
) : recentActivities.length === 0 ? (
|
||
<Empty description="暂无动态" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||
) : (
|
||
recentActivities.map((log) => (
|
||
<div key={log.id} className="erp-activity-item">
|
||
<div className="erp-activity-dot">
|
||
{RESOURCE_ICONS[log.resource_type] || <FileTextOutlined />}
|
||
</div>
|
||
<div className="erp-activity-content">
|
||
<div className="erp-activity-text">
|
||
{formatActionLabel(log.action)}了{formatResourceLabel(log.resource_type)}
|
||
</div>
|
||
<div className="erp-activity-time">{formatTimeAgo(log.created_at)}</div>
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
</Col>
|
||
</Row>
|
||
)}
|
||
|
||
{/* 快捷入口 */}
|
||
<Row gutter={[16, 16]}>
|
||
<Col span={24}>
|
||
<div className="erp-content-card erp-fade-in erp-fade-in-delay-3">
|
||
<div className="erp-section-header">
|
||
<ThunderboltOutlined className="erp-section-icon" />
|
||
<span className="erp-section-title">快捷入口</span>
|
||
</div>
|
||
<Row gutter={[12, 12]}>
|
||
{quickActions.map((action) => (
|
||
<Col xs={12} sm={8} md={4} key={action.path}>
|
||
<div
|
||
className="erp-quick-action"
|
||
style={{ '--action-color': 'var(--erp-primary)' } as React.CSSProperties}
|
||
onClick={() => handleNavigate(action.path)}
|
||
role="button"
|
||
tabIndex={0}
|
||
onKeyDown={(e) => { if (e.key === 'Enter') handleNavigate(action.path); }}
|
||
>
|
||
<div className="erp-quick-action-icon">{action.icon}</div>
|
||
<span className="erp-quick-action-label">{action.label}</span>
|
||
</div>
|
||
</Col>
|
||
))}
|
||
</Row>
|
||
</div>
|
||
</Col>
|
||
</Row>
|
||
|
||
{/* 主任团队概览 — 在快捷入口上方,全宽展示 */}
|
||
{role === 'admin' && (
|
||
<div style={{ marginBottom: 24 }}>
|
||
<TeamOverviewPanel />
|
||
</div>
|
||
)}
|
||
|
||
{/* 行动详情抽屉 */}
|
||
<ActionDetailDrawer
|
||
item={drawerItem}
|
||
open={drawerOpen}
|
||
onClose={() => { setDrawerOpen(false); setDrawerItem(null); }}
|
||
onActionComplete={() => {
|
||
setDrawerOpen(false);
|
||
setDrawerItem(null);
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|