根因:主包 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
754 lines
32 KiB
HTML
754 lines
32 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; }
|
||
.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> |