diff --git a/apps/miniprogram/__tests__/services/ble/BLEManager.test.ts b/apps/miniprogram/__tests__/services/ble/BLEManager.test.ts new file mode 100644 index 0000000..641c755 --- /dev/null +++ b/apps/miniprogram/__tests__/services/ble/BLEManager.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; + +// 使用 vi.hoisted 确保 storage 在 mock 提升前可用 +const { storage } = vi.hoisted(() => ({ + storage: new Map(), +})); + +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'); + }); +}); diff --git a/apps/miniprogram/__tests__/services/ble/DataBuffer.test.ts b/apps/miniprogram/__tests__/services/ble/DataBuffer.test.ts new file mode 100644 index 0000000..5e1e3c4 --- /dev/null +++ b/apps/miniprogram/__tests__/services/ble/DataBuffer.test.ts @@ -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(); +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 { + 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); + }); +}); diff --git a/apps/miniprogram/__tests__/services/ble/DataSyncScheduler.test.ts b/apps/miniprogram/__tests__/services/ble/DataSyncScheduler.test.ts new file mode 100644 index 0000000..35baf04 --- /dev/null +++ b/apps/miniprogram/__tests__/services/ble/DataSyncScheduler.test.ts @@ -0,0 +1,91 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { DataSyncScheduler } from '@/services/ble/DataSyncScheduler'; + +const storage = new Map(); +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; + + 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'); + }); +}); diff --git a/apps/miniprogram/__tests__/services/ble/adapters/GenericBleAdapter.test.ts b/apps/miniprogram/__tests__/services/ble/adapters/GenericBleAdapter.test.ts new file mode 100644 index 0000000..3be39e5 --- /dev/null +++ b/apps/miniprogram/__tests__/services/ble/adapters/GenericBleAdapter.test.ts @@ -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); + }); + }); +}); diff --git a/apps/miniprogram/package.json b/apps/miniprogram/package.json index 96f5df7..6f044cf 100644 --- a/apps/miniprogram/package.json +++ b/apps/miniprogram/package.json @@ -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" } diff --git a/apps/miniprogram/pnpm-lock.yaml b/apps/miniprogram/pnpm-lock.yaml index 68b3034..6142a4d 100644 --- a/apps/miniprogram/pnpm-lock.yaml +++ b/apps/miniprogram/pnpm-lock.yaml @@ -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: diff --git a/apps/miniprogram/src/pages/device-sync/index.tsx b/apps/miniprogram/src/pages/device-sync/index.tsx index 0d17196..cdb522f 100644 --- a/apps/miniprogram/src/pages/device-sync/index.tsx +++ b/apps/miniprogram/src/pages/device-sync/index.tsx @@ -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([]); const [syncCount, setSyncCount] = useState(0); const [errorMsg, setErrorMsg] = useState(''); + const [lastSyncAt, setLastSyncAt] = useState(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() { 连接智能手环、血压计、血糖仪,自动采集健康数据 + {(lastSyncAt || pendingCount > 0) && ( + + {lastSyncAt && ( + + 上次同步: {new Date(lastSyncAt).toLocaleTimeString()} + + )} + {pendingCount > 0 && ( + + {pendingCount} 条数据待上传 + + )} + + )} + 扫描设备 diff --git a/apps/miniprogram/src/services/ble/BLEManager.ts b/apps/miniprogram/src/services/ble/BLEManager.ts index a685b88..ba70433 100644 --- a/apps/miniprogram/src/services/ble/BLEManager.ts +++ b/apps/miniprogram/src/services/ble/BLEManager.ts @@ -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 | null = null; private onConnectionChange?: (state: BLEConnectionState) => void; @@ -29,6 +28,8 @@ export class BLEManager { constructor(config?: Partial) { 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, ): Promise { - 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; } } diff --git a/apps/miniprogram/src/services/ble/DataBuffer.ts b/apps/miniprogram/src/services/ble/DataBuffer.ts new file mode 100644 index 0000000..73dd736 --- /dev/null +++ b/apps/miniprogram/src/services/ble/DataBuffer.ts @@ -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 = { + bucketSize: 500, + maxTotal: 2000, + storageKeyPrefix: 'ble_buffer', +}; + +/** 离线数据缓冲 — 分桶持久化到 Storage,支持去重和容量管理 */ +export class DataBuffer { + private config: Required; + private buckets: NormalizedReading[][] = [[]]; + private currentBucketIndex = 0; + private seenKeys: Set; + + 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++; + } + } +} diff --git a/apps/miniprogram/src/services/ble/DataSyncScheduler.ts b/apps/miniprogram/src/services/ble/DataSyncScheduler.ts new file mode 100644 index 0000000..47ecbd8 --- /dev/null +++ b/apps/miniprogram/src/services/ble/DataSyncScheduler.ts @@ -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 = { + intervalMs: 60 * 60 * 1000, + storageKey: 'last_ble_sync', +}; + +export interface SyncResult { + success: boolean; + uploadedCount: number; + error?: string; +} + +/** BLE 数据同步调度器 — 基于时间间隔判断是否需要同步 */ +export class DataSyncScheduler { + private config: Required; + private timerId: ReturnType | 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): Promise { + 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): Promise { + if (!this.needsSync()) return false; + const result = await this.recordSync(syncFn); + return result.success; + } + + /** 启动周期性检查(页面活跃时调用) */ + startPeriodicCheck(syncFn: () => Promise, 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 写入失败不影响主流程 + } + } +} diff --git a/apps/miniprogram/src/services/ble/adapters/GenericBleAdapter.ts b/apps/miniprogram/src/services/ble/adapters/GenericBleAdapter.ts new file mode 100644 index 0000000..87d79df --- /dev/null +++ b/apps/miniprogram/src/services/ble/adapters/GenericBleAdapter.ts @@ -0,0 +1,145 @@ +import type { DeviceAdapter, NormalizedReading, GenericBLEProfile } from '../types'; + +// ── Bluetooth SIG 标准 Service UUID ── +const SERVICES: Record = { + 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; diff --git a/apps/miniprogram/src/services/ble/adapters/index.ts b/apps/miniprogram/src/services/ble/adapters/index.ts index f47b279..db7a504 100644 --- a/apps/miniprogram/src/services/ble/adapters/index.ts +++ b/apps/miniprogram/src/services/ble/adapters/index.ts @@ -1,3 +1,4 @@ export { XiaomiBandAdapter } from './XiaomiBandAdapter'; export { BloodPressureAdapter } from './BloodPressureAdapter'; export { GlucoseMeterAdapter } from './GlucoseMeterAdapter'; +export { CustomBandAdapter, createGenericBleAdapter } from './GenericBleAdapter'; diff --git a/apps/miniprogram/src/services/ble/index.ts b/apps/miniprogram/src/services/ble/index.ts index e14c881..1810e13 100644 --- a/apps/miniprogram/src/services/ble/index.ts +++ b/apps/miniprogram/src/services/ble/index.ts @@ -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'; diff --git a/apps/miniprogram/src/services/ble/types.ts b/apps/miniprogram/src/services/ble/types.ts index 9350fd9..b02079b 100644 --- a/apps/miniprogram/src/services/ble/types.ts +++ b/apps/miniprogram/src/services/ble/types.ts @@ -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) diff --git a/apps/miniprogram/vitest.config.ts b/apps/miniprogram/vitest.config.ts new file mode 100644 index 0000000..5bd72c4 --- /dev/null +++ b/apps/miniprogram/vitest.config.ts @@ -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'), + }, + }, +});