Files
AI-Health/health_app/lib/app.dart
MingNian 335a3e6440 feat: 饮食记录逻辑重构 + 通知管理分隔线修复 + 多页面 UI 调整
- 饮食: 新增 DietCommentaryPolicy + diet_record_logic, 饮食记录逻辑抽取复用
- 通知管理: 分隔线改为仿通知中心样式, 跳过图标区域只留文字下方
- 侧边栏: 对话记录操作栏浮层修复 + 常用功能间距收紧
- 智能体: 欢迎卡片去 400ms 延迟 + 胶囊去阴影
- 后端: AI 会话仓库新增方法 + 饮食评论策略 + 健康记录规则调整
- 其他: api_client IP 适配 + 多页面 UI 微调 + 新增测试
2026-07-12 22:49:38 +08:00

140 lines
4.5 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'core/app_router.dart';
import 'core/app_theme.dart';
import 'core/navigation_provider.dart';
import 'pages/splash_page.dart';
import 'providers/auth_provider.dart';
import 'providers/data_providers.dart';
/// 健康管家 App 根组件
class HealthApp extends ConsumerWidget {
const HealthApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp(
title: '小脉健康',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('zh', 'CN'), Locale('zh')],
locale: const Locale('zh'),
home: const _RootNavigator(),
// 注入 ShadTheme + 启动闸门Splash 盖在最上层,直到首页今日健康卡数据就绪
// 外层包一层 GestureDetector点击任意空白处取消输入框焦点全 app 生效)
builder: (context, child) => ShadTheme(
data: AppTheme.shadTheme,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
child: _BootGate(child: child!),
),
),
);
}
}
/// 启动是否就绪auth 判定完成,且(已登录的普通用户)今日健康卡数据已到位。
/// 未登录或医生/管理员无需等待健康卡。
final appReadyProvider = Provider<bool>((ref) {
final auth = ref.watch(authProvider);
if (auth.isLoading) return false; // 还在判登录态 → 继续盖 Splash
if (!auth.isLoggedIn) return true; // 未登录 → 就绪,露出登录页
final role = auth.user?.role ?? 'User';
if (role != 'User') return true; // 医生/管理员首页不依赖今日健康卡
// 普通用户:等今日健康卡片数据(成功或失败都算就绪,避免卡死)
final health = ref.watch(latestHealthProvider);
return health.hasValue || health.hasError;
});
/// 启动闸门——就绪前在最上层覆盖 Splash盖住登录页闪现与首页对话流初始态。
/// 带 8 秒安全兜底,避免离线时数据永不返回导致卡在启动页。
class _BootGate extends ConsumerStatefulWidget {
final Widget child;
const _BootGate({required this.child});
@override
ConsumerState<_BootGate> createState() => _BootGateState();
}
class _BootGateState extends ConsumerState<_BootGate> {
bool _timedOut = false;
Timer? _timer;
@override
void initState() {
super.initState();
_timer = Timer(const Duration(seconds: 8), () {
if (mounted) setState(() => _timedOut = true);
});
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final ready = ref.watch(appReadyProvider) || _timedOut;
return Stack(
children: [
widget.child,
if (!ready) const Positioned.fill(child: SplashPage()),
],
);
}
}
/// 根导航——根据 Riverpod 路由状态切换页面
class _RootNavigator extends ConsumerWidget {
const _RootNavigator();
@override
Widget build(BuildContext context, WidgetRef ref) {
final stack = ref.watch(routeStackProvider);
final current = stack.last;
final authState = ref.watch(authProvider);
final isPublicStaticText = current.name == 'staticText';
// 登录后自动跳转(在下一帧完成,无闪烁)
if (authState.isLoggedIn && current.name == 'login') {
WidgetsBinding.instance.addPostFrameCallback((_) {
final role = authState.user?.role ?? 'User';
if (role == 'Admin') {
goRoute(ref, 'adminHome');
} else if (role == 'Doctor') {
goRoute(ref, 'doctorHome');
} else {
goRoute(ref, 'home');
}
});
}
if (!authState.isLoading &&
!authState.isLoggedIn &&
current.name != 'login' &&
!isPublicStaticText) {
WidgetsBinding.instance.addPostFrameCallback((_) {
goRoute(ref, 'login');
});
}
return PopScope(
canPop: stack.length <= 1,
onPopInvokedWithResult: (didPop, result) {
if (!didPop) popRoute(ref);
},
child: buildPage(current, ref),
);
}
}