feat(miniprogram): BLE 增强层 — DataBuffer + GenericBleAdapter + DataSyncScheduler
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- DataBuffer: 离线持久化缓冲(分桶存储 + 去重 + 容量管理)
- GenericBleAdapter: 基于 Bluetooth SIG 标准 Health Profile 的通用适配器
  (Heart Rate 0x180D / Health Thermometer 0x1809 / Blood Pressure 0x1810)
- DataSyncScheduler: 定时自动同步调度(基于时间间隔判断是否需要同步)
- BLEManager: 集成 DataBuffer 替换简单 Storage 缓存
- device-sync 页面: 注册 CustomBandAdapter + 自动同步 + 状态显示
- 新增 vitest 单元测试配置,30 个测试全部通过
This commit is contained in:
iven
2026-05-04 02:42:58 +08:00
parent 70aacf47a0
commit 62c02e0f15
15 changed files with 1272 additions and 119 deletions

View File

@@ -0,0 +1,69 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
// 使用 vi.hoisted 确保 storage 在 mock 提升前可用
const { storage } = vi.hoisted(() => ({
storage: new Map<string, string>(),
}));
vi.mock('@tarojs/taro', () => ({
default: {
openBluetoothAdapter: vi.fn().mockResolvedValue({}),
closeBluetoothAdapter: vi.fn().mockResolvedValue({}),
startBluetoothDevicesDiscovery: vi.fn().mockResolvedValue({}),
stopBluetoothDevicesDiscovery: vi.fn().mockResolvedValue({}),
onBluetoothDeviceFound: vi.fn(),
offBluetoothDeviceFound: vi.fn(),
createBLEConnection: vi.fn().mockResolvedValue({}),
closeBLEConnection: vi.fn().mockResolvedValue({}),
getBLEDeviceServices: vi.fn().mockResolvedValue({ services: [] }),
getBLEDeviceCharacteristics: vi.fn().mockResolvedValue({ characteristics: [] }),
notifyBLECharacteristicValueChange: vi.fn().mockResolvedValue({}),
onBLECharacteristicValueChange: vi.fn(),
onBLEConnectionStateChange: vi.fn(),
getStorageSync: vi.fn((key: string) => storage.get(key) || ''),
setStorageSync: vi.fn((key: string, value: string) => { storage.set(key, value); }),
removeStorageSync: vi.fn((key: string) => { storage.delete(key); }),
},
}));
import { BLEManager } from '@/services/ble/BLEManager';
import { XiaomiBandAdapter } from '@/services/ble/adapters/XiaomiBandAdapter';
describe('BLEManager DataBuffer 集成', () => {
let manager: BLEManager;
beforeEach(() => {
storage.clear();
manager = new BLEManager();
manager.registerAdapter(XiaomiBandAdapter);
});
afterEach(async () => {
await manager.destroy();
});
it('registerAdapter 添加适配器', () => {
const count = (manager as any).adapters.length;
expect(count).toBeGreaterThanOrEqual(1);
});
it('getCachedReadings 返回空数组(未连接时)', () => {
const readings = manager.getCachedReadings();
expect(readings).toEqual([]);
});
it('flushPendingReadings 无缓存时返回 0', async () => {
const uploadFn = vi.fn().mockResolvedValue(0);
const count = await manager.flushPendingReadings(uploadFn);
expect(count).toBe(0);
expect(uploadFn).not.toHaveBeenCalled();
});
it('DataBuffer 实例已初始化', () => {
const buffer = (manager as any).dataBuffer;
expect(buffer).toBeDefined();
expect(typeof buffer.push).toBe('function');
expect(typeof buffer.flush).toBe('function');
expect(typeof buffer.restore).toBe('function');
});
});

View File

@@ -0,0 +1,89 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { DataBuffer } from '@/services/ble/DataBuffer';
import type { NormalizedReading } from '@/services/ble/types';
// Mock Taro Storage
const storage = new Map<string, string>();
vi.mock('@tarojs/taro', () => ({
default: {
getStorageSync: vi.fn((key: string) => storage.get(key) || ''),
setStorageSync: vi.fn((key: string, value: string) => { storage.set(key, value); }),
removeStorageSync: vi.fn((key: string) => { storage.delete(key); }),
getStorageInfoSync: vi.fn(() => ({ keys: Array.from(storage.keys()), limitSize: 10240, currentSize: storage.size })),
},
}));
function makeReading(overrides: Partial<NormalizedReading> = {}): NormalizedReading {
return {
device_type: 'heart_rate',
values: { heart_rate: 72 },
measured_at: new Date().toISOString(),
...overrides,
};
}
describe('DataBuffer', () => {
let buffer: DataBuffer;
beforeEach(() => {
storage.clear();
buffer = new DataBuffer({ bucketSize: 100 });
});
it('push 添加读数并持久化', () => {
const reading = makeReading();
buffer.push(reading);
expect(buffer.size()).toBe(1);
});
it('push 批量添加读数', () => {
const readings = Array.from({ length: 10 }, (_, i) =>
makeReading({ measured_at: new Date(Date.now() + i * 1000).toISOString() }),
);
buffer.push(readings);
expect(buffer.size()).toBe(10);
});
it('flush 返回并清空缓冲区', () => {
buffer.push([
makeReading({ measured_at: '2026-05-04T10:00:00.000Z' }),
makeReading({ measured_at: '2026-05-04T10:00:01.000Z' }),
]);
const flushed = buffer.flush();
expect(flushed.length).toBe(2);
expect(buffer.size()).toBe(0);
});
it('超过 maxTotal 时丢弃最旧数据', () => {
const smallBuffer = new DataBuffer({ bucketSize: 5, maxTotal: 10 });
for (let i = 0; i < 15; i++) {
smallBuffer.push(makeReading({ measured_at: new Date(i * 1000).toISOString() }));
}
expect(smallBuffer.size()).toBe(10);
});
it('去重:相同 measured_at + device_type 不重复存储', () => {
const ts = '2026-05-04T10:00:00.000Z';
buffer.push(makeReading({ measured_at: ts }));
buffer.push(makeReading({ measured_at: ts }));
expect(buffer.size()).toBe(1);
});
it('restore 从 Storage 恢复未上传数据', () => {
buffer.push([
makeReading({ measured_at: '2026-05-04T10:00:00.000Z' }),
makeReading({ measured_at: '2026-05-04T10:00:01.000Z' }),
]);
// 模拟重启:新建 DataBuffer 并 restore
const restored = new DataBuffer({ bucketSize: 100 });
const count = restored.restore();
expect(count).toBe(2);
expect(restored.size()).toBe(2);
});
it('clear 清空缓冲区和 Storage', () => {
buffer.push(makeReading());
buffer.clear();
expect(buffer.size()).toBe(0);
});
});

View File

@@ -0,0 +1,91 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { DataSyncScheduler } from '@/services/ble/DataSyncScheduler';
const storage = new Map<string, string>();
vi.mock('@tarojs/taro', () => ({
default: {
getStorageSync: vi.fn((key: string) => storage.get(key) || ''),
setStorageSync: vi.fn((key: string, value: string) => { storage.set(key, value); }),
removeStorageSync: vi.fn((key: string) => { storage.delete(key); }),
},
}));
describe('DataSyncScheduler', () => {
let scheduler: DataSyncScheduler;
let syncFn: ReturnType<typeof vi.fn>;
beforeEach(() => {
storage.clear();
syncFn = vi.fn().mockResolvedValue({ success: true, uploadedCount: 5 });
scheduler = new DataSyncScheduler({
intervalMs: 60 * 60 * 1000,
storageKey: 'last_ble_sync',
});
});
afterEach(() => {
scheduler.destroy();
});
it('首次同步:无记录时立即需要同步', () => {
expect(scheduler.needsSync()).toBe(true);
});
it('同步后记录时间戳', async () => {
await scheduler.recordSync(syncFn);
expect(storage.has('last_ble_sync')).toBe(true);
expect(syncFn).toHaveBeenCalled();
});
it('同步后不需要再次同步', async () => {
await scheduler.recordSync(syncFn);
expect(scheduler.needsSync()).toBe(false);
});
it('超过间隔后需要再次同步', async () => {
const twoHoursAgo = Date.now() - 2 * 60 * 60 * 1000;
storage.set('last_ble_sync', JSON.stringify({ lastSyncAt: twoHoursAgo }));
scheduler = new DataSyncScheduler({ intervalMs: 60 * 60 * 1000, storageKey: 'last_ble_sync' });
expect(scheduler.needsSync()).toBe(true);
});
it('同步失败不更新时间戳', async () => {
const failFn = vi.fn().mockRejectedValue(new Error('network error'));
const oneHourAgo = Date.now() - 60 * 60 * 1000;
storage.set('last_ble_sync', JSON.stringify({ lastSyncAt: oneHourAgo }));
await scheduler.recordSync(failFn);
const stored = JSON.parse(storage.get('last_ble_sync') || '{}');
expect(stored.lastSyncAt).toBe(oneHourAgo);
});
it('tryAutoSync 首次时触发同步', async () => {
const result = await scheduler.tryAutoSync(syncFn);
expect(result).toBe(true);
expect(syncFn).toHaveBeenCalledTimes(1);
});
it('tryAutoSync 未超时不触发', async () => {
await scheduler.recordSync(syncFn);
syncFn.mockClear();
const result = await scheduler.tryAutoSync(syncFn);
expect(result).toBe(false);
expect(syncFn).not.toHaveBeenCalled();
});
it('destroy 清理定时器', () => {
const clearIntervalSpy = vi.spyOn(global, 'clearInterval');
scheduler.startPeriodicCheck(syncFn, 30000);
scheduler.destroy();
expect(clearIntervalSpy).toHaveBeenCalled();
clearIntervalSpy.mockRestore();
});
it('getLastSyncAt 返回上次同步时间', async () => {
await scheduler.recordSync(syncFn);
const lastSync = scheduler.getLastSyncAt();
expect(lastSync).toBeTruthy();
expect(typeof lastSync).toBe('number');
});
});

View File

@@ -0,0 +1,158 @@
import { describe, it, expect } from 'vitest';
import { createGenericBleAdapter } from '@/services/ble/adapters/GenericBleAdapter';
import type { GenericBLEProfile } from '@/services/ble/types';
// ---- Heart Rate (0x180D / 0x2A37) ----
// Flag byte=0x00 (UINT8), HR=75
function makeHeartRateData(hr: number, isUint16 = false): ArrayBuffer {
const buf = new ArrayBuffer(isUint16 ? 3 : 2);
const view = new DataView(buf);
view.setUint8(0, isUint16 ? 0x01 : 0x00);
if (isUint16) {
view.setUint16(1, hr, true);
} else {
view.setUint8(1, hr);
}
return buf;
}
// ---- Health Thermometer (0x1809 / 0x2A1C) ----
// IEEE 11073 FLOAT: 32-bit — mantissa (24-bit) + exponent (8-bit)
function makeTemperatureData(tempCelsius: number): ArrayBuffer {
const buf = new ArrayBuffer(4);
const view = new DataView(buf);
// flags byte: 0x00 = Celsius, no timestamp, no type
view.setUint8(0, 0x00);
// 11073 FLOAT: mantissa * 10^exponent
// For 36.5: mantissa=365, exponent=-1
const mantissa = Math.round(tempCelsius * 10);
const exponent = -1;
view.setInt16(1, mantissa, true);
view.setInt8(3, exponent);
return buf;
}
describe('GenericBleAdapter', () => {
describe('心率解析', () => {
const adapter = createGenericBleAdapter({
name: 'Test Wristband',
supportedModels: ['TestBand'],
profiles: ['heart_rate'],
});
it('解析 UINT8 心率', () => {
const data = makeHeartRateData(75);
const results = adapter.parseNotification(
'0000180D-0000-1000-8000-00805f9b34fb',
'00002A37-0000-1000-8000-00805f9b34fb',
data,
);
expect(results.length).toBe(1);
expect(results[0].device_type).toBe('heart_rate');
expect(results[0].values.heart_rate).toBe(75);
});
it('解析 UINT16 心率', () => {
const data = makeHeartRateData(200, true);
const results = adapter.parseNotification(
'0000180D-0000-1000-8000-00805f9b34fb',
'00002A37-0000-1000-8000-00805f9b34fb',
data,
);
expect(results.length).toBe(1);
expect(results[0].values.heart_rate).toBe(200);
});
it('忽略非目标 Characteristic', () => {
const data = makeHeartRateData(75);
const results = adapter.parseNotification(
'0000180D-0000-1000-8000-00805f9b34fb',
'00002A38-0000-1000-8000-00805f9b34fb', // Body Sensor Location
data,
);
expect(results.length).toBe(0);
});
});
describe('体温解析', () => {
const adapter = createGenericBleAdapter({
name: 'Test Thermometer',
supportedModels: ['TestThermo'],
profiles: ['health_thermometer'],
});
it('解析体温读数', () => {
const data = makeTemperatureData(36.5);
const results = adapter.parseNotification(
'00001809-0000-1000-8000-00805f9b34fb',
'00002A1C-0000-1000-8000-00805f9b34fb',
data,
);
expect(results.length).toBe(1);
expect(results[0].device_type).toBe('temperature');
expect(results[0].values.value).toBeCloseTo(36.5, 0);
});
});
describe('多 Profile 适配器', () => {
const adapter = createGenericBleAdapter({
name: 'Multi-Profile Band',
supportedModels: ['CustomBand', 'MedicalBand'],
profiles: ['heart_rate', 'health_thermometer'],
});
it('包含两个 Service UUID', () => {
expect(adapter.serviceUUIDs.length).toBe(2);
});
it('包含两个 Profile 的 Characteristic', () => {
expect(adapter.notifyCharacteristics.length).toBe(2);
});
it('supportedModels 配置正确', () => {
expect(adapter.supportedModels).toEqual(['CustomBand', 'MedicalBand']);
});
it('解析心率 + 体温', () => {
const hrResults = adapter.parseNotification(
'0000180D-0000-1000-8000-00805f9b34fb',
'00002A37-0000-1000-8000-00805f9b34fb',
makeHeartRateData(80),
);
expect(hrResults[0].device_type).toBe('heart_rate');
const tempResults = adapter.parseNotification(
'00001809-0000-1000-8000-00805f9b34fb',
'00002A1C-0000-1000-8000-00805f9b34fb',
makeTemperatureData(37.2),
);
expect(tempResults[0].device_type).toBe('temperature');
});
});
describe('边界情况', () => {
const adapter = createGenericBleAdapter({
name: 'Edge Case Band',
supportedModels: ['Edge'],
profiles: ['heart_rate'],
});
it('空数据返回空数组', () => {
const results = adapter.parseNotification(
'0000180D-0000-1000-8000-00805f9b34fb',
'00002A37-0000-1000-8000-00805f9b34fb',
new ArrayBuffer(0),
);
expect(results.length).toBe(0);
});
it('心率超范围 (>300) 返回空数组', () => {
const results = adapter.parseNotification(
'0000180D-0000-1000-8000-00805f9b34fb',
'00002A37-0000-1000-8000-00805f9b34fb',
makeHeartRateData(0),
);
expect(results.length).toBe(0);
});
});
});

View File

@@ -42,6 +42,7 @@
"miniprogram-automator": "^0.12.1",
"sass": "^1.87.0",
"typescript": "^5.8.0",
"vite": "^8.0.10",
"vitest": "^4.1.5",
"webpack": "~5.95.0"
}

View File

@@ -19,13 +19,13 @@ importers:
version: 7.28.5(@babel/core@7.29.0)
'@tarojs/components':
specifier: 4.2.0
version: 4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)))(webpack@5.95.0(@swc/core@1.3.96))
version: 4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
'@tarojs/helper':
specifier: 4.2.0
version: 4.2.0
'@tarojs/plugin-framework-react':
specifier: 4.2.0
version: 4.2.0(@tarojs/helper@4.2.0)(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)(react@18.3.1)(webpack@5.95.0(@swc/core@1.3.96))
version: 4.2.0(@tarojs/helper@4.2.0)(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)(react@18.3.1)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@3.13.1)(sass@1.99.0)(stylus@0.64.0)(terser@5.46.2))(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
'@tarojs/plugin-platform-weapp':
specifier: 4.2.0
version: 4.2.0(@tarojs/service@4.2.0)(@tarojs/shared@4.2.0)
@@ -40,7 +40,7 @@ importers:
version: 4.2.0
'@tarojs/taro':
specifier: 4.2.0
version: 4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)))(webpack@5.95.0(@swc/core@1.3.96)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)))(webpack@5.95.0(@swc/core@1.3.96))
version: 4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
babel-preset-taro:
specifier: ^4.2.0
version: 4.2.0(@babel/core@7.29.0)(@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0))(@babel/preset-react@7.28.5(@babel/core@7.29.0))
@@ -71,7 +71,7 @@ importers:
version: 4.2.0(@types/node@25.6.0)
'@tarojs/webpack5-runner':
specifier: 4.2.0
version: 4.2.0(@babel/core@7.29.0)(@swc/core@1.3.96)(@tarojs/runtime@4.2.0)(less@3.13.1)(postcss@8.5.12)(sass@1.99.0)(stylus@0.64.0)(typescript@5.9.3)(webpack@5.95.0(@swc/core@1.3.96))
version: 4.2.0(@babel/core@7.29.0)(@swc/core@1.3.96)(@tarojs/runtime@4.2.0)(less@3.13.1)(postcss@8.5.12)(sass@1.99.0)(stylus@0.64.0)(typescript@5.9.3)(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
'@types/crypto-js':
specifier: ^4.2.2
version: 4.2.2
@@ -87,12 +87,15 @@ importers:
typescript:
specifier: ^5.8.0
version: 5.9.3
vite:
specifier: ^8.0.10
version: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@3.13.1)(sass@1.99.0)(stylus@0.64.0)(terser@5.46.2)
vitest:
specifier: ^4.1.5
version: 4.1.5(@types/node@25.6.0)(jsdom@24.1.3)
version: 4.1.5(@types/node@25.6.0)(jsdom@24.1.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@3.13.1)(sass@1.99.0)(stylus@0.64.0)(terser@5.46.2))
webpack:
specifier: ~5.95.0
version: 5.95.0(@swc/core@1.3.96)
version: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
packages:
@@ -705,6 +708,15 @@ packages:
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
engines: {node: '>=18'}
'@emnapi/core@1.10.0':
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
'@emnapi/runtime@1.10.0':
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
'@emnapi/wasi-threads@1.2.1':
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
'@esbuild/aix-ppc64@0.21.5':
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
engines: {node: '>=12'}
@@ -1227,6 +1239,12 @@ packages:
'@napi-rs/triples@1.2.0':
resolution: {integrity: sha512-HAPjR3bnCsdXBsATpDIP5WCrw0JcACwhhrwIAQhiR46n+jm+a2F8kBsfseAuWtSyQ+H3Yebt2k43B5dy+04yMA==}
'@napi-rs/wasm-runtime@1.1.4':
resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
peerDependencies:
'@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -1239,6 +1257,9 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@oxc-project/types@0.127.0':
resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==}
'@parcel/watcher-android-arm64@2.5.6':
resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==}
engines: {node: '>= 10.0.0'}
@@ -1350,6 +1371,104 @@ packages:
'@rnx-kit/console@1.1.0':
resolution: {integrity: sha512-N+zFhTSXroiK4eL26vs61Pmtl7wzTPAKLd4JKw9/fk5cNAHUscCXF/uclzuYN61Ye5AwygIvcwbm9wv4Jfa92A==}
'@rolldown/binding-android-arm64@1.0.0-rc.17':
resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-darwin-arm64@1.0.0-rc.17':
resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.0.0-rc.17':
resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-freebsd-x64@1.0.0-rc.17':
resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17':
resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17':
resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.17':
resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17':
resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17':
resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.17':
resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.0-rc.17':
resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.0-rc.17':
resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-wasm32-wasi@1.0.0-rc.17':
resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [wasm32]
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17':
resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.17':
resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/pluginutils@1.0.0-rc.17':
resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==}
'@sideway/address@4.1.5':
resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==}
@@ -1726,6 +1845,9 @@ packages:
stylus:
optional: true
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/archy@0.0.31':
resolution: {integrity: sha512-v+dxizsFVyXgD3EpFuqT9YjdEjbJmPxNf1QIX9ohZOhxh1ZF2yhqv3vYaeum9lg3VghhxS5S0a6yldN9J9lPEQ==}
@@ -4804,6 +4926,11 @@ packages:
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rolldown@1.0.0-rc.17:
resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
rollup@3.30.0:
resolution: {integrity: sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==}
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
@@ -5386,6 +5513,49 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
vite@8.0.10:
resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
'@vitejs/devtools': ^0.1.0
esbuild: ^0.27.0 || ^0.28.0
jiti: '>=1.21.0'
less: ^4.0.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
'@vitejs/devtools':
optional: true
esbuild:
optional: true
jiti:
optional: true
less:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
vitest@4.1.5:
resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==}
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
@@ -6452,6 +6622,22 @@ snapshots:
'@csstools/css-tokenizer@3.0.4': {}
'@emnapi/core@1.10.0':
dependencies:
'@emnapi/wasi-threads': 1.2.1
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.10.0':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.2.1':
dependencies:
tslib: 2.8.1
optional: true
'@esbuild/aix-ppc64@0.21.5':
optional: true
@@ -6920,6 +7106,13 @@ snapshots:
'@napi-rs/triples@1.2.0': {}
'@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
dependencies:
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
'@tybys/wasm-util': 0.10.1
optional: true
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -6932,6 +7125,8 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
'@oxc-project/types@0.127.0': {}
'@parcel/watcher-android-arm64@2.5.6':
optional: true
@@ -7009,6 +7204,57 @@ snapshots:
'@rnx-kit/console@1.1.0': {}
'@rolldown/binding-android-arm64@1.0.0-rc.17':
optional: true
'@rolldown/binding-darwin-arm64@1.0.0-rc.17':
optional: true
'@rolldown/binding-darwin-x64@1.0.0-rc.17':
optional: true
'@rolldown/binding-freebsd-x64@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-x64-musl@1.0.0-rc.17':
optional: true
'@rolldown/binding-openharmony-arm64@1.0.0-rc.17':
optional: true
'@rolldown/binding-wasm32-wasi@1.0.0-rc.17':
dependencies:
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
'@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
optional: true
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17':
optional: true
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.17':
optional: true
'@rolldown/pluginutils@1.0.0-rc.17': {}
'@sideway/address@4.1.5':
dependencies:
'@hapi/hoek': 9.3.0
@@ -7146,12 +7392,12 @@ snapshots:
- debug
- supports-color
'@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)))(webpack@5.95.0(@swc/core@1.3.96))':
'@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))':
dependencies:
'@stencil/core': 2.22.3
'@tarojs/runtime': 4.2.0
'@tarojs/shared': 4.2.0
'@tarojs/taro': 4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)))(webpack@5.95.0(@swc/core@1.3.96)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)))(webpack@5.95.0(@swc/core@1.3.96))
'@tarojs/taro': 4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
classnames: 2.5.1
hammerjs: 2.0.8
hls.js: 1.6.16
@@ -7244,7 +7490,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@tarojs/plugin-framework-react@4.2.0(@tarojs/helper@4.2.0)(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)(react@18.3.1)(webpack@5.95.0(@swc/core@1.3.96))':
'@tarojs/plugin-framework-react@4.2.0(@tarojs/helper@4.2.0)(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)(react@18.3.1)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@3.13.1)(sass@1.99.0)(stylus@0.64.0)(terser@5.46.2))(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))':
dependencies:
'@tarojs/helper': 4.2.0
'@tarojs/runtime': 4.2.0
@@ -7255,7 +7501,8 @@ snapshots:
tslib: 2.8.1
optionalDependencies:
react: 18.3.1
webpack: 5.95.0(@swc/core@1.3.96)
vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@3.13.1)(sass@1.99.0)(stylus@0.64.0)(terser@5.46.2)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
'@tarojs/plugin-platform-weapp@4.2.0(@tarojs/service@4.2.0)(@tarojs/shared@4.2.0)':
dependencies:
@@ -7300,19 +7547,19 @@ snapshots:
'@tarojs/shared@4.2.0': {}
'@tarojs/taro-loader@4.2.0(webpack@5.95.0(@swc/core@1.3.96))':
'@tarojs/taro-loader@4.2.0(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))':
dependencies:
'@tarojs/helper': 4.2.0
'@tarojs/shared': 4.2.0
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
transitivePeerDependencies:
- '@swc/helpers'
- supports-color
'@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)))(webpack@5.95.0(@swc/core@1.3.96)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)))(webpack@5.95.0(@swc/core@1.3.96))':
'@tarojs/taro@4.2.0(@tarojs/components@4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(@tarojs/helper@4.2.0)(@tarojs/shared@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))':
dependencies:
'@tarojs/api': 4.2.0(@tarojs/runtime@4.2.0)(@tarojs/shared@4.2.0)
'@tarojs/components': 4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)))(webpack@5.95.0(@swc/core@1.3.96))
'@tarojs/components': 4.2.0(@tarojs/helper@4.2.0)(@types/react@18.3.28)(html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(postcss@8.5.12)(rollup@3.30.0)(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)))(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
'@tarojs/helper': 4.2.0
'@tarojs/runtime': 4.2.0
'@tarojs/shared': 4.2.0
@@ -7320,77 +7567,77 @@ snapshots:
postcss: 8.5.12
optionalDependencies:
'@types/react': 18.3.28
html-webpack-plugin: 5.6.7(webpack@5.95.0(@swc/core@1.3.96))
html-webpack-plugin: 5.6.7(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
rollup: 3.30.0
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
webpack-chain: 6.5.1
webpack-dev-server: 4.15.2(webpack@5.95.0(@swc/core@1.3.96))
webpack-dev-server: 4.15.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
'@tarojs/webpack5-prebundle@4.2.0(webpack@5.95.0(@swc/core@1.3.96))':
'@tarojs/webpack5-prebundle@4.2.0(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))':
dependencies:
'@tarojs/helper': 4.2.0
'@tarojs/shared': 4.2.0
enhanced-resolve: 5.21.0
es-module-lexer: 0.10.5
lodash: 4.18.1
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
webpack-virtual-modules: 0.6.2
transitivePeerDependencies:
- '@swc/helpers'
- supports-color
'@tarojs/webpack5-runner@4.2.0(@babel/core@7.29.0)(@swc/core@1.3.96)(@tarojs/runtime@4.2.0)(less@3.13.1)(postcss@8.5.12)(sass@1.99.0)(stylus@0.64.0)(typescript@5.9.3)(webpack@5.95.0(@swc/core@1.3.96))':
'@tarojs/webpack5-runner@4.2.0(@babel/core@7.29.0)(@swc/core@1.3.96)(@tarojs/runtime@4.2.0)(less@3.13.1)(postcss@8.5.12)(sass@1.99.0)(stylus@0.64.0)(typescript@5.9.3)(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))':
dependencies:
'@babel/core': 7.29.0
'@tarojs/helper': 4.2.0
'@tarojs/runner-utils': 4.2.0
'@tarojs/runtime': 4.2.0
'@tarojs/shared': 4.2.0
'@tarojs/taro-loader': 4.2.0(webpack@5.95.0(@swc/core@1.3.96))
'@tarojs/webpack5-prebundle': 4.2.0(webpack@5.95.0(@swc/core@1.3.96))
'@tarojs/taro-loader': 4.2.0(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
'@tarojs/webpack5-prebundle': 4.2.0(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
acorn: 8.16.0
acorn-walk: 8.3.5
autoprefixer: 10.5.0(postcss@8.5.12)
babel-loader: 8.2.1(@babel/core@7.29.0)(webpack@5.95.0(@swc/core@1.3.96))
copy-webpack-plugin: 12.0.2(webpack@5.95.0(@swc/core@1.3.96))
css-loader: 7.1.4(webpack@5.95.0(@swc/core@1.3.96))
css-minimizer-webpack-plugin: 6.0.0(esbuild@0.21.5)(lightningcss@1.32.0)(webpack@5.95.0(@swc/core@1.3.96))
babel-loader: 8.2.1(@babel/core@7.29.0)(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
copy-webpack-plugin: 12.0.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
css-loader: 7.1.4(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
css-minimizer-webpack-plugin: 6.0.0(esbuild@0.21.5)(lightningcss@1.32.0)(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
detect-port: 1.6.1
esbuild: 0.21.5
esbuild-loader: 4.4.3(webpack@5.95.0(@swc/core@1.3.96))
esbuild-loader: 4.4.3(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
html-minifier: 4.0.0
html-webpack-plugin: 5.6.7(webpack@5.95.0(@swc/core@1.3.96))
html-webpack-plugin: 5.6.7(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
jsdom: 24.1.3
less-loader: 12.3.2(less@3.13.1)(webpack@5.95.0(@swc/core@1.3.96))
less-loader: 12.3.2(less@3.13.1)(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
lightningcss: 1.32.0
loader-utils: 3.3.1
lodash: 4.18.1
md5: 2.3.0
mini-css-extract-plugin: 2.10.2(webpack@5.95.0(@swc/core@1.3.96))
mini-css-extract-plugin: 2.10.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
miniprogram-simulate: 1.6.1
ora: 5.4.1
picomatch: 4.0.4
postcss: 8.5.12
postcss-html-transform: 4.2.0(postcss@8.5.12)
postcss-import: 16.1.1(postcss@8.5.12)
postcss-loader: 8.2.1(postcss@8.5.12)(typescript@5.9.3)(webpack@5.95.0(@swc/core@1.3.96))
postcss-loader: 8.2.1(postcss@8.5.12)(typescript@5.9.3)(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
postcss-plugin-constparse: 4.2.0(postcss@8.5.12)
postcss-pxtransform: 4.2.0(postcss@8.5.12)
postcss-url: 10.1.3(postcss@8.5.12)
regenerator-runtime: 0.11.1
resolve-url-loader: 5.0.0
sass-loader: 14.2.1(sass@1.99.0)(webpack@5.95.0(@swc/core@1.3.96))
sass-loader: 14.2.1(sass@1.99.0)(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
sax: 1.2.4
style-loader: 3.3.4(webpack@5.95.0(@swc/core@1.3.96))
stylus-loader: 8.1.3(stylus@0.64.0)(webpack@5.95.0(@swc/core@1.3.96))
terser-webpack-plugin: 5.5.0(@swc/core@1.3.96)(esbuild@0.21.5)(webpack@5.95.0(@swc/core@1.3.96))
style-loader: 3.3.4(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
stylus-loader: 8.1.3(stylus@0.64.0)(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
terser-webpack-plugin: 5.5.0(@swc/core@1.3.96)(esbuild@0.21.5)(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
vm2: 3.10.5
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
webpack-chain: 6.5.1
webpack-dev-server: 4.15.2(webpack@5.95.0(@swc/core@1.3.96))
webpack-dev-server: 4.15.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
webpack-format-messages: 3.0.1
webpack-virtual-modules: 0.6.2
webpackbar: 5.0.2(webpack@5.95.0(@swc/core@1.3.96))
webpackbar: 5.0.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
optionalDependencies:
less: 3.13.1
sass: 1.99.0
@@ -7414,6 +7661,11 @@ snapshots:
- utf-8-validate
- webpack-cli
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
optional: true
'@types/archy@0.0.31': {}
'@types/body-parser@1.19.6':
@@ -7597,11 +7849,13 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.0
'@vitest/mocker@4.1.5':
'@vitest/mocker@4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@3.13.1)(sass@1.99.0)(stylus@0.64.0)(terser@5.46.2))':
dependencies:
'@vitest/spy': 4.1.5
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@3.13.1)(sass@1.99.0)(stylus@0.64.0)(terser@5.46.2)
'@vitest/pretty-format@4.1.5':
dependencies:
@@ -7826,7 +8080,7 @@ snapshots:
transitivePeerDependencies:
- debug
babel-loader@8.2.1(@babel/core@7.29.0)(webpack@5.95.0(@swc/core@1.3.96)):
babel-loader@8.2.1(@babel/core@7.29.0)(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)):
dependencies:
'@babel/core': 7.29.0
find-cache-dir: 2.1.0
@@ -7834,7 +8088,7 @@ snapshots:
make-dir: 2.1.0
pify: 4.0.1
schema-utils: 2.7.1
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
babel-plugin-const-enum@1.2.0(@babel/core@7.29.0):
dependencies:
@@ -8243,7 +8497,7 @@ snapshots:
dependencies:
is-what: 3.14.1
copy-webpack-plugin@12.0.2(webpack@5.95.0(@swc/core@1.3.96)):
copy-webpack-plugin@12.0.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)):
dependencies:
fast-glob: 3.3.3
glob-parent: 6.0.2
@@ -8251,7 +8505,7 @@ snapshots:
normalize-path: 3.0.0
schema-utils: 4.3.3
serialize-javascript: 6.0.2
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
core-js-compat@3.49.0:
dependencies:
@@ -8288,7 +8542,7 @@ snapshots:
dependencies:
postcss: 8.5.12
css-loader@7.1.4(webpack@5.95.0(@swc/core@1.3.96)):
css-loader@7.1.4(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)):
dependencies:
icss-utils: 5.1.0(postcss@8.5.12)
postcss: 8.5.12
@@ -8299,9 +8553,9 @@ snapshots:
postcss-value-parser: 4.2.0
semver: 7.7.4
optionalDependencies:
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
css-minimizer-webpack-plugin@6.0.0(esbuild@0.21.5)(lightningcss@1.32.0)(webpack@5.95.0(@swc/core@1.3.96)):
css-minimizer-webpack-plugin@6.0.0(esbuild@0.21.5)(lightningcss@1.32.0)(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
cssnano: 6.1.2(postcss@8.5.12)
@@ -8309,7 +8563,7 @@ snapshots:
postcss: 8.5.12
schema-utils: 4.3.3
serialize-javascript: 6.0.2
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
optionalDependencies:
esbuild: 0.21.5
lightningcss: 1.32.0
@@ -8674,12 +8928,12 @@ snapshots:
has-tostringtag: 1.0.2
hasown: 2.0.3
esbuild-loader@4.4.3(webpack@5.95.0(@swc/core@1.3.96)):
esbuild-loader@4.4.3(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)):
dependencies:
esbuild: 0.27.7
get-tsconfig: 4.14.0
loader-utils: 2.0.4
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
webpack-sources: 3.4.0
esbuild@0.21.5:
@@ -9272,7 +9526,7 @@ snapshots:
relateurl: 0.2.7
uglify-js: 3.19.3
html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)):
html-webpack-plugin@5.6.7(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)):
dependencies:
'@types/html-minifier-terser': 6.1.0
html-minifier-terser: 6.1.0
@@ -9280,7 +9534,7 @@ snapshots:
pretty-error: 4.0.0
tapable: 2.3.3
optionalDependencies:
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
htmlparser2@6.1.0:
dependencies:
@@ -9653,11 +9907,11 @@ snapshots:
picocolors: 1.1.1
shell-quote: 1.8.3
less-loader@12.3.2(less@3.13.1)(webpack@5.95.0(@swc/core@1.3.96)):
less-loader@12.3.2(less@3.13.1)(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)):
dependencies:
less: 3.13.1
optionalDependencies:
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
less@3.13.1:
dependencies:
@@ -9883,11 +10137,11 @@ snapshots:
dependencies:
dom-walk: 0.1.2
mini-css-extract-plugin@2.10.2(webpack@5.95.0(@swc/core@1.3.96)):
mini-css-extract-plugin@2.10.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)):
dependencies:
schema-utils: 4.3.3
tapable: 2.3.3
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
minimalistic-assert@1.0.1: {}
@@ -10295,14 +10549,14 @@ snapshots:
read-cache: 1.0.0
resolve: 1.22.12
postcss-loader@8.2.1(postcss@8.5.12)(typescript@5.9.3)(webpack@5.95.0(@swc/core@1.3.96)):
postcss-loader@8.2.1(postcss@8.5.12)(typescript@5.9.3)(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)):
dependencies:
cosmiconfig: 9.0.1(typescript@5.9.3)
jiti: 2.6.1
postcss: 8.5.12
semver: 7.7.4
optionalDependencies:
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
transitivePeerDependencies:
- typescript
@@ -10699,6 +10953,27 @@ snapshots:
dependencies:
glob: 7.2.3
rolldown@1.0.0-rc.17:
dependencies:
'@oxc-project/types': 0.127.0
'@rolldown/pluginutils': 1.0.0-rc.17
optionalDependencies:
'@rolldown/binding-android-arm64': 1.0.0-rc.17
'@rolldown/binding-darwin-arm64': 1.0.0-rc.17
'@rolldown/binding-darwin-x64': 1.0.0-rc.17
'@rolldown/binding-freebsd-x64': 1.0.0-rc.17
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17
'@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17
'@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.17
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.17
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.17
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17
rollup@3.30.0:
optionalDependencies:
fsevents: 2.3.3
@@ -10723,12 +10998,12 @@ snapshots:
safer-buffer@2.1.2: {}
sass-loader@14.2.1(sass@1.99.0)(webpack@5.95.0(@swc/core@1.3.96)):
sass-loader@14.2.1(sass@1.99.0)(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)):
dependencies:
neo-async: 2.6.2
optionalDependencies:
sass: 1.99.0
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
sass@1.99.0:
dependencies:
@@ -11019,9 +11294,9 @@ snapshots:
dependencies:
escape-string-regexp: 1.0.5
style-loader@3.3.4(webpack@5.95.0(@swc/core@1.3.96)):
style-loader@3.3.4(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)):
dependencies:
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
stylehacks@6.1.1(postcss@8.5.12):
dependencies:
@@ -11029,13 +11304,13 @@ snapshots:
postcss: 8.5.12
postcss-selector-parser: 6.1.2
stylus-loader@8.1.3(stylus@0.64.0)(webpack@5.95.0(@swc/core@1.3.96)):
stylus-loader@8.1.3(stylus@0.64.0)(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)):
dependencies:
fast-glob: 3.3.3
normalize-path: 3.0.0
stylus: 0.64.0
optionalDependencies:
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
stylus@0.64.0:
dependencies:
@@ -11088,17 +11363,28 @@ snapshots:
to-buffer: 1.2.2
xtend: 4.0.2
terser-webpack-plugin@5.5.0(@swc/core@1.3.96)(esbuild@0.21.5)(webpack@5.95.0(@swc/core@1.3.96)):
terser-webpack-plugin@5.5.0(@swc/core@1.3.96)(esbuild@0.21.5)(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
jest-worker: 27.5.1
schema-utils: 4.3.3
terser: 5.46.2
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
optionalDependencies:
'@swc/core': 1.3.96
esbuild: 0.21.5
terser-webpack-plugin@5.5.0(@swc/core@1.3.96)(esbuild@0.27.7)(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
jest-worker: 27.5.1
schema-utils: 4.3.3
terser: 5.46.2
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
optionalDependencies:
'@swc/core': 1.3.96
esbuild: 0.27.7
terser@5.46.2:
dependencies:
'@jridgewell/source-map': 0.3.11
@@ -11272,10 +11558,27 @@ snapshots:
vary@1.1.2: {}
vitest@4.1.5(@types/node@25.6.0)(jsdom@24.1.3):
vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@3.13.1)(sass@1.99.0)(stylus@0.64.0)(terser@5.46.2):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
postcss: 8.5.12
rolldown: 1.0.0-rc.17
tinyglobby: 0.2.16
optionalDependencies:
'@types/node': 25.6.0
esbuild: 0.27.7
fsevents: 2.3.3
jiti: 2.6.1
less: 3.13.1
sass: 1.99.0
stylus: 0.64.0
terser: 5.46.2
vitest@4.1.5(@types/node@25.6.0)(jsdom@24.1.3)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@3.13.1)(sass@1.99.0)(stylus@0.64.0)(terser@5.46.2)):
dependencies:
'@vitest/expect': 4.1.5
'@vitest/mocker': 4.1.5
'@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@3.13.1)(sass@1.99.0)(stylus@0.64.0)(terser@5.46.2))
'@vitest/pretty-format': 4.1.5
'@vitest/runner': 4.1.5
'@vitest/snapshot': 4.1.5
@@ -11292,6 +11595,7 @@ snapshots:
tinyexec: 1.1.1
tinyglobby: 0.2.16
tinyrainbow: 3.1.0
vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@3.13.1)(sass@1.99.0)(stylus@0.64.0)(terser@5.46.2)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 25.6.0
@@ -11328,16 +11632,16 @@ snapshots:
deepmerge: 1.5.2
javascript-stringify: 2.1.0
webpack-dev-middleware@5.3.4(webpack@5.95.0(@swc/core@1.3.96)):
webpack-dev-middleware@5.3.4(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)):
dependencies:
colorette: 2.0.20
memfs: 3.5.3
mime-types: 2.1.35
range-parser: 1.2.1
schema-utils: 4.3.3
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)):
webpack-dev-server@4.15.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)):
dependencies:
'@types/bonjour': 3.5.13
'@types/connect-history-api-fallback': 1.5.4
@@ -11367,10 +11671,10 @@ snapshots:
serve-index: 1.9.2
sockjs: 0.3.24
spdy: 4.0.2
webpack-dev-middleware: 5.3.4(webpack@5.95.0(@swc/core@1.3.96))
webpack-dev-middleware: 5.3.4(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
ws: 8.20.0
optionalDependencies:
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
transitivePeerDependencies:
- bufferutil
- debug
@@ -11391,7 +11695,7 @@ snapshots:
webpack-virtual-modules@0.6.2: {}
webpack@5.95.0(@swc/core@1.3.96):
webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7):
dependencies:
'@types/estree': 1.0.8
'@webassemblyjs/ast': 1.14.1
@@ -11413,7 +11717,7 @@ snapshots:
neo-async: 2.6.2
schema-utils: 3.3.0
tapable: 2.3.3
terser-webpack-plugin: 5.5.0(@swc/core@1.3.96)(esbuild@0.21.5)(webpack@5.95.0(@swc/core@1.3.96))
terser-webpack-plugin: 5.5.0(@swc/core@1.3.96)(esbuild@0.27.7)(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7))
watchpack: 2.5.1
webpack-sources: 3.4.0
transitivePeerDependencies:
@@ -11421,13 +11725,13 @@ snapshots:
- esbuild
- uglify-js
webpackbar@5.0.2(webpack@5.95.0(@swc/core@1.3.96)):
webpackbar@5.0.2(webpack@5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)):
dependencies:
chalk: 4.1.2
consola: 2.15.3
pretty-time: 1.1.0
std-env: 3.10.0
webpack: 5.95.0(@swc/core@1.3.96)
webpack: 5.95.0(@swc/core@1.3.96)(esbuild@0.27.7)
websocket-driver@0.7.4:
dependencies:

View File

@@ -1,10 +1,12 @@
import { useState, useCallback } from 'react';
import { useState, useCallback, useMemo } from 'react';
import { View, Text } from '@tarojs/components';
import Taro, { useDidShow, useRouter } from '@tarojs/taro';
import { BLEManager } from '@/services/ble/BLEManager';
import { XiaomiBandAdapter } from '@/services/ble/adapters/XiaomiBandAdapter';
import { BloodPressureAdapter } from '@/services/ble/adapters/BloodPressureAdapter';
import { GlucoseMeterAdapter } from '@/services/ble/adapters/GlucoseMeterAdapter';
import { CustomBandAdapter } from '@/services/ble/adapters/GenericBleAdapter';
import { DataSyncScheduler } from '@/services/ble/DataSyncScheduler';
import { uploadReadings } from '@/services/device-sync';
import { useAuthStore } from '@/stores/auth';
import type { BLEDevice, NormalizedReading } from '@/services/ble/types';
@@ -14,6 +16,7 @@ const bleManager = new BLEManager({ scanTimeout: 10000, retryCount: 3 });
bleManager.registerAdapter(XiaomiBandAdapter);
bleManager.registerAdapter(BloodPressureAdapter);
bleManager.registerAdapter(GlucoseMeterAdapter);
bleManager.registerAdapter(CustomBandAdapter);
type PageState = 'idle' | 'scanning' | 'connecting' | 'connected' | 'syncing' | 'done' | 'error';
@@ -27,6 +30,12 @@ export default function DeviceSync() {
const [liveReadings, setLiveReadings] = useState<NormalizedReading[]>([]);
const [syncCount, setSyncCount] = useState(0);
const [errorMsg, setErrorMsg] = useState('');
const [lastSyncAt, setLastSyncAt] = useState<number | null>(null);
const [pendingCount, setPendingCount] = useState(0);
const scheduler = useMemo(() => new DataSyncScheduler({
intervalMs: 60 * 60 * 1000,
}), []);
useDidShow(() => {
bleManager.setOnConnectionChange(() => {});
@@ -34,7 +43,29 @@ export default function DeviceSync() {
setLiveReadings((prev) => [...prev, ...readings]);
});
// 显示上次同步时间
setLastSyncAt(scheduler.getLastSyncAt());
// 检查是否有未上传的缓冲数据
const buffer = (bleManager as any).dataBuffer;
if (buffer) {
setPendingCount(buffer.size());
}
// 自动同步:超过间隔时尝试上传缓冲数据
if (currentPatient && scheduler.needsSync()) {
scheduler.tryAutoSync(async () => {
const count = await bleManager.flushPendingReadings(async (readings) => {
return uploadReadings(currentPatient.id, 'buffered', undefined, readings);
});
setLastSyncAt(Date.now());
setPendingCount(0);
return { success: count > 0, uploadedCount: count };
});
}
return () => {
scheduler.destroy();
bleManager.destroy();
};
});
@@ -87,6 +118,7 @@ export default function DeviceSync() {
if (result.success) {
setSyncCount(result.uploadedCount);
setLastSyncAt(Date.now());
setPageState('done');
// 如果从体征录入页跳转而来,将最新读数写入 storage 供回填
@@ -136,6 +168,21 @@ export default function DeviceSync() {
<Text className="sync-hero-desc"></Text>
</View>
{(lastSyncAt || pendingCount > 0) && (
<View className="sync-status-info">
{lastSyncAt && (
<Text className="sync-status-time">
: {new Date(lastSyncAt).toLocaleTimeString()}
</Text>
)}
{pendingCount > 0 && (
<Text className="sync-status-pending">
{pendingCount}
</Text>
)}
</View>
)}
<View className="sync-action" onClick={handleScan}>
<Text className="sync-action-text"></Text>
</View>

View File

@@ -8,9 +8,7 @@ import type {
SyncResult,
BLEManagerConfig,
} from './types';
const CACHE_KEY = 'ble_pending_readings';
const CACHE_MAX = 2000;
import { DataBuffer } from './DataBuffer';
const DEFAULT_CONFIG: BLEManagerConfig = {
scanTimeout: 10000,
@@ -22,6 +20,7 @@ export class BLEManager {
private adapters: DeviceAdapter[] = [];
private connection: BLEConnection | null = null;
private readings: NormalizedReading[] = [];
private dataBuffer: DataBuffer;
private config: BLEManagerConfig;
private scanTimer: ReturnType<typeof setTimeout> | null = null;
private onConnectionChange?: (state: BLEConnectionState) => void;
@@ -29,6 +28,8 @@ export class BLEManager {
constructor(config?: Partial<BLEManagerConfig>) {
this.config = { ...DEFAULT_CONFIG, ...config };
this.dataBuffer = new DataBuffer();
this.dataBuffer.restore();
}
/** 注册设备适配器 */
@@ -182,7 +183,7 @@ export class BLEManager {
);
if (newReadings.length > 0) {
this.readings = [...this.readings, ...newReadings];
this.persistPendingReadings();
this.dataBuffer.push(newReadings);
this.onReadings?.(newReadings);
}
});
@@ -213,8 +214,6 @@ export class BLEManager {
if (!svc) continue;
try {
// Taro readBLECharacteristicValue 触发 onBLECharacteristicValueChange 回调
// 读取结果会通过 BLEManager 已注册的 onBLECharacteristicValueChange 监听器返回
await Taro.readBLECharacteristicValue({
deviceId,
serviceId: svc.uuid,
@@ -241,7 +240,7 @@ export class BLEManager {
this.updateState('syncing');
const batch = this.readings.slice(-this.config.maxReadingsPerSync);
const batch = this.dataBuffer.flush();
if (batch.length === 0) {
this.updateState('connected');
return { success: true, readingsCount: 0, uploadedCount: 0 };
@@ -251,8 +250,7 @@ export class BLEManager {
for (let attempt = 1; attempt <= this.config.retryCount; attempt++) {
try {
const uploaded = await uploadFn(batch);
this.readings = this.readings.slice(batch.length);
this.clearPendingReadings();
this.readings = [];
this.updateState('connected');
return {
success: true,
@@ -261,6 +259,8 @@ export class BLEManager {
};
} catch (e: any) {
lastError = e.message || '上传失败';
// flush 已取出,失败时需要放回
this.dataBuffer.push(batch);
}
}
@@ -309,46 +309,19 @@ export class BLEManager {
this.readings = [];
}
// ── 离线缓存 ──
/** 将当前未上传读数同步写入 Storage */
private persistPendingReadings(): void {
try {
const batch = this.readings.slice(-CACHE_MAX);
Taro.setStorageSync(CACHE_KEY, JSON.stringify(batch));
} catch {
// Storage 写入失败不影响主流程
}
}
/** 上传成功后清除缓存 */
private clearPendingReadings(): void {
try {
Taro.removeStorageSync(CACHE_KEY);
} catch {
// 忽略
}
}
/** 启动时检查缓存,有未上传数据则自动重传。
* 返回重传的记录数 */
/** 启动时检查缓存,有未上传数据则自动重传 */
async flushPendingReadings(
uploadFn: (readings: NormalizedReading[]) => Promise<number>,
): Promise<number> {
try {
const raw = Taro.getStorageSync(CACHE_KEY) as string;
if (!raw) return 0;
const cached: NormalizedReading[] = JSON.parse(raw);
if (!Array.isArray(cached) || cached.length === 0) {
Taro.removeStorageSync(CACHE_KEY);
return 0;
}
const batch = this.dataBuffer.flush();
if (batch.length === 0) return 0;
const batch = cached.slice(0, this.config.maxReadingsPerSync);
try {
const uploaded = await uploadFn(batch);
Taro.removeStorageSync(CACHE_KEY);
return uploaded;
} catch {
// 失败时放回
this.dataBuffer.push(batch);
return 0;
}
}

View File

@@ -0,0 +1,149 @@
import Taro from '@tarojs/taro';
import type { NormalizedReading } from './types';
export interface DataBufferConfig {
/** 单桶最大条数(默认 500 */
bucketSize?: number;
/** 总最大条数,超出丢弃最旧(默认 2000 */
maxTotal?: number;
/** Storage key 前缀(默认 'ble_buffer' */
storageKeyPrefix?: string;
}
const DEFAULT_CONFIG: Required<DataBufferConfig> = {
bucketSize: 500,
maxTotal: 2000,
storageKeyPrefix: 'ble_buffer',
};
/** 离线数据缓冲 — 分桶持久化到 Storage支持去重和容量管理 */
export class DataBuffer {
private config: Required<DataBufferConfig>;
private buckets: NormalizedReading[][] = [[]];
private currentBucketIndex = 0;
private seenKeys: Set<string>;
constructor(config?: DataBufferConfig) {
this.config = { ...DEFAULT_CONFIG, ...config };
this.seenKeys = new Set();
}
/** 添加读数(单条或批量) */
push(readings: NormalizedReading | NormalizedReading[]): void {
const items = Array.isArray(readings) ? readings : [readings];
const deduped = items.filter((r) => {
const key = this.dedupeKey(r);
if (this.seenKeys.has(key)) return false;
this.seenKeys.add(key);
return true;
});
if (deduped.length === 0) return;
let current = this.buckets[this.currentBucketIndex];
for (const r of deduped) {
if (current.length >= this.config.bucketSize) {
this.currentBucketIndex++;
current = [];
this.buckets.push(current);
}
current.push(r);
}
this.trimToMax();
this.persistCurrentBucket();
}
/** 取出所有缓冲数据并清空 */
flush(): NormalizedReading[] {
const all = this.buckets.flat();
this.buckets = [[]];
this.currentBucketIndex = 0;
this.seenKeys.clear();
this.clearStorage();
return all;
}
/** 缓冲区条数 */
size(): number {
return this.buckets.reduce((sum, b) => sum + b.length, 0);
}
/** 从 Storage 恢复(启动时调用) */
restore(): number {
this.buckets = [];
this.seenKeys.clear();
let total = 0;
let idx = 0;
while (true) {
const key = `${this.config.storageKeyPrefix}_${idx}`;
const raw = Taro.getStorageSync(key) as string;
if (!raw) break;
try {
const parsed: NormalizedReading[] = JSON.parse(raw);
if (Array.isArray(parsed) && parsed.length > 0) {
this.buckets.push(parsed);
for (const r of parsed) {
this.seenKeys.add(this.dedupeKey(r));
}
total += parsed.length;
}
} catch {
break;
}
idx++;
}
if (this.buckets.length === 0) {
this.buckets = [[]];
}
this.currentBucketIndex = this.buckets.length - 1;
return total;
}
/** 清空缓冲和 Storage */
clear(): void {
this.buckets = [[]];
this.currentBucketIndex = 0;
this.seenKeys.clear();
this.clearStorage();
}
private dedupeKey(r: NormalizedReading): string {
return `${r.device_type}|${r.measured_at}|${r.metric ?? ''}`;
}
private trimToMax(): void {
let total = this.size();
while (total > this.config.maxTotal && this.buckets.length > 1) {
const removed = this.buckets.shift()!;
total -= removed.length;
this.currentBucketIndex--;
}
if (total > this.config.maxTotal) {
const excess = total - this.config.maxTotal;
this.buckets[0] = this.buckets[0].slice(excess);
}
}
private persistCurrentBucket(): void {
const key = `${this.config.storageKeyPrefix}_${this.currentBucketIndex}`;
try {
Taro.setStorageSync(key, JSON.stringify(this.buckets[this.currentBucketIndex]));
} catch {
// Storage 写入失败不影响主流程
}
}
private clearStorage(): void {
let idx = 0;
while (true) {
const key = `${this.config.storageKeyPrefix}_${idx}`;
const raw = Taro.getStorageSync(key) as string;
if (!raw) break;
try { Taro.removeStorageSync(key); } catch { /* ignore */ }
idx++;
}
}
}

View File

@@ -0,0 +1,100 @@
import Taro from '@tarojs/taro';
export interface SyncSchedulerConfig {
/** 同步间隔(毫秒,默认 1 小时) */
intervalMs?: number;
/** Storage key默认 'last_ble_sync' */
storageKey?: string;
}
interface SyncRecord {
lastSyncAt: number;
}
const DEFAULT_CONFIG: Required<SyncSchedulerConfig> = {
intervalMs: 60 * 60 * 1000,
storageKey: 'last_ble_sync',
};
export interface SyncResult {
success: boolean;
uploadedCount: number;
error?: string;
}
/** BLE 数据同步调度器 — 基于时间间隔判断是否需要同步 */
export class DataSyncScheduler {
private config: Required<SyncSchedulerConfig>;
private timerId: ReturnType<typeof setInterval> | null = null;
constructor(config?: SyncSchedulerConfig) {
this.config = { ...DEFAULT_CONFIG, ...config };
}
/** 判断是否需要同步 */
needsSync(): boolean {
const record = this.loadRecord();
if (!record) return true;
return Date.now() - record.lastSyncAt >= this.config.intervalMs;
}
/** 执行同步并记录时间戳 */
async recordSync(syncFn: () => Promise<SyncResult>): Promise<SyncResult> {
try {
const result = await syncFn();
if (result.success) {
this.saveRecord({ lastSyncAt: Date.now() });
}
return result;
} catch (e: any) {
return { success: false, uploadedCount: 0, error: e.message || '同步失败' };
}
}
/** 自动同步:仅在 needsSync() 为 true 时触发 */
async tryAutoSync(syncFn: () => Promise<SyncResult>): Promise<boolean> {
if (!this.needsSync()) return false;
const result = await this.recordSync(syncFn);
return result.success;
}
/** 启动周期性检查(页面活跃时调用) */
startPeriodicCheck(syncFn: () => Promise<SyncResult>, checkIntervalMs: number): void {
this.destroy();
this.timerId = setInterval(() => {
this.tryAutoSync(syncFn);
}, checkIntervalMs);
}
/** 停止周期性检查 */
destroy(): void {
if (this.timerId !== null) {
clearInterval(this.timerId);
this.timerId = null;
}
}
/** 获取上次同步时间戳 */
getLastSyncAt(): number | null {
const record = this.loadRecord();
return record?.lastSyncAt ?? null;
}
private loadRecord(): SyncRecord | null {
try {
const raw = Taro.getStorageSync(this.config.storageKey) as string;
if (!raw) return null;
return JSON.parse(raw) as SyncRecord;
} catch {
return null;
}
}
private saveRecord(record: SyncRecord): void {
try {
Taro.setStorageSync(this.config.storageKey, JSON.stringify(record));
} catch {
// Storage 写入失败不影响主流程
}
}
}

View File

@@ -0,0 +1,145 @@
import type { DeviceAdapter, NormalizedReading, GenericBLEProfile } from '../types';
// ── Bluetooth SIG 标准 Service UUID ──
const SERVICES: Record<string, { uuid: string; chars: { notify: string; read: string } }> = {
heart_rate: {
uuid: '0000180D-0000-1000-8000-00805F9B34FB',
chars: {
notify: '00002A37-0000-1000-8000-00805F9B34FB',
read: '00002A37-0000-1000-8000-00805F9B34FB',
},
},
health_thermometer: {
uuid: '00001809-0000-1000-8000-00805F9B34FB',
chars: {
notify: '00002A1C-0000-1000-8000-00805F9B34FB',
read: '00002A1C-0000-1000-8000-00805F9B34FB',
},
},
blood_pressure: {
uuid: '00001810-0000-1000-8000-00805F9B34FB',
chars: {
notify: '00002A35-0000-1000-8000-00805F9B34FB',
read: '00002A35-0000-1000-8000-00805F9B34FB',
},
},
};
// ── 解析器 ──
function parseHeartRate(data: ArrayBuffer): NormalizedReading | null {
const view = new DataView(data);
if (view.byteLength < 2) return null;
const flags = view.getUint8(0);
const isUINT16 = (flags & 0x01) !== 0;
const hr = isUINT16
? (view.byteLength >= 3 ? view.getUint16(1, true) : null)
: view.getUint8(1);
if (hr === null || hr <= 0 || hr > 300) return null;
return {
device_type: 'heart_rate',
values: { heart_rate: hr },
measured_at: new Date().toISOString(),
};
}
function parseTemperature(data: ArrayBuffer): NormalizedReading | null {
const view = new DataView(data);
if (view.byteLength < 4) return null;
const flags = view.getUint8(0);
const isFahrenheit = (flags & 0x01) !== 0;
const mantissa = view.getInt16(1, true);
const exponent = view.getInt8(3);
const temp = mantissa * Math.pow(10, exponent);
const tempCelsius = isFahrenheit ? (temp - 32) * 5 / 9 : temp;
return {
device_type: 'temperature',
values: { value: Math.round(tempCelsius * 10) / 10, unit: '°C' },
measured_at: new Date().toISOString(),
};
}
// ── 工厂函数 ──
export interface GenericAdapterConfig {
name: string;
supportedModels: string[];
profiles: GenericBLEProfile[];
}
/** 创建通用 BLE 适配器 — 基于 Bluetooth SIG 标准 Health Profile */
export function createGenericBleAdapter(config: GenericAdapterConfig): DeviceAdapter {
const activeServices = config.profiles
.map((p) => SERVICES[p])
.filter(Boolean);
return {
name: config.name,
supportedModels: config.supportedModels,
serviceUUIDs: activeServices.map((s) => s.uuid),
notifyCharacteristics: activeServices.map((s) => ({
service: s.uuid,
characteristic: s.chars.notify,
})),
readCharacteristics: activeServices.map((s) => ({
service: s.uuid,
characteristic: s.chars.read,
})),
parseNotification(
_serviceUUID: string,
charUUID: string,
data: ArrayBuffer,
): NormalizedReading[] {
const upper = charUUID.toUpperCase();
// Heart Rate Measurement
const hrsChar = SERVICES.heart_rate.chars.notify.toUpperCase();
if (upper === hrsChar || upper.includes('2A37')) {
const result = parseHeartRate(data);
return result ? [result] : [];
}
// Temperature Measurement
const htChar = SERVICES.health_thermometer.chars.notify.toUpperCase();
if (upper === htChar || upper.includes('2A1C')) {
const result = parseTemperature(data);
return result ? [result] : [];
}
return [];
},
parseReadResponse(
serviceUUID: string,
charUUID: string,
data: ArrayBuffer,
): NormalizedReading[] {
return this.parseNotification(serviceUUID, charUUID, data);
},
};
}
/**
* 预配置:通用定制手环(心率 + 体温)
* 未来定制手环只需在 supportedModels 中加入型号名
*/
export const CustomBandAdapter = createGenericBleAdapter({
name: 'Custom Health Band',
supportedModels: [
'Health Band',
'Medical Band',
'Smart Bracelet',
'健康手环',
],
profiles: ['heart_rate', 'health_thermometer'],
});
export default CustomBandAdapter;

View File

@@ -1,3 +1,4 @@
export { XiaomiBandAdapter } from './XiaomiBandAdapter';
export { BloodPressureAdapter } from './BloodPressureAdapter';
export { GlucoseMeterAdapter } from './GlucoseMeterAdapter';
export { CustomBandAdapter, createGenericBleAdapter } from './GenericBleAdapter';

View File

@@ -1,4 +1,8 @@
export { BLEManager } from './BLEManager';
export { DataBuffer } from './DataBuffer';
export type { DataBufferConfig } from './DataBuffer';
export { DataSyncScheduler } from './DataSyncScheduler';
export type { SyncSchedulerConfig, SyncResult as SchedulerSyncResult } from './DataSyncScheduler';
export type {
DeviceType,
NormalizedReading,
@@ -8,4 +12,5 @@ export type {
BLEConnectionState,
SyncResult,
BLEManagerConfig,
GenericBLEProfile,
} from './types';

View File

@@ -91,3 +91,9 @@ export interface BLEManagerConfig {
/** 同步失败重试次数 */
retryCount: number;
}
/** 通用 BLE 适配器可识别的标准 BLE Profile */
export type GenericBLEProfile =
| 'heart_rate' // Heart Rate Service (0x180D)
| 'health_thermometer' // Health Thermometer Service (0x1809)
| 'blood_pressure'; // Blood Pressure Service (0x1810)

View File

@@ -0,0 +1,15 @@
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
test: {
environment: 'node',
include: ['__tests__/**/*.test.ts'],
globals: true,
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
});