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:
@@ -218,11 +218,12 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showAddUserDialog(context),
|
||||
icon: const Icon(Icons.person_add, size: 16),
|
||||
label: const Text('新增用户'),
|
||||
),
|
||||
if (!ref.watch(isReadonlyProvider))
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showAddUserDialog(context),
|
||||
icon: const Icon(Icons.person_add, size: 16),
|
||||
label: const Text('新增用户'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -292,7 +293,9 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
)),
|
||||
DataCell(Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
children: ref.watch(isReadonlyProvider)
|
||||
? const []
|
||||
: [
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
_showEditUserDialog(context, u),
|
||||
@@ -454,16 +457,18 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: _isActivating ? null : _activateLicense,
|
||||
child: _isActivating
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Text('激活'),
|
||||
),
|
||||
if (!ref.watch(isReadonlyProvider)) ...[
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: _isActivating ? null : _activateLicense,
|
||||
child: _isActivating
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Text('激活'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -614,11 +619,13 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace', fontSize: 12))),
|
||||
DataCell(Text('${r.currentNo}')),
|
||||
DataCell(TextButton(
|
||||
onPressed: () =>
|
||||
_showNumberRuleDialog(context, r),
|
||||
child: const Text('编辑',
|
||||
style: TextStyle(fontSize: 12)))),
|
||||
DataCell(ref.watch(isReadonlyProvider)
|
||||
? const SizedBox()
|
||||
: TextButton(
|
||||
onPressed: () =>
|
||||
_showNumberRuleDialog(context, r),
|
||||
child: const Text('编辑',
|
||||
style: TextStyle(fontSize: 12)))),
|
||||
]))
|
||||
.toList(),
|
||||
),
|
||||
@@ -797,6 +804,12 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
|
||||
// ── 数据导入 Tab ──────────────────────────────────────────
|
||||
Widget _buildImportTab() {
|
||||
if (ref.watch(isReadonlyProvider)) {
|
||||
return const Center(
|
||||
child: Text('只读账号无导入权限',
|
||||
style: TextStyle(color: AppTheme.textSecondary)),
|
||||
);
|
||||
}
|
||||
final currentUser = ref.watch(authStateProvider).user;
|
||||
final isSuperAdmin = currentUser?.role == 'superadmin';
|
||||
return _BatchImportWidget(isSuperAdmin: isSuperAdmin);
|
||||
|
||||
Reference in New Issue
Block a user