feat: 添加管理端前端 (HMS 基座 React 管理面板)
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled

- 从 HMS 基座复制 apps/web/ (React + Ant Design + Vite + TypeScript)
- 管理端自动代理 API 到 localhost:3000 (vite.config.ts)
- 更新 scripts/dev.sh 支持三端启动: backend/admin/app
- 登录验证通过, 用户管理/角色权限/审计日志等页面正常
- 添加 .gitignore 排除 node_modules/dist
This commit is contained in:
iven
2026-06-02 10:03:13 +08:00
parent 181bfb1f3e
commit 8111471e93
341 changed files with 72102 additions and 1059 deletions

View File

@@ -0,0 +1,180 @@
import { Card, Tag, Typography, theme } from 'antd';
import {
WarningOutlined,
HeartOutlined,
LineChartOutlined,
ExperimentOutlined,
UserOutlined,
} from '@ant-design/icons';
import type { DisplayHint } from '../../api/ai/chat';
const { Text } = Typography;
const SEVERITY_COLOR: Record<string, string> = {
high: 'red',
medium: 'orange',
low: 'green',
};
const RISK_LEVEL_COLOR: Record<string, string> = {
critical: '#cf1322',
high: 'red',
medium: 'orange',
low: 'green',
};
export default function RichMessage({ hints }: { hints: DisplayHint[] }) {
const { token } = theme.useToken();
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 8 }}>
{hints.map((hint, i) => (
<RichHint key={i} hint={hint} token={token} />
))}
</div>
);
}
function RichHint({ hint, token }: { hint: DisplayHint; token: { colorBorderSecondary: string; colorTextSecondary: string; colorPrimary: string } }) {
switch (hint.type) {
case 'insight_card':
return (
<Card
size="small"
title={
<span style={{ fontSize: 12 }}>
<HeartOutlined style={{ marginRight: 4, color: token.colorPrimary }} />
{hint.title}
</span>
}
styles={{ body: { padding: '6px 12px' } }}
>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
{hint.items.map((item, j) => (
<Tag key={j} color={SEVERITY_COLOR[hint.severity] ?? 'blue'} style={{ fontSize: 11, margin: 0 }}>
{item}
</Tag>
))}
</div>
</Card>
);
case 'risk_alert':
return (
<div
style={{
padding: '8px 12px',
borderRadius: 8,
border: `1px solid ${RISK_LEVEL_COLOR[hint.level] ?? token.colorBorderSecondary}`,
background: `${RISK_LEVEL_COLOR[hint.level] ?? token.colorBorderSecondary}10`,
fontSize: 13,
}}
>
<WarningOutlined style={{ color: RISK_LEVEL_COLOR[hint.level] ?? token.colorPrimary, marginRight: 6 }} />
{hint.message}
</div>
);
case 'lab_report_card':
return (
<Card
size="small"
title={
<span style={{ fontSize: 12 }}>
<ExperimentOutlined style={{ marginRight: 4 }} />
{hint.report_date}
</span>
}
styles={{ body: { padding: '6px 12px', fontSize: 12 } }}
>
{hint.abnormal_count > 0 ? (
<Tag color="red" style={{ fontSize: 11 }}>
{hint.abnormal_count}
</Tag>
) : (
<Tag color="green" style={{ fontSize: 11 }}></Tag>
)}
</Card>
);
case 'trend_chart':
return (
<Card
size="small"
title={
<span style={{ fontSize: 12 }}>
<LineChartOutlined style={{ marginRight: 4 }} />
{hint.period}
</span>
}
styles={{ body: { padding: '6px 12px', fontSize: 12 } }}
>
<div style={{ marginBottom: 4 }}>
{hint.metrics.map((m, j) => (
<Tag key={j} style={{ fontSize: 11, margin: '0 4px 2px 0' }}>{m}</Tag>
))}
</div>
<Text type="secondary" style={{ fontSize: 11 }}>{hint.summary}</Text>
</Card>
);
case 'patient_profile':
return (
<Card
size="small"
title={
<span style={{ fontSize: 12 }}>
<UserOutlined style={{ marginRight: 4 }} />
</span>
}
styles={{ body: { padding: '6px 12px', fontSize: 12 } }}
>
{hint.chronic_conditions.length > 0 && (
<div style={{ marginBottom: 4 }}>
{hint.chronic_conditions.map((c, j) => (
<Tag key={j} color="orange" style={{ fontSize: 11, margin: '0 4px 2px 0' }}>{c}</Tag>
))}
</div>
)}
{hint.medication_count > 0 && (
<Text type="secondary" style={{ fontSize: 11 }}> {hint.medication_count} </Text>
)}
</Card>
);
case 'vital_card':
return (
<Card
size="small"
title={
<span style={{ fontSize: 12 }}>
<HeartOutlined style={{ marginRight: 4, color: token.colorPrimary }} />
</span>
}
styles={{ body: { padding: '6px 12px', fontSize: 12 } }}
>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{hint.values.slice(-5).map(([date, val], j) => (
<span key={j}>
<Text type="secondary" style={{ fontSize: 11 }}>{date.slice(5)}:</Text>{' '}
{val} {hint.unit}
</span>
))}
</div>
</Card>
);
case 'action_confirm':
return (
<Card size="small" styles={{ body: { padding: '8px 12px', fontSize: 13 } }}>
<Text>{hint.summary}</Text>
</Card>
);
case 'text':
default:
return null;
}
}