后端: - 新增 ai_intent_router 意图路由 - 新增 AiEntryDraft 草稿存储 + EF 迁移 - 新增 Prompts 模块化(global/modules/rag/router) - AI Agent handlers 扩展 前端: - 新增 AI 同意门 (ai_consent_gate/provider/details_page) - HealthMetricVisuals 视觉统一 - 设备扫描/管理页面优化 - agent 插图替换 + 趋势指标图标 配置: - DeepSeek 模型改 deepseek-v4-flash - .gitignore 加 artifacts/audit - build.gradle.kts 签名配置调整
189 lines
5.7 KiB
Dart
189 lines
5.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
|
|
|
import '../core/app_module_visuals.dart';
|
|
import 'bp_reading.dart';
|
|
|
|
enum BleDeviceType { bloodPressure, glucose, weightScale, pulseOximeter }
|
|
|
|
enum SupportedMetric { bloodPressure, heartRate, glucose, weight, spo2 }
|
|
|
|
extension BleDeviceTypeX on BleDeviceType {
|
|
String get code => switch (this) {
|
|
BleDeviceType.bloodPressure => 'bloodPressure',
|
|
BleDeviceType.glucose => 'glucose',
|
|
BleDeviceType.weightScale => 'weightScale',
|
|
BleDeviceType.pulseOximeter => 'pulseOximeter',
|
|
};
|
|
|
|
String get label => switch (this) {
|
|
BleDeviceType.bloodPressure => '血压计',
|
|
BleDeviceType.glucose => '血糖仪',
|
|
BleDeviceType.weightScale => '体重秤',
|
|
BleDeviceType.pulseOximeter => '血氧仪',
|
|
};
|
|
|
|
String get serviceUuid => switch (this) {
|
|
BleDeviceType.bloodPressure => BleDeviceIdentifier.bloodPressureServiceUuid,
|
|
BleDeviceType.glucose => BleDeviceIdentifier.glucoseServiceUuid,
|
|
BleDeviceType.weightScale => BleDeviceIdentifier.weightScaleServiceUuid,
|
|
BleDeviceType.pulseOximeter => BleDeviceIdentifier.pulseOximeterServiceUuid,
|
|
};
|
|
|
|
List<SupportedMetric> get metrics => switch (this) {
|
|
BleDeviceType.bloodPressure => [
|
|
SupportedMetric.bloodPressure,
|
|
SupportedMetric.heartRate,
|
|
],
|
|
BleDeviceType.glucose => [SupportedMetric.glucose],
|
|
BleDeviceType.weightScale => [SupportedMetric.weight],
|
|
BleDeviceType.pulseOximeter => [
|
|
SupportedMetric.spo2,
|
|
SupportedMetric.heartRate,
|
|
],
|
|
};
|
|
|
|
IconData get icon => switch (this) {
|
|
BleDeviceType.bloodPressure => HealthMetricVisuals.bloodPressureIcon,
|
|
BleDeviceType.glucose => HealthMetricVisuals.glucoseIcon,
|
|
BleDeviceType.weightScale => Icons.monitor_weight_outlined,
|
|
BleDeviceType.pulseOximeter => HealthMetricVisuals.spo2Icon,
|
|
};
|
|
|
|
static BleDeviceType? fromCode(String? code) {
|
|
for (final type in BleDeviceType.values) {
|
|
if (type.code == code) return type;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
extension SupportedMetricX on SupportedMetric {
|
|
String get code => switch (this) {
|
|
SupportedMetric.bloodPressure => 'BloodPressure',
|
|
SupportedMetric.heartRate => 'HeartRate',
|
|
SupportedMetric.glucose => 'Glucose',
|
|
SupportedMetric.weight => 'Weight',
|
|
SupportedMetric.spo2 => 'SpO2',
|
|
};
|
|
|
|
String get label => switch (this) {
|
|
SupportedMetric.bloodPressure => '血压',
|
|
SupportedMetric.heartRate => '心率',
|
|
SupportedMetric.glucose => '血糖',
|
|
SupportedMetric.weight => '体重',
|
|
SupportedMetric.spo2 => '血氧',
|
|
};
|
|
}
|
|
|
|
class BoundBleDevice {
|
|
final String id;
|
|
final String name;
|
|
final BleDeviceType type;
|
|
final String serviceUuid;
|
|
final DateTime? lastSyncAt;
|
|
|
|
const BoundBleDevice({
|
|
required this.id,
|
|
required this.name,
|
|
required this.type,
|
|
required this.serviceUuid,
|
|
this.lastSyncAt,
|
|
});
|
|
|
|
List<SupportedMetric> get metrics => type.metrics;
|
|
|
|
BoundBleDevice copyWith({
|
|
String? id,
|
|
String? name,
|
|
BleDeviceType? type,
|
|
String? serviceUuid,
|
|
DateTime? lastSyncAt,
|
|
}) {
|
|
return BoundBleDevice(
|
|
id: id ?? this.id,
|
|
name: name ?? this.name,
|
|
type: type ?? this.type,
|
|
serviceUuid: serviceUuid ?? this.serviceUuid,
|
|
lastSyncAt: lastSyncAt ?? this.lastSyncAt,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'id': id,
|
|
'name': name,
|
|
'type': type.code,
|
|
'serviceUuid': serviceUuid,
|
|
'lastSyncAt': lastSyncAt?.toUtc().toIso8601String(),
|
|
};
|
|
|
|
static BoundBleDevice? fromJson(Map<String, dynamic> json) {
|
|
final type = BleDeviceTypeX.fromCode(json['type']?.toString());
|
|
final id = json['id']?.toString();
|
|
if (type == null || id == null || id.isEmpty) return null;
|
|
|
|
final lastSyncRaw = json['lastSyncAt']?.toString();
|
|
return BoundBleDevice(
|
|
id: id,
|
|
name: json['name']?.toString() ?? type.label,
|
|
type: type,
|
|
serviceUuid: json['serviceUuid']?.toString() ?? type.serviceUuid,
|
|
lastSyncAt: lastSyncRaw == null || lastSyncRaw.isEmpty
|
|
? null
|
|
: DateTime.tryParse(lastSyncRaw)?.toLocal(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class BleDeviceIdentifier {
|
|
static const glucoseServiceUuid = '00001808-0000-1000-8000-00805F9B34FB';
|
|
static const bloodPressureServiceUuid =
|
|
'00001810-0000-1000-8000-00805F9B34FB';
|
|
static const weightScaleServiceUuid = '0000181D-0000-1000-8000-00805F9B34FB';
|
|
static const pulseOximeterServiceUuid =
|
|
'00001822-0000-1000-8000-00805F9B34FB';
|
|
|
|
static BleDeviceType? typeFromScan(ScanResult result) {
|
|
return typeFromUuidStrings(
|
|
result.advertisementData.serviceUuids.map((uuid) => uuid.str128),
|
|
);
|
|
}
|
|
|
|
static BleDeviceType? typeFromServices(List<BluetoothService> services) {
|
|
return typeFromUuidStrings(services.map((service) => service.uuid.str128));
|
|
}
|
|
|
|
static BleDeviceType? typeFromUuidStrings(Iterable<String> uuids) {
|
|
final normalized = uuids.map((uuid) => uuid.toUpperCase()).toSet();
|
|
if (normalized.contains(bloodPressureServiceUuid)) {
|
|
return BleDeviceType.bloodPressure;
|
|
}
|
|
if (normalized.contains(glucoseServiceUuid)) {
|
|
return BleDeviceType.glucose;
|
|
}
|
|
if (normalized.contains(weightScaleServiceUuid)) {
|
|
return BleDeviceType.weightScale;
|
|
}
|
|
if (normalized.contains(pulseOximeterServiceUuid)) {
|
|
return BleDeviceType.pulseOximeter;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static bool isSupportedScanResult(ScanResult result) {
|
|
return typeFromScan(result) != null;
|
|
}
|
|
}
|
|
|
|
sealed class HealthBleSyncResult {
|
|
const HealthBleSyncResult({required this.device});
|
|
|
|
final BoundBleDevice device;
|
|
}
|
|
|
|
class BloodPressureSyncResult extends HealthBleSyncResult {
|
|
const BloodPressureSyncResult({required super.device, required this.reading});
|
|
|
|
final BpReading reading;
|
|
}
|