- T40 UI 审计计划和结果文档(docs/qa/) - wiki 更新:miniprogram 设计系统合规审计记录 + index 关键数字更新 - 审计 V2 完整报告(docs/audits/v2/) - 讨论记录文档(docs/discussions/) - 设计规格和实施计划(docs/superpowers/) - 角色测试计划和结果(docs/qa/role-test-*) - Docker 生产部署配置
316 lines
17 KiB
HTML
316 lines
17 KiB
HTML
<!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>
|