d9f0d3feca
- 新增 RegisterScreen(左品牌面板 3 步引导 + 右表单:门店名/电话/地址/ 管理员/账号/密码/确认 + 同意条款),还原原型两栏设计,全 token 化 - AuthRepository.register → POST /api/v1/register(后端已有),成功弹门店编号 引导去登录 - 路由加 /register(公开路由);登录页「前往官网注册」→ 应用内「立即注册」 - 整屏 golden ×三主题入回归闸 (购买流/通知中心按指示暂不做) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSKEiHsvauyxYUW2itzUXX
211 lines
7.9 KiB
Dart
211 lines
7.9 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/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',
|
|
pageBuilder: (_, __) => _noTransition(const ProductsScreen())),
|
|
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,
|
|
'system': 3,
|
|
'license': 4,
|
|
'import': 5,
|
|
};
|
|
final tab = tabIndex[state.uri.queryParameters['tab']] ?? 0;
|
|
return _noTransition(SettingsScreen(initialTab: tab));
|
|
}),
|
|
]),
|
|
// 8 关于我们
|
|
StatefulShellBranch(routes: [
|
|
GoRoute(
|
|
path: '/about',
|
|
pageBuilder: (_, __) => _noTransition(const AboutScreen())),
|
|
]),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
|
|
ref.onDispose(router.dispose);
|
|
|
|
return router;
|
|
});
|