Files
hms/docs/design/mp-03-appointment.html
iven aa27c5174c docs(mp): 新增小程序全页面 HTML 原型 + UI 优化指南
- 新增 12 个核心页面原型(登录/首页/咨询/预约/商城/健康等)
- 新增医生端分包原型(核心 + 临床两个分包)
- 新增 AI 客服对话页原型
- 新增 MP UI 优化指南文档
- 更新 wiki 基础设施和小程序文档
2026-05-17 00:51:07 +08:00

463 lines
20 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; }
.note { color: #666; font-size: 12px; max-width: 900px; text-align: center; line-height: 1.8; }
.screens { display: flex; gap: 40px; 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">预约列表 + 预约创建 + 预约详情 — 温润东方风设计系统,三屏并排展示完整挂号流程</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, darkStatus = false }) {
const statusColor = darkStatus ? '#fff' : '#000';
return (
<div style={iosFrameStyles.wrapper}>
<div style={{ ...iosFrameStyles.screen, width, height }}>
<div style={{ ...iosFrameStyles.statusBar, color: statusColor }}>
<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={statusColor}/><path d="M3 7.5a7 7 0 0110 0" stroke={statusColor} strokeWidth="1.3" fill="none" strokeLinecap="round"/><path d="M1 4.5a11 11 0 0114 0" stroke={statusColor} strokeWidth="1.3" fill="none" strokeLinecap="round" opacity="0.7"/></svg>
<div style={{ width: 26, height: 12, border: `1.5px solid ${statusColor}`, borderRadius: 3, padding: 1, position: 'relative' }}>
<div style={{ width: `${battery}%`, height: '100%', background: statusColor, 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,
};
// ─── 通用导航栏 ───
function NavBar({ title }) {
return (
<div style={{
height: 44, display: 'flex', alignItems: 'center', justifyContent: 'center',
borderBottom: `1px solid ${T.bdL}`, background: T.bg, position: 'relative',
}}>
<svg style={{ position: 'absolute', left: 16, top: '50%', transform: 'translateY(-50%)' }}
width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M15 19l-7-7 7-7" stroke={T.tx} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
<span style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.tx }}>{title}</span>
</div>
);
}
// ─── 状态标签 ───
function StatusTag({ label, variant }) {
const styles = {
pending: { bg: T.accL, color: T.acc },
done: { bg: T.surface, color: T.tx3 },
cancel: { bg: T.danL, color: T.dan },
};
const s = styles[variant] || styles.pending;
return (
<span style={{
display: 'inline-block', padding: '3px 10px', borderRadius: 20,
background: s.bg, color: s.color, fontSize: 12, fontWeight: 600,
}}>
{label}
</span>
);
}
// ─── 屏幕一:预约列表页 ───
function AppointmentList() {
const tabs = ['全部', '待就诊', '已完成', '已取消'];
const activeTab = 0;
const appointments = [
{ doctor: '李医生', dept: '肾内科', status: 'pending', statusLabel: '待就诊',
date: '5月10日 周六', time: '09:00 - 09:30', location: '门诊楼3层 心内科诊室' },
{ doctor: '王医生', dept: '心内科', status: 'done', statusLabel: '已完成',
date: '5月6日 周二', time: '14:00 - 14:30', location: '门诊楼2层 心内科诊室' },
{ doctor: '张医生', dept: '全科', status: 'cancel', statusLabel: '已取消',
date: '4月28日 周一', time: '10:00 - 10:30', location: '' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="我的预约" />
{/* 状态筛选 Tab */}
<div style={{
display: 'flex', gap: 8, padding: '14px 20px', background: T.bg,
}}>
{tabs.map((tab, i) => (
<div key={i} style={{
flex: 1, height: 36, borderRadius: 20, display: 'flex', alignItems: 'center',
justifyContent: 'center', fontSize: 14, fontWeight: 600,
background: i === activeTab ? T.pri : T.surface,
color: i === activeTab ? '#fff' : T.tx2,
cursor: 'pointer',
}}>
{tab}
</div>
))}
</div>
{/* 卡片列表 */}
<div style={{ flex: 1, overflow: 'auto', padding: '4px 20px 100px' }}>
{appointments.map((a, i) => (
<div key={i} style={{
background: T.card, borderRadius: T.r, padding: '18px 20px',
marginBottom: 12, boxShadow: '0 2px 12px rgba(45,42,38,0.06)',
}}>
{/* 头部:医生+科室 + 状态 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{
width: 40, height: 40, borderRadius: 20,
background: `linear-gradient(135deg, ${T.priL} 0%, ${T.pri} 100%)`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<span style={{ fontFamily: T.serif, fontSize: 16, fontWeight: 700, color: '#fff' }}>
{a.doctor[0]}
</span>
</div>
<div>
<div style={{ fontSize: 16, fontWeight: 600, color: T.tx, fontFamily: T.sans }}>
{a.doctor}
</div>
<div style={{ fontSize: 13, color: T.tx3, marginTop: 2 }}>{a.dept}</div>
</div>
</div>
<StatusTag label={a.statusLabel} variant={a.status} />
</div>
{/* 时间 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<rect x="3" y="4" width="18" height="18" rx="2" stroke={T.tx3} strokeWidth="1.5"/>
<path d="M3 10h18M16 2v4M8 2v4" stroke={T.tx3} strokeWidth="1.5" strokeLinecap="round"/>
</svg>
<span style={{ fontSize: 14, color: T.tx2 }}>{a.date} {a.time}</span>
</div>
{/* 地点 */}
{a.location && (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M12 13a3 3 0 100-6 3 3 0 000 6z" stroke={T.tx3} strokeWidth="1.5"/>
<path d="M12 22s-8-5.5-8-11a8 8 0 1116 0c0 5.5-8 11-8 11z" stroke={T.tx3} strokeWidth="1.5"/>
</svg>
<span style={{ fontSize: 14, color: T.tx2 }}>{a.location}</span>
</div>
)}
</div>
))}
</div>
{/* 浮动按钮 */}
<div style={{
position: 'absolute', bottom: 50, right: 24, width: 56, height: 56,
borderRadius: 28, background: T.pri, display: 'flex', alignItems: 'center',
justifyContent: 'center', boxShadow: `0 4px 20px rgba(196,98,58,0.4)`, zIndex: 5,
}}>
<svg width="26" height="26" viewBox="0 0 24 24" fill="none">
<path d="M12 5v14M5 12h14" stroke="#fff" strokeWidth="2.5" strokeLinecap="round"/>
</svg>
</div>
</div>
);
}
// ─── 屏幕二:预约创建页 ───
function AppointmentCreate() {
const timeSlots = {
morning: [
{ time: '09:00', selected: true },
{ time: '09:30', selected: false },
{ time: '10:00', selected: false },
],
afternoon: [
{ time: '14:00', selected: false },
{ time: '14:30', selected: false, disabled: true },
{ time: '15:00', selected: false, disabled: true },
],
};
function FormField({ label, placeholder, value, showArrow = true }) {
return (
<div style={{ marginBottom: 18 }}>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 6, fontWeight: 500 }}>{label}</div>
<div style={{
height: 56, background: T.card, border: `1.5px solid ${T.bd}`, borderRadius: T.r,
display: 'flex', alignItems: 'center', padding: '0 16px', justifyContent: 'space-between',
}}>
<span style={{ fontSize: 16, color: value ? T.tx : T.tx3 }}>{value || placeholder}</span>
{showArrow && (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M6 9l6 6 6-6" stroke={T.tx3} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)}
</div>
</div>
);
}
function TimeSlot({ time, selected, disabled }) {
return (
<div style={{
flex: 1, height: 44, borderRadius: T.rSm,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 15, fontWeight: 600,
background: disabled ? T.surface : selected ? T.pri : T.card,
color: disabled ? T.tx3 + '88' : selected ? '#fff' : T.tx2,
border: disabled ? 'none' : selected ? 'none' : `1.5px solid ${T.bd}`,
opacity: disabled ? 0.5 : 1,
}}>
{time}
</div>
);
}
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="预约挂号" />
<div style={{ flex: 1, overflow: 'auto', padding: '20px 20px 120px' }}>
<FormField label="选择科室" placeholder="请选择科室" />
<FormField label="选择医生" placeholder="请选择医生" />
<FormField label="选择日期" placeholder="请选择日期" />
{/* 时段选择 */}
<div style={{ marginBottom: 18 }}>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 6, fontWeight: 500 }}>选择时段</div>
{/* 上午 */}
<div style={{ marginBottom: 10 }}>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 8 }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" style={{ verticalAlign: 'middle', marginRight: 4 }}>
<circle cx="12" cy="12" r="5" stroke={T.wrn} strokeWidth="1.5"/>
<path d="M12 2v2M12 20v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M2 12h2M20 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" stroke={T.wrn} strokeWidth="1.5" strokeLinecap="round"/>
</svg>
上午
</div>
<div style={{ display: 'flex', gap: 10 }}>
{timeSlots.morning.map((s, i) => <TimeSlot key={i} {...s} />)}
</div>
</div>
{/* 下午 */}
<div>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 8 }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" style={{ verticalAlign: 'middle', marginRight: 4 }}>
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" stroke={T.tx3} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
下午
</div>
<div style={{ display: 'flex', gap: 10 }}>
{timeSlots.afternoon.map((s, i) => <TimeSlot key={i} {...s} />)}
</div>
</div>
</div>
<FormField label="就诊人" placeholder="请选择就诊人" value="张三" />
{/* 备注 */}
<div style={{ marginBottom: 18 }}>
<div style={{ fontSize: 13, color: T.tx3, marginBottom: 6, fontWeight: 500 }}>备注</div>
<div style={{
minHeight: 100, background: T.card, border: `1.5px solid ${T.bd}`, borderRadius: T.r,
padding: '14px 16px', fontSize: 16, color: T.tx3, lineHeight: 1.6,
}}>
请输入病情描述选填
</div>
</div>
</div>
{/* 底部按钮 */}
<div style={{
position: 'absolute', bottom: 34, left: 0, right: 0,
padding: '12px 20px', background: T.bg,
borderTop: `1px solid ${T.bdL}`,
}}>
<div style={{
height: 54, borderRadius: 16, background: T.pri,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontSize: 18, fontWeight: 600,
boxShadow: `0 4px 16px rgba(196,98,58,0.3)`,
}}>
确认预约
</div>
</div>
</div>
);
}
// ─── 屏幕三:预约详情页 ───
function AppointmentDetail() {
const infoItems = [
{ label: '科室', value: '肾内科' },
{ label: '医生', value: '李明华 主任医师' },
{ label: '日期', value: '2025年5月10日周六' },
{ label: '时间', value: '09:00 - 09:30' },
{ label: '地点', value: '门诊楼3层 心内科诊室305' },
{ label: '就诊人', value: '张三' },
];
return (
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
<NavBar title="预约详情" />
<div style={{ flex: 1, overflow: 'auto', padding: '16px 20px 100px' }}>
{/* 状态大卡片 */}
<div style={{
background: T.card, borderRadius: T.r, padding: '24px 20px',
boxShadow: '0 2px 12px rgba(45,42,38,0.06)', marginBottom: 16,
display: 'flex', flexDirection: 'column', alignItems: 'center',
}}>
{/* 状态图标 */}
<div style={{
width: 56, height: 56, borderRadius: 28, background: T.accL,
display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 12,
}}>
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
<rect x="3" y="4" width="18" height="18" rx="2" stroke={T.acc} strokeWidth="1.8"/>
<path d="M3 10h18M16 2v4M8 2v4" stroke={T.acc} strokeWidth="1.8" strokeLinecap="round"/>
<path d="M8 14l2 2 4-4" stroke={T.acc} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
<StatusTag label="待就诊" variant="pending" />
</div>
{/* 信息列表 */}
<div style={{
background: T.card, borderRadius: T.r, padding: '6px 20px',
boxShadow: '0 2px 12px rgba(45,42,38,0.06)', marginBottom: 16,
}}>
{infoItems.map((item, i) => (
<div key={i} style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '15px 0',
borderBottom: i < infoItems.length - 1 ? `1px solid ${T.bdL}` : 'none',
}}>
<span style={{ fontSize: 14, color: T.tx3, minWidth: 56 }}>{item.label}</span>
<span style={{ fontSize: 15, color: T.tx, fontWeight: 500, textAlign: 'right' }}>{item.value}</span>
</div>
))}
</div>
{/* 温馨提示 */}
<div style={{
background: T.accL, borderRadius: T.r, padding: '16px 18px',
display: 'flex', gap: 12,
}}>
<div style={{ flexShrink: 0, marginTop: 1 }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="10" stroke={T.acc} strokeWidth="1.5"/>
<path d="M12 16v-4M12 8h.01" stroke={T.acc} strokeWidth="2" strokeLinecap="round"/>
</svg>
</div>
<div>
<div style={{ fontSize: 14, fontWeight: 600, color: T.acc, marginBottom: 4 }}>温馨提示</div>
<div style={{ fontSize: 13, color: T.acc, lineHeight: 1.7, opacity: 0.85 }}>
请提前15分钟到达携带医保卡和既往病历
</div>
</div>
</div>
</div>
{/* 底部操作 */}
<div style={{
position: 'absolute', bottom: 34, left: 0, right: 0,
padding: '12px 20px', background: T.bg,
borderTop: `1px solid ${T.bdL}`,
display: 'flex', gap: 12,
}}>
<div style={{
flex: 1, height: 54, borderRadius: 16,
border: `1.5px solid ${T.dan}`, background: 'transparent',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: T.dan, fontSize: 16, fontWeight: 600,
}}>
取消预约
</div>
<div style={{
flex: 1, height: 54, borderRadius: 16, background: T.pri,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontSize: 16, fontWeight: 600,
boxShadow: `0 4px 16px rgba(196,98,58,0.3)`,
}}>
导航到科室
</div>
</div>
</div>
);
}
// ─── 渲染 ───
function App() {
return (
<div className="screens">
<div className="screen-wrap">
<span className="screen-label">预约列表</span>
<IosFrame time="9:41" battery={85}>
<AppointmentList />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">预约创建</span>
<IosFrame time="9:42" battery={84}>
<AppointmentCreate />
</IosFrame>
</div>
<div className="screen-wrap">
<span className="screen-label">预约详情</span>
<IosFrame time="9:43" battery={83}>
<AppointmentDetail />
</IosFrame>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>