feat(client): 只读角色隐藏所有写按钮(WriteGuard 统一控件 + 顶栏只读标识)
- 新增 isReadonlyProvider(role == 'readonly')+ WriteGuard 控件,统一隐藏写控件 - 入库/出库/库存/商品/基础数据/往来单位/财务/设置 各屏:只读时隐藏 新增/编辑/删除/审核/提交/驳回/结清/盘点/导入/激活/用户管理 等写操作 - 顶栏显示「只读」标识,让只读用户明确当前权限 - 后端 middleware.ReadOnly() 仍兜底 403,UI 隐藏 + 后端拦截双保险 修正:此前误判为后端漏洞/部署 bug,实为前端写按钮未对只读隐藏。 后端经验证正确拦截只读写操作(线上 stock-in/product-options 均 403)。 121 测试通过,analyze 无 error。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ import '../../providers/product_provider.dart' show productRepositoryProvider;
|
||||
import '../../repositories/product_repository.dart';
|
||||
import '../../providers/finance_provider.dart' show financeRepositoryProvider;
|
||||
import '../../providers/shop_provider.dart' show shopInfoProvider;
|
||||
import '../../core/auth/auth_state.dart';
|
||||
|
||||
class StockInListScreen extends ConsumerStatefulWidget {
|
||||
const StockInListScreen({super.key});
|
||||
@@ -280,7 +281,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
ref.read(stockInListProvider.notifier).setPageSize(s),
|
||||
toolbar: Row(
|
||||
children: [
|
||||
if (showNewButton)
|
||||
if (showNewButton && !ref.watch(isReadonlyProvider))
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => context.go('/stock-in/new'),
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
@@ -344,6 +345,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
|
||||
/// 操作按钮列表,表格与移动端卡片共用。
|
||||
List<Widget> _orderActions(BuildContext context, StockInOrder o) {
|
||||
final readonly = ref.watch(isReadonlyProvider);
|
||||
return [
|
||||
TextButton(
|
||||
onPressed: () => _showDetail(context, o.id),
|
||||
@@ -390,13 +392,13 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
child: const Text('打标签',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
if (o.status == 'approved')
|
||||
if (!readonly && o.status == 'approved')
|
||||
TextButton(
|
||||
onPressed: () => _confirmSettle(context, o.id, 'stock_in'),
|
||||
child: const Text('结清',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.accent)),
|
||||
),
|
||||
if (o.status == 'draft') ...[
|
||||
if (!readonly && o.status == 'draft') ...[
|
||||
TextButton(
|
||||
onPressed: () => context.go('/stock-in/edit/${o.id}'),
|
||||
child: const Text('修改',
|
||||
@@ -413,7 +415,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
|
||||
),
|
||||
],
|
||||
if (o.status == 'pending') ...[
|
||||
if (!readonly && o.status == 'pending') ...[
|
||||
TextButton(
|
||||
key: Key('btn_approve_${o.id}'),
|
||||
onPressed: () => _confirmApprove(context, o),
|
||||
|
||||
Reference in New Issue
Block a user