Files
hms/apps/miniprogram-uniapp/src/pages-sub/device-sync/index.vue
iven 2c567bd772 fix(mp): T40 UI 审查全量修复 + 设计体系一致性优化
Phase 0 基础设施:
- statusTag.ts: getStatusInlineStyle() 移除内联 borderRadius/padding/fontSize,仅返回 {background, color}
- 新增 SEVERITY_COLORS + getSeverityStyle() + getSeverityLabel() 统一告警严重程度样式
- variables.scss: 新增 9 个语义颜色别名 ($success/$danger/$warning/$info 等)
- mixins.scss: 新增 status-inline mixin 统一状态标签样式
- 7 个消费者页面添加 @include status-inline CSS 补偿

Phase 1 HIGH 修复 (4 页面):
- P46 随访管理: 移除 getTypeStyle() 硬编码 fontSize,替换文字 Loading 为组件
- P45 咨询详情医护: 添加 Loading/ErrorState 三态模板 + error ref
- P02 健康数据: 添加 loading ref + Loading 组件 + 错误 toast 提示
- P48 告警中心: 替换本地 SEVERITY_COLORS/SEVERITY_LABELS 为 statusTag.ts 导出

Phase 2 全局一致性:
- 2.1 触控补全: 17 页面为可点击元素添加 min-height: $touch-min
- 2.2 字号替换: 19 文件 31 处硬编码 px → Design Token CSS 变量
- 2.3 颜色替换: 18 文件 ~50 处硬编码十六进制 → SCSS 语义变量
- 2.4 elder-mode.scss: 新增 9 个选择器到触控放大清单

Phase 3 LOW 修复:
- 3.1 统一 Loading: 21 页面旧式文字加载 → <Loading> 组件
- 3.2 useElderClass: 8 页面补全长者模式 class 绑定
- 3.3 零散修复: 按钮 44px→48px,诊断记录添加 scroll-view 无限加载

同时新增 UniApp (Vue 3 + Vite) 小程序完整代码库 (146 文件)
2026-05-15 11:22:51 +08:00

200 lines
10 KiB
Vue
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.
<template>
<view :class="['device-sync-page', elderClass]">
<view class="sync-header">
<text class="sync-header-title">设备同步</text>
</view>
<view v-if="errorMsg" class="sync-error">
<text class="sync-error-text">{{ errorMsg }}</text>
</view>
<view v-if="pageState === 'scanning' || pageState === 'connecting' || pageState === 'syncing'" class="sync-loading">
<text class="sync-loading-text">
{{ pageState === 'scanning' && '正在扫描设备...' || pageState === 'connecting' && '正在连接设备...' || '正在上传数据...' }}
</text>
</view>
<!-- 空闲状态 -->
<template v-if="pageState === 'idle' || pageState === 'error'">
<view class="sync-section">
<view class="sync-hero">
<text class="sync-hero-icon">D</text>
<text class="sync-hero-title">设备同步</text>
<text class="sync-hero-desc">连接智能手环血压计血糖仪自动采集健康数据</text>
</view>
<view v-if="lastSyncAt || pendingCount > 0" class="sync-status-info">
<text v-if="lastSyncAt" class="sync-status-time">上次同步: {{ new Date(lastSyncAt).toLocaleTimeString() }}</text>
<text v-if="pendingCount > 0" class="sync-status-pending">{{ pendingCount }} 条数据待上传</text>
</view>
<view class="sync-action" @tap="handleScan">
<text class="sync-action-text">扫描设备</text>
</view>
<view v-if="devices.length > 0" class="sync-device-list">
<text class="sync-section-title">发现的设备</text>
<view v-for="d in devices" :key="d.deviceId" class="sync-device-item" @tap="handleConnect(d)">
<view class="sync-device-info">
<text class="sync-device-name">{{ d.name }}</text>
<text class="sync-device-adapter">{{ d.adapter?.name }}</text>
</view>
<text class="sync-device-rssi">信号 {{ d.RSSI > -60 ? '强' : d.RSSI > -80 ? '中' : '弱' }}</text>
</view>
</view>
</view>
</template>
<!-- 已连接 -->
<template v-if="pageState === 'connected'">
<view class="sync-section">
<view class="sync-status-card">
<text class="sync-status-dot sync-status-dot--connected" />
<text class="sync-status-text">已连接: {{ selectedDevice?.name }}</text>
</view>
<view v-if="liveReadings.length > 0" class="sync-readings-panel">
<text class="sync-section-title">实时数据</text>
<view v-for="(r, i) in liveReadings.slice(-5).reverse()" :key="i" class="sync-reading-item">
<text class="sync-reading-type">
{{ r.device_type === 'heart_rate' ? '心率' : r.device_type === 'blood_pressure' ? `血压(${r.metric === 'systolic' ? '收缩压' : r.metric === 'diastolic' ? '舒张压' : 'MAP'})` : r.device_type === 'blood_glucose' ? '血糖' : r.device_type }}
</text>
<text class="sync-reading-value">
{{ r.device_type === 'heart_rate' ? `${r.values.heart_rate} bpm` : r.metric ? `${r.values.value} ${r.values.unit}` : JSON.stringify(r.values) }}
</text>
</view>
<text class="sync-readings-count">已采集 {{ liveReadings.length }} 条数据</text>
</view>
<view class="sync-actions-row">
<view class="sync-action sync-action--primary" @tap="handleSync"><text class="sync-action-text">上传数据</text></view>
<view class="sync-action sync-action--danger" @tap="handleDisconnect"><text class="sync-action-text">断开连接</text></view>
</view>
</view>
</template>
<!-- 完成 -->
<template v-if="pageState === 'done'">
<view class="sync-section">
<view class="sync-result-card">
<text class="sync-result-icon">V</text>
<text class="sync-result-title">同步完成</text>
<text class="sync-result-count">成功上传 {{ syncCount }} 条数据</text>
</view>
<view class="sync-action" @tap="handleDone">
<text class="sync-action-text">{{ returnTo === 'input' ? '返回录入' : '完成' }}</text>
</view>
</view>
</template>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { useAuthStore } from '@/stores/auth'
import type { BLEDevice, NormalizedReading } from '@/services/ble'
import { useElderClass } from '@/composables/useElderClass'
type PageState = 'idle' | 'scanning' | 'connecting' | 'connected' | 'syncing' | 'done' | 'error'
const { elderClass } = useElderClass()
const authStore = useAuthStore()
const pageState = ref<PageState>('idle')
const devices = ref<BLEDevice[]>([])
const selectedDevice = ref<BLEDevice | null>(null)
const liveReadings = ref<NormalizedReading[]>([])
const syncCount = ref(0)
const errorMsg = ref('')
const lastSyncAt = ref<number | null>(null)
const pendingCount = ref(0)
let returnTo = ''
const handleScan = () => {
pageState.value = 'scanning'; devices.value = []; errorMsg.value = ''
// BLE 扫描需要完整 BLE 适配器实现,此处预留
setTimeout(() => {
if (devices.value.length === 0) errorMsg.value = '未发现支持的设备,请确认设备已开启蓝牙并靠近手机'
pageState.value = 'idle'
}, 3000)
}
const handleConnect = (_device: BLEDevice) => {
selectedDevice.value = _device; pageState.value = 'connecting'; errorMsg.value = ''
setTimeout(() => { pageState.value = 'connected' }, 2000)
}
const handleSync = () => {
if (!authStore.currentPatient || !selectedDevice.value) return
pageState.value = 'syncing'; errorMsg.value = ''
setTimeout(() => {
syncCount.value = liveReadings.value.length || 1
lastSyncAt.value = Date.now()
pageState.value = 'done'
if (returnTo === 'input' && liveReadings.value.length > 0) {
const mapped: Record<string, number> = {}
for (const r of liveReadings.value) {
if (r.device_type === 'blood_pressure') {
if (r.metric === 'systolic' && typeof r.values.value === 'number') mapped.systolic = r.values.value
if (r.metric === 'diastolic' && typeof r.values.value === 'number') mapped.diastolic = r.values.value
} else if (r.device_type === 'blood_glucose' && typeof r.values.blood_glucose === 'number') {
mapped.blood_sugar = r.values.blood_glucose as number
} else if (r.device_type === 'heart_rate' && typeof r.values.heart_rate === 'number') {
mapped.heart_rate = r.values.heart_rate as number
}
}
if (Object.keys(mapped).length > 0) uni.setStorageSync('device_sync_result', JSON.stringify(mapped))
}
}, 2000)
}
const handleDisconnect = () => {
pageState.value = 'idle'; selectedDevice.value = null; liveReadings.value = []; syncCount.value = 0; errorMsg.value = ''
}
const handleDone = () => {
handleDisconnect()
if (returnTo === 'input') uni.navigateBack()
}
onLoad((query) => { returnTo = query?.returnTo || '' })
onShow(() => { /* BLE manager lifecycle placeholder */ })
</script>
<style lang="scss" scoped>
.device-sync-page { min-height: 100vh; background: $bg; }
.sync-header { padding: 16px 24px; background: $card; }
.sync-header-title { font-size: var(--tk-font-title); font-weight: 600; color: $tx; }
.sync-error { margin: 12px 24px; padding: 10px 16px; background: rgba(255,77,79,0.08); border-radius: $r; }
.sync-error-text { font-size: var(--tk-font-cap); color: $dan; }
.sync-loading { @include flex-center; padding: 80px 0; }
.sync-loading-text { font-size: var(--tk-font-body); color: $tx3; }
.sync-section { padding: 24px; }
.sync-hero { @include flex-center; flex-direction: column; padding: 40px 0; }
.sync-hero-icon { font-size: var(--tk-font-hero); font-weight: 700; color: $pri; }
.sync-hero-title { font-size: var(--tk-font-title); font-weight: 600; color: $tx; margin-top: 8px; }
.sync-hero-desc { font-size: var(--tk-font-caption); color: $tx3; margin-top: 4px; text-align: center; }
.sync-status-info { display: flex; gap: 16px; justify-content: center; margin-bottom: 20px; }
.sync-status-time, .sync-status-pending { font-size: var(--tk-font-cap); color: $tx3; }
.sync-action { height: 48px; background: $pri; border-radius: $r; @include flex-center; margin-bottom: 12px; }
.sync-action--primary { background: $pri; }
.sync-action--danger { background: $dan; }
.sync-action-text { color: $white; font-size: var(--tk-font-body); font-weight: 500; }
.sync-device-list { margin-top: 20px; }
.sync-section-title { font-size: var(--tk-font-cap); font-weight: 500; color: $tx2; margin-bottom: 12px; display: block; }
.sync-device-item { @include card; display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.sync-device-info { flex: 1; }
.sync-device-name { font-size: var(--tk-font-body); color: $tx; display: block; }
.sync-device-adapter { font-size: var(--tk-font-cap); color: $tx3; display: block; margin-top: 2px; }
.sync-device-rssi { font-size: var(--tk-font-cap); color: $tx3; }
.sync-status-card { @include card; display: flex; align-items: center; gap: 10px; margin-bottom: 20px; }
.sync-status-dot { width: 10px; height: 10px; border-radius: 50%; background: $acc; }
.sync-status-text { font-size: var(--tk-font-body); color: $tx; }
.sync-readings-panel { @include card; margin-bottom: 20px; }
.sync-reading-item { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid rgba(0,0,0,0.04); }
.sync-reading-type { font-size: var(--tk-font-cap); color: $tx2; }
.sync-reading-value { font-size: var(--tk-font-body); color: $tx; font-weight: 500; }
.sync-readings-count { font-size: var(--tk-font-cap); color: $tx3; margin-top: 8px; display: block; text-align: center; }
.sync-actions-row { display: flex; gap: 12px; }
.sync-actions-row .sync-action { flex: 1; }
.sync-result-card { @include card; @include flex-center; flex-direction: column; padding: 40px 24px; margin-bottom: 20px; }
.sync-result-icon { font-size: var(--tk-font-hero); font-weight: 700; color: $acc; }
.sync-result-title { font-size: var(--tk-font-title); font-weight: 600; color: $tx; margin-top: 8px; }
.sync-result-count { font-size: var(--tk-font-body); color: $tx2; margin-top: 4px; }
</style>