- OperatorWorkbench: "今日活跃用户" 错误使用体征上报率数据源,改为 pointsStats.active_accounts - OperatorWorkbench: AI 摘要体征上报率显示原始浮点数(22.413793...),改为保留两位小数 - OperatorWorkbench: "科普阅读量" fallback 错误回退到积分发放数据,移除错误 fallback - Home.tsx: 运营角色 ROLE_STATS "内容发布" 数据源错误,修正为 patientStats - Home.tsx: 移除未使用的 TodoList/AiInsightPanel import - .lintstagedrc.js: 修复 Windows 平台 eslint 命令,使用函数式获取文件名列表
444 lines
22 KiB
TypeScript
444 lines
22 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 ActionDetailDrawer from './health/components/workbench/ActionDetailDrawer';
|
|
import TaskQueue from './health/components/workbench/TaskQueue';
|
|
import TaskDetail from './health/components/workbench/TaskDetail';
|
|
import DoctorWorkbench from './health/components/workbench/DoctorWorkbench';
|
|
import NurseWorkbench from './health/components/workbench/NurseWorkbench';
|
|
import OperatorWorkbench from './health/components/workbench/OperatorWorkbench';
|
|
import AdminDashboard from './health/components/workbench/AdminDashboard';
|
|
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: '患者概览与待办事项' },
|
|
health_manager: { 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' },
|
|
],
|
|
health_manager: [
|
|
{ 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-up-tasks' },
|
|
{ key: 'vital-anomaly', title: '体征异常', getValue: (p) => p?.overdue_follow_ups ?? 0, icon: <AlertOutlined />, path: '/health/alert-dashboard' },
|
|
{ key: 'ai-pending', title: 'AI 建议待审', getValue: (p) => p?.consultations_this_month ?? 0, icon: <MedicineBoxOutlined />, path: '/health/ai-analysis' },
|
|
{ key: 'followup-rate', title: '处理率', getValue: (p) => p?.follow_up_rate ?? 0, icon: <CheckCircleOutlined />, suffix: '%', path: '/health/follow-up-tasks' },
|
|
],
|
|
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: (_p, s) => s.patientStats?.total_patients ?? 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' },
|
|
],
|
|
health_manager: [
|
|
{ icon: <HeartOutlined />, label: '随访管理', path: '/health/follow-up-tasks' },
|
|
{ icon: <MedicineBoxOutlined />, label: '体征监测', path: '/health/alert-dashboard' },
|
|
{ icon: <MessageOutlined />, label: '患者咨询', path: '/health/consultations' },
|
|
{ icon: <TeamOutlined />, label: '患者管理', path: '/health/patients' },
|
|
{ icon: <TrophyOutlined />, label: '积分商城', path: '/health/points-products' },
|
|
{ icon: <FileTextOutlined />, 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>
|
|
{/* 角色工作台路由 */}
|
|
{role === 'doctor' ? (
|
|
<DoctorWorkbench />
|
|
) : role === 'health_manager' ? (
|
|
<div style={{ display: 'flex', height: 'calc(100vh - 64px)', overflow: 'hidden', margin: -20 }}>
|
|
<TaskQueue />
|
|
<TaskDetail />
|
|
</div>
|
|
) : role === 'operator' ? (
|
|
<OperatorWorkbench />
|
|
) : role === 'admin' ? (
|
|
<AdminDashboard />
|
|
) : (
|
|
<>
|
|
{/* 欢迎语 */}
|
|
<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 === 'nurse' ? (
|
|
<NurseWorkbench />
|
|
) : (
|
|
<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>
|
|
|
|
{/* 行动详情抽屉 */}
|
|
<ActionDetailDrawer
|
|
item={drawerItem}
|
|
open={drawerOpen}
|
|
onClose={() => { setDrawerOpen(false); setDrawerItem(null); }}
|
|
onActionComplete={() => {
|
|
setDrawerOpen(false);
|
|
setDrawerItem(null);
|
|
}}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|