be9afcf68d
对齐移动原型体系(m-*.html)的 Stage 0 共享基建,桌面形态零改动: - 导航:router 新增 branch 9(/me 我的 + /me/license 授权屏);窄屏 tab 根 (库存/入库/出库/财务/我的)出底部 MTabBar,二级屏隐藏 tabbar + 顶栏返回箭头 (PopScope 拦系统返回键防退 app);窄屏 Drawer/汉堡删除;窄屏顶栏对齐 m-top (标题 + 主题/通知铃/头像→我的) - sheet:showMSheet(grip/标题/86vh/键盘避让)+ showAdaptiveSheet;六个共享 弹层呈现函数加窄屏 sheet 分支(订单/往来/财务往来/商品编辑抽屉 + 退单/登记收支 dialog 的 asSheet 形态) - 徽章:DsBadge/StatusPill/StatusBadge 加图标变体(默认圆点零漂移)+ status_icon_map 转录原型 BADGE_ICON 28 词 - 组件:MKpiGrid(2×2 可点 KPI)/ MSearchRow(搜索+状态钮+详搜钮)/ MHubGroup(hub 入口组)/ showThemeSheet(主题外观 sheet) - 授权:_LicensePanel/_SettingsCard 抽取为公共 LicensePanel/SettingsCard, 窄屏授权跳转 /me/license - golden:app_shell_mobile + me 各三主题(390×844@2x);全量 259 测试绿, check_ds_code / check-l1-sync 闸过 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
232 lines
9.2 KiB
Dart
232 lines
9.2 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
import '../../screens/auth/login_screen.dart';
|
||
import '../../screens/auth/register_screen.dart';
|
||
import '../../screens/shell/app_shell.dart';
|
||
import '../../screens/stock_in/stock_in_list_screen.dart';
|
||
import '../../screens/stock_in/stock_in_form_screen.dart';
|
||
import '../../screens/stock_out/stock_out_list_screen.dart';
|
||
import '../../screens/stock_out/stock_out_form_screen.dart';
|
||
import '../../screens/inventory/inventory_list_screen.dart';
|
||
import '../../screens/inventory/inventory_check_screen.dart';
|
||
import '../../screens/partners/partners_screen.dart';
|
||
import '../../screens/finance/finance_screen.dart';
|
||
import '../../screens/products/products_screen.dart';
|
||
import '../../screens/products/product_detail_screen.dart';
|
||
import '../../screens/public/public_product_screen.dart';
|
||
import '../../screens/public/public_shop_products_screen.dart';
|
||
import '../../screens/settings/settings_screen.dart';
|
||
import '../../screens/settings/users_screen.dart';
|
||
import '../../screens/about/about_screen.dart';
|
||
import '../../screens/devices/device_management_screen.dart';
|
||
import '../../screens/me/me_screen.dart';
|
||
import '../../screens/me/license_screen.dart';
|
||
import '../auth/auth_state.dart';
|
||
|
||
Page<void> _noTransition(Widget child) => NoTransitionPage<void>(child: child);
|
||
|
||
/// ChangeNotifier that bridges Riverpod auth state → GoRouter refreshListenable.
|
||
class _RouterNotifier extends ChangeNotifier {
|
||
final Ref _ref;
|
||
|
||
_RouterNotifier(this._ref) {
|
||
_ref.listen<AuthState>(authStateProvider, (prev, next) {
|
||
debugPrint('[Router] authState changed:'
|
||
' initialized=${next.initialized}'
|
||
' isLoggedIn=${next.isLoggedIn}'
|
||
' user=${next.user?.username}');
|
||
notifyListeners();
|
||
});
|
||
}
|
||
|
||
String? redirect(BuildContext context, GoRouterState state) {
|
||
final authState = _ref.read(authStateProvider);
|
||
final isLoggedIn = authState.isLoggedIn;
|
||
final loc = state.matchedLocation;
|
||
final isPublicRoute = loc == '/login' ||
|
||
loc == '/register' ||
|
||
loc.startsWith('/product/') ||
|
||
loc.startsWith('/shop/');
|
||
final result = !authState.initialized
|
||
? null
|
||
: (!isLoggedIn && !isPublicRoute)
|
||
? '/login'
|
||
: (isLoggedIn && loc == '/login')
|
||
? '/stock-in'
|
||
: null;
|
||
debugPrint('[Router] redirect: location=$loc'
|
||
' initialized=${authState.initialized}'
|
||
' isLoggedIn=$isLoggedIn'
|
||
' → ${result ?? "null (no redirect)"}');
|
||
return result;
|
||
}
|
||
}
|
||
|
||
/// Separate provider so that appRouterProvider has NO dependencies and
|
||
/// is never rebuilt when auth state changes (prevents router reset to /login).
|
||
final _routerNotifierProvider = Provider<_RouterNotifier>((ref) {
|
||
final notifier = _RouterNotifier(ref);
|
||
ref.onDispose(notifier.dispose);
|
||
return notifier;
|
||
});
|
||
|
||
final appRouterProvider = Provider<GoRouter>((ref) {
|
||
// Use ref.read (not ref.watch) so appRouterProvider never rebuilds on auth change.
|
||
final notifier = ref.read(_routerNotifierProvider);
|
||
|
||
final router = GoRouter(
|
||
initialLocation: '/login',
|
||
refreshListenable: notifier,
|
||
redirect: notifier.redirect,
|
||
routes: [
|
||
// Public product scan — no auth, no shell nav bar
|
||
GoRoute(
|
||
path: '/product/:public_id',
|
||
builder: (context, state) =>
|
||
PublicProductScreen(publicId: state.pathParameters['public_id']!),
|
||
),
|
||
// Public shop product list — no auth, no shell nav bar
|
||
GoRoute(
|
||
path: '/shop/:shop_code',
|
||
builder: (context, state) => PublicShopProductsScreen(
|
||
shopCode: state.pathParameters['shop_code']!,
|
||
shopName: state.uri.queryParameters['shopName'] ?? '',
|
||
),
|
||
),
|
||
GoRoute(
|
||
path: '/login',
|
||
builder: (context, state) => const LoginScreen(),
|
||
),
|
||
GoRoute(
|
||
path: '/register',
|
||
builder: (context, state) => const RegisterScreen(),
|
||
),
|
||
// 各栏目拆为独立分支:StatefulShellRoute.indexedStack 让每个分支的 Navigator
|
||
// 及其页面 State 常驻,跨栏目切换不再销毁上一页(半填表单/内部 tab/滚动位置保活)。
|
||
// 分支顺序必须与 AppShell._navItems 一致(navigationShell.currentIndex 据此高亮)。
|
||
StatefulShellRoute.indexedStack(
|
||
// location 透传给壳:窄屏据此判定 tab 根 / 二级屏(底部 tabbar 显隐与返回箭头)。
|
||
builder: (context, state, navigationShell) => AppShell(
|
||
navigationShell: navigationShell,
|
||
location: state.matchedLocation),
|
||
branches: [
|
||
// 0 入库管理
|
||
StatefulShellBranch(routes: [
|
||
GoRoute(
|
||
path: '/stock-in',
|
||
pageBuilder: (_, __) =>
|
||
_noTransition(const StockInListScreen())),
|
||
GoRoute(
|
||
path: '/stock-in/new',
|
||
pageBuilder: (_, __) =>
|
||
_noTransition(const StockInFormScreen())),
|
||
GoRoute(
|
||
path: '/stock-in/edit/:id',
|
||
pageBuilder: (_, state) => _noTransition(StockInFormScreen(
|
||
editOrderId: int.parse(state.pathParameters['id']!)))),
|
||
]),
|
||
// 1 出库管理
|
||
StatefulShellBranch(routes: [
|
||
GoRoute(
|
||
path: '/stock-out',
|
||
pageBuilder: (_, __) =>
|
||
_noTransition(const StockOutListScreen())),
|
||
GoRoute(
|
||
path: '/stock-out/new',
|
||
pageBuilder: (_, __) =>
|
||
_noTransition(const StockOutFormScreen())),
|
||
GoRoute(
|
||
path: '/stock-out/edit/:id',
|
||
pageBuilder: (_, state) => _noTransition(StockOutFormScreen(
|
||
editOrderId: int.parse(state.pathParameters['id']!)))),
|
||
]),
|
||
// 2 库存管理
|
||
StatefulShellBranch(routes: [
|
||
GoRoute(
|
||
path: '/inventory',
|
||
pageBuilder: (_, __) =>
|
||
_noTransition(const InventoryListScreen())),
|
||
GoRoute(
|
||
path: '/inventory/check',
|
||
pageBuilder: (_, __) =>
|
||
_noTransition(const InventoryCheckScreen())),
|
||
]),
|
||
// 3 财务管理
|
||
StatefulShellBranch(routes: [
|
||
GoRoute(
|
||
path: '/finance',
|
||
pageBuilder: (_, __) => _noTransition(const FinanceScreen())),
|
||
]),
|
||
// 4 往来单位
|
||
StatefulShellBranch(routes: [
|
||
GoRoute(
|
||
path: '/partners',
|
||
pageBuilder: (_, __) => _noTransition(const PartnersScreen())),
|
||
]),
|
||
// 5 基础数据
|
||
StatefulShellBranch(routes: [
|
||
GoRoute(
|
||
path: '/products',
|
||
// ?tab=intro → 商品介绍 tab(对齐原型 products.html?tab=intro)
|
||
pageBuilder: (_, state) => _noTransition(ProductsScreen(
|
||
initialTab:
|
||
state.uri.queryParameters['tab'] == 'intro' ? 1 : 0))),
|
||
GoRoute(
|
||
path: '/products/:id',
|
||
pageBuilder: (_, state) => _noTransition(ProductDetailScreen(
|
||
productId: int.parse(state.pathParameters['id']!)))),
|
||
]),
|
||
// 6 设备管理
|
||
StatefulShellBranch(routes: [
|
||
GoRoute(
|
||
path: '/devices',
|
||
pageBuilder: (_, __) =>
|
||
_noTransition(const DeviceManagementScreen())),
|
||
]),
|
||
// 7 系统设置(面板:门店信息/用户管理/编号规则/授权兑换券/偏好设置)
|
||
StatefulShellBranch(routes: [
|
||
GoRoute(
|
||
path: '/settings',
|
||
pageBuilder: (_, state) {
|
||
const tabIndex = {
|
||
'shop': 0,
|
||
'users': 1,
|
||
'number': 2,
|
||
'license': 3,
|
||
'pref': 4,
|
||
};
|
||
final tab = tabIndex[state.uri.queryParameters['tab']] ?? 0;
|
||
return _noTransition(SettingsScreen(initialTab: tab));
|
||
}),
|
||
// 用户管理独立页(照原型 users.html)
|
||
GoRoute(
|
||
path: '/settings/users',
|
||
pageBuilder: (_, __) => _noTransition(const UsersScreen())),
|
||
]),
|
||
// 8 关于我们
|
||
StatefulShellBranch(routes: [
|
||
GoRoute(
|
||
path: '/about',
|
||
pageBuilder: (_, __) => _noTransition(const AboutScreen())),
|
||
]),
|
||
// 9 我的(移动 hub,原型 m-me.html;桌面侧栏无此项,仅窄屏底部 tab 入口)
|
||
StatefulShellBranch(routes: [
|
||
GoRoute(
|
||
path: '/me',
|
||
pageBuilder: (_, __) => _noTransition(const MeScreen())),
|
||
// 授权管理独立屏(原型 m-license.html,从「我的」系统组进入)
|
||
GoRoute(
|
||
path: '/me/license',
|
||
pageBuilder: (_, __) => _noTransition(const LicenseScreen())),
|
||
]),
|
||
],
|
||
),
|
||
],
|
||
);
|
||
|
||
ref.onDispose(router.dispose);
|
||
|
||
return router;
|
||
});
|