From 9d5bc18de17022787016f35c9ca5f905eb79f684 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sun, 21 Jun 2026 19:46:49 +0800 Subject: [PATCH] chore: release client-v1.0.70 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01YZ4DskSRKsSiheQonFtQvx --- CHANGELOG-client.md | 10 ++++++++++ client/lib/core/auth/auth_state.dart | 19 ++++++++++++++++--- client/lib/core/theme/app_theme.dart | 2 ++ client/lib/repositories/auth_repository.dart | 1 + .../stock_in/stock_in_form_screen.dart | 16 +++++++++++++--- .../stock_in/stock_in_list_screen.dart | 7 +++++-- .../stock_out/stock_out_list_screen.dart | 7 +++++-- client/lib/widgets/order_row_actions.dart | 8 ++++---- 8 files changed, 56 insertions(+), 14 deletions(-) diff --git a/CHANGELOG-client.md b/CHANGELOG-client.md index 2c36d0c..08937a1 100644 --- a/CHANGELOG-client.md +++ b/CHANGELOG-client.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.70] - 2026-06-21 + +### 修复 +- 修复超级管理员看不到「撤回」按钮的问题(此前仅 admin 角色显示,superadmin 被漏掉) +- 入库单批次号改为必填项,未填写会拦截提交 + +### 改进 +- 「撤回」按钮:操作员现在也能撤回本人提交的审核中单据(管理员/超管仍可撤回任意单据);按钮改用警示(amber)样式 +- 新建入库单:生产日期列加宽,完整日期(如 2024-05-12)不再显示不全 + ## [1.0.69] - 2026-06-21 ### 新功能 diff --git a/client/lib/core/auth/auth_state.dart b/client/lib/core/auth/auth_state.dart index 2ac7490..927efc6 100644 --- a/client/lib/core/auth/auth_state.dart +++ b/client/lib/core/auth/auth_state.dart @@ -7,6 +7,7 @@ import '../config/app_constants.dart'; const _kAccessToken = 'access_token'; const _kRefreshToken = 'refresh_token'; +const _kUserId = 'user_id'; const _kUsername = 'username'; const _kRealName = 'real_name'; const _kShopNo = 'shop_no'; @@ -16,6 +17,7 @@ const _kRole = 'role'; class AuthUser { final String accessToken; final String refreshToken; + final int id; final String username; final String realName; final String shopNo; @@ -25,6 +27,7 @@ class AuthUser { const AuthUser({ required this.accessToken, required this.refreshToken, + this.id = 0, required this.username, required this.realName, required this.shopNo, @@ -74,12 +77,14 @@ class AuthNotifier extends StateNotifier { final shopIdStr = prefs.getString(_kShopId); final role = prefs.getString(_kRole); + final userId = prefs.getInt(_kUserId) ?? 0; if (accessToken != null && refreshToken != null && username != null) { state = AuthState( initialized: true, user: AuthUser( accessToken: accessToken, refreshToken: refreshToken, + id: userId, username: username, realName: realName ?? username, shopNo: shopNo ?? '', @@ -103,6 +108,7 @@ class AuthNotifier extends StateNotifier { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_kAccessToken, user.accessToken); await prefs.setString(_kRefreshToken, user.refreshToken); + await prefs.setInt(_kUserId, user.id); await prefs.setString(_kUsername, user.username); await prefs.setString(_kRealName, user.realName); await prefs.setString(_kShopNo, user.shopNo); @@ -134,6 +140,7 @@ class AuthNotifier extends StateNotifier { user: AuthUser( accessToken: newToken, refreshToken: refreshToken, + id: state.user!.id, username: state.user!.username, realName: state.user!.realName, shopNo: state.user!.shopNo, @@ -167,6 +174,7 @@ class AuthNotifier extends StateNotifier { final prefs = await SharedPreferences.getInstance(); await prefs.remove(_kAccessToken); await prefs.remove(_kRefreshToken); + await prefs.remove(_kUserId); await prefs.remove(_kUsername); await prefs.remove(_kRealName); await prefs.remove(_kShopNo); @@ -195,9 +203,14 @@ final isReadonlyProvider = Provider((ref) { return role == 'readonly'; }); -/// 当前登录用户是否为管理员(role == 'admin')。 -/// 用于「撤回审核中单据」等仅管理员可用的操作;后端 middleware.AdminOnly() 兜底。 +/// 当前登录用户是否具备管理员权限(admin 或 superadmin)。 +/// 与后端 middleware.AdminOnly() 一致(两者皆放行),用于「撤回审核中单据」等操作。 final isAdminProvider = Provider((ref) { final role = ref.watch(authStateProvider.select((s) => s.user?.role)); - return role == 'admin'; + return role == 'admin' || role == 'superadmin'; +}); + +/// 当前登录用户 id(未登录为 0)。用于「撤回本人单据」等需要判断单据归属的场景。 +final currentUserIdProvider = Provider((ref) { + return ref.watch(authStateProvider.select((s) => s.user?.id)) ?? 0; }); diff --git a/client/lib/core/theme/app_theme.dart b/client/lib/core/theme/app_theme.dart index b1c77fd..2bcd040 100644 --- a/client/lib/core/theme/app_theme.dart +++ b/client/lib/core/theme/app_theme.dart @@ -14,6 +14,8 @@ class AppTheme { static const Color success = Color(0xFF2E8B57); static const Color danger = Color(0xFFD14343); + /// 警示色(amber):用于「撤回」等谨慎但非破坏性的操作,文字在白底对比足够。 + static const Color warning = Color(0xFFB45309); static const Color background = Color(0xFFF5F7FA); static const Color surface = Color(0xFFFFFFFF); static const Color border = Color(0xFFDCE2EB); diff --git a/client/lib/repositories/auth_repository.dart b/client/lib/repositories/auth_repository.dart index 38bfd57..034bc1d 100644 --- a/client/lib/repositories/auth_repository.dart +++ b/client/lib/repositories/auth_repository.dart @@ -55,6 +55,7 @@ class AuthRepository { return AuthUser( accessToken: data['access_token'] as String, refreshToken: data['refresh_token'] as String, + id: (user['id'] as num?)?.toInt() ?? 0, username: user['username'] as String, realName: user['real_name'] as String? ?? username, shopNo: shopCode, diff --git a/client/lib/screens/stock_in/stock_in_form_screen.dart b/client/lib/screens/stock_in/stock_in_form_screen.dart index 98b8413..d49192e 100644 --- a/client/lib/screens/stock_in/stock_in_form_screen.dart +++ b/client/lib/screens/stock_in/stock_in_form_screen.dart @@ -300,6 +300,14 @@ class _StockInFormScreenState extends ConsumerState { ); return; } + if (item.batchNoCtrl.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('第 ${i + 1} 行请填写批次号'), + backgroundColor: AppTheme.danger), + ); + return; + } } } @@ -872,11 +880,13 @@ class _StockInFormScreenState extends ConsumerState { return TextFormField( controller: item.batchNoCtrl, decoration: const InputDecoration( - hintText: '选填', + hintText: '必填', isDense: true, ), style: const TextStyle(fontSize: 13), onChanged: (_) => setState(() {}), + validator: (v) => + (v == null || v.trim().isEmpty) ? '不能为空' : null, ); } @@ -992,7 +1002,7 @@ class _StockInFormScreenState extends ConsumerState { Expanded(flex: 18, child: th('名称')), Expanded(flex: 12, child: th('系列')), Expanded(flex: 12, child: th('规格')), - Expanded(flex: 14, child: th('生产日期')), + Expanded(flex: 20, child: th('生产日期')), Expanded(flex: 12, child: th('批次号')), Expanded(flex: 10, child: th('数量')), Expanded(flex: 10, child: th('单价')), @@ -1049,7 +1059,7 @@ class _StockInFormScreenState extends ConsumerState { padding: const EdgeInsets.all(4), child: _specField(item))), Expanded( - flex: 14, + flex: 20, child: Padding( padding: const EdgeInsets.all(4), child: _dateField(item))), diff --git a/client/lib/screens/stock_in/stock_in_list_screen.dart b/client/lib/screens/stock_in/stock_in_list_screen.dart index a7439c3..2b4fc6a 100644 --- a/client/lib/screens/stock_in/stock_in_list_screen.dart +++ b/client/lib/screens/stock_in/stock_in_list_screen.dart @@ -25,7 +25,8 @@ import '../../providers/finance_provider.dart' show financeRepositoryProvider; import '../../providers/shop_provider.dart' show shopInfoProvider; import '../../widgets/write_guard.dart'; import '../../widgets/order_row_actions.dart'; -import '../../core/auth/auth_state.dart' show isAdminProvider; +import '../../core/auth/auth_state.dart' + show isAdminProvider, currentUserIdProvider; class StockInListScreen extends ConsumerStatefulWidget { const StockInListScreen({super.key}); @@ -479,7 +480,9 @@ class _StockInListScreenState extends ConsumerState { List _orderActions(BuildContext context, StockInOrder o) { return buildOrderRowActions( readonly: WriteGuard.isReadonly(ref), - isAdmin: ref.watch(isAdminProvider), + canWithdraw: ref.watch(isAdminProvider) || + (o.operatorId != null && + o.operatorId == ref.watch(currentUserIdProvider)), status: o.status, orderId: o.id, onDetail: () => _showDetail(context, o.id), diff --git a/client/lib/screens/stock_out/stock_out_list_screen.dart b/client/lib/screens/stock_out/stock_out_list_screen.dart index 7abeec5..21887bd 100644 --- a/client/lib/screens/stock_out/stock_out_list_screen.dart +++ b/client/lib/screens/stock_out/stock_out_list_screen.dart @@ -23,7 +23,8 @@ import '../../providers/product_provider.dart'; import '../../providers/finance_provider.dart' show financeRepositoryProvider; import '../../widgets/write_guard.dart'; import '../../widgets/order_row_actions.dart'; -import '../../core/auth/auth_state.dart' show isAdminProvider; +import '../../core/auth/auth_state.dart' + show isAdminProvider, currentUserIdProvider; class StockOutListScreen extends ConsumerStatefulWidget { const StockOutListScreen({super.key}); @@ -485,7 +486,9 @@ class _StockOutListScreenState extends ConsumerState { List _orderActions(BuildContext context, StockOutOrder o) { return buildOrderRowActions( readonly: WriteGuard.isReadonly(ref), - isAdmin: ref.watch(isAdminProvider), + canWithdraw: ref.watch(isAdminProvider) || + (o.operatorId != null && + o.operatorId == ref.watch(currentUserIdProvider)), status: o.status, orderId: o.id, onDetail: () => _showDetail(context, o.id), diff --git a/client/lib/widgets/order_row_actions.dart b/client/lib/widgets/order_row_actions.dart index a7b0ebc..e66a44b 100644 --- a/client/lib/widgets/order_row_actions.dart +++ b/client/lib/widgets/order_row_actions.dart @@ -24,7 +24,7 @@ List buildOrderRowActions({ required VoidCallback onApprove, required VoidCallback onReject, required VoidCallback onWithdraw, - bool isAdmin = false, + bool canWithdraw = false, List afterPrint = const [], }) { TextButton btn(String text, Color color, VoidCallback onPressed, {Key? key}) => @@ -51,10 +51,10 @@ List buildOrderRowActions({ WriteGuard( child: btn('拒绝', AppTheme.danger, onReject, key: Key('btn_reject_$orderId'))), - // 撤回(审核中→草稿)仅管理员可见 - if (isAdmin) + // 撤回(审核中→草稿):管理员/超管任意单、操作员本人单可见,用 warn 样式 + if (canWithdraw) WriteGuard( - child: btn('撤回', AppTheme.accent, onWithdraw, + child: btn('撤回', AppTheme.warning, onWithdraw, key: Key('btn_withdraw_$orderId'))), ], ];