feat: align iOS client safety and consent experience

This commit is contained in:
MingNian
2026-07-29 13:30:34 +08:00
parent 3b5cec10a6
commit 01f4955e0e
11 changed files with 290 additions and 374 deletions

View File

@@ -8,9 +8,11 @@ import 'core/app_theme.dart';
import 'core/elder_mode_scope.dart'; import 'core/elder_mode_scope.dart';
import 'core/navigation_provider.dart'; import 'core/navigation_provider.dart';
import 'pages/splash_page.dart'; import 'pages/splash_page.dart';
import 'providers/ai_consent_provider.dart';
import 'providers/auth_provider.dart'; import 'providers/auth_provider.dart';
import 'providers/data_providers.dart'; import 'providers/data_providers.dart';
import 'providers/elder_mode_provider.dart'; import 'providers/elder_mode_provider.dart';
import 'widgets/ai_consent_gate.dart';
/// 健康管家 App 根组件 /// 健康管家 App 根组件
class HealthApp extends ConsumerWidget { class HealthApp extends ConsumerWidget {
@@ -51,7 +53,7 @@ class HealthApp extends ConsumerWidget {
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () => FocusManager.instance.primaryFocus?.unfocus(), onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
child: _BootGate(child: child!), child: AiConsentGate(child: _BootGate(child: child!)),
), ),
), ),
), ),
@@ -71,6 +73,15 @@ final appReadyProvider = Provider<bool>((ref) {
if (currentRoute == 'login') return false; // 等根导航切到已登录首页后再撤 Splash if (currentRoute == 'login') return false; // 等根导航切到已登录首页后再撤 Splash
final role = auth.user?.role ?? 'User'; final role = auth.user?.role ?? 'User';
if (role != 'User') return true; // 医生/管理员首页不依赖今日健康卡 if (role != 'User') return true; // 医生/管理员首页不依赖今日健康卡
final consent = ref.watch(aiConsentProvider);
if (consent.userId == auth.user?.id &&
!consent.isLoading &&
!consent.granted) {
return true;
}
if (currentRoute == 'aiConsentDetails' || currentRoute == 'staticText') {
return true;
}
if (!ref.watch(elderModeProvider).isLoaded) return false; if (!ref.watch(elderModeProvider).isLoaded) return false;
// 普通用户:等今日健康卡片数据(成功或失败都算就绪,避免卡死) // 普通用户:等今日健康卡片数据(成功或失败都算就绪,避免卡死)
final health = ref.watch(latestHealthProvider); final health = ref.watch(latestHealthProvider);

View File

@@ -9,9 +9,22 @@ const String baseUrl = String.fromEnvironment(
'API_BASE_URL', 'API_BASE_URL',
defaultValue: kReleaseMode defaultValue: kReleaseMode
? 'https://erpapi.datalumina.cn/xiaomai' ? 'https://erpapi.datalumina.cn/xiaomai'
: 'http://192.168.1.34:5000', : 'http://10.4.232.91:5000',
); );
const String _configuredAppPolicy = String.fromEnvironment(
'APP_POLICY_PROFILE',
defaultValue: '',
);
String get appPolicyProfile {
if (_configuredAppPolicy == 'android-standard' ||
_configuredAppPolicy == 'ios-medical-citations') {
return _configuredAppPolicy;
}
return Platform.isIOS ? 'ios-medical-citations' : 'android-standard';
}
class ApiException implements Exception { class ApiException implements Exception {
final int? code; final int? code;
final String message; final String message;
@@ -56,7 +69,10 @@ class ApiClient {
baseUrl: baseUrl, baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 15), connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 60), receiveTimeout: const Duration(seconds: 60),
headers: {'Content-Type': 'application/json'}, headers: {
'Content-Type': 'application/json',
'X-App-Policy': appPolicyProfile,
},
), ),
) { ) {
_dio.interceptors.add(_AuthInterceptor(this)); _dio.interceptors.add(_AuthInterceptor(this));

View File

@@ -438,9 +438,9 @@ class _HomePageState extends ConsumerState<HomePage>
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const SizedBox(height: 8), const SizedBox(height: 4),
_buildAgentBar(), _buildAgentBar(),
const SizedBox(height: 12), const SizedBox(height: 5),
if (_pickedImagePath != null) _buildImagePreview(), if (_pickedImagePath != null) _buildImagePreview(),
_buildInputBar(), _buildInputBar(),
], ],
@@ -490,13 +490,13 @@ class _HomePageState extends ConsumerState<HomePage>
chatProvider.select((state) => state.isStreaming), chatProvider.select((state) => state.isStreaming),
); );
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 10), padding: const EdgeInsets.fromLTRB(12, 0, 12, 6),
child: Container( child: Container(
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
6, 6,
elderMode ? 7 : 5, elderMode ? 5 : 3,
6, 6,
elderMode ? 7 : 5, elderMode ? 5 : 3,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
@@ -540,7 +540,7 @@ class _HomePageState extends ConsumerState<HomePage>
isDense: true, isDense: true,
contentPadding: EdgeInsets.symmetric( contentPadding: EdgeInsets.symmetric(
horizontal: 4, horizontal: 4,
vertical: elderMode ? 15 : 12, vertical: elderMode ? 12 : 9,
), ),
border: InputBorder.none, border: InputBorder.none,
enabledBorder: InputBorder.none, enabledBorder: InputBorder.none,
@@ -971,7 +971,7 @@ class _VoiceHoldSurface extends StatelessWidget {
return Container( return Container(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: 4, horizontal: 4,
vertical: elderMode ? 14 : 11, vertical: elderMode ? 12 : 9,
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(

View File

@@ -1276,7 +1276,6 @@ class ChatMessagesView extends ConsumerWidget {
/// 处理 AI 回复里的 markdown 链接点击: /// 处理 AI 回复里的 markdown 链接点击:
/// - app://diet → 触发拍照/相册选择,跳到饮食拍照流程 /// - app://diet → 触发拍照/相册选择,跳到饮食拍照流程
/// - app://report → 跳到报告列表(用户可在那里上传新报告) /// - app://report → 跳到报告列表(用户可在那里上传新报告)
/// - app://device → 跳到蓝牙设备页
/// - 其他 app://xxx → 当作 route name 直接跳 /// - 其他 app://xxx → 当作 route name 直接跳
static void _handleMarkdownLink( static void _handleMarkdownLink(
BuildContext context, BuildContext context,
@@ -1296,8 +1295,7 @@ class ChatMessagesView extends ConsumerWidget {
pushRoute(ref, 'reports'); pushRoute(ref, 'reports');
break; break;
case 'device': case 'device':
pushRoute(ref, 'devices'); return;
break;
default: default:
if (uri.host.isNotEmpty) pushRoute(ref, uri.host); if (uri.host.isNotEmpty) pushRoute(ref, uri.host);
} }
@@ -2199,12 +2197,6 @@ final _agentActions = <ActiveAgent, List<_AgentAction>>{
isWide: true, isWide: true,
route: 'trend', route: 'trend',
), ),
_AgentAction(
label: '蓝牙录入',
icon: Icons.bluetooth,
isWide: true,
route: 'devices',
),
], ],
ActiveAgent.diet: [ ActiveAgent.diet: [
_AgentAction( _AgentAction(

View File

@@ -32,7 +32,15 @@ class AiConsentDetailsPage extends ConsumerWidget {
); );
if (confirmed != true || !context.mounted) return; if (confirmed != true || !context.mounted) return;
await ref.read(aiConsentProvider.notifier).revoke(userId); final revoked = await ref.read(aiConsentProvider.notifier).revoke(userId);
if (!revoked) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Authorization could not be revoked.')),
);
}
return;
}
await ref.read(authProvider.notifier).logout(); await ref.read(authProvider.notifier).logout();
} }
@@ -59,6 +67,7 @@ class AiConsentDetailsPage extends ConsumerWidget {
color: AppColors.textPrimary, color: AppColors.textPrimary,
), ),
), ),
centerTitle: true,
), ),
body: SafeArea( body: SafeArea(
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -91,8 +100,11 @@ class AiConsentDetailsPage extends ConsumerWidget {
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: OutlinedButton( child: OutlinedButton(
onPressed: () => _revoke(context, ref), onPressed: consent.isSaving
? null
: () => _revoke(context, ref),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: AppColors.error, foregroundColor: AppColors.error,
side: BorderSide( side: BorderSide(
color: AppColors.error.withValues(alpha: 0.4), color: AppColors.error.withValues(alpha: 0.4),

View File

@@ -1,4 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:shadcn_ui/shadcn_ui.dart';
@@ -56,8 +55,6 @@ class SettingsPage extends ConsumerWidget {
title: '长辈模式', title: '长辈模式',
onTap: () => pushRoute(ref, 'elderMode'), onTap: () => pushRoute(ref, 'elderMode'),
), ),
if (!Platform.isIOS)
// iOS 审核版:蓝牙设备入口已移除
_SettingsTile( _SettingsTile(
icon: LucideIcons.bell, icon: LucideIcons.bell,
title: '消息通知', title: '消息通知',

View File

@@ -6,22 +6,18 @@ const aiConsentVersion = 'ai-data-consent-v1';
class AiConsentState { class AiConsentState {
final bool isLoading; final bool isLoading;
final bool isSaving;
final bool granted; final bool granted;
final String? userId; final String? userId;
final String? error;
const AiConsentState({ const AiConsentState({
this.isLoading = true, this.isLoading = true,
this.isSaving = false,
this.granted = false, this.granted = false,
this.userId, this.userId,
this.error,
}); });
AiConsentState copyWith({bool? isLoading, bool? granted, String? userId}) {
return AiConsentState(
isLoading: isLoading ?? this.isLoading,
granted: granted ?? this.granted,
userId: userId ?? this.userId,
);
}
} }
final aiConsentProvider = NotifierProvider<AiConsentNotifier, AiConsentState>( final aiConsentProvider = NotifierProvider<AiConsentNotifier, AiConsentState>(
@@ -36,21 +32,62 @@ class AiConsentNotifier extends Notifier<AiConsentState> {
Future<void> load(String userId) async { Future<void> load(String userId) async {
state = AiConsentState(isLoading: true, userId: userId); state = AiConsentState(isLoading: true, userId: userId);
try {
final value = await ref.read(localDbProvider).read(_key(userId)); final value = await ref.read(localDbProvider).read(_key(userId));
state = AiConsentState( state = AiConsentState(
isLoading: false, isLoading: false,
granted: value == 'granted', granted: value == 'granted',
userId: userId, userId: userId,
); );
} catch (_) {
state = AiConsentState(
isLoading: false,
userId: userId,
error: 'Authorization status could not be loaded.',
);
}
} }
Future<void> grant(String userId) async { Future<bool> grant(String userId) async {
state = AiConsentState(
isLoading: false,
isSaving: true,
granted: state.granted,
userId: userId,
);
try {
await ref.read(localDbProvider).write(_key(userId), 'granted'); await ref.read(localDbProvider).write(_key(userId), 'granted');
state = AiConsentState(isLoading: false, granted: true, userId: userId); state = AiConsentState(isLoading: false, granted: true, userId: userId);
return true;
} catch (_) {
state = AiConsentState(
isLoading: false,
userId: userId,
error: 'Authorization could not be saved.',
);
return false;
}
} }
Future<void> revoke(String userId) async { Future<bool> revoke(String userId) async {
state = AiConsentState(
isLoading: false,
isSaving: true,
granted: state.granted,
userId: userId,
);
try {
await ref.read(localDbProvider).delete(_key(userId)); await ref.read(localDbProvider).delete(_key(userId));
state = AiConsentState(isLoading: false, granted: false, userId: userId); state = AiConsentState(isLoading: false, granted: false, userId: userId);
return true;
} catch (_) {
state = AiConsentState(
isLoading: false,
granted: true,
userId: userId,
error: 'Authorization could not be revoked.',
);
return false;
}
} }
} }

View File

@@ -63,7 +63,10 @@ class SseHandler {
cancelToken: cancelToken, cancelToken: cancelToken,
options: Options( options: Options(
responseType: ResponseType.stream, responseType: ResponseType.stream,
headers: {'Authorization': 'Bearer $token'}, headers: {
'Authorization': 'Bearer $token',
'X-App-Policy': appPolicyProfile,
},
), ),
); );

View File

@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/app_colors.dart'; import '../core/app_colors.dart';
import '../core/app_design_tokens.dart'; import '../core/app_design_tokens.dart';
import '../core/navigation_provider.dart';
import '../providers/ai_consent_provider.dart'; import '../providers/ai_consent_provider.dart';
import '../providers/auth_provider.dart'; import '../providers/auth_provider.dart';
import '../pages/settings/ai_consent_details_page.dart'; import '../pages/settings/ai_consent_details_page.dart';
@@ -21,7 +22,9 @@ class _AiConsentGateState extends ConsumerState<AiConsentGate> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = ref.watch(authProvider).user; final user = ref.watch(authProvider).user;
if (user == null || user.id.isEmpty) return widget.child; if (user == null || user.id.isEmpty || user.role != 'User') {
return widget.child;
}
if (_loadedUserId != user.id) { if (_loadedUserId != user.id) {
_loadedUserId = user.id; _loadedUserId = user.id;
@@ -31,19 +34,33 @@ class _AiConsentGateState extends ConsumerState<AiConsentGate> {
} }
final consent = ref.watch(aiConsentProvider); final consent = ref.watch(aiConsentProvider);
final currentRoute = ref.watch(currentRouteProvider).name;
final isConsentInformationRoute =
currentRoute == 'aiConsentDetails' || currentRoute == 'staticText';
if (consent.userId != user.id || consent.isLoading) { if (consent.userId != user.id || consent.isLoading) {
return const Scaffold(backgroundColor: Colors.white); return const Scaffold(
body: Center(
child: CircularProgressIndicator(color: AppColors.primary),
),
);
} }
if (consent.granted) return widget.child; if (consent.granted || isConsentInformationRoute) return widget.child;
return Scaffold( return Scaffold(
backgroundColor: AppColors.background, body: DecoratedBox(
body: Center( decoration: const BoxDecoration(gradient: AppColors.bgGradient),
child: Padding( child: SafeArea(
padding: const EdgeInsets.all(24), child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: _ConsentCard(userId: user.id), child: _ConsentCard(userId: user.id),
), ),
), ),
),
),
),
); );
} }
} }
@@ -53,7 +70,12 @@ class _ConsentCard extends ConsumerWidget {
const _ConsentCard({required this.userId}); const _ConsentCard({required this.userId});
Future<void> _grant(BuildContext context, WidgetRef ref) async { Future<void> _grant(BuildContext context, WidgetRef ref) async {
await ref.read(aiConsentProvider.notifier).grant(userId); final succeeded = await ref.read(aiConsentProvider.notifier).grant(userId);
if (!succeeded && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Authorization could not be saved.')),
);
}
} }
Future<void> _decline(BuildContext context, WidgetRef ref) async { Future<void> _decline(BuildContext context, WidgetRef ref) async {
@@ -62,17 +84,55 @@ class _ConsentCard extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return Card( final consent = ref.watch(aiConsentProvider);
elevation: 0, return Container(
color: Colors.white, padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
shape: RoundedRectangleBorder(borderRadius: AppRadius.xlBorder), decoration: BoxDecoration(
child: Padding( color: Colors.white.withValues(alpha: 0.96),
padding: const EdgeInsets.fromLTRB(22, 24, 22, 20), borderRadius: AppRadius.cardBorder,
border: Border.all(color: AppColors.primaryLight, width: 1),
boxShadow: AppShadows.panel,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Align(
child: Image.asset(
'assets/branding/health_login_character_transparent.png',
height: 112,
fit: BoxFit.contain,
),
),
const SizedBox(height: 2),
Align(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColors.primarySoft,
borderRadius: AppRadius.pillBorder,
),
child: const Text(
'AI Service',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppColors.primaryDark,
),
),
),
),
const SizedBox(height: 14),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const Icon(Icons.auto_awesome, size: 40, color: AppColors.primary), const Icon(
Icons.auto_awesome,
size: 40,
color: AppColors.primary,
),
const SizedBox(height: 14), const SizedBox(height: 14),
const Text( const Text(
'AI 健康服务授权', 'AI 健康服务授权',
@@ -110,17 +170,23 @@ class _ConsentCard extends ConsumerWidget {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
FilledButton( FilledButton(
onPressed: () => _grant(context, ref), onPressed: consent.isSaving
? null
: () => _grant(context, ref),
child: const Text('同意并继续'), child: const Text('同意并继续'),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
OutlinedButton( OutlinedButton(
onPressed: () => _decline(context, ref), onPressed: consent.isSaving
? null
: () => _decline(context, ref),
child: const Text('不同意并退出'), child: const Text('不同意并退出'),
), ),
], ],
), ),
), ),
],
),
); );
} }
} }

View File

@@ -1,149 +0,0 @@
import 'package:flutter/material.dart';
import '../core/app_colors.dart';
import '../core/app_design_tokens.dart';
import '../core/app_module_visuals.dart';
import '../models/bp_reading.dart';
Future<void> showBleSyncDialog(
BuildContext context, {
required BpReading reading,
}) {
return showDialog<void>(
context: context,
barrierDismissible: false,
barrierColor: Colors.black.withValues(alpha: 0.54),
builder: (ctx) => Dialog(
insetPadding: const EdgeInsets.symmetric(horizontal: 28),
backgroundColor: Colors.transparent,
child: Container(
width: double.infinity,
constraints: const BoxConstraints(maxWidth: 380),
padding: const EdgeInsets.fromLTRB(22, 26, 22, 22),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: AppRadius.xlBorder,
boxShadow: AppShadows.floating,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppModuleVisuals.device.color,
borderRadius: AppRadius.mdBorder,
),
child: Icon(
AppModuleVisuals.device.icon,
color: Colors.white,
size: 25,
),
),
const SizedBox(height: 14),
Text(
'数据已录入',
textAlign: TextAlign.center,
style: AppTextStyles.summaryTitle.copyWith(fontSize: 22),
),
const SizedBox(height: 22),
Row(
children: [
Expanded(
child: _MetricCard(
label: '收缩压',
value: '${reading.systolic}',
unit: 'mmHg',
),
),
const SizedBox(width: 14),
Expanded(
child: _MetricCard(
label: '舒张压',
value: '${reading.diastolic}',
unit: 'mmHg',
),
),
],
),
if (reading.pulse != null) ...[
const SizedBox(height: 14),
Row(
children: [
Expanded(
child: _MetricCard(
label: '脉搏',
value: '${reading.pulse}',
unit: 'bpm',
),
),
const Expanded(child: SizedBox.shrink()),
],
),
],
const SizedBox(height: 26),
GestureDetector(
onTap: () => Navigator.pop(ctx),
child: Container(
height: 50,
width: double.infinity,
alignment: Alignment.center,
decoration: BoxDecoration(
gradient: AppModuleVisuals.device.gradient,
borderRadius: AppRadius.lgBorder,
boxShadow: AppColors.buttonShadow,
),
child: Text(
'知道了',
style: AppTextStyles.button.copyWith(color: Colors.white),
),
),
),
],
),
),
),
);
}
class _MetricCard extends StatelessWidget {
final String label;
final String value;
final String unit;
const _MetricCard({
required this.label,
required this.value,
required this.unit,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: AppModuleVisuals.device.lightColor,
borderRadius: AppRadius.mdBorder,
border: Border.all(color: AppModuleVisuals.device.borderColor),
),
child: Column(
children: [
Text(
value,
style: const TextStyle(
fontSize: 36,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
height: 1,
),
),
const SizedBox(height: 6),
Text(unit, style: AppTextStyles.tag),
const SizedBox(height: 4),
Text(label, style: AppTextStyles.tag),
],
),
);
}
}

View File

@@ -1,5 +1,3 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:shadcn_ui/shadcn_ui.dart';
@@ -328,40 +326,16 @@ class _MetricTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final elderMode = ElderModeScope.enabledOf(context); final elderMode = ElderModeScope.enabledOf(context);
final height = elderMode ? 112.0 : 100.0; return InkWell(
return ClipRRect(
borderRadius: BorderRadius.circular(18),
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap, onTap: onTap,
child: Container(
height: height,
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 7),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white.withValues(alpha: 0.62),
Colors.white.withValues(alpha: 0.42),
const Color(0xFFD9FCFF).withValues(alpha: 0.34),
],
),
borderRadius: BorderRadius.circular(18), borderRadius: BorderRadius.circular(18),
border: Border.all( child: Container(
color: Colors.white.withValues(alpha: 0.82), height: elderMode ? 108 : 100,
width: 1.2, padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 10),
), decoration: BoxDecoration(
boxShadow: [ color: Colors.white.withValues(alpha: 0.22),
BoxShadow( borderRadius: BorderRadius.circular(18),
color: const Color(0xFF0369A1).withValues(alpha: 0.10), border: Border.all(color: Colors.white.withValues(alpha: 0.40)),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
), ),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -375,8 +349,8 @@ class _MetricTile extends StatelessWidget {
maxLines: 1, maxLines: 1,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
fontSize: 23, fontSize: 26,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w700,
color: Colors.white, color: Colors.white,
), ),
), ),
@@ -386,9 +360,9 @@ class _MetricTile extends StatelessWidget {
Text( Text(
info.unit!, info.unit!,
style: const TextStyle( style: const TextStyle(
fontSize: 13, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xE8FFFFFF), color: Color(0xE0FFFFFF),
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
@@ -396,16 +370,13 @@ class _MetricTile extends StatelessWidget {
info.label, info.label,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w600,
color: Colors.white, color: Colors.white,
), ),
), ),
], ],
), ),
), ),
),
),
),
); );
} }
} }
@@ -460,12 +431,6 @@ class _NavigationSection extends StatelessWidget {
route: 'exercisePlan', route: 'exercisePlan',
colors: AppColors.exerciseGradient.colors, colors: AppColors.exerciseGradient.colors,
), ),
_NavItem(
icon: LucideIcons.bluetooth,
title: '设备',
route: 'devices',
colors: AppColors.deviceGradient.colors,
),
]; ];
return _LightSection( return _LightSection(
@@ -742,43 +707,9 @@ class _HealthDashboardBackgroundPainter extends CustomPainter {
..shader = const LinearGradient( ..shader = const LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [Color(0xFF4FACFE), Color(0xFF00F2FE)], colors: [Color(0xFF00C6FB), Color(0xFF005BEA)],
).createShader(rect); ).createShader(rect);
canvas.drawRect(rect, paint); canvas.drawRect(rect, paint);
final glow = Paint()
..shader =
RadialGradient(
colors: [
Colors.white.withValues(alpha: 0.32),
Colors.white.withValues(alpha: 0),
],
).createShader(
Rect.fromCircle(
center: Offset(size.width * 0.12, size.height * 0.05),
radius: size.width * 0.7,
),
);
canvas.drawCircle(
Offset(size.width * 0.12, size.height * 0.05),
size.width * 0.7,
glow,
);
final highlight = Paint()
..color = Colors.white.withValues(alpha: 0.20)
..style = PaintingStyle.stroke
..strokeWidth = 1.2;
canvas.drawLine(
Offset(size.width * 0.52, -12),
Offset(size.width * 0.16, size.height + 18),
highlight,
);
canvas.drawLine(
Offset(size.width * 0.96, -10),
Offset(size.width * 0.58, size.height + 22),
highlight,
);
} }
@override @override