Files
AI-Health/health_app/lib/providers/omron_device_provider.dart
MingNian d82e006cf4 refactor: 死代码清理 + 趋势图重构 + 侧边栏交互优化 + 智能体卡片去延迟
- 死代码清理: 删除 AppCard/AppMenuItem/AppTabChip/AppButtons 等 4 个未使用 Widget 文件; 清理 common_widgets/enterprise_widgets/app_status_badge 中未使用 class; 移除 HealthService 等未使用方法; 后端移除 ExerciseService.CreateFromItemsAsync/HealthArchiveService.GetOrCreateAsync/MedicationAgentHandler 死分支/ChatRequest DTO
- 趋势图: 直线折线 + 渐变填充 + 阴影; 去网格; X 轴日+月份; Y 轴整数刻度最多 4 个; 点击数据点/录入记录显示上方浮层; 选中点变实心
- 侧边栏: 对话记录改单选删除(灰底高亮); 操作栏浮层修复触摸问题; 常用功能/健康仪表盘间距收紧; _Panel 去外框
- 智能体: 欢迎卡片去掉 400ms 延迟; 胶囊去阴影
- 其他: api_client IP 适配; 多页面 UI 微调; 新增 chat_provider/prelaunch_guardrails 测试
2026-07-09 15:04:02 +08:00

243 lines
7.1 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/ble_device.dart';
import '../models/bp_reading.dart';
import '../services/health_ble_service.dart';
import 'auth_provider.dart';
import 'data_providers.dart';
const _boundDevicesKey = 'bound_ble_devices';
const _readingFingerprintsKey = 'ble_reading_fingerprints';
const _legacyBpMacKey = 'bp_device_mac';
const _legacyBpNameKey = 'bp_device_name';
const _legacyBpLastSyncKey = 'bp_last_sync';
final healthBleServiceProvider = Provider<HealthBleService>((ref) {
final service = HealthBleService();
ref.onDispose(service.dispose);
return service;
});
class DeviceBindState {
final List<BoundBleDevice> devices;
final bool isConnected;
const DeviceBindState({this.devices = const [], this.isConnected = false});
bool get isBound => devices.isNotEmpty;
DeviceBindState copyWith({List<BoundBleDevice>? devices, bool? isConnected}) {
return DeviceBindState(
devices: devices ?? this.devices,
isConnected: isConnected ?? this.isConnected,
);
}
List<BoundBleDevice> devicesOfType(BleDeviceType type) {
return devices.where((device) => device.type == type).toList();
}
BoundBleDevice? findById(String id) {
for (final device in devices) {
if (device.id == id) return device;
}
return null;
}
}
class DeviceBindNotifier extends Notifier<DeviceBindState> {
StreamSubscription<bool>? _connSub;
@override
DeviceBindState build() {
ref.onDispose(() => _connSub?.cancel());
_loadBinding();
_listenConnection();
return const DeviceBindState();
}
void _listenConnection() {
_connSub?.cancel();
_connSub = ref.read(healthBleServiceProvider).connectionState.listen((
connected,
) {
if (state.isConnected != connected) {
state = state.copyWith(isConnected: connected);
}
});
}
Future<void> _loadBinding() async {
final db = ref.read(localDbProvider);
await _migrateLegacyBloodPressureDevice();
final raw = await db.read(_boundDevicesKey);
final devices = _decodeDevices(raw);
state = state.copyWith(devices: devices);
}
Future<void> _migrateLegacyBloodPressureDevice() async {
final db = ref.read(localDbProvider);
final existing = await db.read(_boundDevicesKey);
final legacyMac = await db.read(_legacyBpMacKey);
if (existing != null || legacyMac == null || legacyMac.isEmpty) return;
final legacyName = await db.read(_legacyBpNameKey);
final migrated = BoundBleDevice(
id: legacyMac,
name: legacyName?.isNotEmpty == true ? legacyName! : '血压计',
type: BleDeviceType.bloodPressure,
serviceUuid: BleDeviceType.bloodPressure.serviceUuid,
);
await db.write(_boundDevicesKey, jsonEncode([migrated.toJson()]));
await db.delete(_legacyBpMacKey);
await db.delete(_legacyBpNameKey);
await db.delete(_legacyBpLastSyncKey);
}
List<BoundBleDevice> _decodeDevices(String? raw) {
if (raw == null || raw.isEmpty) return [];
try {
final decoded = jsonDecode(raw);
if (decoded is! List) return [];
return decoded
.whereType<Map>()
.map(
(item) => BoundBleDevice.fromJson(Map<String, dynamic>.from(item)),
)
.whereType<BoundBleDevice>()
.toList();
} catch (_) {
return [];
}
}
Future<void> _saveDevices(List<BoundBleDevice> devices) async {
final db = ref.read(localDbProvider);
await db.write(
_boundDevicesKey,
jsonEncode(devices.map((device) => device.toJson()).toList()),
);
state = state.copyWith(devices: devices);
}
Future<void> bind(BoundBleDevice device) async {
final devices = [...state.devices];
final index = devices.indexWhere((item) => item.id == device.id);
if (index >= 0) {
devices[index] = device.copyWith(lastSyncAt: devices[index].lastSyncAt);
} else {
devices.add(device);
}
await _saveDevices(devices);
}
Future<void> unbind(String id) async {
await _saveDevices(
state.devices.where((device) => device.id != id).toList(),
);
}
Future<void> recordSuccessfulSync({
required BoundBleDevice device,
required BpReading reading,
}) async {
await _rememberReadingFingerprint(device, reading);
final syncedAt = DateTime.now();
final devices = state.devices.map((item) {
if (item.id != device.id) return item;
return item.copyWith(lastSyncAt: syncedAt);
}).toList();
await _saveDevices(devices);
ref.invalidate(latestHealthProvider);
}
Future<bool> isDuplicateReading(
BoundBleDevice device,
BpReading reading,
) async {
final fingerprints = await _loadFingerprints();
final now = DateTime.now();
final pruned = _pruneFingerprints(fingerprints, now);
if (pruned.length != fingerprints.length) {
await _saveFingerprints(pruned);
}
final key = _readingFingerprint(device, reading);
final lastSeenRaw = pruned[key];
if (lastSeenRaw == null) return false;
final lastSeen = DateTime.tryParse(lastSeenRaw)?.toLocal();
if (lastSeen == null) return false;
if (reading.hasDeviceTimestamp) return true;
return now.difference(lastSeen).inSeconds < 10;
}
Future<void> _rememberReadingFingerprint(
BoundBleDevice device,
BpReading reading,
) async {
final now = DateTime.now();
final fingerprints = _pruneFingerprints(await _loadFingerprints(), now);
fingerprints[_readingFingerprint(device, reading)] = now
.toUtc()
.toIso8601String();
await _saveFingerprints(fingerprints);
}
Future<Map<String, String>> _loadFingerprints() async {
final db = ref.read(localDbProvider);
final raw = await db.read(_readingFingerprintsKey);
if (raw == null || raw.isEmpty) return {};
try {
final decoded = jsonDecode(raw);
if (decoded is! Map) return {};
return decoded.map(
(key, value) => MapEntry(key.toString(), value.toString()),
);
} catch (_) {
return {};
}
}
Future<void> _saveFingerprints(Map<String, String> fingerprints) async {
final db = ref.read(localDbProvider);
await db.write(_readingFingerprintsKey, jsonEncode(fingerprints));
}
Map<String, String> _pruneFingerprints(
Map<String, String> fingerprints,
DateTime now,
) {
final pruned = <String, String>{};
for (final entry in fingerprints.entries) {
final seenAt = DateTime.tryParse(entry.value)?.toLocal();
if (seenAt == null) continue;
if (now.difference(seenAt).inHours <= 24) {
pruned[entry.key] = entry.value;
}
}
return pruned;
}
String _readingFingerprint(BoundBleDevice device, BpReading reading) {
final timePart = reading.hasDeviceTimestamp
? reading.timestamp.toUtc().toIso8601String()
: 'no-device-time';
return [
device.id,
device.type.code,
timePart,
reading.systolic,
reading.diastolic,
reading.pulse ?? 0,
].join('|');
}
}
final omronDeviceProvider =
NotifierProvider<DeviceBindNotifier, DeviceBindState>(
DeviceBindNotifier.new,
);