Files
jiu/client/lib/core/router/app_router.dart
T
wangjia 6238b86dcb feat(client): 登录/注册页照原型重建,ds 真相源组件族统一全部屏
- 登录/注册(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
2026-07-03 09:58:14 +08:00

218 lines
8.4 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.
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;
});