实现了入门测/出门测报告的完整链路: 1. 新增UserInfo模型的出门测报告标记字段与相关接口 2. 新增全局报告路由拦截器,自动在登录后/应用回前台时检查报告状态 3. 新增报告相关API、页面与路由配置,包含欢迎页、H5壳子、AI计算页 4. 适配登录成功后的报告跳转逻辑,更新用户状态管理方法
195 lines
7.6 KiB
Dart
195 lines
7.6 KiB
Dart
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;
|
||
}
|