- docs/: 设计规格、讨论记录、销售数据、健康管理文档 - scripts/: 辅助脚本 - package.json + pnpm-lock.yaml: monorepo 根配置
320 lines
17 KiB
HTML
320 lines
17 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>HMS Web 端重设计 · 温润东方风</title>
|
||
<script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
|
||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
|
||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&family=Noto+Serif+SC:wght@400;600;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { background: #E8E2DC; font-family: 'Noto Sans SC', -apple-system, sans-serif; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="root"></div>
|
||
<script type="text/babel">
|
||
/* ─── Design Tokens(从小程序端映射) ─── */
|
||
const C = {
|
||
primary: '#C4623A', primaryHover: '#B55A33', primaryActive: '#8B3E1F',
|
||
primaryLight: '#F0DDD4', primaryBg: '#FAF5F0',
|
||
bg: '#F5F0EB', surface: '#FFFFFF', surfaceAlt: '#EDE8E2',
|
||
tx1: '#2D2A26', tx2: '#7A756E', tx3: '#A8A29E', txInv: '#FFFFFF',
|
||
ok: '#5B7A5E', okBg: '#E8F0E8',
|
||
warn: '#C4873A', warnBg: '#FFF3E0',
|
||
err: '#B54A4A', errBg: '#FDEAEA',
|
||
bd: '#E8E2DC', bdLt: '#F0EBE5',
|
||
shadow: 'rgba(45,42,38,0.08)',
|
||
};
|
||
const serif = '"Noto Serif SC", serif';
|
||
const sans = '"Noto Sans SC", -apple-system, sans-serif';
|
||
|
||
/* ─── 浏览器窗口框 ─── */
|
||
function BrowserFrame({ title, children }) {
|
||
return (
|
||
<div style={{ background: '#1A1816', borderRadius: 12, overflow: 'hidden', boxShadow: '0 20px 60px rgba(0,0,0,0.2)' }}>
|
||
<div style={{ height: 38, background: '#2D2A26', display: 'flex', alignItems: 'center', padding: '0 14px', gap: 8 }}>
|
||
{['#E85C4A','#E8B44A','#5BB85B'].map(c => <div key={c} style={{ width: 12, height: 12, borderRadius: 6, background: c }} />)}
|
||
<div style={{ flex: 1, marginLeft: 16, background: 'rgba(255,255,255,0.08)', borderRadius: 6, height: 24, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, color: '#A8A29E' }}>{title}</div>
|
||
</div>
|
||
<div style={{ overflow: 'auto', background: C.bg }}>{children}</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ─── Dashboard 页面 ─── */
|
||
function DashboardPage() {
|
||
const stats = [
|
||
{ label: '今日预约', value: '24', trend: '+12%', icon: '📅', color: C.primary },
|
||
{ label: '活跃患者', value: '1,847', trend: '+3.2%', icon: '👥', color: C.ok },
|
||
{ label: '待随访', value: '38', trend: '-5%', icon: '📋', color: C.warn },
|
||
{ label: 'AI 报告', value: '156', trend: '+28%', icon: '🤖', color: '#7A6E5E' },
|
||
];
|
||
const tasks = [
|
||
{ title: '王建国 · 血压异常预警', meta: '心内科 · 10分钟前', priority: 'high' },
|
||
{ title: '李小红 · 随访到期提醒', meta: '内分泌 · 今日截止', priority: 'medium' },
|
||
{ title: '批量体检报告待审核', meta: '共12份 · 队列处理中', priority: 'low' },
|
||
];
|
||
const quickActions = [
|
||
{ label: '新增患者', desc: '建档登记' },
|
||
{ label: '预约挂号', desc: '排班管理' },
|
||
{ label: '健康录入', desc: '体征数据' },
|
||
{ label: 'AI 分析', desc: '智能报告' },
|
||
];
|
||
|
||
return (
|
||
<div style={{ padding: 24 }}>
|
||
{/* 页面标题 */}
|
||
<div style={{ marginBottom: 24 }}>
|
||
<div style={{ fontFamily: serif, fontSize: 24, fontWeight: 700, color: C.tx1 }}>工作台</div>
|
||
<div style={{ fontSize: 13, color: C.tx2, marginTop: 4 }}>2026年4月27日 · 欢迎回来,李明医生</div>
|
||
</div>
|
||
|
||
{/* 统计卡片 */}
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16, marginBottom: 24 }}>
|
||
{stats.map(s => (
|
||
<div key={s.label} style={{ background: C.surface, borderRadius: 12, padding: '20px 20px 16px', border: `1px solid ${C.bdLt}`, position: 'relative', overflow: 'hidden', cursor: 'pointer', transition: 'box-shadow 0.15s', boxShadow: `0 1px 3px ${C.shadow}` }}>
|
||
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 3, background: s.color }} />
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||
<div>
|
||
<div style={{ fontSize: 13, color: C.tx2, marginBottom: 8 }}>{s.label}</div>
|
||
<div style={{ fontFamily: serif, fontSize: 32, fontWeight: 700, color: C.tx1, letterSpacing: -1 }}>{s.value}</div>
|
||
</div>
|
||
<div style={{ fontSize: 28, opacity: 0.7 }}>{s.icon}</div>
|
||
</div>
|
||
<div style={{ fontSize: 12, color: s.trend.startsWith('+') ? C.ok : s.trend.startsWith('-') ? C.tx3 : C.tx2, marginTop: 8 }}>
|
||
{s.trend} 较上周
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||
{/* 待办事项 */}
|
||
<div style={{ background: C.surface, borderRadius: 12, padding: 20, border: `1px solid ${C.bdLt}`, boxShadow: `0 1px 3px ${C.shadow}` }}>
|
||
<div style={{ fontFamily: serif, fontSize: 16, fontWeight: 600, color: C.tx1, marginBottom: 16 }}>待办事项</div>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||
{tasks.map(t => {
|
||
const colors = { high: [C.err, C.errBg], medium: [C.warn, C.warnBg], low: [C.tx2, C.surfaceAlt] };
|
||
const [tc, tb] = colors[t.priority];
|
||
return (
|
||
<div key={t.title} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px', borderRadius: 10, background: C.bg, cursor: 'pointer', transition: 'background 0.15s', borderLeft: `3px solid ${tc}` }}>
|
||
<div style={{ flex: 1 }}>
|
||
<div style={{ fontSize: 14, fontWeight: 500, color: C.tx1 }}>{t.title}</div>
|
||
<div style={{ fontSize: 12, color: C.tx3, marginTop: 2 }}>{t.meta}</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 快捷操作 */}
|
||
<div style={{ background: C.surface, borderRadius: 12, padding: 20, border: `1px solid ${C.bdLt}`, boxShadow: `0 1px 3px ${C.shadow}` }}>
|
||
<div style={{ fontFamily: serif, fontSize: 16, fontWeight: 600, color: C.tx1, marginBottom: 16 }}>快捷操作</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
|
||
{quickActions.map(a => (
|
||
<div key={a.label} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '14px 16px', borderRadius: 10, background: C.bg, cursor: 'pointer', transition: 'all 0.15s', border: `1px solid ${C.bdLt}` }}>
|
||
<div style={{ width: 40, height: 40, borderRadius: 10, background: C.primaryLight, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||
<div style={{ width: 18, height: 18, borderRadius: 4, background: C.primary }} />
|
||
</div>
|
||
<div>
|
||
<div style={{ fontSize: 14, fontWeight: 500, color: C.tx1 }}>{a.label}</div>
|
||
<div style={{ fontSize: 12, color: C.tx3 }}>{a.desc}</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ─── 患者列表页 ─── */
|
||
function PatientListPage() {
|
||
const patients = [
|
||
{ name: '李小红', age: 56, dept: '心内科', status: '治疗中', phone: '138****2345', lastVisit: '2026-04-25' },
|
||
{ name: '王建国', age: 63, dept: '内分泌', status: '随访中', phone: '139****8901', lastVisit: '2026-04-24' },
|
||
{ name: '陈美玲', age: 45, dept: '心内科', status: '待诊', phone: '136****5678', lastVisit: '2026-04-23' },
|
||
{ name: '赵大伟', age: 71, dept: '肾内科', status: '治疗中', phone: '137****3456', lastVisit: '2026-04-22' },
|
||
{ name: '孙丽华', age: 58, dept: '内分泌', status: '已完成', phone: '135****7890', lastVisit: '2026-04-21' },
|
||
];
|
||
const statusMap = { '治疗中': [C.primary, C.primaryLight], '随访中': [C.ok, C.okBg], '待诊': [C.warn, C.warnBg], '已完成': [C.tx3, C.surfaceAlt] };
|
||
const columns = ['姓名', '年龄', '科室', '联系电话', '最近就诊', '状态', '操作'];
|
||
|
||
return (
|
||
<div style={{ padding: 24 }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20, paddingBottom: 16, borderBottom: `1px solid ${C.bd}` }}>
|
||
<div>
|
||
<div style={{ fontFamily: serif, fontSize: 20, fontWeight: 700, color: C.tx1 }}>患者管理</div>
|
||
<div style={{ fontSize: 13, color: C.tx3, marginTop: 2 }}>共 1,847 位患者</div>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 8 }}>
|
||
<div style={{ padding: '8px 16px', borderRadius: 8, border: `1px solid ${C.bd}`, background: C.surface, fontSize: 13, color: C.tx2, display: 'flex', alignItems: 'center', gap: 6 }}>
|
||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke={C.tx2} strokeWidth="1.5"><circle cx="7" cy="7" r="4"/><path d="M14 14l-3-3"/></svg>
|
||
搜索患者...
|
||
</div>
|
||
<div style={{ padding: '8px 16px', borderRadius: 8, background: C.primary, color: '#fff', fontSize: 13, fontWeight: 500, cursor: 'pointer' }}>+ 新增患者</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 表格 */}
|
||
<div style={{ background: C.surface, borderRadius: 12, overflow: 'hidden', border: `1px solid ${C.bd}`, boxShadow: `0 1px 3px ${C.shadow}` }}>
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ background: C.surfaceAlt }}>
|
||
{columns.map(col => <th key={col} style={{ padding: '12px 16px', fontSize: 12, fontWeight: 600, color: C.tx2, textAlign: 'left', borderBottom: `1px solid ${C.bd}` }}>{col}</th>)}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{patients.map((p, i) => {
|
||
const [sc, sb] = statusMap[p.status] || statusMap['已完成'];
|
||
return (
|
||
<tr key={p.name} style={{ borderBottom: i < patients.length - 1 ? `1px solid ${C.bdLt}` : 'none', transition: 'background 0.1s' }}>
|
||
<td style={{ padding: '14px 16px', fontSize: 14, fontWeight: 500, color: C.tx1 }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||
<div style={{ width: 32, height: 32, borderRadius: 16, background: i === 0 ? C.primaryLight : C.surfaceAlt, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||
<span style={{ fontFamily: serif, fontSize: 13, fontWeight: 600, color: i === 0 ? C.primary : C.tx2 }}>{p.name[0]}</span>
|
||
</div>
|
||
{p.name}
|
||
</div>
|
||
</td>
|
||
<td style={{ padding: '14px 16px', fontSize: 13, color: C.tx2 }}>{p.age}岁</td>
|
||
<td style={{ padding: '14px 16px', fontSize: 13, color: C.tx2 }}>{p.dept}</td>
|
||
<td style={{ padding: '14px 16px', fontSize: 13, color: C.tx2 }}>{p.phone}</td>
|
||
<td style={{ padding: '14px 16px', fontSize: 13, color: C.tx3 }}>{p.lastVisit}</td>
|
||
<td style={{ padding: '14px 16px' }}>
|
||
<span style={{ display: 'inline-block', padding: '2px 10px', borderRadius: 6, fontSize: 12, fontWeight: 500, background: sb, color: sc }}>{p.status}</span>
|
||
</td>
|
||
<td style={{ padding: '14px 16px', fontSize: 13, color: C.primary, cursor: 'pointer' }}>查看详情</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ─── 侧边栏 ─── */
|
||
function Sidebar({ active = 'dashboard' }) {
|
||
const menuItems = [
|
||
{ group: '概览', items: [{ key: 'dashboard', label: '工作台' }] },
|
||
{ group: '健康管理', items: [
|
||
{ key: 'patients', label: '患者管理' },
|
||
{ key: 'appointments', label: '预约排班' },
|
||
{ key: 'followup', label: '随访管理' },
|
||
{ key: 'consultation', label: '咨询管理' },
|
||
{ key: 'articles', label: '内容管理' },
|
||
]},
|
||
{ group: '数据中心', items: [
|
||
{ key: 'ai', label: 'AI 分析' },
|
||
{ key: 'statistics', label: '统计报表' },
|
||
{ key: 'alerts', label: '预警管理' },
|
||
]},
|
||
{ group: '系统', items: [
|
||
{ key: 'users', label: '用户权限' },
|
||
{ key: 'settings', label: '系统设置' },
|
||
]},
|
||
];
|
||
|
||
return (
|
||
<div style={{ width: 240, height: '100%', background: C.surface, borderRight: `1px solid ${C.bd}`, display: 'flex', flexDirection: 'column', flexShrink: 0 }}>
|
||
{/* Logo */}
|
||
<div style={{ height: 56, display: 'flex', alignItems: 'center', padding: '0 20px', borderBottom: `1px solid ${C.bd}` }}>
|
||
<div style={{ width: 28, height: 28, borderRadius: 6, background: C.primary, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 13, fontWeight: 800 }}>H</div>
|
||
<span style={{ marginLeft: 10, fontFamily: serif, fontSize: 15, fontWeight: 700, color: C.tx1 }}>HMS 健康</span>
|
||
</div>
|
||
{/* 菜单 */}
|
||
<div style={{ flex: 1, padding: '8px 0', overflow: 'auto' }}>
|
||
{menuItems.map(g => (
|
||
<div key={g.group}>
|
||
<div style={{ padding: '16px 20px 6px', fontSize: 11, fontWeight: 600, color: C.tx3, letterSpacing: '0.5px', textTransform: 'uppercase' }}>{g.group}</div>
|
||
{g.items.map(item => (
|
||
<div key={item.key} style={{
|
||
display: 'flex', alignItems: 'center', height: 36, margin: '1px 8px', padding: '0 12px',
|
||
borderRadius: 8, cursor: 'pointer', fontSize: 14,
|
||
background: active === item.key ? C.primaryLight : 'transparent',
|
||
color: active === item.key ? C.primary : C.tx2,
|
||
fontWeight: active === item.key ? 500 : 400,
|
||
transition: 'all 0.15s',
|
||
}}>{item.label}</div>
|
||
))}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ─── 顶部栏 ─── */
|
||
function Header({ title }) {
|
||
return (
|
||
<div style={{ height: 56, padding: '0 24px', background: C.surface, borderBottom: `1px solid ${C.bd}`, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||
<div style={{ fontSize: 15, fontWeight: 600, color: C.tx1 }}>{title}</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||
<div style={{ width: 32, height: 32, borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', color: C.tx2 }}>
|
||
<svg width="18" height="18" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M8 2a5 5 0 015 5c0 4-5 7-5 7S3 11 3 7a5 5 0 015-5z"/></svg>
|
||
</div>
|
||
<div style={{ width: 32, height: 32, borderRadius: 16, background: C.primary, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 13, fontWeight: 600 }}>李</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ─── 完整布局 ─── */
|
||
function FullLayout({ children, title }) {
|
||
return (
|
||
<div style={{ display: 'flex', height: '100vh' }}>
|
||
<Sidebar />
|
||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||
<Header title={title} />
|
||
<div style={{ flex: 1, overflow: 'auto', background: C.bg }}>{children}</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ─── 页面路由 ─── */
|
||
function App() {
|
||
const [page, setPage] = React.useState('both');
|
||
|
||
return (
|
||
<div style={{ padding: 40, display: 'flex', gap: 48, justifyContent: 'center', flexWrap: 'wrap', minHeight: '100vh', alignItems: 'flex-start' }}>
|
||
{/* Dashboard */}
|
||
<div>
|
||
<div style={{ textAlign: 'center', marginBottom: 16 }}>
|
||
<div style={{ fontFamily: serif, fontSize: 18, fontWeight: 700, color: C.tx1 }}>Dashboard · 工作台</div>
|
||
<div style={{ fontSize: 12, color: C.tx2, marginTop: 4 }}>Home.tsx → 新设计</div>
|
||
</div>
|
||
<BrowserFrame title="localhost:5174/dashboard">
|
||
<div style={{ width: 1100, height: 700 }}>
|
||
<FullLayout title="工作台"><DashboardPage /></FullLayout>
|
||
</div>
|
||
</BrowserFrame>
|
||
</div>
|
||
|
||
{/* 患者列表 */}
|
||
<div>
|
||
<div style={{ textAlign: 'center', marginBottom: 16 }}>
|
||
<div style={{ fontFamily: serif, fontSize: 18, fontWeight: 700, color: C.tx1 }}>患者管理 · 列表</div>
|
||
<div style={{ fontSize: 12, color: C.tx2, marginTop: 4 }}>PatientList.tsx → 新设计</div>
|
||
</div>
|
||
<BrowserFrame title="localhost:5174/health/patients">
|
||
<div style={{ width: 1100, height: 700 }}>
|
||
<FullLayout title="患者管理"><PatientListPage /></FullLayout>
|
||
</div>
|
||
</BrowserFrame>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||
</script>
|
||
</body>
|
||
</html>
|