feat: UI 系统中心化 + 趋势图重构 + 法律文档 H5 上线

- UI 系统: 新增 design_tokens / module_visuals / app_buttons / app_status_badge / app_toast / ai_content 六个共享模块; 28 个页面接入, SnackBar 全部替换为 AppToast
- 趋势图: 直线折线 + 渐变填充 + 阴影; 去网格线; X 轴日+月份分隔; Y 轴整数刻度最多 4 个; 点击数据点/录入记录显示上方浮层, 选中点变实心
- 法律文档 H5: 后端 wwwroot 托管隐私政策/服务协议/关于/个人信息收集清单/第三方 SDK 清单 + 索引页; Program.cs 启用 UseDefaultFiles + UseStaticFiles
- 其他: auth_provider 登录流程调整; api_client IP 适配; 主页智能体栏间距优化
This commit is contained in:
MingNian
2026-07-08 21:25:07 +08:00
parent 7a93237069
commit 1c020b8ae5
45 changed files with 3480 additions and 1000 deletions

View File

@@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../core/app_colors.dart';
import '../../core/app_module_visuals.dart';
import '../../core/app_theme.dart';
import '../../core/navigation_provider.dart';
import '../../models/ble_device.dart';
@@ -15,6 +16,9 @@ import '../../providers/auth_provider.dart';
import '../../providers/omron_device_provider.dart';
import '../../services/health_ble_service.dart';
import '../../widgets/ble_sync_dialog.dart';
import '../../widgets/app_toast.dart';
const _deviceVisual = AppModuleVisuals.device;
class DeviceManagementPage extends ConsumerStatefulWidget {
const DeviceManagementPage({super.key});
@@ -39,6 +43,7 @@ class _DeviceManagementPageState extends ConsumerState<DeviceManagementPage>
DateTime? _scanStartedAt;
String _activeScanBoundKey = '';
bool _scanning = false;
bool _permissionBlocked = false;
bool _disposed = false;
@override
@@ -66,18 +71,20 @@ class _DeviceManagementPageState extends ConsumerState<DeviceManagementPage>
}
Future<void> _startForegroundScan() async {
await [Permission.bluetoothScan, Permission.bluetoothConnect].request();
if (!await _ensureBlePermissions()) {
if (mounted) setState(() => _scanning = false);
return;
}
if (_disposed || !mounted) return;
if (Platform.isAndroid) {
final locStatus = await Permission.locationWhenInUse.serviceStatus;
if (_disposed || !mounted) return;
if (locStatus != ServiceStatus.enabled && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('请先打开定位服务,否则可能无法发现蓝牙设备'),
backgroundColor: AppColors.warning,
),
AppToast.show(
context,
'请先打开定位服务,否则可能无法发现蓝牙设备',
type: AppToastType.warning,
);
}
}
@@ -120,6 +127,42 @@ class _DeviceManagementPageState extends ConsumerState<DeviceManagementPage>
}
}
Future<bool> _ensureBlePermissions() async {
final statuses = await [
Permission.bluetoothScan,
Permission.bluetoothConnect,
].request();
final granted = statuses.values.every((status) => status.isGranted);
if (granted) {
_permissionBlocked = false;
return true;
}
_permissionBlocked = true;
if (_disposed || !mounted) return false;
AppToast.show(context, '请开启蓝牙权限后再同步设备', type: AppToastType.warning);
await showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('需要蓝牙权限'),
content: const Text('同步已绑定设备需要蓝牙权限,请在系统设置中开启后重试。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('稍后再说'),
),
TextButton(
onPressed: () {
Navigator.pop(ctx);
openAppSettings();
},
child: const Text('去设置'),
),
],
),
);
return false;
}
void _onScanResults(List<ScanResult> results) {
if (!mounted || _busyDeviceId != null) return;
final scanStartedAt = _scanStartedAt;
@@ -213,21 +256,11 @@ class _DeviceManagementPageState extends ConsumerState<DeviceManagementPage>
}
} on TimeoutException {
if (!_disposed && mounted && _connectedDeviceId == bound.id) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已连接设备,但本次未收到测量数据'),
backgroundColor: AppColors.warning,
),
);
AppToast.show(context, '已连接设备,但本次未收到测量数据', type: AppToastType.warning);
}
} on Exception {
if (!_disposed && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('数据录入失败,请稍后重试'),
backgroundColor: AppColors.error,
),
);
AppToast.show(context, '数据录入失败,请稍后重试', type: AppToastType.error);
}
} finally {
await _bleService.disconnect();
@@ -272,6 +305,7 @@ class _DeviceManagementPageState extends ConsumerState<DeviceManagementPage>
if (devices.isNotEmpty &&
!_scanning &&
_busyDeviceId == null &&
!_permissionBlocked &&
_activeScanBoundKey != boundKey) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_disposed && mounted) unawaited(_startForegroundScan());
@@ -288,6 +322,7 @@ class _DeviceManagementPageState extends ConsumerState<DeviceManagementPage>
return;
}
if (nextKey != _activeScanBoundKey && _busyDeviceId == null) {
if (_permissionBlocked) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_disposed && mounted) unawaited(_startForegroundScan());
});
@@ -308,8 +343,8 @@ class _DeviceManagementPageState extends ConsumerState<DeviceManagementPage>
tooltip: '新增设备',
onPressed: () => pushRoute(ref, 'deviceScan'),
style: IconButton.styleFrom(
backgroundColor: const Color(0xFFEFF6FF),
foregroundColor: const Color(0xFF2563EB),
backgroundColor: _deviceVisual.lightColor,
foregroundColor: _deviceVisual.color,
fixedSize: const Size(40, 40),
),
icon: const Icon(Icons.add_rounded),
@@ -402,11 +437,7 @@ class _DevicesHeader extends StatelessWidget {
width: 50,
height: 50,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF60A5FA), Color(0xFF8B5CF6)],
),
gradient: _deviceVisual.gradient,
borderRadius: BorderRadius.circular(16),
),
child: const Icon(
@@ -474,15 +505,11 @@ class _DeviceSection extends StatelessWidget {
width: 34,
height: 34,
decoration: BoxDecoration(
color: const Color(0xFFEFF6FF),
color: _deviceVisual.lightColor,
borderRadius: BorderRadius.circular(11),
border: Border.all(color: AppColors.borderLight),
),
child: Icon(
type.icon,
size: 19,
color: const Color(0xFF2563EB),
),
child: Icon(type.icon, size: 19, color: _deviceVisual.color),
),
const SizedBox(width: 10),
Text(

View File

@@ -14,6 +14,7 @@ import '../../models/bp_reading.dart';
import '../../providers/auth_provider.dart';
import '../../providers/omron_device_provider.dart';
import '../../services/health_ble_service.dart';
import '../../widgets/app_toast.dart';
import '../../widgets/ble_sync_dialog.dart';
class DeviceScanPage extends ConsumerStatefulWidget {
@@ -71,16 +72,18 @@ class _DeviceScanPageState extends ConsumerState<DeviceScanPage>
_results.clear();
});
await [Permission.bluetoothScan, Permission.bluetoothConnect].request();
if (!await _ensureBlePermissions()) {
if (mounted) setState(() => _scanning = false);
return;
}
if (Platform.isAndroid) {
final locStatus = await Permission.locationWhenInUse.serviceStatus;
if (locStatus != ServiceStatus.enabled && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('请先打开定位服务,否则可能无法发现蓝牙设备'),
backgroundColor: AppColors.warning,
),
AppToast.show(
context,
'请先打开定位服务,否则可能无法发现蓝牙设备',
type: AppToastType.warning,
);
}
}
@@ -183,16 +186,12 @@ class _DeviceScanPageState extends ConsumerState<DeviceScanPage>
result.timeStamp.isBefore(scanStartedAt) ||
!result.advertisementData.connectable ||
DateTime.now().difference(result.timeStamp) > _visibleDeviceWindow) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('设备已离线,请重新进入通信状态')));
AppToast.show(context, '设备已离线,请重新进入通信状态', type: AppToastType.warning);
return;
}
if (ref.read(omronDeviceProvider).isBound &&
ref.read(omronDeviceProvider).findById(remoteId) != null) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('该设备已绑定')));
AppToast.show(context, '该设备已绑定', type: AppToastType.info);
return;
}
setState(() => _connectingId = remoteId);
@@ -231,31 +230,24 @@ class _DeviceScanPageState extends ConsumerState<DeviceScanPage>
if (mounted) popRoute(ref);
} on TimeoutException {
if (mounted && boundDevice != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${boundDevice.type.label}已绑定,但本次未收到数据')),
AppToast.show(
context,
'${boundDevice.type.label}已绑定,但本次未收到数据',
type: AppToastType.warning,
);
popRoute(ref);
} else if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('连接超时,请确认设备仍处于通信状态'),
backgroundColor: AppColors.error,
),
);
AppToast.show(context, '连接超时,请确认设备仍处于通信状态', type: AppToastType.error);
unawaited(_startScan());
}
} on UnsupportedBleDeviceException catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.message), backgroundColor: AppColors.error),
);
AppToast.show(context, e.message, type: AppToastType.error);
unawaited(_startScan());
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('连接失败:$e'), backgroundColor: AppColors.error),
);
AppToast.show(context, '连接失败:$e', type: AppToastType.error);
unawaited(_startScan());
}
} finally {
@@ -334,6 +326,38 @@ class _DeviceScanPageState extends ConsumerState<DeviceScanPage>
}
return true;
}
Future<bool> _ensureBlePermissions() async {
final statuses = await [
Permission.bluetoothScan,
Permission.bluetoothConnect,
].request();
final granted = statuses.values.every((status) => status.isGranted);
if (granted) return true;
if (!mounted) return false;
AppToast.show(context, '请开启蓝牙权限后再搜索设备', type: AppToastType.warning);
await showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('需要蓝牙权限'),
content: const Text('搜索和连接健康设备需要蓝牙权限,请在系统设置中开启后重试。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('稍后再说'),
),
TextButton(
onPressed: () {
Navigator.pop(ctx);
openAppSettings();
},
child: const Text('去设置'),
),
],
),
);
return false;
}
}
class _ScanningEmpty extends StatelessWidget {