feat(client): 录单表单优化 + 可搜索下拉支持新建 + 日期选择器组件

新增 DatePickerField 日期选择器与 date_util;可搜索下拉 SearchableOptionField
支持内联新建选项;入库/出库录单表单、商品页、财务页、app_shell 导航相应调整。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-20 07:05:22 +08:00
parent e892ab45b2
commit a76724b385
12 changed files with 2868 additions and 1624 deletions
+101 -70
View File
@@ -20,8 +20,7 @@ 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);
Page<void> _noTransition(Widget child) => NoTransitionPage<void>(child: child);
/// ChangeNotifier that bridges Riverpod auth state → GoRouter refreshListenable.
class _RouterNotifier extends ChangeNotifier {
@@ -94,74 +93,106 @@ final appRouterProvider = Provider<GoRouter>((ref) {
path: '/login',
builder: (context, state) => const LoginScreen(),
),
ShellRoute(
builder: (context, state, child) => AppShell(child: child),
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']!)))),
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']!)))),
GoRoute(
path: '/inventory',
pageBuilder: (_, __) =>
_noTransition(const InventoryListScreen())),
GoRoute(
path: '/inventory/check',
pageBuilder: (_, __) =>
_noTransition(const InventoryCheckScreen())),
GoRoute(
path: '/partners',
pageBuilder: (_, __) => _noTransition(const PartnersScreen())),
GoRoute(
path: '/finance',
pageBuilder: (_, __) => _noTransition(const FinanceScreen())),
GoRoute(
path: '/products',
pageBuilder: (_, __) => _noTransition(const ProductsScreen())),
GoRoute(
path: '/products/:id',
pageBuilder: (_, state) => _noTransition(ProductDetailScreen(
productId: int.parse(state.pathParameters['id']!)))),
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));
}),
GoRoute(
path: '/devices',
pageBuilder: (_, __) =>
_noTransition(const DeviceManagementScreen())),
GoRoute(
path: '/about',
pageBuilder: (_, __) => _noTransition(const AboutScreen())),
// 各栏目拆为独立分支: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())),
]),
],
),
],
+22
View File
@@ -0,0 +1,22 @@
/// 日期工具:统一 `yyyy-MM-dd` 的格式化与解析,收敛各处重复的 padLeft 逻辑。
library;
/// 格式化为 `yyyy-MM-dd`(提交后端的统一格式)。
String formatYmd(DateTime d) =>
'${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
/// 解析 `yyyy-MM-dd`(或更长的带时间字符串,取前 10 位)。无效返回 null。
DateTime? parseYmd(String? s) {
if (s == null || s.isEmpty) return null;
final t = s.length >= 10 ? s.substring(0, 10) : s;
return DateTime.tryParse(t);
}
/// 组合年/月/日为 `yyyy-MM-dd`,自动把超出当月的「日」clamp 回当月最大值
/// (如 2 月选 31 → 28/29)。任一为空返回 null。
String? composeYmd(int? year, int? month, int? day) {
if (year == null || month == null || day == null) return null;
final maxDay = DateTime(year, month + 1, 0).day; // 下月第 0 天 = 当月最后一天
final d = day > maxDay ? maxDay : day;
return formatYmd(DateTime(year, month, d));
}