Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d5bc18de1 |
@@ -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/),
|
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).
|
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
|
## [1.0.69] - 2026-06-21
|
||||||
|
|
||||||
### 新功能
|
### 新功能
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import '../config/app_constants.dart';
|
|||||||
|
|
||||||
const _kAccessToken = 'access_token';
|
const _kAccessToken = 'access_token';
|
||||||
const _kRefreshToken = 'refresh_token';
|
const _kRefreshToken = 'refresh_token';
|
||||||
|
const _kUserId = 'user_id';
|
||||||
const _kUsername = 'username';
|
const _kUsername = 'username';
|
||||||
const _kRealName = 'real_name';
|
const _kRealName = 'real_name';
|
||||||
const _kShopNo = 'shop_no';
|
const _kShopNo = 'shop_no';
|
||||||
@@ -16,6 +17,7 @@ const _kRole = 'role';
|
|||||||
class AuthUser {
|
class AuthUser {
|
||||||
final String accessToken;
|
final String accessToken;
|
||||||
final String refreshToken;
|
final String refreshToken;
|
||||||
|
final int id;
|
||||||
final String username;
|
final String username;
|
||||||
final String realName;
|
final String realName;
|
||||||
final String shopNo;
|
final String shopNo;
|
||||||
@@ -25,6 +27,7 @@ class AuthUser {
|
|||||||
const AuthUser({
|
const AuthUser({
|
||||||
required this.accessToken,
|
required this.accessToken,
|
||||||
required this.refreshToken,
|
required this.refreshToken,
|
||||||
|
this.id = 0,
|
||||||
required this.username,
|
required this.username,
|
||||||
required this.realName,
|
required this.realName,
|
||||||
required this.shopNo,
|
required this.shopNo,
|
||||||
@@ -74,12 +77,14 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||||||
final shopIdStr = prefs.getString(_kShopId);
|
final shopIdStr = prefs.getString(_kShopId);
|
||||||
|
|
||||||
final role = prefs.getString(_kRole);
|
final role = prefs.getString(_kRole);
|
||||||
|
final userId = prefs.getInt(_kUserId) ?? 0;
|
||||||
if (accessToken != null && refreshToken != null && username != null) {
|
if (accessToken != null && refreshToken != null && username != null) {
|
||||||
state = AuthState(
|
state = AuthState(
|
||||||
initialized: true,
|
initialized: true,
|
||||||
user: AuthUser(
|
user: AuthUser(
|
||||||
accessToken: accessToken,
|
accessToken: accessToken,
|
||||||
refreshToken: refreshToken,
|
refreshToken: refreshToken,
|
||||||
|
id: userId,
|
||||||
username: username,
|
username: username,
|
||||||
realName: realName ?? username,
|
realName: realName ?? username,
|
||||||
shopNo: shopNo ?? '',
|
shopNo: shopNo ?? '',
|
||||||
@@ -103,6 +108,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setString(_kAccessToken, user.accessToken);
|
await prefs.setString(_kAccessToken, user.accessToken);
|
||||||
await prefs.setString(_kRefreshToken, user.refreshToken);
|
await prefs.setString(_kRefreshToken, user.refreshToken);
|
||||||
|
await prefs.setInt(_kUserId, user.id);
|
||||||
await prefs.setString(_kUsername, user.username);
|
await prefs.setString(_kUsername, user.username);
|
||||||
await prefs.setString(_kRealName, user.realName);
|
await prefs.setString(_kRealName, user.realName);
|
||||||
await prefs.setString(_kShopNo, user.shopNo);
|
await prefs.setString(_kShopNo, user.shopNo);
|
||||||
@@ -134,6 +140,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||||||
user: AuthUser(
|
user: AuthUser(
|
||||||
accessToken: newToken,
|
accessToken: newToken,
|
||||||
refreshToken: refreshToken,
|
refreshToken: refreshToken,
|
||||||
|
id: state.user!.id,
|
||||||
username: state.user!.username,
|
username: state.user!.username,
|
||||||
realName: state.user!.realName,
|
realName: state.user!.realName,
|
||||||
shopNo: state.user!.shopNo,
|
shopNo: state.user!.shopNo,
|
||||||
@@ -167,6 +174,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.remove(_kAccessToken);
|
await prefs.remove(_kAccessToken);
|
||||||
await prefs.remove(_kRefreshToken);
|
await prefs.remove(_kRefreshToken);
|
||||||
|
await prefs.remove(_kUserId);
|
||||||
await prefs.remove(_kUsername);
|
await prefs.remove(_kUsername);
|
||||||
await prefs.remove(_kRealName);
|
await prefs.remove(_kRealName);
|
||||||
await prefs.remove(_kShopNo);
|
await prefs.remove(_kShopNo);
|
||||||
@@ -195,9 +203,14 @@ final isReadonlyProvider = Provider<bool>((ref) {
|
|||||||
return role == 'readonly';
|
return role == 'readonly';
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 当前登录用户是否为管理员(role == 'admin')。
|
/// 当前登录用户是否具备管理员权限(admin 或 superadmin)。
|
||||||
/// 用于「撤回审核中单据」等仅管理员可用的操作;后端 middleware.AdminOnly() 兜底。
|
/// 与后端 middleware.AdminOnly() 一致(两者皆放行),用于「撤回审核中单据」等操作。
|
||||||
final isAdminProvider = Provider<bool>((ref) {
|
final isAdminProvider = Provider<bool>((ref) {
|
||||||
final role = ref.watch(authStateProvider.select((s) => s.user?.role));
|
final role = ref.watch(authStateProvider.select((s) => s.user?.role));
|
||||||
return role == 'admin';
|
return role == 'admin' || role == 'superadmin';
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 当前登录用户 id(未登录为 0)。用于「撤回本人单据」等需要判断单据归属的场景。
|
||||||
|
final currentUserIdProvider = Provider<int>((ref) {
|
||||||
|
return ref.watch(authStateProvider.select((s) => s.user?.id)) ?? 0;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class AppTheme {
|
|||||||
|
|
||||||
static const Color success = Color(0xFF2E8B57);
|
static const Color success = Color(0xFF2E8B57);
|
||||||
static const Color danger = Color(0xFFD14343);
|
static const Color danger = Color(0xFFD14343);
|
||||||
|
/// 警示色(amber):用于「撤回」等谨慎但非破坏性的操作,文字在白底对比足够。
|
||||||
|
static const Color warning = Color(0xFFB45309);
|
||||||
static const Color background = Color(0xFFF5F7FA);
|
static const Color background = Color(0xFFF5F7FA);
|
||||||
static const Color surface = Color(0xFFFFFFFF);
|
static const Color surface = Color(0xFFFFFFFF);
|
||||||
static const Color border = Color(0xFFDCE2EB);
|
static const Color border = Color(0xFFDCE2EB);
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ class AuthRepository {
|
|||||||
return AuthUser(
|
return AuthUser(
|
||||||
accessToken: data['access_token'] as String,
|
accessToken: data['access_token'] as String,
|
||||||
refreshToken: data['refresh_token'] as String,
|
refreshToken: data['refresh_token'] as String,
|
||||||
|
id: (user['id'] as num?)?.toInt() ?? 0,
|
||||||
username: user['username'] as String,
|
username: user['username'] as String,
|
||||||
realName: user['real_name'] as String? ?? username,
|
realName: user['real_name'] as String? ?? username,
|
||||||
shopNo: shopCode,
|
shopNo: shopCode,
|
||||||
|
|||||||
@@ -300,6 +300,14 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
|||||||
);
|
);
|
||||||
return;
|
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<StockInFormScreen> {
|
|||||||
return TextFormField(
|
return TextFormField(
|
||||||
controller: item.batchNoCtrl,
|
controller: item.batchNoCtrl,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
hintText: '选填',
|
hintText: '必填',
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
style: const TextStyle(fontSize: 13),
|
style: const TextStyle(fontSize: 13),
|
||||||
onChanged: (_) => setState(() {}),
|
onChanged: (_) => setState(() {}),
|
||||||
|
validator: (v) =>
|
||||||
|
(v == null || v.trim().isEmpty) ? '不能为空' : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -992,7 +1002,7 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
|||||||
Expanded(flex: 18, child: th('名称')),
|
Expanded(flex: 18, child: th('名称')),
|
||||||
Expanded(flex: 12, child: th('系列')),
|
Expanded(flex: 12, 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: 12, child: th('批次号')),
|
||||||
Expanded(flex: 10, child: th('数量')),
|
Expanded(flex: 10, child: th('数量')),
|
||||||
Expanded(flex: 10, child: th('单价')),
|
Expanded(flex: 10, child: th('单价')),
|
||||||
@@ -1049,7 +1059,7 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
|||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
child: _specField(item))),
|
child: _specField(item))),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 14,
|
flex: 20,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
child: _dateField(item))),
|
child: _dateField(item))),
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ import '../../providers/finance_provider.dart' show financeRepositoryProvider;
|
|||||||
import '../../providers/shop_provider.dart' show shopInfoProvider;
|
import '../../providers/shop_provider.dart' show shopInfoProvider;
|
||||||
import '../../widgets/write_guard.dart';
|
import '../../widgets/write_guard.dart';
|
||||||
import '../../widgets/order_row_actions.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 {
|
class StockInListScreen extends ConsumerStatefulWidget {
|
||||||
const StockInListScreen({super.key});
|
const StockInListScreen({super.key});
|
||||||
@@ -479,7 +480,9 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
List<Widget> _orderActions(BuildContext context, StockInOrder o) {
|
List<Widget> _orderActions(BuildContext context, StockInOrder o) {
|
||||||
return buildOrderRowActions(
|
return buildOrderRowActions(
|
||||||
readonly: WriteGuard.isReadonly(ref),
|
readonly: WriteGuard.isReadonly(ref),
|
||||||
isAdmin: ref.watch(isAdminProvider),
|
canWithdraw: ref.watch(isAdminProvider) ||
|
||||||
|
(o.operatorId != null &&
|
||||||
|
o.operatorId == ref.watch(currentUserIdProvider)),
|
||||||
status: o.status,
|
status: o.status,
|
||||||
orderId: o.id,
|
orderId: o.id,
|
||||||
onDetail: () => _showDetail(context, o.id),
|
onDetail: () => _showDetail(context, o.id),
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ import '../../providers/product_provider.dart';
|
|||||||
import '../../providers/finance_provider.dart' show financeRepositoryProvider;
|
import '../../providers/finance_provider.dart' show financeRepositoryProvider;
|
||||||
import '../../widgets/write_guard.dart';
|
import '../../widgets/write_guard.dart';
|
||||||
import '../../widgets/order_row_actions.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 {
|
class StockOutListScreen extends ConsumerStatefulWidget {
|
||||||
const StockOutListScreen({super.key});
|
const StockOutListScreen({super.key});
|
||||||
@@ -485,7 +486,9 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
List<Widget> _orderActions(BuildContext context, StockOutOrder o) {
|
List<Widget> _orderActions(BuildContext context, StockOutOrder o) {
|
||||||
return buildOrderRowActions(
|
return buildOrderRowActions(
|
||||||
readonly: WriteGuard.isReadonly(ref),
|
readonly: WriteGuard.isReadonly(ref),
|
||||||
isAdmin: ref.watch(isAdminProvider),
|
canWithdraw: ref.watch(isAdminProvider) ||
|
||||||
|
(o.operatorId != null &&
|
||||||
|
o.operatorId == ref.watch(currentUserIdProvider)),
|
||||||
status: o.status,
|
status: o.status,
|
||||||
orderId: o.id,
|
orderId: o.id,
|
||||||
onDetail: () => _showDetail(context, o.id),
|
onDetail: () => _showDetail(context, o.id),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ List<Widget> buildOrderRowActions({
|
|||||||
required VoidCallback onApprove,
|
required VoidCallback onApprove,
|
||||||
required VoidCallback onReject,
|
required VoidCallback onReject,
|
||||||
required VoidCallback onWithdraw,
|
required VoidCallback onWithdraw,
|
||||||
bool isAdmin = false,
|
bool canWithdraw = false,
|
||||||
List<Widget> afterPrint = const [],
|
List<Widget> afterPrint = const [],
|
||||||
}) {
|
}) {
|
||||||
TextButton btn(String text, Color color, VoidCallback onPressed, {Key? key}) =>
|
TextButton btn(String text, Color color, VoidCallback onPressed, {Key? key}) =>
|
||||||
@@ -51,10 +51,10 @@ List<Widget> buildOrderRowActions({
|
|||||||
WriteGuard(
|
WriteGuard(
|
||||||
child: btn('拒绝', AppTheme.danger, onReject,
|
child: btn('拒绝', AppTheme.danger, onReject,
|
||||||
key: Key('btn_reject_$orderId'))),
|
key: Key('btn_reject_$orderId'))),
|
||||||
// 撤回(审核中→草稿)仅管理员可见
|
// 撤回(审核中→草稿):管理员/超管任意单、操作员本人单可见,用 warn 样式
|
||||||
if (isAdmin)
|
if (canWithdraw)
|
||||||
WriteGuard(
|
WriteGuard(
|
||||||
child: btn('撤回', AppTheme.accent, onWithdraw,
|
child: btn('撤回', AppTheme.warning, onWithdraw,
|
||||||
key: Key('btn_withdraw_$orderId'))),
|
key: Key('btn_withdraw_$orderId'))),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user