Files
崔宝新 5e4cfe9ac6 feat: 新增诊断报告全流程功能,适配登录后报告跳转逻辑
实现了入门测/出门测报告的完整链路:
1. 新增UserInfo模型的出门测报告标记字段与相关接口
2. 新增全局报告路由拦截器,自动在登录后/应用回前台时检查报告状态
3. 新增报告相关API、页面与路由配置,包含欢迎页、H5壳子、AI计算页
4. 适配登录成功后的报告跳转逻辑,更新用户状态管理方法
2026-09-09 16:49:51 +08:00

195 lines
7.6 KiB
Dart
Raw Permalink 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/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:new_learn/core/components/privacy_startup_gate.dart';
import 'package:new_learn/core/config/app_config.dart';
import 'package:new_learn/core/logger/app_talker.dart';
import 'package:new_learn/core/providers/network_status_provider.dart';
import 'package:new_learn/core/providers/privacy_provider.dart';
import 'package:new_learn/core/report/report_redirect.dart';
import 'package:new_learn/core/router/app_router.dart';
import 'package:new_learn/core/services/local_notification_service.dart';
import 'package:new_learn/core/utils/screen_adapt_template.dart'; // 复用 AppAdapt 适配配置
import 'package:talker_flutter/talker_flutter.dart';
/// ============================================================
/// 【TabApp】App 顶层入口(MaterialApp)
///
/// 职责分工(对照 Vue 项目):
/// main.dart = 启动脚本(main 函数)
/// app.dart = 根组件:ScreenUtilInit + MaterialApp(主题 + 首页)
/// main_tab_page.dart = 底部 4 tab 主框架
/// pages/ = 各业务页面
///
/// 日志体系(talker_flutter + talker_dio_logger):
/// 1. 全局唯一实例:core/logger/app_talker.dart 的 talker(全项目 import 即用);
/// 2. Dio 网络日志:core/network/dio_client.dart 已接入 TalkerDioLogger;
/// 3. App 内查看:本文件在 MaterialApp.builder 外层注入 TalkerWrapper + DebugTalkerBubble,
/// 非 Release 模式会出现右下角悬浮小圆球,点开进入 TalkerScreen 查看全部日志;
/// 4. Release 自动关闭:kReleaseMode 时不注入 TalkerWrapper/悬浮球,线上不可见。
///
/// 业务页面直接使用(不需要 new Talker):
/// import 'package:new_learn/core/logger/app_talker.dart';
/// talker.debug('调试信息');
/// talker.warning('警告');
/// talker.error('错误');
///
/// 一、ScreenUtilInit 屏幕适配(flutter_screenutil 5.9.0):
/// designSize = 设计稿尺寸(这里 375x812 = iPhone 标准逻辑尺寸,
/// 如果你的设计稿是 750 宽(uni-app rpx),就写 Size(750, 1624))。
/// 适配原理:按"实际屏幕 / 设计稿"的比例统一缩放,保证不同手机上
/// UI 比例一致,不会大屏显小、小屏溢出。
///
/// 二、页面里怎么用(核心三件套):
/// .w 宽度/横向间距 Container(width: 100.w)
/// .h 高度/纵向间距 Container(height: 50.h)
/// .sp 字号 TextStyle(fontSize: 16.sp)
/// .r 圆角 BorderRadius.circular(12.r)
///
/// 三、高频坑:
/// 1. 必须在 ScreenUtilInit 的 builder 内部才能用 .w/.h/.sp/.r,
/// 所以初始化要包在最外层(本文件就是最外层);
/// 2. 改 main.dart 或这里,必须 Shift+R 热重启,热重载不生效;
/// 3. 不要对图片宽高做 .w 缩放后还叠加 transform,会双倍缩放。
/// ============================================================
class TabApp extends StatelessWidget {
const TabApp({super.key});
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
// 方案 B:平板(宽≥600dp)自动用大设计稿,缩放比例回到 ~1,
// 否则 Pad 上 .sp 会随宽度放大 2 倍+,字体爆炸。
designSize: AppAdapt.designSizeOf(context),
minTextAdapt: true, // 字号最小自适应,防小屏文字过小
splitScreenMode: true, // 支持分屏/平板
// 兜底:字体放大封顶 1.2 倍(防横屏/极端宽屏)
fontSizeResolver: AppAdapt.fontSizeResolver,
builder: (context, child) {
// 品牌 × 环境 聚合:App 名 + 主题色板随品牌走(见 core/config/app_config.dart)
final brandTheme = AppConfig.theme;
return MaterialApp.router(
title: AppConfig.appName,
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
// 品牌 seed 色生成整套 Material 色板(换品牌 = 全局换色)
colorSchemeSeed: brandTheme.seedColor,
scaffoldBackgroundColor: const Color(0xFFF5F7FA),
appBarTheme: const AppBarTheme(
backgroundColor: Colors.white,
foregroundColor: Color(0xFF333333),
elevation: 0,
centerTitle: true,
),
),
// 方案 A:启动层隐私 Gate。
// 这样冷启动时不会先看到登录页/主页闪一下,再被路由重定向到隐私页,
// 而是直接由全屏拦截页先接管。
builder: (context, child) {
// ReportRedirectNotifier 必须包在 MaterialApp 里面(能拿到 GoRouterState)
// 但在 PrivacyStartupGate 外层(隐私同意后的页面才能拦截)
final withRedirect = ReportRedirectNotifier(
child: child ?? const SizedBox.shrink(),
);
final content = PrivacyStartupGate(child: withRedirect);
final bootstrapped = _LocalNotificationBootstrapper(child: content);
if (kReleaseMode) return bootstrapped;
return TalkerWrapper(
talker: talker,
child: DebugTalkerBubble(
child: _NetworkStatusGlobalLogger(child: bootstrapped),
),
);
},
// 全局路由表:登录守卫 + 4 tab ShellRoute(见 app_router.dart)
routerConfig: appRouter,
);
},
);
}
}
class _LocalNotificationBootstrapper extends StatefulWidget {
const _LocalNotificationBootstrapper({required this.child});
final Widget child;
@override
State<_LocalNotificationBootstrapper> createState() =>
_LocalNotificationBootstrapperState();
}
class _LocalNotificationBootstrapperState
extends State<_LocalNotificationBootstrapper> {
@override
void initState() {
super.initState();
privacyAcceptedNotifier.addListener(_onPrivacyChanged);
_onPrivacyChanged();
}
@override
void dispose() {
privacyAcceptedNotifier.removeListener(_onPrivacyChanged);
super.dispose();
}
void _onPrivacyChanged() {
if (!privacyAcceptedNotifier.value) return;
unawaited(LocalNotificationService.instance.ensureInitialized());
}
@override
Widget build(BuildContext context) => widget.child;
}
class _NetworkStatusGlobalLogger extends ConsumerStatefulWidget {
const _NetworkStatusGlobalLogger({required this.child});
final Widget child;
@override
ConsumerState<_NetworkStatusGlobalLogger> createState() =>
_NetworkStatusGlobalLoggerState();
}
class _NetworkStatusGlobalLoggerState
extends ConsumerState<_NetworkStatusGlobalLogger> {
late final ProviderSubscription<NetworkStatus> _sub;
@override
void initState() {
super.initState();
final current = ref.read(networkStatusProvider);
talker.debug(
'网络状态(初始):'
'types=[${current.networkTypes.map((e) => e.name).join(',')}] '
'hasInternet=${current.hasInternet} '
'hasBackend=${current.hasBackend}',
);
_sub = ref.listenManual(networkStatusProvider, (prev, next) {
talker.debug(
'网络状态变化:'
'types=[${next.networkTypes.map((e) => e.name).join(',')}] '
'hasInternet=${next.hasInternet} '
'hasBackend=${next.hasBackend}',
);
});
}
@override
void dispose() {
_sub.close();
super.dispose();
}
@override
Widget build(BuildContext context) => widget.child;
}