import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../providers/auth_provider.dart'; import '../utils/permissions.dart'; import '../screens/admin/admin_hub_page.dart'; import '../screens/admin/app/app_version_page.dart'; import '../screens/admin/app/tv_app_page.dart'; import '../screens/admin/channel/channel_management_page.dart'; import '../screens/admin/intro/intro_category_page.dart'; import '../screens/admin/intro/intro_store_content_page.dart'; import '../screens/admin/intro/intro_store_list_page.dart'; import '../screens/admin/marquee/marquee_management_page.dart'; import '../screens/admin/media/media_list_page.dart'; import '../screens/admin/media/media_rank_page.dart'; import '../screens/admin/sys/config_page.dart'; import '../screens/admin/sys/log_page.dart'; import '../screens/admin/sys/menu_page.dart'; import '../screens/admin/sys/role_page.dart'; import '../screens/admin/sys/schedule_page.dart'; import '../screens/admin/sys/user_page.dart'; import '../screens/device/device_activation_page.dart'; import '../screens/device/device_group_page.dart'; import '../screens/device/device_scan_page.dart'; import '../screens/device/store_config_page.dart'; import '../screens/home/device_detail_page.dart'; import '../screens/home/device_list_page.dart'; import '../screens/home/home_page.dart'; import '../screens/login/login_page.dart'; import '../models/store_info.dart'; import '../screens/profile/settings_page.dart'; import '../screens/store/org_tree_page.dart'; import '../screens/store/store_detail_page.dart'; import '../screens/store/store_list_page.dart'; import '../widgets/app_bottom_nav.dart'; /// 全局路由配置。 /// /// 路由表: /// - `/login` 登录页(未登录时重定向到此) /// - `/` 首页仪表盘(底部导航第 1 个 tab) /// - `/device` 设备列表(底部导航第 2 个 tab) /// - `/device/:id` 设备详情 /// - `/device/group` 设备分组 /// - `/device/activation` 激活码管理 /// - `/device/store-config` 门店配置 /// - `/store` 门店管理(底部导航第 3 个 tab) /// - `/store/org-tree` 组织架构 /// - `/store/:id` 门店详情 /// - `/admin` 管理中心(底部导航第 4 个 tab) /// - `/admin/sys/*` 系统管理子模块 /// - `/admin/app/*` 应用管理子模块 /// - `/admin/media/*` 媒资管理子模块 /// - `/admin/intro/*` 公司介绍子模块 /// - `/profile` 我的(底部导航第 5 个 tab) /// - `/profile/settings` 设置 final appRouterProvider = Provider((ref) { // 登录态变化时触发路由重定向刷新,而不重建整个 GoRouter。 final refreshNotifier = ValueNotifier(0); ref ..onDispose(refreshNotifier.dispose) ..listen(authProvider, (_, __) => refreshNotifier.value++); return GoRouter( initialLocation: '/', refreshListenable: refreshNotifier, redirect: (context, state) { final auth = ref.read(authProvider); final loggedIn = auth.isLoggedIn; final isLoginRoute = state.matchedLocation == '/login'; if (!loggedIn && !isLoginRoute) { return '/login'; } if (loggedIn && isLoginRoute) { return '/'; } // 功能模块按账号权限收敛(菜单可见性与此处路由可达性由同一套 // [SessionPermissions] 谓词驱动,确保一致): // - 系统管理中仅 /admin/sys/user(用户管理)对所有已登录账号开放; // 其余 /admin/sys/*(角色/菜单/配置/日志/定时任务)仅管理员可访问; // - 媒资 /admin/media/*、应用 /admin/app/*、公司介绍(分类维度) // /admin/intro/category 仅管理员可见/可访问; // - 渠道商管理 /admin/channel 对管理员与渠道商开放,代理商不可访问; // 非管理员(受限账号)即使通过深链/返回键进入上述受限路由也重定向回 // 管理首页,作为入口收敛的兜底,防止越权查看/操作跨组织数据。 // 注:公司介绍门店维度入口(/admin/intro/store-list、/admin/intro/ // store-content/*)按门店维度展示、数据已由 getMyStores 收敛,对所有账号开放。 final loc = state.matchedLocation; final user = ref.read(authProvider).user; final cat = SessionPermissions.category(user); if (cat != AccountCategory.admin) { final isUserManagement = loc == '/admin/sys/user' || loc.startsWith('/admin/sys/user/'); final isSysModule = loc.startsWith('/admin/sys') && !isUserManagement; if (isSysModule || loc.startsWith('/admin/media') || loc.startsWith('/admin/app') || loc.startsWith('/admin/intro/category')) { return '/admin'; } // 代理商(非渠道商)访问渠道商管理 → 弹回(其菜单中本就隐藏该入口)。 if (loc.startsWith('/admin/channel') && cat != AccountCategory.channelMerchant) { return '/admin'; } } return null; }, routes: [ GoRoute( path: '/login', name: 'login', builder: (context, state) => const LoginPage(), ), StatefulShellRoute.indexedStack( builder: (context, state, navigationShell) => AppBottomNav(shell: navigationShell), branches: [ // 首页(仪表盘) StatefulShellBranch( routes: [ GoRoute( path: '/', name: 'home', builder: (context, state) => const HomePage(), ), ], ), // 设备 StatefulShellBranch( routes: [ GoRoute( path: '/device', name: 'device', builder: (context, state) => const DeviceListPage(), routes: [ // 具名子路由(group/activation/store-config/scan)必须放在参数路由 :id 之前, // 否则 `/device/scan` 会被 `:id` 匹配成设备 ID="scan",导致进入错误的详情页。 GoRoute( path: 'group', name: 'deviceGroup', builder: (context, state) => const DeviceGroupPage(), ), GoRoute( path: 'activation', name: 'deviceActivation', builder: (context, state) => const DeviceActivationPage(), ), GoRoute( path: 'store-config', name: 'storeConfig', builder: (context, state) => const StoreConfigPage(), ), GoRoute( path: 'scan', name: 'deviceScan', builder: (context, state) { final mode = state.uri.queryParameters['mode'] ?? 'query'; final deviceId = state.uri.queryParameters['deviceId']; final storeId = state.uri.queryParameters['storeId']; final storeName = state.uri.queryParameters['storeName']; final groupId = state.uri.queryParameters['groupId']; return DeviceScanPage( mode: mode, deviceId: deviceId, storeId: storeId, storeName: storeName, groupId: groupId, ); }, ), GoRoute( path: ':id', name: 'deviceDetail', builder: (context, state) { final id = state.pathParameters['id'] ?? ''; return DeviceDetailPage(deviceId: id); }, ), ], ), ], ), // 门店 StatefulShellBranch( routes: [ GoRoute( path: '/store', name: 'store', builder: (context, state) => const StoreListPage(), routes: [ GoRoute( path: 'org-tree', name: 'orgTree', builder: (context, state) => const OrgTreePage(), ), GoRoute( path: ':id', name: 'storeDetail', builder: (context, state) { final id = state.pathParameters['id'] ?? ''; return StoreDetailPage(storeId: id); }, ), ], ), ], ), // 管理 StatefulShellBranch( routes: [ GoRoute( path: '/admin', name: 'admin', builder: (context, state) => const AdminHubPage(), routes: [ // 管理模块内的设备/激活码别名路由:复用设备分支的页面组件, // 但在 /admin 分支栈下 push,使底部导航仍停留「管理」tab, // 返回键可正常 pop 回管理首页,避免此前 push 到 /device 分支栈导致的返回键异常。 GoRoute( path: 'device', name: 'adminDevice', builder: (context, state) => const DeviceListPage(), ), GoRoute( path: 'activation', name: 'adminActivation', builder: (context, state) => const DeviceActivationPage(), ), // 系统管理 GoRoute( path: 'sys/user', name: 'sysUser', builder: (context, state) => const UserPage(), ), GoRoute( path: 'sys/role', name: 'sysRole', builder: (context, state) => const RolePage(), ), GoRoute( path: 'sys/menu', name: 'sysMenu', builder: (context, state) => const MenuPage(), ), GoRoute( path: 'sys/config', name: 'sysConfig', builder: (context, state) => const ConfigPage(), ), GoRoute( path: 'sys/log', name: 'sysLog', builder: (context, state) => const LogPage(), ), GoRoute( path: 'sys/schedule', name: 'sysSchedule', builder: (context, state) => const SchedulePage(), ), // 应用管理 GoRoute( path: 'app/version', name: 'appVersion', builder: (context, state) => const AppVersionPage(), ), GoRoute( path: 'app/tv-app', name: 'tvApp', builder: (context, state) => const TvAppPage(), ), // 滚动字幕管理 GoRoute( path: 'marquee', name: 'marqueeManagement', builder: (context, state) => const MarqueeManagementPage(), ), // 渠道商管理 GoRoute( path: 'channel', name: 'channelManagement', builder: (context, state) => const ChannelManagementPage(), ), // 媒资管理 GoRoute( path: 'media/list', name: 'mediaList', builder: (context, state) => const MediaListPage(), ), GoRoute( path: 'media/rank', name: 'mediaRank', builder: (context, state) => const MediaRankPage(), ), // 公司介绍(门店维度) GoRoute( path: 'intro/store-list', name: 'introStoreList', builder: (context, state) => const IntroStoreListPage(), ), GoRoute( path: 'intro/store-content/:storeId', name: 'introStoreContent', builder: (context, state) { final storeId = state.pathParameters['storeId'] ?? ''; final store = state.extra is StoreInfo ? state.extra as StoreInfo : null; return IntroStoreContentPage( storeId: storeId, store: store, ); }, ), // 公司介绍(原分类维度,保留作为管理备用入口) GoRoute( path: 'intro/category', name: 'introCategory', builder: (context, state) => const IntroCategoryPage(), ), ], ), ], ), // 我的 StatefulShellBranch( routes: [ GoRoute( path: '/profile', name: 'profile', builder: (context, state) => const SettingsPage(), routes: [ GoRoute( path: 'settings', name: 'profileSettings', builder: (context, state) => const SettingsPage(), ), ], ), ], ), ], ), ], ); });