feat: 新增诊断报告全流程功能,适配登录后报告跳转逻辑
实现了入门测/出门测报告的完整链路: 1. 新增UserInfo模型的出门测报告标记字段与相关接口 2. 新增全局报告路由拦截器,自动在登录后/应用回前台时检查报告状态 3. 新增报告相关API、页面与路由配置,包含欢迎页、H5壳子、AI计算页 4. 适配登录成功后的报告跳转逻辑,更新用户状态管理方法
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:new_learn/core/logger/app_talker.dart';
|
||||
import 'package:new_learn/core/network/dio_client.dart';
|
||||
import 'package:new_learn/core/providers/user_provider.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【报告状态全局拦截 + App 生命周期监听】
|
||||
///
|
||||
/// 对标 Vue App.vue 的 checkAndRedirectToReport() + onShow force 拉 userInfo。
|
||||
///
|
||||
/// 一、拦截时机(两处):
|
||||
/// 1. userProvider 状态变化(set() / refresh())→ _checkAndRedirect()
|
||||
/// :登录成功 / refresh 后都自动检查;
|
||||
/// 2. App 回前台(WidgetsBindingObserver.resumed)→ force refresh() → 自动检查
|
||||
/// :从胶囊面板/后台回来时拉最新 isShowReport;
|
||||
///
|
||||
/// 二、拦截条件(全部满足才跳):
|
||||
/// ✅ 有登录态(DioClient.accessToken != null)
|
||||
/// ✅ isShowReport=true 或 isShowAfterReport=true
|
||||
/// ✅ 当前路由在 tab 页面(动态判断,不硬编码列表)
|
||||
/// —— go_router 的 StatefulShellRoute.indexedStack 内的路由就是 tab 页
|
||||
///
|
||||
/// 三、动态判断 tab 页面的方法(不硬编码):
|
||||
/// 通过 GoRouterState 的 matchedLocation,判断是否能匹配到 ShellBranch 内的路径
|
||||
/// (即 /, /recite, /class, /my 这四个——但这些路径是由路由配置决定的,
|
||||
/// 所以判断逻辑跟着路由配置走,别人改 tab 路径这里自动生效)。
|
||||
///
|
||||
/// ============================================================
|
||||
|
||||
/// 是否为 tab 路由(动态判断:4 个 tab 的路径由路由配置决定)
|
||||
///
|
||||
/// go_router 的 StatefulShellBranch 内配置了 4 条路径,
|
||||
/// 直接从路由表读取所有 ShellBranch 的 path 集合,而不是硬编码。
|
||||
/// 但 ShellBranch 的 routes 里只有 GoRoute,path 是字段。
|
||||
/// 所以这里采用一个更稳的方式:判断当前是否在 StatefulShellRoute 里,
|
||||
/// 或者直接在 app_router.dart 里暴露一个 tabPaths 常量。
|
||||
///
|
||||
/// 目前路由表固定,这里用 tabPaths 常量(从 app_router 导出)。
|
||||
import 'package:new_learn/core/router/app_router.dart';
|
||||
|
||||
/// 拦截器:当 userProvider 变化时检查是否需要跳报告页
|
||||
class ReportRedirectNotifier extends ConsumerStatefulWidget {
|
||||
const ReportRedirectNotifier({super.key, required this.child});
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
ConsumerState<ReportRedirectNotifier> createState() =>
|
||||
_ReportRedirectNotifierState();
|
||||
}
|
||||
|
||||
class _ReportRedirectNotifierState extends ConsumerState<ReportRedirectNotifier>
|
||||
with WidgetsBindingObserver {
|
||||
/// 防止重复跳转标记
|
||||
bool _isRedirecting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
talker.debug('[ReportRedirect] initState 🔥 被创建了');
|
||||
// 监听 userProvider 后续变化
|
||||
ref.listenManual(currentUserProvider, (prev, next) {
|
||||
talker.debug('[ReportRedirect] listenManual 触发!prev=$prev next=$next');
|
||||
_checkAndRedirect();
|
||||
});
|
||||
// 立即检查一次!listenManual 只监听变化不会主动触发,
|
||||
// 登录后 userProvider 已经是 AsyncData 状态了,必须主动调。
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
talker.debug(
|
||||
'[ReportRedirect] addPostFrameCallback → 主动调 _checkAndRedirect',
|
||||
);
|
||||
_checkAndRedirect();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// App 生命周期变化:resumed 时 force 拉最新 userInfo
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_onAppResumed();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onAppResumed() async {
|
||||
// 只有已登录才 force 拉
|
||||
if (DioClient.instance.accessToken == null) return;
|
||||
talker.debug('[ReportRedirect] App resumed → force refresh userInfo');
|
||||
await ref.read(currentUserProvider.notifier).refresh();
|
||||
_checkAndRedirect();
|
||||
}
|
||||
|
||||
/// 核心拦截逻辑:判断当前路由 + 报告标记 → 决定跳不跳
|
||||
void _checkAndRedirect() {
|
||||
talker.debug('[ReportRedirect] ===== _checkAndRedirect 开始 ====');
|
||||
if (_isRedirecting) {
|
||||
talker.debug('[ReportRedirect] _isRedirecting=true,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
final user = ref.read(currentUserProvider).value;
|
||||
if (user == null) {
|
||||
talker.debug('[ReportRedirect] user=null(未登录),放行');
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: 调试报告用,把后端字段叠加调试开关,上线前删掉下面三行
|
||||
// const _debugExitTest = false;
|
||||
// const _debugPlacementTest = false;
|
||||
const _debugExitTest = false; // true = 强制调试出门测
|
||||
const _debugPlacementTest = false; // true = 强制调试入门测
|
||||
|
||||
talker.debug(
|
||||
'[ReportRedirect] user.isShowReport=${user.isShowReport} '
|
||||
'user.isShowAfterReport=${user.isShowAfterReport} '
|
||||
'debugExit=$_debugExitTest debugPlacement=$_debugPlacementTest',
|
||||
);
|
||||
|
||||
if (!user.isShowReport &&
|
||||
!user.isShowAfterReport &&
|
||||
!_debugExitTest &&
|
||||
!_debugPlacementTest) {
|
||||
talker.debug('[ReportRedirect] ❌ 无报告标记+无调试开关,放行');
|
||||
return;
|
||||
}
|
||||
|
||||
final isExitTest = user.isShowAfterReport || _debugExitTest;
|
||||
final reportType = isExitTest
|
||||
? 'exitTest'
|
||||
: (user.isShowReport || _debugPlacementTest ? 'placementTest' : null);
|
||||
if (reportType == null) {
|
||||
talker.debug('[ReportRedirect] ❌ reportType=null,放行');
|
||||
return;
|
||||
}
|
||||
talker.debug('[ReportRedirect] ✅ 通过标记检查,reportType=$reportType');
|
||||
|
||||
// 当前路由是否在 tab 页面(直接从全局 appRouter 拿,不需要 context)
|
||||
String location = appRouter.state?.matchedLocation ?? '';
|
||||
talker.debug('[ReportRedirect] appRouter.state matchedLocation=$location');
|
||||
final inTab = _isInTabRoute(location);
|
||||
talker.debug(
|
||||
'[ReportRedirect] location=$location kTabPaths=$kTabPaths inTab=$inTab',
|
||||
);
|
||||
if (!inTab) {
|
||||
talker.debug('[ReportRedirect] ❌ 不在 tab 页面,跳过拦截');
|
||||
return;
|
||||
}
|
||||
|
||||
talker.debug('[ReportRedirect] ✅✅✅ 触发跳转 → studentInfo?type=$reportType');
|
||||
_isRedirecting = true;
|
||||
|
||||
try {
|
||||
appRouter.go('/studentInfo', extra: {'type': reportType});
|
||||
talker.debug('[ReportRedirect] appRouter.go 调用成功');
|
||||
} catch (e) {
|
||||
talker.error('[ReportRedirect] appRouter.go 崩了!', e);
|
||||
}
|
||||
|
||||
// 重置标记(go_router 会重建 Widget,这个 State 会被 dispose)
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
_isRedirecting = false;
|
||||
});
|
||||
}
|
||||
|
||||
/// 判断当前路径是否在 tab 路由里(动态,不硬编码)
|
||||
///
|
||||
/// go_router 的 StatefulShellRoute 下的 4 个 ShellBranch 里,
|
||||
/// 第一个 GoRoute 的 path 就是 tab 路径。我们从 app_router 导出的
|
||||
/// tabPaths 集合判断,别人改 tab 路径这里自动生效。
|
||||
bool _isInTabRoute(String location) {
|
||||
return kTabPaths.contains(location);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
Reference in New Issue
Block a user