Files
xianyan/lib/core/router/app_router.dart
Developer f281e465bb chore: v6.6.6 版本迭代更新
主要变更:
1. 重构"国学"相关模块为"经典名句",统一命名规范
2. 重命名"阅读报告"为"使用报告",调整相关文案与配置
3. 修复iOS模拟器图片缓存兼容问题,优化图表渲染逻辑
4. 新增设备活跃状态前端兜底判断,修复在线计数异常
5. 完善登录/注册流程,新增忘记密码路由与账户编辑提示
6. 优化文件传输与字体导入逻辑,废弃过时的bytes属性使用
7. 添加Spotlight全局快捷键支持,更新隐私权限与通知配置
8. 补充数据库迁移脚本与部署文档,修复后端接口兼容问题
9. 调整部分UI交互细节,优化内存占用与应用稳定性
2026-06-07 06:56:52 +08:00

453 lines
15 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.
// ============================================================
// 闲言APP — 路由配置(主入口)
// 创建时间: 2026-04-20
// 更新时间: 2026-06-05
// 作用: go_router 路由表组装 + ShellRoute 布局壳 + iOS 风格转场 + 深链接重定向
// 上次更新: 路由观察日志降级为debug级别减少IDE日志量
// ============================================================
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/cupertino.dart';
import 'package:go_router/go_router.dart';
import '../utils/logger.dart' show Log, LogCategory;
import '../utils/platform/platform_utils.dart' as pu;
import '../../features/onboarding/presentation/onboarding_page.dart';
import '../../features/home/presentation/home_page.dart';
import '../../features/discover/presentation/pages/home/discover_page.dart';
import '../../features/mine/profile/presentation/profile_page.dart';
import '../layout/app_shell.dart';
import '../storage/kv_storage.dart';
import '../utils/ui/page_transitions.dart';
import '../services/device/shake_detector.dart';
import 'app_routes.dart';
import 'settings_routes.dart';
import 'tool_routes.dart';
import 'editor_router.dart';
import 'user_routes.dart';
import 'content_routes.dart';
import 'feature_routes.dart';
export 'app_routes.dart';
final rootNavigatorKey = GlobalKey<NavigatorState>();
final shellNavigatorKey = GlobalKey<NavigatorState>();
final _homeNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'home');
final _discoverNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'discover');
final _profileNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'profile');
final _shakeScopeObserver = ShakeScopeObserver();
final GoRouter appRouter = GoRouter(
navigatorKey: rootNavigatorKey,
// 初始路由始终为home由redirect动态判断是否需要跳转引导页
// KvStorage在GoRouter构造时可能尚未初始化不能在initialLocation中依赖它
initialLocation: AppRoutes.home,
debugLogDiagnostics: true,
observers: pu.isOhos
? [_OhosRouteObserver(), _shakeScopeObserver]
: [BotToastNavigatorObserver(), _shakeScopeObserver],
redirect: _handleRedirect,
routes: [
GoRoute(
path: AppRoutes.onboarding,
name: 'onboarding',
parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) =>
iosSlideTransition(state: state, child: const OnboardingPage()),
),
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) =>
AppShell(child: navigationShell),
branches: [
StatefulShellBranch(
navigatorKey: _homeNavigatorKey,
routes: [
GoRoute(
path: AppRoutes.home,
name: 'home',
pageBuilder: (context, state) =>
iosFadeTransition(state: state, child: const HomePage()),
),
],
),
StatefulShellBranch(
navigatorKey: _discoverNavigatorKey,
routes: [
GoRoute(
path: AppRoutes.discover,
name: 'discover',
pageBuilder: (context, state) =>
iosFadeTransition(state: state, child: const DiscoverPage()),
),
],
),
StatefulShellBranch(
navigatorKey: _profileNavigatorKey,
routes: [
GoRoute(
path: AppRoutes.profile,
name: 'profile',
pageBuilder: (context, state) =>
iosFadeTransition(state: state, child: const ProfilePage()),
),
],
),
],
),
...buildEditorRoutes(rootNavigatorKey),
...buildSettingsRoutes(rootNavigatorKey),
...buildToolRoutes(rootNavigatorKey),
...buildUserRoutes(rootNavigatorKey),
...buildContentRoutes(rootNavigatorKey),
...buildFeatureRoutes(rootNavigatorKey),
],
errorBuilder: (context, state) => const NotFoundPage(),
);
/// 统一重定向:引导页判断 + 深度链接解析
/// 在每次导航时执行此时KvStorage已初始化完成
String? _handleRedirect(BuildContext context, GoRouterState state) {
final currentPath = state.matchedLocation;
// 1. 引导页判断仅在KvStorage已初始化时生效
if (KvStorage.isReady) {
final onboardingDone = KvStorage.isOnboardingCompleted;
final shouldShow = KvStorage.shouldShowOnboarding;
// 需要显示引导页且当前不在引导页 → 跳转引导页
if ((!onboardingDone || shouldShow) && currentPath != AppRoutes.onboarding) {
if (pu.isOhos) {
Log.d('🟢 [OHOS] redirect: onboardingDone=$onboardingDone, shouldShow=$shouldShow → /onboarding', null, null, LogCategory.router);
}
return AppRoutes.onboarding;
}
// 引导页已完成但当前在引导页 → 跳转首页
// 例外:如果带有 review=true 参数,说明是用户主动查看,不拦截
if (onboardingDone && !shouldShow && currentPath == AppRoutes.onboarding) {
final isReview = state.uri.queryParameters['review'] == 'true';
if (!isReview) {
final savedPage = KvStorage.getString('general_startup_page');
final location = switch (savedPage) {
'inspiration' => AppRoutes.inspiration,
'discover' => AppRoutes.discover,
'profile' => AppRoutes.profile,
_ => AppRoutes.home,
};
return location;
}
}
}
// 2. 深度链接解析
final uri = state.uri;
final resolved = AppRouter.resolveDeepLinkUri(uri);
if (resolved != null && resolved != currentPath) {
final scheme = uri.scheme.toLowerCase();
if (scheme == 'xianyan') {
Log.d('🔗 [DeepLink] xianyan://${uri.host}${uri.path}$resolved', null, null, LogCategory.router);
} else if (scheme == 'https' || scheme == 'http') {
Log.d('🔗 [DeepLink] ${uri.scheme}://${uri.host}${uri.path}$resolved', null, null, LogCategory.router);
}
return resolved;
}
return null;
}
/// 统一深度链接URI解析入口
/// 支持 xianyan:// scheme 和 https://s2ss.com 通用链接
/// 供 GoRouter redirect 和 DeepLinkService 共用
class AppRouter {
AppRouter._();
/// 解析 URI 为内部路由路径
/// 返回 null 表示不是深度链接,不需要重定向
static String? resolveDeepLinkUri(Uri uri) {
final scheme = uri.scheme.toLowerCase();
if (scheme == 'xianyan') {
return _resolveCustomScheme(uri);
}
if (scheme == 'https' || scheme == 'http') {
final host = uri.host.toLowerCase();
if (host == 's2ss.com' || host == 'www.s2ss.com') {
return _resolveHttps(uri);
}
}
return null;
}
/// 解析 xianyan:// scheme 链接
/// 格式: xianyan://<host>[/<path>]
/// 例如: xianyan://tool/hanzi, xianyan://settings/theme
static String? _resolveCustomScheme(Uri uri) {
final host = uri.host;
final path = uri.path;
return switch (host) {
'home' => AppRoutes.home,
'discover' => AppRoutes.discover,
'profile' => AppRoutes.profile,
'search' => AppRoutes.search,
'fortune' => AppRoutes.dailyFortune,
'article' => path.isNotEmpty ? path : AppRoutes.home,
'notes' => AppRoutes.noteList,
'inspiration' => AppRoutes.inspiration,
'favorites' => AppRoutes.favorites,
'history' => AppRoutes.history,
'tool' => _resolveToolPath(path),
'editor' => AppRoutes.editor,
'signin' => AppRoutes.signin,
'weather' =>
path.isNotEmpty ? AppRoutes.weatherSettings : AppRoutes.weather,
'poetry' => path.isNotEmpty ? AppRoutes.poetrySettings : AppRoutes.poetry,
'pomodoro' => AppRoutes.pomodoro,
'countdown' => AppRoutes.countdown,
'solar-term' => AppRoutes.solarTerm,
'knowledge-graph' => AppRoutes.knowledgeGraph,
'study-plan' => AppRoutes.studyPlan,
'notification-settings' => AppRoutes.notificationSettings,
'statistics' => AppRoutes.statistics,
'achievement' => AppRoutes.achievement,
'rank' => AppRoutes.rank,
'learning' => AppRoutes.learning,
'checkin' => AppRoutes.achievement,
'settings' => _resolveSettingsPath(path),
_ => _resolvePathFallback(path),
};
}
/// 解析 https://s2ss.com 通用链接
/// 格式: https://s2ss.com/<segment>[/<sub>]
static String? _resolveHttps(Uri uri) {
final segments = uri.pathSegments;
if (segments.isEmpty) return AppRoutes.home;
final first = segments.first;
return switch (first) {
'fortune' => AppRoutes.dailyFortune,
'article' => '/${segments.join('/')}',
'notes' => AppRoutes.noteList,
'home' => AppRoutes.home,
'discover' => AppRoutes.discover,
'profile' => AppRoutes.profile,
'inspiration' => AppRoutes.inspiration,
'search' => AppRoutes.search,
'favorites' => AppRoutes.favorites,
'history' => AppRoutes.history,
'tool' =>
segments.length > 1
? _resolveToolPath('/${segments[1]}')
: AppRoutes.discover,
'editor' => AppRoutes.editor,
'weather' =>
segments.length > 1 ? AppRoutes.weatherSettings : AppRoutes.weather,
'poetry' =>
segments.length > 1 ? AppRoutes.poetrySettings : AppRoutes.poetry,
'settings' => _resolveSettingsPath('/${segments.skip(1).join('/')}'),
'achievement' => AppRoutes.achievement,
'rank' => AppRoutes.rank,
'learning' => AppRoutes.learning,
'checkin' => AppRoutes.achievement,
_ => _resolvePathFallback('/${segments.join('/')}'),
};
}
/// 解析工具子路由路径
static String? _resolveToolPath(String path) {
return switch (path) {
'/hanzi' => AppRoutes.hanziTool,
'/ocr' => '/tool/ocr',
'/colors' => '/tool/china_colors',
'/china_colors' => '/tool/china_colors',
'/hot' => '/tool/list',
'/calc' => '/tool/calc',
'/list' => '/tool/list',
'/offline' => AppRoutes.offline,
'/cache' => AppRoutes.cacheManagement,
'/readlater' => AppRoutes.readLater,
'/favorites' => AppRoutes.favorites,
'/history' => AppRoutes.history,
'/notes' => AppRoutes.noteList,
'/signin' => AppRoutes.signin,
'/weather' => AppRoutes.weather,
'/poetry' => AppRoutes.poetry,
'/pomodoro' => AppRoutes.pomodoro,
'/countdown' => AppRoutes.countdown,
'/solar-term' => AppRoutes.solarTerm,
'/knowledge-graph' => AppRoutes.knowledgeGraph,
'/study-plan' => AppRoutes.studyPlan,
'/chengyu' => '/tool/chengyu',
'/zuci' => '/tool/zuci',
'/cidian' => '/tool/cidian',
'/jinyici' => '/tool/jinyici',
'/juzi' => '/tool/juzi',
'/danci' => '/tool/danci',
'/suoxie' => '/tool/suoxie',
'/nick' => '/tool/nick',
'/drug' => '/tool/drug',
'/food' => '/tool/food',
'/herbal' => '/tool/herbal',
'/pianfang' => '/tool/pianfang',
'/tisana' => '/tool/tisana',
'/changshi' => '/tool/changshi',
'/zgjm' => '/tool/zgjm',
'/illness' => '/tool/illness',
'/surname' => '/tool/surname',
'/jieqi' => '/tool/jieqi',
'/nation' => '/tool/nation',
'/xiehouyu' => '/tool/xiehouyu',
'/riddle' => '/tool/riddle',
'/brainteaser' => '/tool/brainteaser',
'/couplet' => '/tool/couplet',
'/wisdom' => '/tool/wisdom',
'/saying' => '/tool/saying',
'/lyric' => '/tool/lyric',
'/story' => '/tool/story',
'/zuowen' => '/tool/zuowen',
'/why' => '/tool/why',
'/joke' => '/tool/joke',
'/jiufang' => '/tool/jiufang',
'/exchange_rate' => '/tool/exchange_rate',
'/rss_reader' => '/tool/rss_reader',
'/game' => '/game',
'/classics' => '/classics',
'/health' => '/health',
'/articles' => '/articles',
'/check' => '/check',
_ => AppRoutes.discover,
};
}
/// 解析设置子路由路径
static String? _resolveSettingsPath(String path) {
return switch (path) {
'/theme' => AppRoutes.themeSettings,
'/general' => AppRoutes.generalSettings,
'/account' => AppRoutes.accountSettings,
'/data' => AppRoutes.dataManagement,
'/notifications' => AppRoutes.notificationSettings,
'/language' => AppRoutes.languageSettings,
'/fonts' => AppRoutes.fontManagement,
'/app-lock' => AppRoutes.appLockSettings,
_ => AppRoutes.generalSettings,
};
}
/// 路径兜底解析:将 URI path 段直接映射为内部路由
static String? _resolvePathFallback(String path) {
if (path.isEmpty || path == '/') return AppRoutes.home;
final segments = path.split('/').where((s) => s.isNotEmpty).toList();
if (segments.isEmpty) return AppRoutes.home;
return switch (segments.first) {
'fortune' => AppRoutes.dailyFortune,
'article' => path,
'notes' => AppRoutes.noteList,
'home' => AppRoutes.home,
'discover' => AppRoutes.discover,
'profile' => AppRoutes.profile,
'inspiration' => AppRoutes.inspiration,
'search' => AppRoutes.search,
'favorites' => AppRoutes.favorites,
'history' => AppRoutes.history,
'settings' => path,
'weather' =>
segments.length > 1 ? AppRoutes.weatherSettings : AppRoutes.weather,
'poetry' =>
segments.length > 1 ? AppRoutes.poetrySettings : AppRoutes.poetry,
'achievement' => AppRoutes.achievement,
'rank' => AppRoutes.rank,
'learning' => AppRoutes.learning,
'checkin' => AppRoutes.achievement,
_ => path,
};
}
}
class _OhosRouteObserver extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
Log.d(
'🟢 [OHOS] Route didPush: ${route.settings.name} (${route.runtimeType})',
null,
null,
LogCategory.router,
);
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
Log.d('🟢 [OHOS] Route didPop: ${route.settings.name}', null, null, LogCategory.router);
}
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
Log.d(
'🟢 [OHOS] Route didReplace: ${oldRoute?.settings.name}${newRoute?.settings.name}',
null,
null,
LogCategory.router,
);
}
@override
void didStartUserGesture(
Route<dynamic> route,
Route<dynamic>? previousRoute,
) {
Log.d('🟢 [OHOS] Route didStartUserGesture: ${route.settings.name}', null, null, LogCategory.router);
}
}
/// 摇一摇路由作用域观察者
/// 监听 GoRouter 路由变化,自动更新 ShakeDetector 的作用域
/// 仅当当前路由为 /home 时允许摇一摇触发
///
/// 继承 NavigatorObserver 以便加入 GoRouter.observers 列表,
/// 确保变量被初始化;同时懒加载 routerDelegate.addListener
/// 以捕获 StatefulShellRoute 内的 tab 切换事件
class ShakeScopeObserver extends NavigatorObserver {
bool _listenerAttached = false;
void _ensureListenerAttached() {
if (_listenerAttached) return;
_listenerAttached = true;
appRouter.routerDelegate.addListener(_onRouteChanged);
}
void _onRouteChanged() {
final location =
appRouter.routerDelegate.currentConfiguration.last.matchedLocation;
final scope = location == AppRoutes.home ? AppRoutes.home : '';
ShakeDetector.instance.setScope(scope);
}
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
_ensureListenerAttached();
_onRouteChanged();
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
_onRouteChanged();
}
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
_onRouteChanged();
}
}