diff --git a/apps/miniprogram/native/pkg-veepoo/index.js b/apps/miniprogram/native/pkg-veepoo/index.js
new file mode 100644
index 0000000..ecebd62
--- /dev/null
+++ b/apps/miniprogram/native/pkg-veepoo/index.js
@@ -0,0 +1,479 @@
+/**
+ * Veepoo M2 原生小程序页面 — 连接 + 测量
+ *
+ * 完全脱离 Taro 框架,直接使用微信原生 API + Veepoo SDK。
+ * 连接流程严格对齐官方 Demo:
+ * scan → stopScan → connect(callback, 等待 connection:true)
+ * → registerDataListener → delay 500ms → authenticate
+ * → 轮询 deviceChipStatus → ready
+ */
+// eslint-disable-next-line no-undef
+const { veepooBle, veepooFeature, veepooLogger } = require('./libs/veepoo-sdk');
+
+// ── 常量 ──
+
+var SDK_EVENT_AUTH = 1;
+var SDK_EVENT_BATTERY = 2;
+var SDK_EVENT_TEMPERATURE = 6;
+var SDK_EVENT_BLOOD_PRESSURE = 18;
+var SDK_EVENT_BLOOD_OXYGEN = 31;
+var SDK_EVENT_HEART_RATE = 51;
+var SDK_EVENT_PRESSURE = 58;
+
+var MEASURE_TYPES = [
+ { type: 'heart_rate', label: '心率', unit: 'bpm', icon: '♥', color: '#EF4444', sdkType: SDK_EVENT_HEART_RATE },
+ { type: 'blood_oxygen', label: '血氧', unit: '%', icon: 'O₂', color: '#3B82F6', sdkType: SDK_EVENT_BLOOD_OXYGEN },
+ { type: 'blood_pressure', label: '血压', unit: 'mmHg', icon: '↕', color: '#8B5CF6', sdkType: SDK_EVENT_BLOOD_PRESSURE },
+ { type: 'temperature', label: '体温', unit: '°C', icon: 'T', color: '#F59E0B', sdkType: SDK_EVENT_TEMPERATURE },
+ { type: 'pressure', label: '压力', unit: '', icon: '~', color: '#6366F1', sdkType: SDK_EVENT_PRESSURE },
+];
+
+var MEASURE_TIMEOUTS = {
+ heart_rate: 60000,
+ blood_oxygen: 60000,
+ blood_pressure: 120000,
+ temperature: 60000,
+ pressure: 90000,
+};
+
+var MEASURE_SETTLE_DELAY = 1500;
+
+function _findConfig(type) {
+ for (var i = 0; i < MEASURE_TYPES.length; i++) {
+ if (MEASURE_TYPES[i].type === type) return MEASURE_TYPES[i];
+ }
+ return MEASURE_TYPES[0];
+}
+
+// ── Page ──
+
+// eslint-disable-next-line no-undef
+Page({
+ data: {
+ phase: 'idle',
+ deviceId: '',
+ deviceName: 'M2',
+ batteryLevel: null,
+ error: '',
+ selectedType: 'heart_rate',
+ selectedIcon: '♥',
+ selectedColor: '#EF4444',
+ selectedLabel: '心率',
+ selectedUnit: 'bpm',
+ measureTypes: MEASURE_TYPES,
+ measurePhase: 'idle',
+ measureProgress: 0,
+ measureDisplayValue: '',
+ measureError: '',
+ results: {},
+ hasResults: false,
+ },
+
+ _authTimer: null,
+ _authTimeout: null,
+ _scanTimer: null,
+ _scanFound: null,
+ _measureTimer: null,
+ _settleTimer: null,
+ _lastValues: null,
+ _connected: false,
+ _eventChannel: null,
+ _connecting: false,
+
+ // ── 生命周期 ──
+
+ onLoad: function () {
+ // eslint-disable-next-line no-undef
+ this._eventChannel = this.getOpenerEventChannel();
+ // eslint-disable-next-line no-undef
+ wx.setNavigationBarTitle({ title: 'M2 手环测量' });
+ this._updateSelectedDisplay('heart_rate');
+ },
+
+ onUnload: function () {
+ this._cleanup();
+ },
+
+ _cleanup: function () {
+ if (this._authTimer) { clearInterval(this._authTimer); this._authTimer = null; }
+ if (this._authTimeout) { clearTimeout(this._authTimeout); this._authTimeout = null; }
+ if (this._scanTimer) { clearTimeout(this._scanTimer); this._scanTimer = null; }
+ if (this._measureTimer) { clearTimeout(this._measureTimer); this._measureTimer = null; }
+ if (this._settleTimer) { clearTimeout(this._settleTimer); this._settleTimer = null; }
+ this._connecting = false;
+ if (this._connected) {
+ try { veepooBle.veepooWeiXinSDKloseBluetoothAdapterManager(function () {}); } catch (e) {
+ // eslint-disable-next-line no-undef
+ console.warn('[veepoo-native] 断开异常:', e);
+ }
+ this._connected = false;
+ }
+ },
+
+ _updateSelectedDisplay: function (type) {
+ var cfg = _findConfig(type);
+ this.setData({
+ selectedType: type,
+ selectedIcon: cfg.icon,
+ selectedColor: cfg.color,
+ selectedLabel: cfg.label,
+ selectedUnit: cfg.unit,
+ });
+ },
+
+ // ── 连接流程(严格对齐官方 Demo) ──
+
+ handleConnect: function () {
+ // H2 防重入
+ if (this.data.phase !== 'idle' && this.data.phase !== 'error' && this.data.phase !== 'disconnected') return;
+ if (this._connecting) return;
+ this._connecting = true;
+
+ this.setData({ phase: 'scanning', error: '' });
+ veepooLogger.setLevel(0);
+
+ var self = this;
+ self._scanFound = null;
+
+ veepooBle.veepooWeiXinSDKStartScanDeviceAndReceiveScanningDevice(function (res) {
+ var device = Array.isArray(res) ? res[0] : res;
+ if (!device) return;
+ var name = (device.localName || device.name || '').toUpperCase();
+ // eslint-disable-next-line no-undef
+ console.log('[veepoo-native] 扫描到:', name, device.deviceId || device.mac);
+ if (name.indexOf('M2') !== -1 && !self._scanFound) {
+ self._scanFound = device;
+ veepooBle.veepooWeiXinSDKStopSearchBleManager(function () {
+ self._doConnect(device);
+ });
+ }
+ });
+
+ this._scanTimer = setTimeout(function () {
+ if (!self._scanFound) {
+ veepooBle.veepooWeiXinSDKStopSearchBleManager(function () {});
+ self._connecting = false;
+ self.setData({ phase: 'error', error: '未找到 M2 设备,请确保手环已开机' });
+ }
+ }, 10000);
+ },
+
+ _doConnect: function (device) {
+ this.setData({ phase: 'connecting' });
+ // eslint-disable-next-line no-undef
+ console.log('[veepoo-native] 连接设备:', device.deviceId || device.mac);
+ var self = this;
+
+ veepooBle.veepooWeiXinSDKBleConnectionServicesCharacteristicsNotifyManager(device, function (result) {
+ // eslint-disable-next-line no-undef
+ console.log('[veepoo-native] 连接回调:', JSON.stringify(result));
+
+ // C4 修复:按官方 Demo 检查 connection:true,同时兼容 errno:0
+ if (result.connection === true || result.errno === 0 || result.errCode === 0) {
+ self._connected = true;
+ self._connecting = false;
+ self.setData({
+ deviceId: device.deviceId || device.mac || '',
+ });
+
+ self._registerListeners();
+
+ // eslint-disable-next-line no-undef
+ setTimeout(function () {
+ veepooFeature.veepooBlePasswordCheckManager();
+ self.setData({ phase: 'authenticating' });
+ // eslint-disable-next-line no-undef
+ console.log('[veepoo-native] 认证指令已发送');
+ }, 500);
+
+ self._authTimer = setInterval(function () {
+ try {
+ // eslint-disable-next-line no-undef
+ var status = wx.getStorageSync('deviceChipStatus');
+ if (status === 'successfulVerification' || status === 'passTheVerification') {
+ clearInterval(self._authTimer);
+ self._authTimer = null;
+ self._onReady();
+ }
+ } catch (_) { /* ignore */ }
+ }, 500);
+
+ self._authTimeout = setTimeout(function () {
+ if (self._authTimer) {
+ clearInterval(self._authTimer);
+ self._authTimer = null;
+ self._connecting = false;
+ self.setData({ phase: 'error', error: '设备认证超时,请重新连接' });
+ }
+ }, 8000);
+ }
+ });
+ },
+
+ _registerListeners: function () {
+ var self = this;
+ veepooBle.veepooWeiXinSDKNotifyMonitorValueChange(function (data) {
+ self._handleSdkEvent(data);
+ });
+ veepooBle.veepooWeiXinSDKBLEConnectionStateChangeManager(function (res) {
+ if (!res.connected) {
+ self._connected = false;
+ self._connecting = false;
+ self._cancelPendingMeasure();
+ self.setData({ phase: 'disconnected' });
+ }
+ });
+ },
+
+ _onReady: function () {
+ // C5 修复:清除 authTimeout 防止泄漏
+ if (this._authTimeout) { clearTimeout(this._authTimeout); this._authTimeout = null; }
+ this._connecting = false;
+ // eslint-disable-next-line no-undef
+ console.log('[veepoo-native] 认证成功,设备就绪');
+ this.setData({ phase: 'ready' });
+ veepooFeature.veepooReadElectricQuantityManager();
+ },
+
+ // ── SDK 事件路由 ──
+
+ _handleSdkEvent: function (data) {
+ if (!data || data.type === undefined) return;
+ var type = data.type;
+
+ if (type === SDK_EVENT_AUTH) {
+ var password = (data.content || {}).VPDevicepassword;
+ if (password === 'passTheVerification' || password === 'successfulVerification') {
+ if (this._authTimer) { clearInterval(this._authTimer); this._authTimer = null; }
+ if (this._authTimeout) { clearTimeout(this._authTimeout); this._authTimeout = null; }
+ this._onReady();
+ }
+ return;
+ }
+
+ if (type === SDK_EVENT_BATTERY) {
+ var pct = (data.content || {}).VPDeviceElectricPercent;
+ if (pct !== undefined && pct !== null) {
+ this.setData({ batteryLevel: Number(pct) });
+ }
+ return;
+ }
+
+ for (var i = 0; i < MEASURE_TYPES.length; i++) {
+ if (MEASURE_TYPES[i].sdkType === type) {
+ this._handleMeasureEvent(MEASURE_TYPES[i].type, data);
+ return;
+ }
+ }
+ },
+
+ // ── 测量流程 ──
+
+ handleSelectType: function (e) {
+ var type = e.currentTarget.dataset.type;
+ if (this.data.measurePhase === 'measuring') return;
+ this._updateSelectedDisplay(type);
+ this.setData({ measureError: '' });
+ },
+
+ handleStartMeasure: function () {
+ var type = this.data.selectedType;
+ if (this.data.measurePhase === 'measuring') return;
+ if (!this._connected) {
+ this.setData({ measureError: '设备未连接' });
+ return;
+ }
+
+ var self = this;
+ self._lastValues = null;
+
+ self.setData({
+ measurePhase: 'measuring',
+ measureProgress: 0,
+ measureDisplayValue: '',
+ measureError: '',
+ });
+
+ self._sendMeasureCommand(type, true);
+
+ self._measureTimer = setTimeout(function () {
+ self._onMeasureError('测量超时,请重试');
+ }, MEASURE_TIMEOUTS[type] || 60000);
+ },
+
+ handleCancelMeasure: function () {
+ this._cancelPendingMeasure();
+ this.setData({
+ measurePhase: 'idle',
+ measureProgress: 0,
+ measureDisplayValue: '',
+ });
+ },
+
+ handleDisconnect: function () {
+ this._cleanup();
+ this.setData({ phase: 'idle', deviceId: '', batteryLevel: null, error: '' });
+ },
+
+ handleBack: function () {
+ var results = this.data.results;
+ if (Object.keys(results).length > 0) {
+ try {
+ // eslint-disable-next-line no-undef
+ wx.setStorageSync('hms:veepoo_measure_results', JSON.stringify(results));
+ } catch (_) { /* ignore */ }
+ }
+ if (this._eventChannel) {
+ this._eventChannel.emit('measureComplete', { results: results, count: Object.keys(results).length });
+ }
+ // eslint-disable-next-line no-undef
+ wx.navigateBack({ delta: 1 });
+ },
+
+ handleResetResult: function () {
+ var type = this.data.selectedType;
+ var newResults = Object.assign({}, this.data.results);
+ delete newResults[type];
+ this.setData({
+ measurePhase: 'idle',
+ measureProgress: 0,
+ measureDisplayValue: '',
+ measureError: '',
+ results: newResults,
+ hasResults: Object.keys(newResults).length > 0,
+ });
+ },
+
+ // ── 测量事件处理 ──
+
+ _handleMeasureEvent: function (type, data) {
+ if (this.data.selectedType !== type || this.data.measurePhase !== 'measuring') return;
+
+ var content = data.content || {};
+ var self = this;
+
+ if (content.deviceBusy === true) { self._onMeasureError('设备正忙,请稍后重试'); return; }
+ if (content.notWear === true || data.state === 6) { self._onMeasureError('请将手环佩戴到手腕上'); return; }
+ if (data.state === 7) { self._onMeasureError('设备正在充电'); return; }
+ if (data.state === 8) { self._onMeasureError('设备电量不足'); return; }
+ if (type === 'pressure' && data.ack === 2) { self._onMeasureError('设备电量不足'); return; }
+ if (type === 'pressure' && data.ack === 3) { self._onMeasureError('设备正在测量其他数据'); return; }
+ if (type === 'pressure' && data.ack === 4) { self._onMeasureError('佩戴检测未通过'); return; }
+
+ var values = self._extractValues(type, content);
+ if (!values) return;
+
+ self._lastValues = values;
+
+ var displayVal = self._formatValues(type, values);
+ var progress = data.Progress !== undefined ? data.Progress : 0;
+ self.setData({
+ measureDisplayValue: displayVal,
+ measureProgress: Math.max(progress, 0),
+ });
+
+ if (progress >= 100) {
+ self._onMeasureSuccess(type, values);
+ return;
+ }
+
+ if (!self._settleTimer) {
+ self._settleTimer = setTimeout(function () {
+ if (self._lastValues && self.data.measurePhase === 'measuring') {
+ self._onMeasureSuccess(type, self._lastValues);
+ }
+ }, MEASURE_SETTLE_DELAY);
+ }
+ },
+
+ _extractValues: function (type, content) {
+ switch (type) {
+ case 'heart_rate': {
+ var hr = Number(content.heartRate);
+ return (hr >= 30 && hr <= 250) ? { heart_rate: hr } : null;
+ }
+ case 'blood_oxygen': {
+ var bo = Number(content.bloodOxygen);
+ return (bo >= 70 && bo <= 100) ? { blood_oxygen: bo } : null;
+ }
+ case 'blood_pressure': {
+ var high = Number(content.bloodPressureHigh);
+ var low = Number(content.bloodPressureLow);
+ return (high > 0 && low > 0) ? { systolic: high, diastolic: low } : null;
+ }
+ case 'temperature': {
+ var temp = Number(content.bodyTemperature);
+ return (temp > 30 && temp < 45) ? { temperature: temp } : null;
+ }
+ case 'pressure': {
+ var p = Number(content.pressure);
+ return (p >= 0 && p <= 100) ? { pressure: p } : null;
+ }
+ default: return null;
+ }
+ },
+
+ _formatValues: function (type, values) {
+ if (type === 'blood_pressure') {
+ return (values.systolic ?? '--') + '/' + (values.diastolic ?? '--');
+ }
+ var v = Object.values(values)[0];
+ // H5 修复:零值合法,仅 undefined/null 显示占位符
+ return (v !== undefined && v !== null) ? String(v) : '--';
+ },
+
+ _onMeasureSuccess: function (type, values) {
+ this._cancelPendingMeasure();
+
+ var result = { type: type, values: values, measuredAt: Date.now() };
+ var newResults = Object.assign({}, this.data.results);
+ newResults[type] = result;
+
+ this.setData({
+ measurePhase: 'success',
+ measureProgress: 100,
+ measureDisplayValue: this._formatValues(type, values),
+ results: newResults,
+ hasResults: true,
+ });
+
+ if (this._eventChannel) {
+ this._eventChannel.emit('measureResult', result);
+ }
+ },
+
+ _onMeasureError: function (msg) {
+ this._cancelPendingMeasure();
+ this.setData({ measurePhase: 'error', measureError: msg });
+ },
+
+ _cancelPendingMeasure: function () {
+ if (this._measureTimer) { clearTimeout(this._measureTimer); this._measureTimer = null; }
+ if (this._settleTimer) { clearTimeout(this._settleTimer); this._settleTimer = null; }
+ this._lastValues = null;
+
+ var type = this.data.selectedType;
+ if (type) this._sendMeasureCommand(type, false);
+ },
+
+ _sendMeasureCommand: function (type, on) {
+ switch (type) {
+ case 'heart_rate':
+ veepooFeature.veepooSendHeartRateTestSwitchManager({ switch: !!on });
+ break;
+ case 'blood_oxygen':
+ veepooFeature.veepooSendBloodOxygenControlDataManager({ switch: on ? 'start' : 'stop' });
+ break;
+ case 'blood_pressure':
+ veepooFeature.veepooSendReadUniversalBloodPressureDataManager({ switch: on ? 'start' : 'stop' });
+ break;
+ // C1 修复:体温测量传 { switch: boolean } 参数,停止时也调用
+ case 'temperature':
+ veepooFeature.veepooSendTemperatureMeasurementSwitchManager({ switch: !!on });
+ break;
+ case 'pressure':
+ veepooFeature.veepooSendPressureTestManager({ switch: !!on });
+ break;
+ }
+ },
+});
diff --git a/apps/miniprogram/native/pkg-veepoo/index.json b/apps/miniprogram/native/pkg-veepoo/index.json
new file mode 100644
index 0000000..f422545
--- /dev/null
+++ b/apps/miniprogram/native/pkg-veepoo/index.json
@@ -0,0 +1,6 @@
+{
+ "navigationBarTitleText": "M2 手环测量",
+ "navigationBarBackgroundColor": "#FFFFFF",
+ "navigationBarTextStyle": "black",
+ "backgroundColor": "#F5F5F4"
+}
diff --git a/apps/miniprogram/native/pkg-veepoo/index.wxml b/apps/miniprogram/native/pkg-veepoo/index.wxml
new file mode 100644
index 0000000..4568ac8
--- /dev/null
+++ b/apps/miniprogram/native/pkg-veepoo/index.wxml
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+ BT
+
+
+ M2 手环健康测量
+ 请确保手环已开机且蓝牙已开启
+
+
+ {{error}}
+
+
+
+
+ {{phase === 'error' ? '重新连接' : phase === 'disconnected' ? '重新连接' : '连接 M2 手环'}}
+
+
+
+
+ 查看测量结果并返回
+
+
+
+
+
+
+
+
+
+
+ BT
+
+
+
+ {{phase === 'scanning' ? '正在搜索 M2 手环...' : phase === 'connecting' ? '正在连接...' : '正在认证...'}}
+
+ 请确保手环已开机且靠近手机
+
+
+
+
+
+
+
+
+
+
+
+
+ {{item.icon}}
+ {{item.label}}
+ ✓
+
+
+
+
+
+
+
+
+ {{selectedIcon}}
+ 点击下方按钮开始测量{{selectedLabel}}
+
+
+
+ {{measureDisplayValue}}
+ 测量中...
+ {{selectedUnit}}
+
+
+
+ {{measureDisplayValue}}
+ {{selectedUnit}}
+
+
+
+ !
+ {{measureError}}
+
+
+
+
+
+
+
+
+
+
+ 本数据由智能手环采集,仅供健康趋势参考,不作为医疗诊断依据
+
+
+
+
+
+ 开始测量{{selectedLabel}}
+
+
+ 停止测量
+
+
+
+ 重新测量
+ 完成并返回
+
+
+
+ 重新测量
+
+
+
+
diff --git a/apps/miniprogram/native/pkg-veepoo/index.wxss b/apps/miniprogram/native/pkg-veepoo/index.wxss
new file mode 100644
index 0000000..817f4ae
--- /dev/null
+++ b/apps/miniprogram/native/pkg-veepoo/index.wxss
@@ -0,0 +1,355 @@
+/* Veepoo M2 原生页面样式 */
+
+page {
+ background: #F5F5F4;
+ min-height: 100vh;
+}
+
+/* ═══ 连接屏幕 ═══ */
+
+.connect-screen {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 100vh;
+ padding: 40rpx;
+}
+
+.connect-anim {
+ position: relative;
+ width: 200rpx;
+ height: 200rpx;
+ margin-bottom: 48rpx;
+}
+
+.connect-ring {
+ position: absolute;
+ top: 0; left: 0;
+ width: 100%; height: 100%;
+ border-radius: 50%;
+ border: 4rpx solid #D6D3D1;
+}
+
+.connect-ring--active {
+ border-color: #C4623A;
+ animation: pulse 1.5s ease-in-out infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% { transform: scale(1); opacity: 0.6; }
+ 50% { transform: scale(1.1); opacity: 1; }
+}
+
+.connect-center {
+ position: absolute;
+ top: 50%; left: 50%;
+ transform: translate(-50%, -50%);
+ width: 80rpx; height: 80rpx;
+ border-radius: 50%;
+ background: #292524;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.connect-bt {
+ color: #FAFAF9;
+ font-size: 28rpx;
+ font-weight: 600;
+}
+
+.connect-title {
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #1C1917;
+ margin-bottom: 16rpx;
+}
+
+.connect-hint {
+ font-size: 26rpx;
+ color: #78716C;
+ margin-bottom: 48rpx;
+}
+
+.connect-error {
+ background: #FEF2F2;
+ border-radius: 16rpx;
+ padding: 24rpx 32rpx;
+ margin-bottom: 32rpx;
+ width: 100%;
+ max-width: 600rpx;
+}
+
+.connect-error-text {
+ font-size: 26rpx;
+ color: #DC2626;
+}
+
+.connect-btn-wrap {
+ width: 100%;
+ max-width: 600rpx;
+}
+
+.connect-back {
+ width: 100%;
+ max-width: 600rpx;
+ margin-top: 24rpx;
+}
+
+/* ═══ 按钮 ═══ */
+
+.btn-primary {
+ background: #C4623A;
+ color: #FFFFFF;
+ font-size: 30rpx;
+ font-weight: 600;
+ text-align: center;
+ padding: 24rpx 0;
+ border-radius: 16rpx;
+}
+
+.btn-secondary {
+ background: #FFFFFF;
+ color: #44403C;
+ font-size: 30rpx;
+ font-weight: 500;
+ text-align: center;
+ padding: 24rpx 0;
+ border-radius: 16rpx;
+ border: 2rpx solid #D6D3D1;
+}
+
+.btn-large {
+ width: 100%;
+ max-width: 600rpx;
+}
+
+.actions-row {
+ display: flex;
+ gap: 24rpx;
+ width: 100%;
+ max-width: 600rpx;
+}
+
+.actions-row .btn-secondary,
+.actions-row .btn-primary {
+ flex: 1;
+}
+
+/* ═══ 测量页面 ═══ */
+
+.measure-page {
+ padding: 24rpx;
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+/* ── Header ── */
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16rpx 24rpx;
+ background: #FFFFFF;
+ border-radius: 16rpx;
+ margin-bottom: 24rpx;
+}
+
+.header-device {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+}
+
+.header-dot {
+ width: 16rpx; height: 16rpx;
+ border-radius: 50%;
+ background: #A8A29E;
+}
+
+.header-dot--on {
+ background: #22C55E;
+}
+
+.header-name {
+ font-size: 28rpx;
+ font-weight: 600;
+ color: #1C1917;
+}
+
+.header-battery {
+ font-size: 24rpx;
+ color: #78716C;
+}
+
+.header-disconnect {
+ font-size: 26rpx;
+ color: #C4623A;
+}
+
+/* ── Selector ── */
+
+.selector {
+ display: flex;
+ justify-content: space-around;
+ padding: 24rpx 0;
+ background: #FFFFFF;
+ border-radius: 16rpx;
+ margin-bottom: 24rpx;
+}
+
+.selector-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8rpx;
+ padding: 12rpx 16rpx;
+ border-radius: 12rpx;
+ position: relative;
+}
+
+.selector-item--active {
+ background: #FFF7ED;
+}
+
+.selector-item--done::after {
+ content: '';
+}
+
+.selector-icon {
+ font-size: 40rpx;
+}
+
+.selector-label {
+ font-size: 22rpx;
+ color: #57534E;
+}
+
+.selector-check {
+ position: absolute;
+ top: 4rpx; right: 4rpx;
+ width: 28rpx; height: 28rpx;
+ border-radius: 50%;
+ color: #FFFFFF;
+ font-size: 18rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* ── Gauge ── */
+
+.gauge {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 48rpx 0;
+}
+
+.gauge-circle {
+ width: 400rpx;
+ height: 400rpx;
+ border-radius: 50%;
+ background: #FFFFFF;
+ border: 8rpx solid #E7E5E4;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 24rpx;
+}
+
+.gauge-icon {
+ font-size: 64rpx;
+ margin-bottom: 16rpx;
+}
+
+.gauge-hint {
+ font-size: 26rpx;
+ color: #78716C;
+}
+
+.gauge-value {
+ font-size: 80rpx;
+ font-weight: 700;
+ line-height: 1.1;
+}
+
+.gauge-loading {
+ font-size: 30rpx;
+ color: #78716C;
+}
+
+.gauge-err {
+ font-size: 72rpx;
+ font-weight: 700;
+ color: #DC2626;
+ margin-bottom: 8rpx;
+}
+
+.gauge-err-text {
+ font-size: 26rpx;
+ color: #DC2626;
+}
+
+.gauge-progress-bar {
+ width: 500rpx;
+ height: 8rpx;
+ background: #E7E5E4;
+ border-radius: 4rpx;
+ overflow: hidden;
+}
+
+.gauge-progress-fill {
+ height: 100%;
+ background: #C4623A;
+ border-radius: 4rpx;
+ transition: width 0.3s ease;
+}
+
+/* ── Assessment ── */
+
+.assessment {
+ text-align: center;
+ padding: 16rpx;
+}
+
+.assessment-text {
+ font-size: 26rpx;
+ color: #16A34A;
+}
+
+/* ── Disclaimer ── */
+
+.disclaimer {
+ text-align: center;
+ padding: 16rpx 32rpx;
+}
+
+.disclaimer-text {
+ font-size: 22rpx;
+ color: #A8A29E;
+}
+
+/* ── Actions ── */
+
+.actions {
+ padding: 24rpx 0;
+}
+
+/* ── Measure Error ── */
+
+.measure-error {
+ text-align: center;
+ padding: 16rpx;
+}
+
+.measure-error-text {
+ font-size: 26rpx;
+ color: #DC2626;
+}
diff --git a/apps/miniprogram/src/pages/pkg-health/veepoo-measure/index.config.ts b/apps/miniprogram/src/pages/pkg-health/veepoo-measure/index.config.ts
new file mode 100644
index 0000000..9fa6a93
--- /dev/null
+++ b/apps/miniprogram/src/pages/pkg-health/veepoo-measure/index.config.ts
@@ -0,0 +1,3 @@
+export default definePageConfig({
+ navigationBarTitleText: '健康测量',
+});
diff --git a/apps/miniprogram/src/pages/pkg-health/veepoo-measure/index.scss b/apps/miniprogram/src/pages/pkg-health/veepoo-measure/index.scss
new file mode 100644
index 0000000..ce116d2
--- /dev/null
+++ b/apps/miniprogram/src/pages/pkg-health/veepoo-measure/index.scss
@@ -0,0 +1,398 @@
+// Veepoo 实时测量页样式
+.vm-page {
+ min-height: 100vh;
+ background: var(--tk-bg-primary);
+}
+
+// ── 连接中 ──
+.vm-connect {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 100vh;
+ padding: 0 32px;
+
+ &__anim {
+ position: relative;
+ width: 120px;
+ height: 120px;
+ margin-bottom: 24px;
+ }
+
+ &__ring {
+ position: absolute;
+ inset: 0;
+ border-radius: 50%;
+ border: 3px solid var(--tk-brand, #3B82F6);
+ animation: vm-pulse-ring 2s ease-out infinite;
+ }
+
+ &__center {
+ position: absolute;
+ inset: 20px;
+ border-radius: 50%;
+ background: var(--tk-brand, #3B82F6);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ &__bt {
+ color: #fff;
+ font-size: 20px;
+ font-weight: 700;
+ }
+
+ &__title {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--tk-text-primary);
+ margin-bottom: 8px;
+ }
+
+ &__hint {
+ font-size: 14px;
+ color: var(--tk-text-tertiary);
+ margin-bottom: 24px;
+ }
+
+ &__error {
+ margin-top: 16px;
+ text-align: center;
+ }
+
+ &__error-text {
+ font-size: 14px;
+ color: var(--tk-color-danger, #EF4444);
+ display: block;
+ margin-bottom: 16px;
+ }
+
+ &__error-btn {
+ width: 200px;
+ margin: 0 auto;
+ }
+}
+
+@keyframes vm-pulse-ring {
+ 0% { transform: scale(1); opacity: 1; }
+ 100% { transform: scale(1.4); opacity: 0; }
+}
+
+// ── 就绪/测量中 ──
+.vm-body {
+ padding: 16px;
+}
+
+// ── 设备状态栏 ──
+.vm-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+ background: var(--tk-bg-secondary);
+ border-radius: 12px;
+ margin-bottom: 16px;
+
+ &__device {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ &__dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--tk-color-success, #10B981);
+
+ &--on { background: var(--tk-color-success, #10B981); }
+ }
+
+ &__name {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--tk-text-primary);
+ }
+
+ &__battery {
+ font-size: 12px;
+ color: var(--tk-text-tertiary);
+ }
+
+ &__disconnect {
+ font-size: 13px;
+ color: var(--tk-text-tertiary);
+ padding: 4px 8px;
+ }
+}
+
+// ── 指标选择器 ──
+.vm-selector {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ gap: 8px;
+ margin-bottom: 24px;
+
+ &__item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 12px 4px;
+ border-radius: 12px;
+ border: 2px solid transparent;
+ background: var(--tk-bg-secondary);
+ position: relative;
+ transition: all 0.2s;
+
+ &--active {
+ border-color: var(--tk-brand, #3B82F6);
+ background: var(--tk-bg-tertiary);
+ }
+
+ &--measuring {
+ opacity: 0.7;
+ }
+
+ &--done {
+ border-color: var(--tk-color-success, #10B981);
+ }
+ }
+
+ &__icon {
+ font-size: 22px;
+ margin-bottom: 4px;
+ }
+
+ &__label {
+ font-size: 12px;
+ color: var(--tk-text-secondary);
+ }
+
+ &__check {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ color: #fff;
+ font-size: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+}
+
+// ── 仪表盘 ──
+.vm-gauge {
+ display: flex;
+ justify-content: center;
+ padding: 16px 0;
+
+ &__ring {
+ position: relative;
+ width: 220px;
+ height: 220px;
+
+ &--measuring {
+ animation: vm-gauge-breathe 2s ease-in-out infinite;
+ }
+ }
+
+ &__svg {
+ width: 100%;
+ height: 100%;
+ }
+
+ &__progress {
+ transition: stroke-dasharray 0.3s ease-out;
+ }
+
+ &__center {
+ position: absolute;
+ inset: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ }
+
+ &__idle {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ }
+
+ &__icon {
+ font-size: 40px;
+ }
+
+ &__hint {
+ font-size: 13px;
+ color: var(--tk-text-tertiary);
+ }
+
+ &__measuring, &__success {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ &__value {
+ font-size: 48px;
+ font-weight: 700;
+ line-height: 1;
+ }
+
+ &__unit {
+ font-size: 14px;
+ color: var(--tk-text-secondary);
+ margin-top: 4px;
+ }
+
+ &__loading {
+ font-size: 16px;
+ color: var(--tk-text-secondary);
+ }
+
+ &__error {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ }
+
+ &__err-icon {
+ font-size: 36px;
+ color: var(--tk-color-danger, #EF4444);
+ font-weight: 700;
+ }
+
+ &__err-text {
+ font-size: 13px;
+ color: var(--tk-text-secondary);
+ text-align: center;
+ }
+}
+
+@keyframes vm-gauge-breathe {
+ 0%, 100% { transform: scale(1); }
+ 50% { transform: scale(1.03); }
+}
+
+// ── 健康评估 ──
+.vm-assessment {
+ text-align: center;
+ padding: 12px;
+ border-radius: 8px;
+ margin: 0 16px 16px;
+
+ &--normal {
+ background: #ECFDF5;
+ color: #059669;
+ }
+
+ &--warning {
+ background: #FFFBEB;
+ color: #D97706;
+ }
+
+ &--danger {
+ background: #FEF2F2;
+ color: #DC2626;
+ }
+
+ &__text {
+ font-size: 14px;
+ font-weight: 500;
+ }
+}
+
+// ── 免责声明 ──
+.vm-disclaimer {
+ text-align: center;
+ padding: 8px 16px;
+ margin-bottom: 16px;
+
+ &__text {
+ font-size: 11px;
+ color: var(--tk-text-quaternary);
+ line-height: 1.5;
+ }
+}
+
+// ── 操作按钮 ──
+.vm-actions {
+ padding: 0 16px;
+
+ &__row {
+ display: flex;
+ gap: 12px;
+ }
+}
+
+// ── 测量错误 ──
+.vm-measure-error {
+ text-align: center;
+ padding: 8px 16px;
+ margin-top: 12px;
+
+ &__text {
+ font-size: 13px;
+ color: var(--tk-color-danger, #EF4444);
+ }
+}
+
+// ── 长者模式 ──
+.elder-mode {
+ .vm-selector {
+ grid-template-columns: repeat(3, 1fr);
+ gap: 12px;
+ }
+
+ .vm-selector__item {
+ padding: 16px 8px;
+ }
+
+ .vm-selector__icon {
+ font-size: 28px;
+ }
+
+ .vm-selector__label {
+ font-size: 16px;
+ }
+
+ .vm-gauge {
+ &__ring {
+ width: 260px;
+ height: 260px;
+ }
+
+ &__value {
+ font-size: 64px;
+ }
+
+ &__unit {
+ font-size: 18px;
+ }
+
+ &__hint {
+ font-size: 16px;
+ }
+ }
+
+ .vm-header__name {
+ font-size: 16px;
+ }
+
+ .vm-disclaimer__text {
+ font-size: 14px;
+ }
+
+ .vm-assessment__text {
+ font-size: 16px;
+ }
+}
diff --git a/apps/miniprogram/src/pages/pkg-health/veepoo-measure/index.tsx b/apps/miniprogram/src/pages/pkg-health/veepoo-measure/index.tsx
new file mode 100644
index 0000000..a5261f6
--- /dev/null
+++ b/apps/miniprogram/src/pages/pkg-health/veepoo-measure/index.tsx
@@ -0,0 +1,110 @@
+import React from 'react';
+import { View, Text } from '@tarojs/components';
+import Taro, { useDidShow } from '@tarojs/taro';
+import { useAuthStore } from '@/stores/auth';
+import { uploadReadings } from '@/services/device-sync';
+import type { NormalizedReading } from '@/services/ble/types';
+import { useElderClass } from '@/hooks/useElderClass';
+import PageShell from '@/components/ui/PageShell';
+import PrimaryButton from '@/components/ui/PrimaryButton';
+import './index.scss';
+
+/** 原生页面返回的测量结果格式 */
+interface NativeMeasureResult {
+ type: string;
+ values: Record;
+ measuredAt: number;
+}
+
+export default function VeepooMeasure() {
+ const modeClass = useElderClass();
+ const patient = useAuthStore((s) => s.currentPatient);
+ const navigatedRef = useRef(false);
+ const [results, setResults] = React.useState>({});
+ const [uploadStatus, setUploadStatus] = React.useState('');
+
+ // C3 修复:用 ref 防重入,避免 React Strict Mode 双触发
+ if (!navigatedRef.current) {
+ navigatedRef.current = true;
+ const patientId = patient?.id || '';
+ // 延迟到下一个微任务,确保页面渲染完成后再跳转
+ setTimeout(() => {
+ Taro.navigateTo({
+ url: `/pkg-veepoo/index?patientId=${patientId}`,
+ events: {
+ measureResult: (data: NativeMeasureResult) => {
+ setResults((prev) => ({ ...prev, [data.type]: data }));
+ },
+ measureComplete: (data: { results: Record; count: number }) => {
+ if (data.results) setResults(data.results);
+ },
+ },
+ });
+ }, 50);
+ }
+
+ // 页面恢复时读取原生页面返回的测量结果
+ useDidShow(() => {
+ try {
+ const raw = Taro.getStorageSync('hms:veepoo_measure_results');
+ if (raw) {
+ const parsed = JSON.parse(raw) as Record;
+ setResults(parsed);
+ Taro.removeStorageSync('hms:veepoo_measure_results');
+ }
+ } catch { /* ignore */ }
+ });
+
+ const handleUpload = async () => {
+ if (!patient) return;
+ const allResults = Object.values(results);
+ if (allResults.length === 0) return;
+
+ setUploadStatus('上传中...');
+ try {
+ // C2 修复:使用 uploadReadings,类型与 NormalizedReading 对齐
+ const readings: NormalizedReading[] = allResults.map((r) => ({
+ device_type: r.type as NormalizedReading['device_type'],
+ values: r.values,
+ measured_at: new Date(r.measuredAt).toISOString(),
+ }));
+ await uploadReadings(patient.id, 'veepoo_m2', 'Veepoo M2', readings);
+ setUploadStatus('上传成功');
+ Taro.showToast({ title: '数据已上传', icon: 'success' });
+ } catch {
+ setUploadStatus('上传失败');
+ Taro.showToast({ title: '上传失败', icon: 'none' });
+ }
+ };
+
+ const hasResults = Object.keys(results).length > 0;
+
+ return (
+
+
+
+
+ BT
+
+ M2 手环健康测量
+
+ {hasResults ? (
+
+ {Object.entries(results).map(([type, r]) => (
+
+ {type}
+ {JSON.stringify(r.values)}
+
+ ))}
+ {uploadStatus}
+
+ 上传测量数据
+
+
+ ) : (
+ 即将跳转到设备测量页面...
+ )}
+
+
+ );
+}