Files
hms/docs/design/mp-redesign-appointment.html
iven df1d85bfde 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 生产部署配置
2026-05-13 23:29:42 +08:00

316 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>