docs(mp): 新增小程序全页面 HTML 原型 + UI 优化指南

- 新增 12 个核心页面原型(登录/首页/咨询/预约/商城/健康等)
- 新增医生端分包原型(核心 + 临床两个分包)
- 新增 AI 客服对话页原型
- 新增 MP UI 优化指南文档
- 更新 wiki 基础设施和小程序文档
This commit is contained in:
iven
2026-05-17 00:51:07 +08:00
parent 710b2e2423
commit aa27c5174c
93 changed files with 15506 additions and 70 deletions

View File

@@ -0,0 +1,462 @@
<!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>