docs: T40 UI 审计报告 + wiki 更新 + Docker 配置

- T40 UI 审计计划和结果文档(docs/qa/)
- wiki 更新:miniprogram 设计系统合规审计记录 + index 关键数字更新
- 审计 V2 完整报告(docs/audits/v2/)
- 讨论记录文档(docs/discussions/)
- 设计规格和实施计划(docs/superpowers/)
- 角色测试计划和结果(docs/qa/role-test-*)
- Docker 生产部署配置
This commit is contained in:
iven
2026-05-13 23:29:42 +08:00
parent 212c08b7ae
commit df1d85bfde
78 changed files with 10345 additions and 39 deletions

View File

@@ -0,0 +1,315 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HMS 小程序重构 — 预约挂号流程</title>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #1a1a1a; font-family: -apple-system, 'PingFang SC', sans-serif; display: flex; flex-direction: column; align-items: center; padding: 40px 20px; gap: 24px; }
.page-title { color: #999; font-size: 13px; letter-spacing: 0.15em; }
.flow { display: flex; gap: 24px; flex-wrap: wrap; justify-content: center; align-items: flex-start; }
.flow-step { display: flex; flex-direction: column; align-items: center; gap: 10px; }
.flow-label { color: #888; font-size: 12px; font-style: italic; }
.flow-arrow { color: #555; font-size: 24px; align-self: center; margin-top: 40px; }
</style>
</head>
<body>
<div class="page-title">预约挂号 · 三步流程</div>
<div id="root"></div>
<script type="text/babel">
const iosFrameStyles = {
wrapper: { display: 'inline-block', padding: 12, background: '#000', borderRadius: 60, boxShadow: '0 0 0 2px #1f2937, 0 20px 60px rgba(0,0,0,0.3)' },
screen: { position: 'relative', borderRadius: 48, overflow: 'hidden', background: '#fff' },
statusBar: { position: 'absolute', top: 0, left: 0, right: 0, height: 54, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 32px', fontSize: 16, fontWeight: 600, fontFamily: '-apple-system, "SF Pro Text", sans-serif', zIndex: 20, pointerEvents: 'none', color: '#000' },
dynamicIsland: { position: 'absolute', top: 12, left: '50%', transform: 'translateX(-50%)', width: 124, height: 36, background: '#000', borderRadius: 999, zIndex: 30 },
content: { position: 'absolute', top: 54, left: 0, right: 0, bottom: 0, overflow: 'auto' },
homeIndicator: { position: 'absolute', bottom: 10, left: '50%', transform: 'translateX(-50%)', width: 140, height: 5, background: 'rgba(0,0,0,0.3)', borderRadius: 999, zIndex: 10 },
};
function IosFrame({ children, width = 393, height = 852, time = '9:41', battery = 85 }) {
return (
<div style={iosFrameStyles.wrapper}>
<div style={{ ...iosFrameStyles.screen, width, height }}>
<div style={iosFrameStyles.statusBar}><span>{time}</span><div style={{ display:'flex',alignItems:'center',gap:6 }}><svg width="16" height="12" viewBox="0 0 16 12" fill="none"><path d="M8 11.5a1 1 0 100-2 1 1 0 000 2z" fill="#000"/><path d="M3 7.5a7 7 0 0110 0" stroke="#000" strokeWidth="1.3" fill="none" strokeLinecap="round"/></svg><div style={{ width:26,height:12,border:'1.5px solid #000',borderRadius:3,padding:1 }}><div style={{ width:`${battery}%`,height:'100%',background:'#000',borderRadius:1 }} /></div></div></div>
<div style={iosFrameStyles.dynamicIsland} />
<div style={iosFrameStyles.content}>{children}</div>
<div style={iosFrameStyles.homeIndicator} />
</div>
</div>
);
}
const T = {
pri: '#C4623A', priL: '#F0DDD4', priD: '#8B3E1F',
bg: '#F5F0EB', card: '#FFFFFF', surface: '#EDE8E2',
tx: '#2D2A26', tx2: '#5A554F', tx3: '#78716C',
bd: '#E8E2DC', bdL: '#F0EBE5',
acc: '#5B7A5E', accL: '#E8F0E8',
wrn: '#C4873A', wrnL: '#FFF3E0',
dan: '#B54A4A', danL: '#FDEAEA',
serif: "Georgia, 'Times New Roman', serif",
r: 16, rSm: 12, rXs: 8,
};
// ─── 步骤指示器 ───
function Steps({ steps, current }) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0, padding: '16px 24px 0' }}>
{steps.map((s, i) => (
<React.Fragment key={i}>
{i > 0 && <div style={{ width: 40, height: 2, background: i <= current ? T.pri : T.bd, transition: 'background 0.3s' }} />}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
<div style={{ width: 28, height: 28, borderRadius: 14, background: i < current ? T.acc : i === current ? T.pri : T.surface, display: 'flex', alignItems: 'center', justifyContent: 'center', color: i <= current ? '#fff' : T.tx3, fontSize: 13, fontWeight: 700, fontFamily: T.serif }}>
{i < current ? '✓' : i + 1}
</div>
<span style={{ fontSize: 12, color: i <= current ? T.tx : T.tx3, fontWeight: i === current ? 600 : 400 }}>{s}</span>
</div>
</React.Fragment>
))}
</div>
);
}
// ─── Step 1: 选科室 ───
function ApptStep1() {
const depts = [
{ label: '内科', icon: '内' },
{ label: '外科', icon: '外' },
{ label: '妇科', icon: '妇' },
{ label: '儿科', icon: '儿' },
{ label: '体检中心', icon: '检' },
{ label: '中医科', icon: '中' },
];
const [selected, setSelected] = React.useState('内科');
return (
<div style={{ height: '100%', background: T.bg }}>
<div style={{ padding: '0 20px', marginBottom: 8 }}>
{/* 导航栏 */}
<div style={{ height: 44, display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}>
<span style={{ position: 'absolute', left: 0, color: T.pri, fontSize: 16 }}> 返回</span>
<span style={{ fontSize: 17, fontWeight: 600, color: T.tx }}>预约挂号</span>
</div>
<Steps steps={['选科室','选医生','选时段']} current={0} />
</div>
<div style={{ padding: '20px 20px 100px' }}>
<div style={{ fontFamily: T.serif, fontSize: 20, fontWeight: 700, color: T.tx, marginBottom: 16 }}>请选择就诊科室</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12 }}>
{depts.map((d) => (
<div key={d.label} onClick={() => setSelected(d.label)} style={{
background: selected === d.label ? T.pri : T.card,
borderRadius: T.r, padding: '20px 12px', textAlign: 'center',
boxShadow: selected === d.label ? `0 2px 12px rgba(196,98,58,0.3)` : '0 1px 4px rgba(45,42,38,0.04)',
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10,
}}>
<div style={{ width: 48, height: 48, borderRadius: 24, background: selected === d.label ? 'rgba(255,255,255,0.2)' : T.priL, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ fontFamily: T.serif, fontSize: 22, fontWeight: 700, color: selected === d.label ? '#fff' : T.pri }}>{d.icon}</span>
</div>
<span style={{ fontSize: 15, fontWeight: 500, color: selected === d.label ? '#fff' : T.tx }}>{d.label}</span>
</div>
))}
</div>
</div>
{/* 底部按钮 */}
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, padding: '16px 20px 36px', background: T.card, borderTop: `1px solid ${T.bdL}` }}>
<div style={{ height: 52, borderRadius: 14, background: T.pri, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 17, fontWeight: 600, boxShadow: '0 2px 8px rgba(196,98,58,0.25)' }}>下一步</div>
</div>
</div>
);
}
// ─── Step 2: 选医生 ───
function ApptStep2() {
const doctors = [
{ id: 1, name: '王明', title: '主任医师', dept: '内科', specialty: '心血管疾病、高血压管理' },
{ id: 2, name: '李华', title: '副主任医师', dept: '内科', specialty: '呼吸系统疾病' },
{ id: 3, name: '赵丽', title: '主治医师', dept: '内科', specialty: '消化内科' },
];
const [selected, setSelected] = React.useState(1);
return (
<div style={{ height: '100%', background: T.bg }}>
<div style={{ padding: '0 20px', marginBottom: 8 }}>
<div style={{ height: 44, display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}>
<span style={{ position: 'absolute', left: 0, color: T.pri, fontSize: 16 }}> 返回</span>
<span style={{ fontSize: 17, fontWeight: 600, color: T.tx }}>预约挂号</span>
</div>
<Steps steps={['选科室','选医生','选时段']} current={1} />
</div>
<div style={{ padding: '20px 20px 100px' }}>
<div style={{ fontFamily: T.serif, fontSize: 20, fontWeight: 700, color: T.tx, marginBottom: 4 }}>内科 · 请选择医生</div>
<div style={{ fontSize: 14, color: T.tx3, marginBottom: 16 }}> 3 位医生可预约</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{doctors.map((d) => (
<div key={d.id} onClick={() => setSelected(d.id)} style={{
background: T.card, borderRadius: T.r, padding: 16,
boxShadow: selected === d.id ? `0 0 0 2px ${T.pri}` : '0 1px 4px rgba(45,42,38,0.04)',
display: 'flex', gap: 14, alignItems: 'center',
}}>
<div style={{ width: 48, height: 48, borderRadius: 24, background: T.priL, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ fontFamily: T.serif, fontSize: 20, fontWeight: 700, color: T.pri }}>{d.name.charAt(0)}</span>
</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 4 }}>
<span style={{ fontSize: 17, fontWeight: 600, color: T.tx }}>{d.name}</span>
<span style={{ fontSize: 13, color: T.tx3 }}>{d.title}</span>
</div>
<span style={{ fontSize: 13, color: T.tx2, lineHeight: 1.5 }}>{d.specialty}</span>
</div>
{selected === d.id && (
<div style={{ width: 24, height: 24, borderRadius: 12, background: T.pri, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ color: '#fff', fontSize: 14 }}></span>
</div>
)}
</div>
))}
</div>
</div>
{/* 底部双按钮 */}
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, padding: '16px 20px 36px', background: T.card, borderTop: `1px solid ${T.bdL}`, display: 'flex', gap: 10 }}>
<div style={{ flex: 1, height: 52, borderRadius: 14, background: T.surface, display: 'flex', alignItems: 'center', justifyContent: 'center', color: T.tx2, fontSize: 17, fontWeight: 600 }}>上一步</div>
<div style={{ flex: 2, height: 52, borderRadius: 14, background: T.pri, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 17, fontWeight: 600, boxShadow: '0 2px 8px rgba(196,98,58,0.25)' }}>下一步</div>
</div>
</div>
);
}
// ─── Step 3: 选时段 ───
function ApptStep3() {
const [selectedDate, setSelectedDate] = React.useState('5/9');
const [selectedSlot, setSelectedSlot] = React.useState('09:00-09:30');
const dates = [
{ day: '四', date: '5/8', has: true },
{ day: '五', date: '5/9', has: true },
{ day: '六', date: '5/10', has: false },
{ day: '日', date: '5/11', has: false },
{ day: '一', date: '5/12', has: true },
{ day: '二', date: '5/13', has: true },
{ day: '三', date: '5/14', has: true },
];
const slots = [
{ time: '08:30-09:00', avail: 2 },
{ time: '09:00-09:30', avail: 5 },
{ time: '09:30-10:00', avail: 0 },
{ time: '10:00-10:30', avail: 3 },
{ time: '14:00-14:30', avail: 8 },
{ time: '14:30-15:00', avail: 1 },
];
return (
<div style={{ height: '100%', background: T.bg }}>
<div style={{ padding: '0 20px', marginBottom: 8 }}>
<div style={{ height: 44, display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}>
<span style={{ position: 'absolute', left: 0, color: T.pri, fontSize: 16 }}> 返回</span>
<span style={{ fontSize: 17, fontWeight: 600, color: T.tx }}>预约挂号</span>
</div>
<Steps steps={['选科室','选医生','选时段']} current={2} />
</div>
<div style={{ padding: '16px 20px 100px' }}>
{/* 已选医生摘要 */}
<div style={{ background: T.card, borderRadius: T.rSm, padding: 14, display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16, boxShadow: '0 1px 4px rgba(45,42,38,0.04)' }}>
<div style={{ width: 36, height: 36, borderRadius: T.rSm, background: T.priL, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: T.pri }}></span>
</div>
<div style={{ flex: 1 }}>
<span style={{ fontSize: 15, fontWeight: 600, color: T.tx }}>王明</span>
<span style={{ fontSize: 12, color: T.tx3, marginLeft: 6 }}>主任医师</span>
</div>
<span style={{ fontSize: 12, padding: '2px 8px', borderRadius: 999, background: T.priL, color: T.pri, fontWeight: 500 }}>内科</span>
</div>
{/* 周日历 */}
<div style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.tx, marginBottom: 12 }}>选择日期</div>
<div style={{ display: 'flex', gap: 0, background: T.card, borderRadius: T.rSm, padding: 12, boxShadow: '0 1px 4px rgba(45,42,38,0.04)', marginBottom: 16 }}>
{dates.map((d) => {
const active = selectedDate === d.date;
return (
<div key={d.date} onClick={() => d.has && setSelectedDate(d.date)} style={{
flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, padding: '8px 0',
borderRadius: T.rXs, background: active ? T.pri : 'transparent',
opacity: d.has ? 1 : 0.35, cursor: d.has ? 'pointer' : 'default',
}}>
<span style={{ fontSize: 12, color: active ? 'rgba(255,255,255,0.7)' : T.tx3 }}>{d.day}</span>
<span style={{ fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: active ? '#fff' : T.tx }}>{d.date.split('/')[1]}</span>
{d.has && <div style={{ width: 4, height: 4, borderRadius: 2, background: active ? '#fff' : T.pri, opacity: active ? 1 : 0.5 }} />}
</div>
);
})}
</div>
{/* 时段网格 */}
<div style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.tx, marginBottom: 12 }}>选择时段</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 8 }}>
{slots.map((s) => {
const full = s.avail === 0;
const active = selectedSlot === s.time;
return (
<div key={s.time} onClick={() => !full && setSelectedSlot(s.time)} style={{
background: full ? T.surface : active ? T.pri : T.card,
borderRadius: T.rSm, padding: '12px 8px', textAlign: 'center',
boxShadow: active ? `0 2px 8px rgba(196,98,58,0.3)` : '0 1px 4px rgba(45,42,38,0.04)',
opacity: full ? 0.5 : 1, cursor: full ? 'default' : 'pointer',
}}>
<div style={{ fontFamily: T.serif, fontSize: 15, fontWeight: 600, color: full ? T.tx3 : active ? '#fff' : T.tx, marginBottom: 4 }}>{s.time}</div>
<div style={{ fontSize: 11, color: full ? T.tx3 : active ? 'rgba(255,255,255,0.7)' : s.avail <= 3 ? T.wrn : T.tx3 }}>
{full ? '已满' : `剩余 ${s.avail}`}
</div>
</div>
);
})}
</div>
{/* 备注 */}
<div style={{ fontSize: 14, fontWeight: 600, color: T.tx2, marginTop: 16, marginBottom: 8 }}>备注选填</div>
<div style={{ background: T.card, borderRadius: T.rSm, height: 48, border: `1.5px solid ${T.bd}`, display: 'flex', alignItems: 'center', padding: '0 14px' }}>
<span style={{ fontSize: 15, color: T.tx3 }}>请简要描述症状</span>
</div>
</div>
{/* 底部确认 */}
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, padding: '16px 20px 36px', background: T.card, borderTop: `1px solid ${T.bdL}`, display: 'flex', gap: 10 }}>
<div style={{ flex: 1, height: 52, borderRadius: 14, background: T.surface, display: 'flex', alignItems: 'center', justifyContent: 'center', color: T.tx2, fontSize: 17, fontWeight: 600 }}>上一步</div>
<div style={{ flex: 2, height: 52, borderRadius: 14, background: T.pri, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 17, fontWeight: 600, boxShadow: '0 2px 8px rgba(196,98,58,0.25)' }}>确认预约</div>
</div>
</div>
);
}
// ─── 渲染 ───
function App() {
return (
<div className="flow">
<div className="flow-step">
<span className="flow-label">Step 1 · 选科室</span>
<IosFrame time="9:41" battery={85} height={852}>
<ApptStep1 />
</IosFrame>
</div>
<div className="flow-arrow"></div>
<div className="flow-step">
<span className="flow-label">Step 2 · 选医生</span>
<IosFrame time="9:41" battery={85} height={852}>
<ApptStep2 />
</IosFrame>
</div>
<div className="flow-arrow"></div>
<div className="flow-step">
<span className="flow-label">Step 3 · 选时段</span>
<IosFrame time="9:41" battery={85} height={852}>
<ApptStep3 />
</IosFrame>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>

View File

@@ -0,0 +1,227 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HMS 小程序重构 — 咨询流程</title>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #1a1a1a; font-family: -apple-system, 'PingFang SC', sans-serif; display: flex; flex-direction: column; align-items: center; padding: 40px 20px; gap: 24px; }
.page-title { color: #999; font-size: 13px; letter-spacing: 0.15em; }
.flow { display: flex; gap: 32px; flex-wrap: wrap; justify-content: center; align-items: flex-start; }
.flow-step { display: flex; flex-direction: column; align-items: center; gap: 10px; }
.flow-label { color: #888; font-size: 12px; font-style: italic; }
.flow-arrow { color: #555; font-size: 24px; align-self: center; margin-top: 40px; }
</style>
</head>
<body>
<div class="page-title">在线咨询 · 列表 + 聊天详情</div>
<div id="root"></div>
<script type="text/babel">
const iosFrameStyles = {
wrapper: { display: 'inline-block', padding: 12, background: '#000', borderRadius: 60, boxShadow: '0 0 0 2px #1f2937, 0 20px 60px rgba(0,0,0,0.3)' },
screen: { position: 'relative', borderRadius: 48, overflow: 'hidden', background: '#fff' },
statusBar: { position: 'absolute', top: 0, left: 0, right: 0, height: 54, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 32px', fontSize: 16, fontWeight: 600, fontFamily: '-apple-system, "SF Pro Text", sans-serif', zIndex: 20, pointerEvents: 'none', color: '#000' },
dynamicIsland: { position: 'absolute', top: 12, left: '50%', transform: 'translateX(-50%)', width: 124, height: 36, background: '#000', borderRadius: 999, zIndex: 30 },
content: { position: 'absolute', top: 54, left: 0, right: 0, bottom: 0, overflow: 'auto' },
homeIndicator: { position: 'absolute', bottom: 10, left: '50%', transform: 'translateX(-50%)', width: 140, height: 5, background: 'rgba(0,0,0,0.3)', borderRadius: 999, zIndex: 10 },
};
function IosFrame({ children, width = 393, height = 852, time = '9:41', battery = 85 }) {
return (
<div style={iosFrameStyles.wrapper}>
<div style={{ ...iosFrameStyles.screen, width, height }}>
<div style={iosFrameStyles.statusBar}><span>{time}</span><div style={{ display:'flex',alignItems:'center',gap:6 }}><svg width="16" height="12" viewBox="0 0 16 12" fill="none"><path d="M8 11.5a1 1 0 100-2 1 1 0 000 2z" fill="#000"/><path d="M3 7.5a7 7 0 0110 0" stroke="#000" strokeWidth="1.3" fill="none" strokeLinecap="round"/></svg><div style={{ width:26,height:12,border:'1.5px solid #000',borderRadius:3,padding:1 }}><div style={{ width:`${battery}%`,height:'100%',background:'#000',borderRadius:1 }} /></div></div></div>
<div style={iosFrameStyles.dynamicIsland} />
<div style={iosFrameStyles.content}>{children}</div>
<div style={iosFrameStyles.homeIndicator} />
</div>
</div>
);
}
const T = {
pri: '#C4623A', priL: '#F0DDD4', priD: '#8B3E1F',
bg: '#F5F0EB', card: '#FFFFFF', surface: '#EDE8E2',
tx: '#2D2A26', tx2: '#5A554F', tx3: '#78716C',
bd: '#E8E2DC', bdL: '#F0EBE5',
acc: '#5B7A5E', accL: '#E8F0E8',
wrn: '#C4873A', wrnL: '#FFF3E0',
dan: '#B54A4A',
serif: "Georgia, 'Times New Roman', serif",
r: 16, rSm: 12, rXs: 8,
};
// ─── 咨询列表 ───
function ConsultList() {
const sessions = [
{ id: 1, subject: '血压波动咨询', doctor: '王明 · 心内科', lastMsg: '您的检查报告已出,建议下周复查一次', time: '10 分钟前', status: 'active', statusLabel: '进行中', unread: 2 },
{ id: 2, subject: '肾功能复查', doctor: '李华 · 肾内科', lastMsg: '复查结果整体平稳,继续观察', time: '昨天', status: 'active', statusLabel: '进行中', unread: 0 },
{ id: 3, subject: '用药调整', doctor: '赵丽 · 内科', lastMsg: '好的,按新方案服药两周后反馈', time: '3 天前', status: 'pending', statusLabel: '等待接诊', unread: 0 },
{ id: 4, subject: '体检报告解读', doctor: '张伟 · 全科', lastMsg: '各项指标正常,继续保持', time: '上周', status: 'closed', statusLabel: '已结束', unread: 0 },
];
const statusStyle = {
active: { bg: T.accL, color: T.acc },
pending: { bg: T.wrnL, color: T.wrn },
closed: { bg: T.surface, color: T.tx3 },
};
return (
<div style={{ height: '100%', background: T.bg }}>
<div style={{ padding: '20px 20px 0' }}>
{/* 导航栏 */}
<div style={{ height: 44, display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}>
<span style={{ position: 'absolute', left: 0, color: T.pri, fontSize: 16 }}> 返回</span>
<span style={{ fontSize: 17, fontWeight: 600, color: T.tx }}>在线咨询</span>
</div>
</div>
<div style={{ padding: '12px 20px 20px' }}>
{/* 副标题 */}
<div style={{ fontSize: 14, color: T.tx3, marginBottom: 20 }}>随时随地连接专业医生</div>
{/* 新建咨询入口 */}
<div style={{ background: T.pri, borderRadius: T.r, height: 48, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, marginBottom: 20, boxShadow: '0 2px 8px rgba(196,98,58,0.25)' }}>
<span style={{ color: '#fff', fontSize: 17, fontWeight: 600 }}>发起咨询</span>
</div>
{/* 会话列表 */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{sessions.map((s) => {
const ss = statusStyle[s.status];
return (
<div key={s.id} style={{
background: T.card, borderRadius: T.r, padding: 16,
boxShadow: '0 1px 4px rgba(45,42,38,0.04)',
opacity: s.status === 'closed' ? 0.6 : 1,
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ width: 36, height: 36, borderRadius: 18, background: T.priL, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: T.pri }}>{s.doctor.charAt(0)}</span>
</div>
<div>
<div style={{ fontSize: 15, fontWeight: 600, color: T.tx }}>{s.subject}</div>
<div style={{ fontSize: 12, color: T.tx3, marginTop: 1 }}>{s.doctor}</div>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4 }}>
<span style={{ fontSize: 12, color: T.tx3 }}>{s.time}</span>
<span style={{ fontSize: 10, padding: '2px 6px', borderRadius: 4, background: ss.bg, color: ss.color, fontWeight: 500 }}>{s.statusLabel}</span>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: 13, color: T.tx2, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginRight: 8 }}>{s.lastMsg}</span>
{s.unread > 0 && (
<span style={{ minWidth: 18, height: 18, borderRadius: 9, background: T.dan, color: '#fff', fontSize: 11, fontWeight: 600, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0 5px', flexShrink: 0 }}>{s.unread}</span>
)}
</div>
</div>
);
})}
</div>
</div>
</div>
);
}
// ─── 聊天详情 ───
function ChatDetail() {
const messages = [
{ id: 1, role: 'system', text: '今天', type: 'date' },
{ id: 2, role: 'patient', text: '王医生您好,我最近血压波动比较大,早上起来经常 140+,想咨询一下。', time: '09:12' },
{ id: 3, role: 'doctor', text: '您好,请问最近有按时服药吗?有没有头晕、胸闷的情况?', time: '09:15' },
{ id: 4, role: 'patient', text: '药一直在吃,偶尔会有一点头晕。', time: '09:18' },
{ id: 5, role: 'doctor', text: '看了一下您最近的体征数据,收缩压确实有上升趋势。建议加做一个肾功能检查,排除继发性因素。', time: '09:22' },
{ id: 6, role: 'patient', text: '好的,需要空腹吗?', time: '09:25' },
{ id: 7, role: 'doctor', text: '需要空腹。我帮您开一个检查单,您明天早上来就可以。', time: '09:28' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
{/* 导航栏 */}
<div style={{ height: 44, display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative', background: T.card, borderBottom: `1px solid ${T.bdL}`, flexShrink: 0 }}>
<span style={{ position: 'absolute', left: 16, color: T.pri, fontSize: 16 }}> 返回</span>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: 17, fontWeight: 600, color: T.tx }}>血压波动咨询</div>
<div style={{ fontSize: 11, color: T.acc }}>王明 · 进行中</div>
</div>
</div>
{/* 消息区 */}
<div style={{ flex: 1, padding: '16px 16px 0', overflowY: 'auto' }}>
{messages.map((msg) => {
if (msg.type === 'date') {
return (
<div key={msg.id} style={{ textAlign: 'center', margin: '12px 0' }}>
<span style={{ fontSize: 12, color: T.tx3, background: T.surface, padding: '2px 12px', borderRadius: 999 }}>{msg.text}</span>
</div>
);
}
const isSelf = msg.role === 'patient';
return (
<div key={msg.id} style={{ display: 'flex', justifyContent: isSelf ? 'flex-end' : 'flex-start', marginBottom: 16, gap: 8 }}>
{!isSelf && (
<div style={{ width: 32, height: 32, borderRadius: 16, background: T.priL, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ fontFamily: T.serif, fontSize: 13, fontWeight: 700, color: T.pri }}></span>
</div>
)}
<div style={{ maxWidth: '70%' }}>
<div style={{
background: isSelf ? T.pri : T.card,
borderRadius: isSelf ? '16px 16px 4px 16px' : '16px 16px 16px 4px',
padding: '12px 16px',
boxShadow: '0 1px 4px rgba(45,42,38,0.06)',
}}>
<span style={{ fontSize: 15, color: isSelf ? '#fff' : T.tx, lineHeight: 1.6 }}>{msg.text}</span>
</div>
<div style={{ fontSize: 11, color: T.tx3, marginTop: 4, textAlign: isSelf ? 'right' : 'left' }}>{msg.time}</div>
</div>
</div>
);
})}
</div>
{/* 输入栏 */}
<div style={{ background: T.card, borderTop: `1px solid ${T.bdL}`, padding: '10px 16px 38px', display: 'flex', gap: 10, alignItems: 'center', flexShrink: 0 }}>
<div style={{ flex: 1, height: 40, background: T.bg, borderRadius: 20, border: `1.5px solid ${T.bd}`, display: 'flex', alignItems: 'center', padding: '0 14px' }}>
<span style={{ fontSize: 15, color: T.tx3 }}>输入消息...</span>
</div>
<div style={{ width: 40, height: 40, borderRadius: 20, background: T.pri, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, boxShadow: '0 2px 6px rgba(196,98,58,0.3)' }}>
<span style={{ color: '#fff', fontSize: 14, fontWeight: 600 }}></span>
</div>
</div>
</div>
);
}
// ─── 渲染 ───
function App() {
return (
<div className="flow">
<div className="flow-step">
<span className="flow-label">咨询列表</span>
<IosFrame time="9:41" battery={85}>
<ConsultList />
</IosFrame>
</div>
<div className="flow-arrow"></div>
<div className="flow-step">
<span className="flow-label">聊天详情</span>
<IosFrame time="9:41" battery={85}>
<ChatDetail />
</IosFrame>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>

View File

@@ -0,0 +1,578 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HMS 小程序重构 — 首页</title>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #1a1a1a; font-family: -apple-system, 'PingFang SC', sans-serif; display: flex; flex-direction: column; align-items: center; padding: 40px 20px; gap: 24px; }
.page-title { color: #999; font-size: 13px; letter-spacing: 0.15em; text-transform: uppercase; }
.note { color: #666; font-size: 12px; max-width: 600px; text-align: center; line-height: 1.8; }
.screens { display: flex; gap: 48px; flex-wrap: wrap; justify-content: center; align-items: flex-start; }
.screen-wrap { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.screen-label { color: #888; font-size: 12px; font-style: italic; }
</style>
</head>
<body>
<div class="page-title">HMS 小程序重构 · 首页设计</div>
<div class="note">设计假设:保持温润东方风设计系统,提升留白节奏与视觉层级。待办融入首页智能提醒卡片,不新增独立 Tab。</div>
<div id="root"></div>
<script type="text/babel">
// ─── iOS 设备框 ───
const iosFrameStyles = {
wrapper: { display: 'inline-block', padding: 12, background: '#000', borderRadius: 60, boxShadow: '0 0 0 2px #1f2937, 0 20px 60px rgba(0,0,0,0.3)', position: 'relative' },
screen: { position: 'relative', borderRadius: 48, overflow: 'hidden', background: '#fff' },
statusBar: { position: 'absolute', top: 0, left: 0, right: 0, height: 54, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 32px', fontSize: 16, fontWeight: 600, fontFamily: '-apple-system, "SF Pro Text", sans-serif', zIndex: 20, pointerEvents: 'none' },
dynamicIsland: { position: 'absolute', top: 12, left: '50%', transform: 'translateX(-50%)', width: 124, height: 36, background: '#000', borderRadius: 999, zIndex: 30 },
content: { position: 'absolute', top: 54, left: 0, right: 0, bottom: 34, overflow: 'auto' },
homeIndicator: { position: 'absolute', bottom: 10, left: '50%', transform: 'translateX(-50%)', width: 140, height: 5, background: 'rgba(0,0,0,0.3)', borderRadius: 999, zIndex: 10 },
};
function IosFrame({ children, width = 393, height = 852, time = '9:41', battery = 85 }) {
return (
<div style={iosFrameStyles.wrapper}>
<div style={{ ...iosFrameStyles.screen, width, height }}>
<div style={{ ...iosFrameStyles.statusBar, color: '#000' }}>
<span>{time}</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<svg width="16" height="12" viewBox="0 0 16 12" fill="none"><path d="M8 11.5a1 1 0 100-2 1 1 0 000 2z" fill="#000"/><path d="M3 7.5a7 7 0 0110 0" stroke="#000" strokeWidth="1.3" fill="none" strokeLinecap="round"/><path d="M1 4.5a11 11 0 0114 0" stroke="#000" strokeWidth="1.3" fill="none" strokeLinecap="round" opacity="0.7"/></svg>
<div style={{ width: 26, height: 12, border: '1.5px solid #000', borderRadius: 3, padding: 1, position: 'relative' }}>
<div style={{ width: `${battery}%`, height: '100%', background: '#000', borderRadius: 1 }} />
</div>
</div>
</div>
<div style={iosFrameStyles.dynamicIsland} />
<div style={iosFrameStyles.content}>{children}</div>
<div style={iosFrameStyles.homeIndicator} />
</div>
</div>
);
}
// ─── 设计 Token ───
const T = {
pri: '#C4623A', priL: '#F0DDD4', priD: '#8B3E1F',
bg: '#F5F0EB', card: '#FFFFFF', surface: '#EDE8E2',
tx: '#2D2A26', tx2: '#5A554F', tx3: '#78716C',
bd: '#E8E2DC', bdL: '#F0EBE5',
acc: '#5B7A5E', accL: '#E8F0E8',
wrn: '#C4873A', wrnL: '#FFF3E0',
dan: '#B54A4A', danL: '#FDEAEA',
serif: "Georgia, 'Times New Roman', serif",
sans: "-apple-system, 'PingFang SC', sans-serif",
r: 16, rSm: 12, rXs: 8,
};
// ─── 首页:设计方案 A当前风格优化 ───
function HomeA() {
return (
<div style={{ height: '100%', background: T.bg, padding: '20px 20px 100px', overflowY: 'auto' }}>
{/* 问候区 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 24 }}>
<div>
<div style={{ fontSize: 26, fontWeight: 700, color: T.tx, fontFamily: T.serif }}>上午好张三</div>
<div style={{ fontSize: 14, color: T.tx3, marginTop: 4 }}>5月8日 周四</div>
</div>
<div style={{ width: 44, height: 44, borderRadius: 22, background: T.priL, display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}>
<span style={{ fontSize: 18, color: T.priD }}></span>
<div style={{ position: 'absolute', top: 2, right: 2, width: 8, height: 8, borderRadius: 4, background: T.dan }} />
</div>
</div>
{/* 今日体征进度 */}
<div style={{ background: T.card, borderRadius: T.r, padding: 20, boxShadow: '0 2px 12px rgba(45,42,38,0.06)', marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
{/* 进度环 */}
<div style={{ width: 64, height: 64, position: 'relative', flexShrink: 0 }}>
<svg width="64" height="64" viewBox="0 0 64 64">
<circle cx="32" cy="32" r="28" fill="none" stroke={T.bd} strokeWidth="4" />
<circle cx="32" cy="32" r="28" fill="none" stroke={T.pri} strokeWidth="4" strokeDasharray={`${0.75 * 176} ${0.25 * 176}`} strokeDashoffset="0" strokeLinecap="round" transform="rotate(-90 32 32)" />
</svg>
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: T.pri }}>3/4</div>
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 16, fontWeight: 600, color: T.tx, marginBottom: 8 }}>今日已记录 3 项体征</div>
<div style={{ display: 'flex', gap: 6 }}>
{['血压 ✓','心率 ✓','血糖 ✓','体重'].map((t, i) => (
<span key={i} style={{ fontSize: 11, padding: '3px 8px', borderRadius: 999, background: i < 3 ? T.accL : T.surface, color: i < 3 ? T.acc : T.tx3, fontWeight: 500 }}>{t}</span>
))}
</div>
</div>
</div>
</div>
{/* 体征 2x2 */}
<div style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.tx, marginBottom: 12 }}>今日体征</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, marginBottom: 20 }}>
{[
{ label: '血压', value: '130/85', unit: 'mmHg', status: '偏高', statusType: 'wrn' },
{ label: '心率', value: '72', unit: 'bpm', status: '正常', statusType: 'acc' },
{ label: '血糖', value: '5.6', unit: 'mmol/L', status: '正常', statusType: 'acc' },
{ label: '体重', value: '—', unit: 'kg', status: '未记录', statusType: 'empty' },
].map((v, i) => (
<div key={i} style={{ background: T.card, borderRadius: T.r, padding: '14px 16px', boxShadow: '0 1px 4px rgba(45,42,38,0.04)' }}>
<div style={{ fontSize: 13, color: T.tx2, marginBottom: 6 }}>{v.label}</div>
<div style={{ display: 'flex', alignItems: 'baseline', marginBottom: 6 }}>
<span style={{ fontFamily: T.serif, fontSize: 30, fontWeight: 700, color: T.tx, lineHeight: 1 }}>{v.value}</span>
<span style={{ fontSize: 12, color: T.tx3, marginLeft: 3 }}>{v.unit}</span>
</div>
<span style={{ fontSize: 11, padding: '2px 8px', borderRadius: 999, fontWeight: 500,
background: v.statusType === 'acc' ? T.accL : v.statusType === 'wrn' ? T.wrnL : T.surface,
color: v.statusType === 'acc' ? T.acc : v.statusType === 'wrn' ? T.wrn : T.tx3
}}>{v.status}</span>
</div>
))}
</div>
{/* 智能提醒卡片 */}
<div style={{ background: `linear-gradient(135deg, ${T.pri} 0%, ${T.priD} 100%)`, borderRadius: T.r, padding: 18, marginBottom: 16, color: '#fff' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
<span style={{ fontSize: 15, fontWeight: 600 }}>智能提醒</span>
<span style={{ fontSize: 12, opacity: 0.7 }}>2 条待处理</span>
</div>
{[
{ text: '血压连续 3 日偏高,建议预约复查', type: 'AI 建议' },
{ text: '明日 09:00 有预约 — 李医生门诊', type: '预约' },
].map((r, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 0', borderTop: i === 1 ? '1px solid rgba(255,255,255,0.15)' : 'none' }}>
<span style={{ fontSize: 10, padding: '2px 6px', borderRadius: 4, background: 'rgba(255,255,255,0.2)', fontWeight: 500 }}>{r.type}</span>
<span style={{ fontSize: 13, flex: 1 }}>{r.text}</span>
<span style={{ opacity: 0.5 }}></span>
</div>
))}
</div>
{/* 快捷操作 */}
<div style={{ display: 'flex', gap: 10 }}>
<div style={{ flex: 1, height: 52, borderRadius: 14, background: T.pri, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 17, fontWeight: 600 }}>记录体征</div>
<div style={{ flex: 1, height: 52, borderRadius: 14, background: 'transparent', border: `2px solid ${T.pri}`, display: 'flex', alignItems: 'center', justifyContent: 'center', color: T.pri, fontSize: 17, fontWeight: 600 }}>预约挂号</div>
</div>
{/* 底部 TabBar 占位 */}
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 80, background: '#fff', borderTop: `1px solid ${T.bdL}`, display: 'flex', alignItems: 'center', justifyContent: 'space-around', paddingBottom: 10 }}>
{['首页','健康','消息','我的'].map((t, i) => (
<div key={i} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2, color: i === 0 ? T.pri : T.tx3, fontSize: 10 }}>
<div style={{ width: 24, height: 24, borderRadius: 6, background: i === 0 ? T.pri : T.surface, display: 'flex', alignItems: 'center', justifyContent: 'center', color: i === 0 ? '#fff' : T.tx3, fontSize: 12 }} />
<span>{t}</span>
</div>
))}
</div>
</div>
);
}
// ─── 健康页 ───
function HealthA() {
const [tab, setTab] = React.useState(0);
const vitalTabs = ['血压', '心率', '血糖', '体重'];
const trendData = [132, 128, 135, 130, 138, 126, 130];
const days = ['一','二','三','四','五','六','日'];
const maxV = Math.max(...trendData);
const threshold = 140;
return (
<div style={{ height: '100%', background: T.bg, overflowY: 'auto' }}>
<div style={{ padding: '20px 20px 100px' }}>
{/* 页头 */}
<div style={{ fontFamily: T.serif, fontSize: 26, fontWeight: 700, color: T.tx, marginBottom: 20 }}>健康数据</div>
{/* AI 建议卡片 — 温暖提示风格 */}
<div style={{ background: T.accL, borderRadius: T.r, padding: 16, marginBottom: 20, borderLeft: `4px solid ${T.acc}` }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<span style={{ fontSize: 14, fontWeight: 600, color: T.acc }}>AI 健康建议</span>
<span style={{ fontSize: 12, color: T.acc, opacity: 0.7 }}>1 条待查看</span>
</div>
<div style={{ fontSize: 13, color: T.tx2, lineHeight: 1.6 }}> 7 日收缩压呈上升趋势建议关注饮食并预约复查</div>
</div>
{/* 类型 Tab */}
<div style={{ display: 'flex', gap: 8, marginBottom: 20 }}>
{vitalTabs.map((t, i) => (
<div key={i} onClick={() => setTab(i)} style={{
flex: 1, height: 44, borderRadius: T.rSm,
background: tab === i ? T.pri : T.surface,
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', transition: 'all 0.2s',
boxShadow: tab === i ? '0 2px 8px rgba(196,98,58,0.25)' : 'none',
}}>
<span style={{ fontSize: 15, fontWeight: 600, color: tab === i ? '#fff' : T.tx2 }}>{t}</span>
</div>
))}
</div>
{/* 录入区 */}
<div style={{ background: T.card, borderRadius: T.r, padding: 20, boxShadow: '0 2px 12px rgba(45,42,38,0.06)', marginBottom: 20 }}>
{tab === 0 && (
<div>
<div style={{ marginBottom: 16 }}>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 6 }}>收缩压高压</div>
<div style={{ height: 56, background: T.bg, border: `2px solid ${T.bd}`, borderRadius: T.rSm, display: 'flex', alignItems: 'center', padding: '0 16px' }}>
<span style={{ fontFamily: T.serif, fontSize: 28, fontWeight: 700, color: T.tx }}>130</span>
<span style={{ fontSize: 13, color: T.tx3, marginLeft: 6 }}>mmHg</span>
</div>
</div>
<div style={{ marginBottom: 16 }}>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 6 }}>舒张压低压</div>
<div style={{ height: 56, background: T.bg, border: `2px solid ${T.bd}`, borderRadius: T.rSm, display: 'flex', alignItems: 'center', padding: '0 16px' }}>
<span style={{ fontFamily: T.serif, fontSize: 28, fontWeight: 700, color: T.tx }}>85</span>
<span style={{ fontSize: 13, color: T.tx3, marginLeft: 6 }}>mmHg</span>
</div>
</div>
<div style={{ fontSize: 12, color: T.tx3, lineHeight: 1.6 }}>参考范围收缩压 90-140 / 舒张压 60-90 mmHg</div>
</div>
)}
{tab === 1 && (
<div>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 6 }}>心率</div>
<div style={{ height: 56, background: T.bg, border: `2px solid ${T.bd}`, borderRadius: T.rSm, display: 'flex', alignItems: 'center', padding: '0 16px' }}>
<span style={{ fontFamily: T.serif, fontSize: 28, fontWeight: 700, color: T.tx }}>72</span>
<span style={{ fontSize: 13, color: T.tx3, marginLeft: 6 }}>bpm</span>
</div>
<div style={{ fontSize: 12, color: T.tx3, marginTop: 8 }}>参考范围60-100 bpm</div>
</div>
)}
{tab === 2 && (
<div>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 6 }}>血糖值</div>
<div style={{ height: 56, background: T.bg, border: `2px solid ${T.bd}`, borderRadius: T.rSm, display: 'flex', alignItems: 'center', padding: '0 16px' }}>
<span style={{ fontFamily: T.serif, fontSize: 28, fontWeight: 700, color: T.tx }}>5.6</span>
<span style={{ fontSize: 13, color: T.tx3, marginLeft: 6 }}>mmol/L</span>
</div>
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
<div style={{ flex: 1, height: 40, borderRadius: T.rSm, background: T.pri, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ fontSize: 14, fontWeight: 600, color: '#fff' }}>空腹</span>
</div>
<div style={{ flex: 1, height: 40, borderRadius: T.rSm, background: T.surface, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ fontSize: 14, fontWeight: 600, color: T.tx2 }}>餐后 2h</span>
</div>
</div>
</div>
)}
{tab === 3 && (
<div>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 6 }}>体重</div>
<div style={{ height: 56, background: T.bg, border: `2px solid ${T.bd}`, borderRadius: T.rSm, display: 'flex', alignItems: 'center', padding: '0 16px' }}>
<span style={{ fontFamily: T.serif, fontSize: 28, fontWeight: 700, color: T.tx, opacity: 0.3 }}></span>
<span style={{ fontSize: 13, color: T.tx3, marginLeft: 6 }}>kg</span>
</div>
</div>
)}
{/* 保存按钮 */}
<div style={{ height: 52, borderRadius: 14, background: T.pri, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 17, fontWeight: 600, marginTop: 20, boxShadow: '0 2px 8px rgba(196,98,58,0.25)' }}>保存</div>
</div>
{/* 趋势图 */}
<div style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.tx, marginBottom: 12 }}> 7 天趋势</div>
<div style={{ background: T.card, borderRadius: T.r, padding: 20, boxShadow: '0 1px 4px rgba(45,42,38,0.04)' }}>
{/* 阈值线 + 柱状图 */}
<div style={{ position: 'relative', height: 140, background: T.bg, borderRadius: T.rSm, padding: '12px 8px', display: 'flex', alignItems: 'flex-end', gap: 0 }}>
{/* 阈值标线 */}
<div style={{ position: 'absolute', left: 8, right: 8, bottom: `${12 + (threshold / maxV) * 100}px`, borderTop: `1.5px dashed ${T.wrn}`, opacity: 0.6 }} />
<div style={{ position: 'absolute', right: 12, bottom: `${18 + (threshold / maxV) * 100}px`, fontSize: 10, color: T.wrn, opacity: 0.7 }}>140</div>
{trendData.map((v, i) => {
const hPct = Math.max(10, (v / maxV) * 100);
const isWarn = v >= threshold;
return (
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', height: '100%', justifyContent: 'flex-end' }}>
<div style={{ width: 28, borderRadius: '6px 6px 0 0', minHeight: 8, height: `${hPct}%`, background: isWarn ? T.wrn : T.pri, opacity: isWarn ? 1 : 0.7, transition: 'height 0.3s' }} />
<span style={{ fontSize: 11, color: T.tx3, marginTop: 6 }}>{days[i]}</span>
</div>
);
})}
</div>
<div style={{ display: 'flex', justifyContent: 'center', gap: 16, marginTop: 10 }}>
<span style={{ fontSize: 11, color: T.tx3 }}><span style={{ display: 'inline-block', width: 10, height: 10, borderRadius: 2, background: T.pri, marginRight: 4, verticalAlign: 'middle' }} />正常</span>
<span style={{ fontSize: 11, color: T.tx3 }}><span style={{ display: 'inline-block', width: 10, height: 10, borderRadius: 2, background: T.wrn, marginRight: 4, verticalAlign: 'middle' }} />偏高</span>
<span style={{ fontSize: 11, color: T.tx3 }}><span style={{ display: 'inline-block', width: 10, height: 0, borderTop: '1.5px dashed ' + T.wrn, marginRight: 4, verticalAlign: 'middle' }} />阈值</span>
</div>
</div>
{/* 资讯入口 */}
<div style={{ background: T.card, borderRadius: T.r, padding: 16, marginTop: 16, boxShadow: '0 1px 4px rgba(45,42,38,0.04)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: 15, color: T.tx, fontWeight: 500 }}>最新健康资讯</span>
<span style={{ color: T.tx3 }}></span>
</div>
</div>
{/* TabBar */}
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 80, background: '#fff', borderTop: `1px solid ${T.bdL}`, display: 'flex', alignItems: 'center', justifyContent: 'space-around', paddingBottom: 10 }}>
{['首页','健康','消息','我的'].map((t, i) => (
<div key={i} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2, color: i === 1 ? T.pri : T.tx3, fontSize: 10 }}>
<div style={{ width: 24, height: 24, borderRadius: 6, background: i === 1 ? T.pri : T.surface, display: 'flex', alignItems: 'center', justifyContent: 'center', color: i === 1 ? '#fff' : T.tx3, fontSize: 12 }} />
<span>{t}</span>
</div>
))}
</div>
</div>
);
}
// ─── 消息页 ───
function MessagesA() {
const [tab, setTab] = React.useState(0);
const tabs = ['咨询', '通知'];
const consultations = [
{ id: 1, type: 'online', doctor: '王医生 · 心内科', preview: '您的检查报告已出,建议...', time: '10 分钟前', unread: 2 },
{ id: 2, type: 'online', doctor: '李医生 · 肾内科', preview: '复查结果整体平稳', time: '昨天', unread: 0 },
{ id: 3, type: 'offline', doctor: '张医生 · 全科', preview: '门诊随访已完成', time: '3 天前', unread: 0 },
];
const notifications = [
{ id: 1, title: '预约确认', desc: '明日 09:00 李医生门诊已确认', time: '2 小时前', type: 'appointment', read: false },
{ id: 2, title: '体征异常提醒', desc: '今日收缩压偏高138 mmHg', time: '今天 08:30', type: 'alert', read: false },
{ id: 3, title: '随访到期', desc: '肾功能复查随访将于 5/12 到期', time: '昨天', type: 'followup', read: true },
{ id: 4, title: '签到成功', desc: '连续打卡 7 天,获得 50 积分', time: '昨天', type: 'points', read: true },
{ id: 5, title: '报告已生成', desc: '您的 5 月体检报告已生成', time: '3 天前', type: 'report', read: true },
];
const typeIcon = { appointment: '约', alert: '警', followup: '随', points: '分', report: '报' };
const typeBg = { appointment: T.priL, alert: T.wrnL, followup: T.accL, points: T.priL, report: T.accL };
const typeColor = { appointment: T.pri, alert: T.wrn, followup: T.acc, points: T.pri, report: T.acc };
return (
<div style={{ height: '100%', background: T.bg, overflowY: 'auto' }}>
<div style={{ padding: '20px 20px 100px' }}>
{/* 页头 */}
<div style={{ fontFamily: T.serif, fontSize: 26, fontWeight: 700, color: T.tx, marginBottom: 20 }}>消息</div>
{/* Tab */}
<div style={{ display: 'flex', gap: 0, marginBottom: 0, background: T.surface, borderRadius: T.rSm, padding: 3 }}>
{tabs.map((t, i) => (
<div key={i} onClick={() => setTab(i)} style={{
flex: 1, height: 40, borderRadius: T.rXs,
background: tab === i ? T.card : 'transparent',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', boxShadow: tab === i ? '0 1px 4px rgba(45,42,38,0.06)' : 'none',
}}>
<span style={{ fontSize: 15, fontWeight: 600, color: tab === i ? T.tx : T.tx3 }}>{t}</span>
{i === 0 && consultations.filter(c => c.unread > 0).length > 0 && (
<span style={{ marginLeft: 6, minWidth: 16, height: 16, borderRadius: 8, background: T.dan, color: '#fff', fontSize: 10, fontWeight: 600, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0 4px' }}>2</span>
)}
</div>
))}
</div>
<div style={{ height: 12 }} />
{/* 咨询列表 */}
{tab === 0 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{consultations.map((c) => (
<div key={c.id} style={{ background: T.card, borderRadius: T.r, padding: 16, boxShadow: '0 1px 4px rgba(45,42,38,0.04)', display: 'flex', gap: 12, alignItems: 'center', opacity: c.unread > 0 ? 1 : 0.7 }}>
{/* 头像 */}
<div style={{ width: 44, height: 44, borderRadius: 22, background: c.unread > 0 ? T.priL : T.surface, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: c.unread > 0 ? T.pri : T.tx3 }}>{c.doctor.charAt(0)}</span>
</div>
{/* 内容 */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
<span style={{ fontSize: 15, fontWeight: 600, color: T.tx }}>{c.doctor}</span>
<span style={{ fontSize: 12, color: T.tx3 }}>{c.time}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: 13, color: T.tx2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1, marginRight: 8 }}>{c.preview}</span>
{c.unread > 0 && (
<span style={{ minWidth: 18, height: 18, borderRadius: 9, background: T.dan, color: '#fff', fontSize: 11, fontWeight: 600, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0 4px', flexShrink: 0 }}>{c.unread}</span>
)}
</div>
</div>
</div>
))}
</div>
)}
{/* 通知列表 */}
{tab === 1 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{notifications.map((n) => (
<div key={n.id} style={{ background: T.card, borderRadius: T.r, padding: 16, boxShadow: '0 1px 4px rgba(45,42,38,0.04)', display: 'flex', gap: 12, alignItems: 'flex-start', opacity: n.read ? 0.65 : 1 }}>
{/* 类型图标 */}
<div style={{ width: 36, height: 36, borderRadius: T.rSm, background: typeBg[n.type], display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: typeColor[n.type] }}>{typeIcon[n.type]}</span>
</div>
{/* 内容 */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
<span style={{ fontSize: 15, fontWeight: n.read ? 400 : 600, color: T.tx }}>{n.title}</span>
<span style={{ fontSize: 12, color: T.tx3, flexShrink: 0, marginLeft: 8 }}>{n.time}</span>
</div>
<span style={{ fontSize: 13, color: T.tx2, lineHeight: 1.5, display: 'block' }}>{n.desc}</span>
</div>
{/* 未读点 */}
{!n.read && <div style={{ width: 8, height: 8, borderRadius: 4, background: T.pri, flexShrink: 0, marginTop: 6 }} />}
</div>
))}
</div>
)}
</div>
{/* TabBar */}
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 80, background: '#fff', borderTop: `1px solid ${T.bdL}`, display: 'flex', alignItems: 'center', justifyContent: 'space-around', paddingBottom: 10 }}>
{['首页','健康','消息','我的'].map((t, i) => (
<div key={i} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2, color: i === 2 ? T.pri : T.tx3, fontSize: 10 }}>
<div style={{ width: 24, height: 24, borderRadius: 6, background: i === 2 ? T.pri : T.surface, display: 'flex', alignItems: 'center', justifyContent: 'center', color: i === 2 ? '#fff' : T.tx3, fontSize: 12, position: 'relative' }}>
{i === 2 && <div style={{ position: 'absolute', top: -2, right: -2, width: 8, height: 8, borderRadius: 4, background: T.dan, border: '1.5px solid #fff' }} />}
</div>
<span>{t}</span>
</div>
))}
</div>
</div>
);
}
// ─── 个人中心页 ───
function ProfileA() {
const menuGroups = [
{
title: '健康管理',
items: [
{ label: '健康记录', icon: '健', bg: T.priL, color: T.pri },
{ label: '我的报告', icon: '报', bg: T.accL, color: T.acc },
{ label: 'AI 分析', icon: '智', bg: T.priL, color: T.pri },
{ label: '诊断记录', icon: '诊', bg: T.accL, color: T.acc },
{ label: '用药记录', icon: '药', bg: T.priL, color: T.pri },
],
},
{
title: '就诊服务',
items: [
{ label: '我的预约', icon: '约', bg: T.priL, color: T.pri },
{ label: '我的随访', icon: '随', bg: T.accL, color: T.acc },
{ label: '在线咨询', icon: '问', bg: T.priL, color: T.pri },
],
},
{
title: '透析管理',
items: [
{ label: '透析记录', icon: '透', bg: T.priL, color: T.pri },
{ label: '透析处方', icon: '方', bg: T.accL, color: T.acc },
{ label: '知情同意', icon: '知', bg: T.priL, color: T.pri },
],
},
{
title: '生活服务',
items: [
{ label: '积分商城', icon: '礼', bg: T.priL, color: T.pri },
{ label: '线下活动', icon: '活', bg: T.accL, color: T.acc },
],
},
{
title: '账号',
items: [
{ label: '就诊人管理', icon: '家', bg: T.priL, color: T.pri },
{ label: '设备同步', icon: '设', bg: T.surface, color: T.tx3 },
{ label: '设置', icon: '齿', bg: T.surface, color: T.tx3 },
],
},
];
return (
<div style={{ height: '100%', background: T.bg, overflowY: 'auto' }}>
<div style={{ padding: '20px 20px 100px' }}>
{/* 用户卡片 */}
<div style={{ background: T.card, borderRadius: T.r, padding: 20, boxShadow: '0 2px 12px rgba(45,42,38,0.06)', marginBottom: 16, display: 'flex', alignItems: 'center', gap: 16 }}>
<div style={{ width: 60, height: 60, borderRadius: 30, background: `linear-gradient(135deg, ${T.priL} 0%, ${T.pri} 100%)`, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ fontFamily: T.serif, fontSize: 28, fontWeight: 700, color: '#fff' }}></span>
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 22, fontWeight: 700, color: T.tx, fontFamily: T.serif, marginBottom: 2 }}>张三</div>
<div style={{ fontSize: 14, color: T.tx3 }}>138****1234</div>
</div>
<span style={{ color: T.tx3, fontSize: 16 }}></span>
</div>
{/* 积分 + 打卡 */}
<div style={{ display: 'flex', gap: 10, marginBottom: 24 }}>
<div style={{ flex: 1, background: T.card, borderRadius: T.r, padding: 16, textAlign: 'center', boxShadow: '0 1px 4px rgba(45,42,38,0.04)' }}>
<div style={{ fontFamily: T.serif, fontSize: 28, fontWeight: 700, color: T.pri, display: 'block' }}>1,280</div>
<div style={{ fontSize: 13, color: T.tx3, marginTop: 2 }}>健康积分</div>
</div>
<div style={{ flex: 1, background: T.card, borderRadius: T.r, padding: 16, textAlign: 'center', boxShadow: '0 1px 4px rgba(45,42,38,0.04)' }}>
<div style={{ fontFamily: T.serif, fontSize: 28, fontWeight: 700, color: T.acc, display: 'block' }}>7<span style={{ fontSize: 16, fontWeight: 400 }}></span></div>
<div style={{ fontSize: 13, color: T.tx3, marginTop: 2 }}>连续打卡</div>
</div>
</div>
{/* 分组菜单 */}
{menuGroups.map((group, gi) => (
<div key={gi} style={{ marginBottom: 14 }}>
<div style={{ fontSize: 14, fontWeight: 600, color: T.tx2, marginBottom: 8, paddingLeft: 4 }}>{group.title}</div>
<div style={{ background: T.card, borderRadius: T.r, overflow: 'hidden', boxShadow: '0 1px 4px rgba(45,42,38,0.04)' }}>
{group.items.map((item, ii) => (
<div key={ii} style={{
display: 'flex', alignItems: 'center', gap: 14, padding: '14px 16px',
borderBottom: ii < group.items.length - 1 ? `1px solid ${T.bdL}` : 'none',
}}>
<div style={{ width: 36, height: 36, borderRadius: T.rSm, background: item.bg, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: item.color }}>{item.icon}</span>
</div>
<span style={{ flex: 1, fontSize: 15, color: T.tx }}>{item.label}</span>
<span style={{ color: T.tx3, fontSize: 14 }}></span>
</div>
))}
</div>
</div>
))}
{/* 退出 */}
<div style={{ textAlign: 'center', padding: '16px 0' }}>
<span style={{ fontSize: 14, color: T.tx3 }}>退出登录</span>
</div>
</div>
{/* TabBar */}
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 80, background: '#fff', borderTop: `1px solid ${T.bdL}`, display: 'flex', alignItems: 'center', justifyContent: 'space-around', paddingBottom: 10 }}>
{['首页','健康','消息','我的'].map((t, i) => (
<div key={i} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2, color: i === 3 ? T.pri : T.tx3, fontSize: 10 }}>
<div style={{ width: 24, height: 24, borderRadius: 6, background: i === 3 ? T.pri : T.surface, display: 'flex', alignItems: 'center', justifyContent: 'center', color: i === 3 ? '#fff' : T.tx3, fontSize: 12 }} />
<span>{t}</span>
</div>
))}
</div>
</div>
);
}
// ─── 渲染 ───
function App() {
return (
<div className="screens">
<div className="screen-wrap">
<span className="screen-label">首页</span>
<IosFrame time="9:41" battery={85}>
<HomeA />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">健康数据</span>
<IosFrame time="9:41" battery={85}>
<HealthA />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">消息</span>
<IosFrame time="9:41" battery={85}>
<MessagesA />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">我的</span>
<IosFrame time="9:41" battery={85}>
<ProfileA />
</IosFrame>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>