fix(mp): DevTools 卡死 + 主包 2MB→766KB + 代码质量 4 项全通过
根因:主包 2MB 全量组件注入导致 DevTools 渲染引擎内存渐增, 叠加离线时固定 3s 抑制期后的请求洪泛。 修复: - app.config.ts 添加 lazyCodeLoading: requiredComponents 主包 2.0MB→766KB,taro.js 526→131KB,vendors.js 230→28KB - request.ts 离线抑制改为指数退避(3s→6s→12s→30s cap) 后端不可达时自动延长抑制,防止请求风暴 - SegmentTabs Tab 接口改为 readonly,修复 TS 编译错误 - AbortController polyfill 补齐小程序运行时缺失 - 健康首页/设备同步/健康档案/报告/设置页 UI 重构 - 文章页公开端点适配游客访问 - 健康首页 Swiper 间隔优化 4s→5s,动画 500→300ms
This commit is contained in:
BIN
docs/design/mp-device-sync-redesign-preview.png
Normal file
BIN
docs/design/mp-device-sync-redesign-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 272 KiB |
754
docs/design/mp-device-sync-redesign.html
Normal file
754
docs/design/mp-device-sync-redesign.html
Normal file
@@ -0,0 +1,754 @@
|
||||
<!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: 1200px; text-align: center; line-height: 1.8; }
|
||||
.screens { display: flex; gap: 32px; 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; }
|
||||
|
||||
/* 蓝牙脉冲动画 */
|
||||
@keyframes pulse-ring {
|
||||
0% { transform: scale(0.8); opacity: 0.6; }
|
||||
50% { transform: scale(1.3); opacity: 0; }
|
||||
100% { transform: scale(0.8); opacity: 0; }
|
||||
}
|
||||
@keyframes pulse-dot {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
}
|
||||
@keyframes connect-spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
@keyframes fade-in-up {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="page-title">HMS 小程序 · 设备同步(重新设计)</div>
|
||||
<div class="note">7 个状态屏幕:空闲 → 扫描中 → 设备列表 → 连接中 → 已连接(实时数据)→ 同步完成 → 错误状态</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 = 360, height = 780, time = '9:41', battery = 85, darkStatus = false }) {
|
||||
const c = darkStatus ? '#fff' : '#000';
|
||||
return (
|
||||
<div style={iosFrameStyles.wrapper}>
|
||||
<div style={{ ...iosFrameStyles.screen, width, height }}>
|
||||
<div style={{ ...iosFrameStyles.statusBar, color: c }}>
|
||||
<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={c}/><path d="M3 7.5a7 7 0 0110 0" stroke={c} strokeWidth="1.3" fill="none" strokeLinecap="round"/><path d="M1 4.5a11 11 0 0114 0" stroke={c} strokeWidth="1.3" fill="none" strokeLinecap="round" opacity="0.7"/></svg>
|
||||
<div style={{ width: 26, height: 12, border: `1.5px solid ${c}`, borderRadius: 3, padding: 1, position: 'relative' }}>
|
||||
<div style={{ width: `${battery}%`, height: '100%', background: c, 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,
|
||||
};
|
||||
|
||||
// ─── SVG 图标 ───
|
||||
function BluetoothIcon({ size = 24, color = T.pri }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||||
<path d="M6 7l8 8-4 4V3l4 4-8 8" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function HeartIcon({ size = 20, color = T.dan }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z" fill={color} opacity="0.15"/>
|
||||
<path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z" stroke={color} strokeWidth="1.5" fill="none"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckIcon({ size = 32, color = T.acc }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="11" fill={color} opacity="0.12"/>
|
||||
<path d="M8 12.5l2.5 2.5 5.5-5.5" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorIcon({ size = 32, color = T.dan }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="11" fill={color} opacity="0.12"/>
|
||||
<path d="M15 9l-6 6M9 9l6 6" stroke={color} strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 信号强度条 ───
|
||||
function SignalBars({ level = 3 }) {
|
||||
const bars = [4, 7, 10, 13];
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 2, height: 16 }}>
|
||||
{bars.map((h, i) => (
|
||||
<div key={i} style={{
|
||||
width: 3, height: h, borderRadius: 1,
|
||||
background: i < level ? T.acc : T.bd,
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 导航栏 ───
|
||||
function NavBar({ title, dark = false }) {
|
||||
return (
|
||||
<div style={{
|
||||
height: 44, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: dark ? T.pri : 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={dark ? '#fff' : T.tx} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
<span style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: dark ? '#fff' : T.tx }}>{title}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 设备类型标签 ───
|
||||
function DeviceTypeTag({ icon, label }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
background: T.card, border: `1px solid ${T.bdL}`,
|
||||
borderRadius: T.rXs, padding: '8px 12px',
|
||||
}}>
|
||||
{icon}
|
||||
<span style={{ fontSize: 13, color: T.tx2 }}>{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 屏幕一:空闲态 ───
|
||||
function IdleScreen() {
|
||||
return (
|
||||
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
|
||||
<NavBar title="设备同步" dark />
|
||||
|
||||
{/* Hero 区域 */}
|
||||
<div style={{
|
||||
background: `linear-gradient(135deg, ${T.pri} 0%, ${T.priD} 100%)`,
|
||||
padding: '32px 20px 28px',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
}}>
|
||||
{/* 蓝牙设备插图 */}
|
||||
<div style={{
|
||||
width: 72, height: 72, borderRadius: '50%',
|
||||
background: 'rgba(255,255,255,0.15)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
marginBottom: 16,
|
||||
}}>
|
||||
<BluetoothIcon size={36} color="#fff" />
|
||||
</div>
|
||||
<div style={{ fontFamily: T.serif, fontSize: 22, fontWeight: 700, color: '#fff', marginBottom: 6 }}>
|
||||
智能设备同步
|
||||
</div>
|
||||
<div style={{ fontSize: 14, color: 'rgba(255,255,255,0.75)', textAlign: 'center', lineHeight: 1.5 }}>
|
||||
连接蓝牙设备,自动采集健康数据
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, padding: '16px 16px 100px', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{/* 支持的设备类型 */}
|
||||
<div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: T.tx, marginBottom: 10, paddingLeft: 2 }}>
|
||||
支持的设备
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<DeviceTypeTag icon={<HeartIcon size={16} color={T.dan} />} label="心率手环" />
|
||||
<DeviceTypeTag icon={
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 21V3M8 6l4-3 4 3M8 18l4 3 4-3" stroke={T.pri} strokeWidth="1.5" strokeLinecap="round"/>
|
||||
<circle cx="12" cy="12" r="3" stroke={T.pri} strokeWidth="1.5"/>
|
||||
</svg>
|
||||
} label="血压计" />
|
||||
<DeviceTypeTag icon={
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2v6M12 22v-4M4.93 4.93l4.24 4.24M14.83 14.83l4.24 4.24M2 12h6M16 12h6" stroke={T.wrn} strokeWidth="1.5" strokeLinecap="round"/>
|
||||
<circle cx="12" cy="12" r="4" stroke={T.wrn} strokeWidth="1.5"/>
|
||||
</svg>
|
||||
} label="血糖仪" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 上次同步信息 */}
|
||||
<div style={{
|
||||
background: T.card, borderRadius: T.rSm,
|
||||
padding: '14px 16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
border: `1px solid ${T.bdL}`,
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{ width: 36, height: 36, borderRadius: '50%', background: T.accL, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<CheckIcon size={20} color={T.acc} />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 14, fontWeight: 500, color: T.tx }}>上次同步</div>
|
||||
<div style={{ fontSize: 12, color: T.tx3, marginTop: 2 }}>今天 08:30</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
background: T.accL, borderRadius: T.rXs,
|
||||
padding: '4px 10px', fontSize: 12, color: T.acc, fontWeight: 500,
|
||||
}}>
|
||||
12 条数据
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 待上传提示 */}
|
||||
<div style={{
|
||||
background: T.wrnL, borderRadius: T.rSm,
|
||||
padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 10,
|
||||
}}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 9v4M12 17h.01M12 2L2 22h20L12 2z" stroke={T.wrn} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
<span style={{ fontSize: 13, color: T.wrn, fontWeight: 500 }}>3 条数据待上传</span>
|
||||
</div>
|
||||
|
||||
{/* 扫描按钮 */}
|
||||
<div style={{
|
||||
background: T.pri, borderRadius: T.rSm,
|
||||
padding: '16px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
|
||||
boxShadow: `0 4px 16px rgba(196, 98, 58, 0.3)`,
|
||||
marginTop: 'auto',
|
||||
}}>
|
||||
<BluetoothIcon size={20} color="#fff" />
|
||||
<span style={{ color: '#fff', fontSize: 17, fontWeight: 600 }}>扫描附近设备</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 屏幕二:扫描中 ───
|
||||
function ScanningScreen() {
|
||||
return (
|
||||
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
|
||||
<NavBar title="设备同步" dark />
|
||||
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 40 }}>
|
||||
{/* 脉冲圆环 */}
|
||||
<div style={{ position: 'relative', width: 140, height: 140, marginBottom: 32 }}>
|
||||
<div style={{
|
||||
position: 'absolute', top: 0, left: 0, width: 140, height: 140,
|
||||
borderRadius: '50%', border: `2px solid ${T.priL}`,
|
||||
animation: 'pulse-ring 2s ease-out infinite',
|
||||
}} />
|
||||
<div style={{
|
||||
position: 'absolute', top: 15, left: 15, width: 110, height: 110,
|
||||
borderRadius: '50%', border: `2px solid ${T.priL}`,
|
||||
animation: 'pulse-ring 2s ease-out infinite 0.5s',
|
||||
}} />
|
||||
<div style={{
|
||||
position: 'absolute', top: 30, left: 30, width: 80, height: 80,
|
||||
borderRadius: '50%', background: T.priL,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
animation: 'pulse-dot 2s ease-in-out infinite',
|
||||
}}>
|
||||
<BluetoothIcon size={36} color={T.pri} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ fontFamily: T.serif, fontSize: 20, fontWeight: 700, color: T.tx, marginBottom: 8, textAlign: 'center' }}>
|
||||
正在搜索设备...
|
||||
</div>
|
||||
<div style={{ fontSize: 14, color: T.tx3, textAlign: 'center', lineHeight: 1.6 }}>
|
||||
请确保设备已开启蓝牙并靠近手机
|
||||
</div>
|
||||
|
||||
{/* 进度条 */}
|
||||
<div style={{
|
||||
width: 180, height: 3, borderRadius: 2, background: T.bdL,
|
||||
marginTop: 24, overflow: 'hidden',
|
||||
}}>
|
||||
<div style={{
|
||||
width: '60%', height: '100%', borderRadius: 2,
|
||||
background: `linear-gradient(90deg, ${T.priL}, ${T.pri})`,
|
||||
}} />
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: T.tx3, marginTop: 8 }}>已用时 6 秒</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 屏幕三:设备列表 ───
|
||||
function DeviceListScreen() {
|
||||
const devices = [
|
||||
{ name: 'Mi Band 8', type: '小米手环适配器', signal: 4, color: T.pri },
|
||||
{ name: 'AND UA-651', type: '血压计适配器', signal: 3, color: T.pri },
|
||||
{ name: 'Accu-Chek', type: '血糖仪适配器', signal: 2, color: T.wrn },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
|
||||
<NavBar title="设备同步" dark />
|
||||
|
||||
<div style={{ flex: 1, padding: '16px 16px 100px' }}>
|
||||
{/* 结果标题 */}
|
||||
<div style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
}}>
|
||||
<div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: T.tx }}>发现 {devices.length} 台设备</div>
|
||||
<div style={{ fontSize: 12, color: T.tx3, marginTop: 2 }}>点击设备名称开始连接</div>
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 13, color: T.pri, fontWeight: 500, cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', gap: 4,
|
||||
}}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M23 4v6h-6M1 20v-6h6" stroke={T.pri} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15" stroke={T.pri} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
重新扫描
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 设备卡片 */}
|
||||
{devices.map((d, i) => (
|
||||
<div key={i} style={{
|
||||
background: T.card, borderRadius: T.rSm,
|
||||
padding: '16px', marginBottom: 10,
|
||||
border: `1px solid ${T.bdL}`,
|
||||
display: 'flex', alignItems: 'center', gap: 14,
|
||||
}}>
|
||||
{/* 设备图标 */}
|
||||
<div style={{
|
||||
width: 44, height: 44, borderRadius: T.rSm,
|
||||
background: T.priL, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<BluetoothIcon size={22} color={T.pri} />
|
||||
</div>
|
||||
{/* 设备信息 */}
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 16, fontWeight: 600, color: T.tx }}>{d.name}</div>
|
||||
<div style={{ fontSize: 12, color: T.tx3, marginTop: 3 }}>{d.type}</div>
|
||||
</div>
|
||||
{/* 信号 + 箭头 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<SignalBars level={d.signal} />
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M9 18l6-6-6-6" stroke={T.tx3} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 未发现设备提示 */}
|
||||
<div style={{
|
||||
marginTop: 16, background: T.card, borderRadius: T.rSm,
|
||||
padding: '14px 16px', border: `1px dashed ${T.bd}`,
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
}}>
|
||||
<div style={{
|
||||
width: 32, height: 32, borderRadius: '50%', background: T.surface,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
||||
}}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="11" cy="11" r="8" stroke={T.tx3} strokeWidth="1.5"/>
|
||||
<path d="M21 21l-4.35-4.35" stroke={T.tx3} strokeWidth="1.5" strokeLinecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, color: T.tx2 }}>没有找到你的设备?</div>
|
||||
<div style={{ fontSize: 12, color: T.tx3, marginTop: 2 }}>确保设备已开机且蓝牙已开启</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 屏幕四:连接中 ───
|
||||
function ConnectingScreen() {
|
||||
return (
|
||||
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
|
||||
<NavBar title="设备同步" dark />
|
||||
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 40 }}>
|
||||
{/* 连接动画 */}
|
||||
<div style={{ position: 'relative', width: 100, height: 100, marginBottom: 28 }}>
|
||||
{/* 旋转环 */}
|
||||
<div style={{
|
||||
position: 'absolute', top: 0, left: 0, width: 100, height: 100,
|
||||
borderRadius: '50%', border: `3px solid ${T.bdL}`,
|
||||
borderTopColor: T.pri,
|
||||
animation: 'connect-spin 1s linear infinite',
|
||||
}} />
|
||||
{/* 中心图标 */}
|
||||
<div style={{
|
||||
position: 'absolute', top: 20, left: 20, width: 60, height: 60,
|
||||
borderRadius: '50%', background: T.priL,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<BluetoothIcon size={28} color={T.pri} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ fontFamily: T.serif, fontSize: 18, fontWeight: 700, color: T.tx, marginBottom: 6, textAlign: 'center' }}>
|
||||
正在连接 Mi Band 8
|
||||
</div>
|
||||
<div style={{ fontSize: 14, color: T.tx3, textAlign: 'center' }}>
|
||||
正在进行蓝牙配对...
|
||||
</div>
|
||||
|
||||
{/* 步骤指示 */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8, marginTop: 24,
|
||||
}}>
|
||||
<div style={{ width: 8, height: 8, borderRadius: '50%', background: T.acc }} />
|
||||
<span style={{ fontSize: 12, color: T.tx3 }}>发现设备</span>
|
||||
<div style={{ width: 24, height: 1, background: T.pri }} />
|
||||
<div style={{ width: 8, height: 8, borderRadius: '50%', background: T.pri, animation: 'pulse-dot 1s ease-in-out infinite' }} />
|
||||
<span style={{ fontSize: 12, color: T.pri, fontWeight: 500 }}>连接中</span>
|
||||
<div style={{ width: 24, height: 1, background: T.bd }} />
|
||||
<div style={{ width: 8, height: 8, borderRadius: '50%', background: T.bd }} />
|
||||
<span style={{ fontSize: 12, color: T.tx3 }}>同步数据</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 屏幕五:已连接 + 实时数据 ───
|
||||
function ConnectedScreen() {
|
||||
const readings = [
|
||||
{ type: '心率', value: '72', unit: 'bpm', color: T.dan, time: '刚刚' },
|
||||
{ type: '收缩压', value: '128', unit: 'mmHg', color: T.pri, time: '2分钟前' },
|
||||
{ type: '舒张压', value: '82', unit: 'mmHg', color: T.pri, time: '2分钟前' },
|
||||
{ type: '心率', value: '68', unit: 'bpm', color: T.dan, time: '5分钟前' },
|
||||
{ type: '心率', value: '74', unit: 'bpm', color: T.dan, time: '8分钟前' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
|
||||
<NavBar title="设备同步" dark />
|
||||
|
||||
<div style={{ flex: 1, padding: '16px 16px 100px', overflow: 'auto' }}>
|
||||
{/* 连接状态卡片 */}
|
||||
<div style={{
|
||||
background: `linear-gradient(135deg, ${T.acc} 0%, #4A6B4E 100%)`,
|
||||
borderRadius: T.r, padding: '16px',
|
||||
display: 'flex', alignItems: 'center', gap: 12,
|
||||
marginBottom: 16,
|
||||
}}>
|
||||
<div style={{
|
||||
width: 40, height: 40, borderRadius: '50%',
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<BluetoothIcon size={22} color="#fff" />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 16, fontWeight: 600, color: '#fff' }}>Mi Band 8</div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(255,255,255,0.7)', marginTop: 2 }}>已连接 · 正在采集数据</div>
|
||||
</div>
|
||||
<div style={{
|
||||
background: 'rgba(255,255,255,0.2)', borderRadius: T.rXs,
|
||||
padding: '4px 10px', fontSize: 12, color: '#fff',
|
||||
}}>
|
||||
实时
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 最新读数高亮 */}
|
||||
<div style={{
|
||||
background: T.card, borderRadius: T.r, padding: '20px',
|
||||
display: 'flex', alignItems: 'center', gap: 16,
|
||||
marginBottom: 16, boxShadow: '0 2px 12px rgba(45,42,38,0.08)',
|
||||
}}>
|
||||
<div style={{
|
||||
width: 52, height: 52, borderRadius: T.rSm,
|
||||
background: `${T.dan}10`, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<HeartIcon size={28} color={T.dan} />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 13, color: T.tx3 }}>心率 · 刚刚</div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4, marginTop: 4 }}>
|
||||
<span style={{ fontFamily: T.serif, fontSize: 36, fontWeight: 700, color: T.tx }}>72</span>
|
||||
<span style={{ fontSize: 14, color: T.tx3 }}>bpm</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 历史读数列表 */}
|
||||
<div style={{
|
||||
fontSize: 14, fontWeight: 600, color: T.tx, marginBottom: 10, paddingLeft: 2,
|
||||
}}>
|
||||
历史读数
|
||||
</div>
|
||||
<div style={{
|
||||
background: T.card, borderRadius: T.rSm, overflow: 'hidden',
|
||||
boxShadow: '0 1px 4px rgba(45,42,38,0.06)',
|
||||
}}>
|
||||
{readings.slice(1).map((r, i) => (
|
||||
<div key={i} style={{
|
||||
display: 'flex', alignItems: 'center', padding: '12px 16px',
|
||||
borderBottom: i < readings.length - 2 ? `1px solid ${T.bdL}` : 'none',
|
||||
}}>
|
||||
<div style={{ width: 90, fontSize: 14, color: T.tx2 }}>{r.type}</div>
|
||||
<div style={{ flex: 1, display: 'flex', alignItems: 'baseline', gap: 3 }}>
|
||||
<span style={{ fontFamily: T.serif, fontSize: 20, fontWeight: 700, color: T.tx }}>{r.value}</span>
|
||||
<span style={{ fontSize: 12, color: T.tx3 }}>{r.unit}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: T.tx3 }}>{r.time}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
textAlign: 'center', marginTop: 12, fontSize: 12, color: T.tx3,
|
||||
}}>
|
||||
已采集 {readings.length} 条数据
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div style={{ display: 'flex', gap: 10, marginTop: 20 }}>
|
||||
<div style={{
|
||||
flex: 1, background: T.pri, borderRadius: T.rSm,
|
||||
padding: '14px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
boxShadow: `0 4px 16px rgba(196, 98, 58, 0.3)`,
|
||||
}}>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
<span style={{ color: '#fff', fontSize: 16, fontWeight: 600 }}>上传数据</span>
|
||||
</div>
|
||||
<div style={{
|
||||
width: 52, background: T.danL, borderRadius: T.rSm,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M18 6L6 18M6 6l12 12" stroke={T.dan} strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 屏幕六:同步完成 ───
|
||||
function DoneScreen() {
|
||||
return (
|
||||
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
|
||||
<NavBar title="设备同步" dark />
|
||||
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '24px 20px 100px' }}>
|
||||
{/* 成功图标 */}
|
||||
<div style={{
|
||||
width: 80, height: 80, borderRadius: '50%', background: T.accL,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
marginBottom: 24,
|
||||
}}>
|
||||
<CheckIcon size={44} color={T.acc} />
|
||||
</div>
|
||||
|
||||
<div style={{ fontFamily: T.serif, fontSize: 24, fontWeight: 700, color: T.tx, marginBottom: 8 }}>
|
||||
同步完成
|
||||
</div>
|
||||
<div style={{ fontSize: 15, color: T.tx3, textAlign: 'center', lineHeight: 1.6 }}>
|
||||
数据已安全上传至健康管理平台
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div style={{
|
||||
display: 'flex', gap: 12, marginTop: 24, width: '100%',
|
||||
}}>
|
||||
<div style={{
|
||||
flex: 1, background: T.card, borderRadius: T.rSm, padding: '16px',
|
||||
textAlign: 'center', border: `1px solid ${T.bdL}`,
|
||||
}}>
|
||||
<div style={{ fontFamily: T.serif, fontSize: 28, fontWeight: 700, color: T.pri }}>5</div>
|
||||
<div style={{ fontSize: 12, color: T.tx3, marginTop: 4 }}>上传条数</div>
|
||||
</div>
|
||||
<div style={{
|
||||
flex: 1, background: T.card, borderRadius: T.rSm, padding: '16px',
|
||||
textAlign: 'center', border: `1px solid ${T.bdL}`,
|
||||
}}>
|
||||
<div style={{ fontFamily: T.serif, fontSize: 28, fontWeight: 700, color: T.tx }}>3</div>
|
||||
<div style={{ fontSize: 12, color: T.tx3, marginTop: 4 }}>数据类型</div>
|
||||
</div>
|
||||
<div style={{
|
||||
flex: 1, background: T.card, borderRadius: T.rSm, padding: '16px',
|
||||
textAlign: 'center', border: `1px solid ${T.bdL}`,
|
||||
}}>
|
||||
<div style={{ fontFamily: T.serif, fontSize: 22, fontWeight: 700, color: T.acc }}>100%</div>
|
||||
<div style={{ fontSize: 12, color: T.tx3, marginTop: 4 }}>成功率</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 完成按钮 */}
|
||||
<div style={{
|
||||
width: '100%', background: T.pri, borderRadius: T.rSm,
|
||||
padding: '16px', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
boxShadow: `0 4px 16px rgba(196, 98, 58, 0.3)`,
|
||||
marginTop: 32,
|
||||
}}>
|
||||
<span style={{ color: '#fff', fontSize: 17, fontWeight: 600 }}>完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 屏幕七:错误状态 ───
|
||||
function ErrorScreen() {
|
||||
return (
|
||||
<div style={{ height: '100%', background: T.bg, display: 'flex', flexDirection: 'column' }}>
|
||||
<NavBar title="设备同步" dark />
|
||||
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '24px 20px 100px' }}>
|
||||
{/* 错误图标 */}
|
||||
<div style={{
|
||||
width: 80, height: 80, borderRadius: '50%', background: T.danL,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
marginBottom: 24,
|
||||
}}>
|
||||
<ErrorIcon size={44} color={T.dan} />
|
||||
</div>
|
||||
|
||||
<div style={{ fontFamily: T.serif, fontSize: 22, fontWeight: 700, color: T.tx, marginBottom: 8 }}>
|
||||
连接失败
|
||||
</div>
|
||||
<div style={{ fontSize: 14, color: T.tx3, textAlign: 'center', lineHeight: 1.6, maxWidth: 260 }}>
|
||||
无法连接到 Mi Band 8,请检查设备是否在范围内并重试
|
||||
</div>
|
||||
|
||||
{/* 错误详情卡片 */}
|
||||
<div style={{
|
||||
width: '100%', background: T.card, borderRadius: T.rSm,
|
||||
padding: '16px', marginTop: 24, border: `1px solid ${T.bdL}`,
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 }}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke={T.tx3} strokeWidth="1.5"/>
|
||||
<path d="M12 16v.01M12 8v4" stroke={T.tx3} strokeWidth="1.5" strokeLinecap="round"/>
|
||||
</svg>
|
||||
<span style={{ fontSize: 13, fontWeight: 500, color: T.tx }}>错误详情</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: T.tx3, lineHeight: 1.7 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<span>错误码</span><span style={{ color: T.tx }}>BLE_TIMEOUT</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<span>设备</span><span style={{ color: T.tx }}>Mi Band 8</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span>时间</span><span style={{ color: T.tx }}>09:15:32</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 重试按钮 */}
|
||||
<div style={{
|
||||
width: '100%', background: T.pri, borderRadius: T.rSm,
|
||||
padding: '16px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
|
||||
boxShadow: `0 4px 16px rgba(196, 98, 58, 0.3)`,
|
||||
marginTop: 24,
|
||||
}}>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M23 4v6h-6M1 20v-6h6" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
<span style={{ color: '#fff', fontSize: 17, fontWeight: 600 }}>重新扫描</span>
|
||||
</div>
|
||||
|
||||
{/* 返回按钮 */}
|
||||
<div style={{
|
||||
width: '100%', borderRadius: T.rSm,
|
||||
padding: '14px', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
marginTop: 10, border: `1px solid ${T.bd}`,
|
||||
}}>
|
||||
<span style={{ color: T.tx2, fontSize: 16, fontWeight: 500 }}>返回</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 主渲染 ───
|
||||
const screens = [
|
||||
{ label: '空闲态', Component: IdleScreen },
|
||||
{ label: '扫描中', Component: ScanningScreen },
|
||||
{ label: '设备列表', Component: DeviceListScreen },
|
||||
{ label: '连接中', Component: ConnectingScreen },
|
||||
{ label: '已连接', Component: ConnectedScreen },
|
||||
{ label: '同步完成', Component: DoneScreen },
|
||||
{ label: '错误状态', Component: ErrorScreen },
|
||||
];
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="screens">
|
||||
{screens.map(({ label, Component }) => (
|
||||
<div className="screen-wrap" key={label}>
|
||||
<div className="screen-label">{label}</div>
|
||||
<IosFrame width={360} height={780}>
|
||||
<Component />
|
||||
</IosFrame>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
12
docs/design/mp-device-sync-redesign/META.yml
Normal file
12
docs/design/mp-device-sync-redesign/META.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
prototype: mp-device-sync-redesign.html
|
||||
source: docs/design/mp-device-sync-redesign.html
|
||||
variant: patient
|
||||
generated_at: "2026-05-23T12:00:00+08:00"
|
||||
tokens:
|
||||
matched: 23
|
||||
unmatched: 2
|
||||
components:
|
||||
total: 12
|
||||
mapped: 8
|
||||
new: 2
|
||||
interactions: 9
|
||||
246
docs/design/mp-device-sync-redesign/SPEC.md
Normal file
246
docs/design/mp-device-sync-redesign/SPEC.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# 设备同步页面 设计规格
|
||||
|
||||
> 来源: mp-device-sync-redesign.html | 平台: 小程序(患者端) | 页面数: 7 | 生成: 2026-05-23
|
||||
|
||||
## 页面索引
|
||||
|
||||
| 页面 | 截图 | 路由 |
|
||||
|------|------|------|
|
||||
| 空闲态 |  | pages/pkg-health/device-sync/index |
|
||||
| 扫描中 |  | pages/pkg-health/device-sync/index |
|
||||
| 设备列表 |  | pages/pkg-health/device-sync/index |
|
||||
| 连接中 |  | pages/pkg-health/device-sync/index |
|
||||
| 已连接 |  | pages/pkg-health/device-sync/index |
|
||||
| 同步完成 |  | pages/pkg-health/device-sync/index |
|
||||
| 错误状态 |  | pages/pkg-health/device-sync/index |
|
||||
|
||||
## 一、Token 映射
|
||||
|
||||
| 原型值 | 项目 Token | 状态 |
|
||||
|--------|-----------|------|
|
||||
| T.pri (#C4623A) | --tk-pri | ✅ |
|
||||
| T.priL (#F0DDD4) | --tk-pri-l | ✅ |
|
||||
| T.priD (#8B3E1F) | --tk-pri-d | ✅ |
|
||||
| T.bg (#F5F0EB) | $bg SCSS 变量 | ⚠️ 无 CSS Token,直接用 $bg |
|
||||
| T.card (#FFFFFF) | --tk-card-bg ($card) | ✅ |
|
||||
| T.surface (#EDE8E2) | --tk-card-bg (≈) | ⚠️ 近似,用 $surface-alt SCSS 变量 |
|
||||
| T.tx (#2D2A26) | $tx SCSS 变量 | ⚠️ 无 CSS Token,直接用 $tx |
|
||||
| T.tx2 (#5A554F) | $tx2 SCSS 变量 | ⚠️ 无 CSS Token,直接用 $tx2 |
|
||||
| T.tx3 (#78716C) | --tk-text-secondary ($tx3) | ✅ |
|
||||
| T.bd (#E8E2DC) | $bd SCSS 变量 | ⚠️ 无 CSS Token,直接用 $bd |
|
||||
| T.bdL (#F0EBE5) | $bd-l SCSS 变量 | ⚠️ 无 CSS Token |
|
||||
| T.acc (#5B7A5E) | $acc SCSS 变量 | ⚠️ 无 CSS Token |
|
||||
| T.accL (#E8F0E8) | $acc-l SCSS 变量 | ⚠️ 无 CSS Token |
|
||||
| T.wrn (#C4873A) | $wrn SCSS 变量 | ⚠️ 无 CSS Token |
|
||||
| T.wrnL (#FFF3E0) | $wrn-l SCSS 变量 | ⚠️ 无 CSS Token |
|
||||
| T.dan (#B54A4A) | $dan SCSS 变量 | ⚠️ 无 CSS Token |
|
||||
| T.danL (#FDEAEA) | $dan-l SCSS 变量 | ⚠️ 无 CSS Token |
|
||||
| T.r (16) | --tk-card-radius ($r) | ✅ |
|
||||
| T.rSm (12) | $r-sm SCSS 变量 | ⚠️ 无 CSS Token |
|
||||
| T.rXs (8) | $r-xs SCSS 变量 | ⚠️ 无 CSS Token |
|
||||
| T.serif (Georgia...) | 字体栈 | ❌ 不映射,直接硬编码 |
|
||||
| T.sans (-apple-system...) | 字体栈 | ❌ 不映射,直接硬编码 |
|
||||
|
||||
> 状态标记: ✅ confirmed 直接使用 | ⚠️ pending 需 SCSS 变量 | ❌ unmatched 需硬编码
|
||||
|
||||
## 二、页面结构
|
||||
|
||||
### 1. 空闲态(idle)
|
||||
|
||||

|
||||
|
||||
布局层级(从上到下):
|
||||
- **NavBar** — 深色主色背景,标题"设备同步"
|
||||
- **Hero 区域** — 主色渐变背景(135deg pri→priD),包含:
|
||||
- 蓝牙图标(72px 圆形,半透明白底)
|
||||
- 标题"智能设备同步"(serif 22px 700)
|
||||
- 副标题(14px 0.75 白色透明度)
|
||||
- **支持设备** — 三列标签(心率手环/血压计/血糖仪),每个含 SVG 图标
|
||||
- **上次同步卡片** — ContentCard 样式,左侧绿色勾选图标 + 时间 + 右侧数据量 badge
|
||||
- **待上传提示** — 黄色背景警告条($wrnL),三角感叹号图标
|
||||
- **扫描按钮** — 全宽主色按钮,蓝牙图标 + "扫描附近设备"
|
||||
|
||||
### 2. 扫描中(scanning)
|
||||
|
||||

|
||||
|
||||
布局层级:
|
||||
- **NavBar** — 同上
|
||||
- **居中脉冲区域**:
|
||||
- 三层脉冲圆环(CSS animation: pulse-ring),外层→中层→内层递进
|
||||
- 中心 80px 圆形蓝牙图标($priL 底色)
|
||||
- **标题** — serif 20px "正在搜索设备..."
|
||||
- **副文本** — 14px $tx3 提示文字
|
||||
- **进度条** — 180px 宽,渐变填充 $priL→$pri
|
||||
- **计时文字** — 12px "已用时 6 秒"
|
||||
|
||||
### 3. 设备列表(found)
|
||||
|
||||

|
||||
|
||||
布局层级:
|
||||
- **NavBar** — 同上
|
||||
- **结果头部** — 左侧"发现 N 台设备"标题 + 右侧"重新扫描"链接(含刷新图标)
|
||||
- **设备卡片列表**(×3)— 每张卡片含:
|
||||
- 左:44px 圆角方块图标($priL 底色 + 蓝牙 SVG)
|
||||
- 中:设备名(16px 600)+ 适配器类型(12px $tx3)
|
||||
- 右:信号强度条(4 级竖条) + 箭头
|
||||
- **未发现设备提示** — 虚线边框卡片,搜索图标 + 提示文字
|
||||
|
||||
### 4. 连接中(connecting)
|
||||
|
||||

|
||||
|
||||
布局层级:
|
||||
- **NavBar** — 同上
|
||||
- **居中动画区域**:
|
||||
- 100px 旋转环(border-top-color: $pri,CSS animation: connect-spin)
|
||||
- 60px 中心圆形蓝牙图标
|
||||
- **标题** — serif 18px "正在连接 {设备名}"
|
||||
- **副文本** — "正在进行蓝牙配对..."
|
||||
- **步骤指示器** — 三点一线:发现设备(✓) → 连接中(●脉冲) → 同步数据(○)
|
||||
|
||||
### 5. 已连接(connected)
|
||||
|
||||

|
||||
|
||||
布局层级:
|
||||
- **NavBar** — 同上
|
||||
- **连接状态卡片** — 绿色渐变背景(acc→#4A6B4E),蓝牙图标 + 设备名 + "实时" badge
|
||||
- **最新读数高亮卡片** — 大卡片(r=16 圆角 + shadow),含:
|
||||
- 52px 心形图标
|
||||
- 类型+时间小字
|
||||
- 数值(serif 36px 700)+ 单位
|
||||
- **历史读数列表** — 标题 + 表格行(类型/数值/时间),每行 12px 分隔线
|
||||
- **采集计数** — 居中小字
|
||||
- **操作按钮行** — 左侧全宽"上传数据"主色按钮 + 右侧 52px 红色断开按钮
|
||||
|
||||
### 6. 同步完成(done)
|
||||
|
||||

|
||||
|
||||
布局层级:
|
||||
- **NavBar** — 同上
|
||||
- **居中成功区域**:
|
||||
- 80px 绿色圆形勾选图标
|
||||
- 标题"同步完成"(serif 24px 700)
|
||||
- 副文本"数据已安全上传至健康管理平台"
|
||||
- **三列统计卡片** — 上传条数(5)/数据类型(3)/成功率(100%)
|
||||
- **完成按钮** — 全宽主色按钮
|
||||
|
||||
### 7. 错误状态(error)
|
||||
|
||||

|
||||
|
||||
布局层级:
|
||||
- **NavBar** — 同上
|
||||
- **居中错误区域**:
|
||||
- 80px 红色圆形叉号图标
|
||||
- 标题"连接失败"(serif 22px 700)
|
||||
- 错误描述文字
|
||||
- **错误详情卡片** — 含错误码/设备/时间三行键值对
|
||||
- **重试按钮** — 全宽主色按钮,含刷新图标
|
||||
- **返回按钮** — 描边按钮
|
||||
|
||||
## 三、组件映射
|
||||
|
||||
| 原型元素 | 推荐组件 | 来源 | 备注 |
|
||||
|----------|---------|------|------|
|
||||
| 页面外壳 | PageShell | @components/ui/PageShell | padding="none",NavBar 自带 |
|
||||
| 连接状态卡片 | ContentCard | @components/ui/ContentCard | variant="elevated",绿色渐变背景自定义 |
|
||||
| 成功结果卡片 | ContentCard | @components/ui/ContentCard | variant="elevated",居中布局 |
|
||||
| 错误详情卡片 | ContentCard | @components/ui/ContentCard | variant="outlined" |
|
||||
| 扫描按钮/上传按钮 | PrimaryButton | @components/ui/PrimaryButton | size="large",full width |
|
||||
| 断开连接按钮 | — | 自定义 | 红色小方块图标按钮 |
|
||||
| 返回按钮 | SecondaryButton | @components/ui/SecondaryButton | — |
|
||||
| 设备类型标签 | — | 自定义 DeviceTypeTag | 小图标+文字,$bdL 边框 |
|
||||
| 信号强度 | — | 自定义 SignalBars | 4 级竖条 |
|
||||
| 上次同步信息 | ListItem | @components/ui/ListItem | leftIcon + title + subtitle + extra |
|
||||
| 历史读数行 | InfoRow | @components/ui/InfoRow | label + value + last |
|
||||
| 待上传警告 | AlertCard | @components/ui/AlertCard | variant="bordered",黄色 |
|
||||
|
||||
> ⚠️ **需新建**: SignalBars — 4 级竖条信号强度指示器(20 行以内小组件)
|
||||
> ⚠️ **需新建**: DeviceTypeTag — 设备类型标签(图标+文字,已非常简单,可直接内联)
|
||||
|
||||
## 四、交互规格
|
||||
|
||||
| 元素 | 交互 | 触发 | 反馈 | 备注 |
|
||||
|------|------|------|------|------|
|
||||
| 扫描按钮 | 调用 handleScan | onClick | 按钮变灰+loading,状态→scanning | 触发 BLE 扫描 |
|
||||
| 设备卡片 | 调用 handleConnect | onClick | 状态→connecting,显示旋转动画 | 传递选中的 BLEDevice |
|
||||
| 重新扫描链接 | 调用 handleScan | onClick | 同扫描按钮 | 刷新设备列表 |
|
||||
| 上传数据按钮 | 调用 handleSync | onClick | 状态→syncing → done/error | 上传采集数据到后端 |
|
||||
| 断开连接按钮 | 调用 handleDisconnect | onClick | 断开 BLE,状态→idle | 清空 liveReadings |
|
||||
| 完成按钮 | handleDisconnect + navigateBack | onClick | 返回上一页 | 如果 returnTo=input 则回填 Storage |
|
||||
| 重试按钮 | handleScan | onClick | 重新扫描 | 从 error 恢复 |
|
||||
| 返回按钮 | Taro.navigateBack | onClick | 返回上一页 | 错误状态 |
|
||||
| 实时数据面板 | 被动更新 | BLE 通知 | 新数据插入列表顶部,数值动画 | useBLEManager hook 驱动 |
|
||||
|
||||
## 五、状态变体
|
||||
|
||||
- **idle**: 默认状态,展示 Hero + 设备类型 + 上次同步 + 扫描按钮
|
||||
- **scanning**: 脉冲动画 + 进度条 + 计时,不可操作(无按钮)
|
||||
- **found**: 设备列表 + 重新扫描链接,点击设备进入 connecting
|
||||
- **connecting**: 旋转环动画 + 步骤指示器,不可操作
|
||||
- **connected**: 绿色连接状态卡 + 实时数据面板 + 上传/断开按钮
|
||||
- **done**: 成功图标 + 统计卡片 + 完成按钮
|
||||
- **error**: 错误图标 + 错误详情 + 重试/返回按钮
|
||||
- **syncing**: 复用 scanning 的加载态样式,文字改为"正在上传数据..."
|
||||
|
||||
## 六、样式清单
|
||||
|
||||
### 关键样式参数
|
||||
|
||||
```
|
||||
/* Hero 渐变 */
|
||||
background: linear-gradient(135deg, $pri 0%, $pri-d 100%)
|
||||
padding: 32px 20px 28px
|
||||
|
||||
/* 脉冲圆环 */
|
||||
animation: pulse-ring 2s ease-out infinite
|
||||
三层: 140px / 110px / 80px (center)
|
||||
|
||||
/* 旋转环 */
|
||||
animation: connect-spin 1s linear infinite
|
||||
border-top-color: $pri
|
||||
|
||||
/* 最新读数数值 */
|
||||
font-family: $serif; font-size: 36px; font-weight: 700
|
||||
|
||||
/* 连接状态卡片渐变 */
|
||||
background: linear-gradient(135deg, $acc 0%, #4A6B4E 100%)
|
||||
|
||||
/* 信号条 */
|
||||
4 根竖条: height [4, 7, 10, 13]px, width: 3px, gap: 2px
|
||||
活跃色: $acc, 非活跃: $bd
|
||||
|
||||
/* 主按钮 */
|
||||
background: $pri; border-radius: $r-sm; padding: 16px;
|
||||
box-shadow: 0 4px 16px rgba(196, 98, 58, 0.3)
|
||||
```
|
||||
|
||||
### 字号映射
|
||||
|
||||
| 原型字号 | Token | 用途 |
|
||||
|---------|-------|------|
|
||||
| 36px | 超大数值,直接用 serif bold | 最新读数数值 |
|
||||
| 28px | --tk-font-h1 | 统计卡片数值 |
|
||||
| 24px | — | 成功/错误标题 |
|
||||
| 22px | --tk-font-h2 | Hero 标题、连接中标题 |
|
||||
| 20px | — | 历史读数数值 |
|
||||
| 18px | --tk-font-body-lg | NavBar 标题、按钮文字 |
|
||||
| 17px | — | 主按钮文字 |
|
||||
| 16px | --tk-font-body | 设备名、按钮文字 |
|
||||
| 15px | — | 完成页副文本 |
|
||||
| 14px | --tk-font-body-sm | 副文本、描述、列表类型 |
|
||||
| 13px | --tk-font-cap | 标签文字、小字 |
|
||||
| 12px | — | 时间、提示 |
|
||||
|
||||
---
|
||||
|
||||
> 此规格由 design-handoff skill 自动生成。LLM 实施时请:
|
||||
> 1. 先阅读截图建立视觉印象
|
||||
> 2. 按 Token 映射表使用项目 Token(✅ 标记的直接用,⚠️ 用 SCSS 变量)
|
||||
> 3. 优先使用"组件映射"中列出的已有组件
|
||||
> 4. 参考"交互规格"实现对应的交互逻辑
|
||||
> 5. "需新建"的组件参考截图和布局描述从头实现
|
||||
Reference in New Issue
Block a user