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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user