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

@@ -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 {