6238b86dcb
- 登录/注册(login.html/register.html 1:1):两栏卡片+品牌渐变面板+主题小衣服 pill(onSurface 变体)+记住我(记录并预填最近账号)+原型式 toast 校验; 登录 fidelity 1.4–2.1% 三主题全绿;注册暂不入闸(少 门店编号/兑换券 字段, 已记 CONTRACT,screens.mjs 留存根) - ds 原子补齐:DsToast(.toast 单例)/DsCheck(.check/.agree)/DsButton lg 档/ DsSelect 替换全部旧 DropdownButton/DsField label 在上/DsInput 后缀与密码形态 - 全屏统一:对话框按钮全 DsButton、盒式输入主题钉死(visualDensity.standard、 h38、InputDecorationTheme 渗漏修复)、图标全 lucide、JetBrains Mono 三端同源、 BrandMark 真相源 logo、只读模式写操作全量守卫(WriteGuard+DsToast) - 出入库列表:版式对齐原型(卡片对齐+搜索框居中)、KPI 近30天滚动、结清后 失效财务应收应付表;商品编辑抽屉介绍库改搜索下拉、图片双击全屏预览 - 删除旧 UI 死代码:DataTableCard/FormDialog/PageScaffold/SearchChip/ SelectProductDialog/tabStateProvider - golden/fidelity:stock-in/out 补注册(9%)、login 入册(8%)、goldens 全量重打; 修复 pubspec flutter_web_plugins 非法声明(CI pub get 阻断) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
218 lines
8.4 KiB
Dart
218 lines
8.4 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 '../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(
|
||
builder: (context, state, navigationShell) =>
|
||
AppShell(navigationShell: navigationShell),
|
||
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())),
|
||
]),
|
||
],
|
||
),
|
||
],
|
||
);
|
||
|
||
ref.onDispose(router.dispose);
|
||
|
||
return router;
|
||
});
|