refactor(web): 重写工作台 UI 匹配原型设计
Home.tsx 改用 CSS Grid 布局替代 antd Row/Col,统计卡片添加 顶部渐变色条。TodoList/AiInsightPanel/TeamOverviewPanel 全部 重写为自定义 inline style,复刻原型中的紧急度圆点、类型标签、 AI 渐变图标、成员进度条、风险分布色块等视觉元素。
This commit is contained in:
@@ -96,6 +96,14 @@ const ROLE_WELCOME: Record<DashboardRole, { title: string; subtitle: string }> =
|
||||
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, icon: <TeamOutlined />, path: '/health/patients' },
|
||||
@@ -238,126 +246,139 @@ export default function Home() {
|
||||
</div>
|
||||
|
||||
{/* 统计卡片行 */}
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16, marginBottom: 24 }}>
|
||||
{statDefs.map((def, i) => {
|
||||
const value = def.getValue(personalStats, statsData);
|
||||
return (
|
||||
<Col xs={24} sm={12} lg={6} key={def.key}>
|
||||
<div
|
||||
className={`erp-stat-card erp-fade-in erp-fade-in-delay-${i + 1}`}
|
||||
onClick={() => handleNavigate(def.path)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleNavigate(def.path); }}
|
||||
>
|
||||
<div className="erp-stat-card-bar" />
|
||||
<div className="erp-stat-card-body">
|
||||
<div className="erp-stat-card-info">
|
||||
<div className="erp-stat-card-title">{def.title}</div>
|
||||
<div className="erp-stat-card-value">
|
||||
<StatValue value={value} loading={loading} />
|
||||
{def.suffix && <span style={{ fontSize: 14, marginLeft: 2, color: 'var(--erp-text-tertiary)' }}>{def.suffix}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="erp-stat-card-icon">{def.icon}</div>
|
||||
<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>
|
||||
</div>
|
||||
</Col>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
{/* 待办任务 + 最近活动 */}
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
{(role === 'doctor' || role === 'nurse') ? (
|
||||
<>
|
||||
<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>
|
||||
</div>
|
||||
<TodoList onItemClick={(item) => { setDrawerItem(item); setDrawerOpen(true); }} />
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} lg={10}>
|
||||
<AiInsightPanel />
|
||||
</Col>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
{/* 双栏布局 */}
|
||||
{(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>
|
||||
|
||||
<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>
|
||||
{/* 右: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>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<span className="erp-task-priority erp-task-priority-medium">一般</span>
|
||||
<RightOutlined style={{ color: 'var(--erp-text-tertiary)', fontSize: 12 }} />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
</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]}>
|
||||
@@ -388,13 +409,11 @@ export default function Home() {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 主任团队概览 */}
|
||||
{/* 主任团队概览 — 在快捷入口上方,全宽展示 */}
|
||||
{role === 'admin' && (
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
<Col span={24}>
|
||||
<TeamOverviewPanel />
|
||||
</Col>
|
||||
</Row>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<TeamOverviewPanel />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 行动详情抽屉 */}
|
||||
|
||||
Reference in New Issue
Block a user