feat(miniprogram): BLE 增强层 — DataBuffer + GenericBleAdapter + DataSyncScheduler
- 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:
69
apps/miniprogram/__tests__/services/ble/BLEManager.test.ts
Normal file
69
apps/miniprogram/__tests__/services/ble/BLEManager.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
89
apps/miniprogram/__tests__/services/ble/DataBuffer.test.ts
Normal file
89
apps/miniprogram/__tests__/services/ble/DataBuffer.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
456
apps/miniprogram/pnpm-lock.yaml
generated
456
apps/miniprogram/pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
149
apps/miniprogram/src/services/ble/DataBuffer.ts
Normal file
149
apps/miniprogram/src/services/ble/DataBuffer.ts
Normal 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
100
apps/miniprogram/src/services/ble/DataSyncScheduler.ts
Normal file
100
apps/miniprogram/src/services/ble/DataSyncScheduler.ts
Normal 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 写入失败不影响主流程
|
||||
}
|
||||
}
|
||||
}
|
||||
145
apps/miniprogram/src/services/ble/adapters/GenericBleAdapter.ts
Normal file
145
apps/miniprogram/src/services/ble/adapters/GenericBleAdapter.ts
Normal 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;
|
||||
@@ -1,3 +1,4 @@
|
||||
export { XiaomiBandAdapter } from './XiaomiBandAdapter';
|
||||
export { BloodPressureAdapter } from './BloodPressureAdapter';
|
||||
export { GlucoseMeterAdapter } from './GlucoseMeterAdapter';
|
||||
export { CustomBandAdapter, createGenericBleAdapter } from './GenericBleAdapter';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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)
|
||||
|
||||
15
apps/miniprogram/vitest.config.ts
Normal file
15
apps/miniprogram/vitest.config.ts
Normal 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'),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user