refactor(client): 只读判断统一走 WriteGuard 控件

将各屏内联的 ref.watch(isReadonlyProvider) 只读判断统一改为
WriteGuard(child:) 或 WriteGuard.isReadonly(ref),全项目只读逻辑
单一入口(仅 write_guard.dart 内部保留 provider 读取)。行为不变。

121 测试通过,analyze 无 error。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-17 00:56:31 +08:00
parent 196901b6d3
commit f15692bbd5
8 changed files with 48 additions and 45 deletions
@@ -11,7 +11,7 @@ import '../../widgets/page_scaffold.dart';
import '../../providers/connectivity_provider.dart'; import '../../providers/connectivity_provider.dart';
import '../../core/utils/export_util.dart'; import '../../core/utils/export_util.dart';
import '../../repositories/finance_repository.dart'; import '../../repositories/finance_repository.dart';
import '../../core/auth/auth_state.dart'; import '../../widgets/write_guard.dart';
class FinanceScreen extends ConsumerWidget { class FinanceScreen extends ConsumerWidget {
const FinanceScreen({super.key}); const FinanceScreen({super.key});
@@ -161,7 +161,7 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
MobileCardField('状态', null, valueWidget: _StatusBadge(r.status)), MobileCardField('状态', null, valueWidget: _StatusBadge(r.status)),
if (r.remark?.isNotEmpty == true) MobileCardField('备注', r.remark), if (r.remark?.isNotEmpty == true) MobileCardField('备注', r.remark),
], ],
actions: (canClose && !ref.watch(isReadonlyProvider)) actions: (canClose && !WriteGuard.isReadonly(ref))
? [ ? [
TextButton( TextButton(
onPressed: () => _closeRecord(r), onPressed: () => _closeRecord(r),
@@ -307,7 +307,7 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
case 'actions': case 'actions':
if ((r.type == 'payable' || r.type == 'receivable') && if ((r.type == 'payable' || r.type == 'receivable') &&
r.status == 'open' && r.status == 'open' &&
!ref.watch(isReadonlyProvider)) { !WriteGuard.isReadonly(ref)) {
return DataCell(TextButton( return DataCell(TextButton(
onPressed: () => _closeRecord(r), onPressed: () => _closeRecord(r),
child: const Text('结清', child: const Text('结清',
@@ -408,13 +408,13 @@ class _FinanceTabState extends ConsumerState<_FinanceTab> {
}, },
toolbar: Row( toolbar: Row(
children: [ children: [
if (addLabel != null && !ref.watch(isReadonlyProvider)) if (addLabel != null && !WriteGuard.isReadonly(ref))
ElevatedButton.icon( ElevatedButton.icon(
onPressed: _showAddDialog, onPressed: _showAddDialog,
icon: const Icon(Icons.add, size: 16), icon: const Icon(Icons.add, size: 16),
label: Text(addLabel), label: Text(addLabel),
), ),
if (addLabel != null && !ref.watch(isReadonlyProvider)) if (addLabel != null && !WriteGuard.isReadonly(ref))
const SizedBox(width: 8), const SizedBox(width: 8),
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () { onPressed: () {
@@ -9,6 +9,7 @@ import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart'; import '../../core/theme/app_theme.dart';
import '../../models/inventory.dart'; import '../../models/inventory.dart';
import '../../core/auth/auth_state.dart'; import '../../core/auth/auth_state.dart';
import '../../widgets/write_guard.dart';
import '../../providers/inventory_provider.dart'; import '../../providers/inventory_provider.dart';
import '../../widgets/data_table_card.dart'; import '../../widgets/data_table_card.dart';
import '../../widgets/mobile_list_card.dart'; import '../../widgets/mobile_list_card.dart';
@@ -292,7 +293,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
message: item.remark.isEmpty ? '' : item.remark, message: item.remark.isEmpty ? '' : item.remark,
waitDuration: const Duration(milliseconds: 300), waitDuration: const Duration(milliseconds: 300),
child: GestureDetector( child: GestureDetector(
onTap: ref.watch(isReadonlyProvider) onTap: WriteGuard.isReadonly(ref)
? null ? null
: () => _editRemark(context, item), : () => _editRemark(context, item),
child: Row( child: Row(
@@ -360,7 +361,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
MobileCardField('供应商', item.supplierName), MobileCardField('供应商', item.supplierName),
], ],
actions: [ actions: [
if (!ref.watch(isReadonlyProvider)) if (!WriteGuard.isReadonly(ref))
TextButton( TextButton(
onPressed: () => _editRemark(context, item), onPressed: () => _editRemark(context, item),
child: const Text('备注', style: TextStyle(fontSize: 13)), child: const Text('备注', style: TextStyle(fontSize: 13)),
@@ -554,7 +555,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
ref.read(inventoryListProvider.notifier).setPageSize(s), ref.read(inventoryListProvider.notifier).setPageSize(s),
toolbar: Row( toolbar: Row(
children: [ children: [
if (!ref.watch(isReadonlyProvider)) ...[ if (!WriteGuard.isReadonly(ref)) ...[
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () => context.go('/inventory/check'), onPressed: () => context.go('/inventory/check'),
icon: const Icon(Icons.fact_check, size: 16), icon: const Icon(Icons.fact_check, size: 16),
@@ -11,7 +11,7 @@ import '../../widgets/data_table_card.dart';
import '../../widgets/mobile_list_card.dart'; import '../../widgets/mobile_list_card.dart';
import '../../widgets/page_scaffold.dart'; import '../../widgets/page_scaffold.dart';
import '../../core/utils/export_util.dart'; import '../../core/utils/export_util.dart';
import '../../core/auth/auth_state.dart'; import '../../widgets/write_guard.dart';
class PartnersScreen extends ConsumerStatefulWidget { class PartnersScreen extends ConsumerStatefulWidget {
const PartnersScreen({super.key}); const PartnersScreen({super.key});
@@ -167,7 +167,7 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
if (p.phone?.isNotEmpty == true) MobileCardField('电话', p.phone), if (p.phone?.isNotEmpty == true) MobileCardField('电话', p.phone),
if (p.address?.isNotEmpty == true) MobileCardField('地址', p.address), if (p.address?.isNotEmpty == true) MobileCardField('地址', p.address),
], ],
actions: ref.watch(isReadonlyProvider) actions: WriteGuard.isReadonly(ref)
? const [] ? const []
: [ : [
TextButton( TextButton(
@@ -194,7 +194,7 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
mobileCards: partners.map(partnerCard).toList(), mobileCards: partners.map(partnerCard).toList(),
toolbar: Row( toolbar: Row(
children: [ children: [
if (!ref.watch(isReadonlyProvider)) ...[ if (!WriteGuard.isReadonly(ref)) ...[
ElevatedButton.icon( ElevatedButton.icon(
onPressed: onAdd, onPressed: onAdd,
icon: const Icon(Icons.add, size: 16), icon: const Icon(Icons.add, size: 16),
@@ -285,7 +285,7 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
)), )),
DataCell(Row( DataCell(Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: ref.watch(isReadonlyProvider) children: WriteGuard.isReadonly(ref)
? const [] ? const []
: [ : [
TextButton( TextButton(
@@ -10,7 +10,7 @@ import '../../widgets/data_table_card.dart';
import '../../widgets/mobile_list_card.dart'; import '../../widgets/mobile_list_card.dart';
import '../../widgets/page_scaffold.dart'; import '../../widgets/page_scaffold.dart';
import '../../core/utils/export_util.dart'; import '../../core/utils/export_util.dart';
import '../../core/auth/auth_state.dart'; import '../../widgets/write_guard.dart';
class ProductsScreen extends ConsumerStatefulWidget { class ProductsScreen extends ConsumerStatefulWidget {
const ProductsScreen({super.key}); const ProductsScreen({super.key});
@@ -355,10 +355,9 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
required VoidCallback onAdd, required VoidCallback onAdd,
required VoidCallback onExport, required VoidCallback onExport,
}) { }) {
final readonly = ref.watch(isReadonlyProvider);
return Row( return Row(
children: [ children: [
if (!readonly) ...[ if (!WriteGuard.isReadonly(ref)) ...[
ElevatedButton.icon( ElevatedButton.icon(
onPressed: onAdd, onPressed: onAdd,
icon: const Icon(Icons.add, size: 16), icon: const Icon(Icons.add, size: 16),
@@ -420,7 +419,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
MobileCardField('单品数量', '$quantity'), MobileCardField('单品数量', '$quantity'),
if (remark?.isNotEmpty == true) MobileCardField('备注', remark), if (remark?.isNotEmpty == true) MobileCardField('备注', remark),
], ],
actions: ref.watch(isReadonlyProvider) actions: WriteGuard.isReadonly(ref)
? const [] ? const []
: [ : [
TextButton( TextButton(
@@ -437,19 +436,20 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
} }
Widget _actionButtons({required VoidCallback onEdit, required VoidCallback onDelete}) { Widget _actionButtons({required VoidCallback onEdit, required VoidCallback onDelete}) {
if (ref.watch(isReadonlyProvider)) return const SizedBox.shrink(); return WriteGuard(
return Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TextButton( TextButton(
onPressed: onEdit, onPressed: onEdit,
child: const Text('编辑', style: TextStyle(fontSize: 12)), child: const Text('编辑', style: TextStyle(fontSize: 12)),
), ),
TextButton( TextButton(
onPressed: onDelete, onPressed: onDelete,
child: const Text('删除', style: TextStyle(fontSize: 12, color: AppTheme.danger)), child: const Text('删除', style: TextStyle(fontSize: 12, color: AppTheme.danger)),
), ),
], ],
),
); );
} }
@@ -9,6 +9,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/auth/auth_state.dart'; import '../../core/auth/auth_state.dart';
import '../../widgets/write_guard.dart';
import '../../core/config/app_config.dart'; import '../../core/config/app_config.dart';
import '../../core/config/app_info.dart'; import '../../core/config/app_info.dart';
import '../../core/responsive/responsive.dart'; import '../../core/responsive/responsive.dart';
@@ -218,12 +219,13 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row( child: Row(
children: [ children: [
if (!ref.watch(isReadonlyProvider)) WriteGuard(
ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () => _showAddUserDialog(context), onPressed: () => _showAddUserDialog(context),
icon: const Icon(Icons.person_add, size: 16), icon: const Icon(Icons.person_add, size: 16),
label: const Text('新增用户'), label: const Text('新增用户'),
), ),
),
], ],
), ),
), ),
@@ -293,7 +295,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
)), )),
DataCell(Row( DataCell(Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: ref.watch(isReadonlyProvider) children: WriteGuard.isReadonly(ref)
? const [] ? const []
: [ : [
TextButton( TextButton(
@@ -457,7 +459,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
style: const TextStyle(fontSize: 13), style: const TextStyle(fontSize: 13),
), ),
), ),
if (!ref.watch(isReadonlyProvider)) ...[ if (!WriteGuard.isReadonly(ref)) ...[
const SizedBox(width: 8), const SizedBox(width: 8),
ElevatedButton( ElevatedButton(
onPressed: _isActivating ? null : _activateLicense, onPressed: _isActivating ? null : _activateLicense,
@@ -619,13 +621,12 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
style: const TextStyle( style: const TextStyle(
fontFamily: 'monospace', fontSize: 12))), fontFamily: 'monospace', fontSize: 12))),
DataCell(Text('${r.currentNo}')), DataCell(Text('${r.currentNo}')),
DataCell(ref.watch(isReadonlyProvider) DataCell(WriteGuard(
? const SizedBox() child: TextButton(
: TextButton(
onPressed: () => onPressed: () =>
_showNumberRuleDialog(context, r), _showNumberRuleDialog(context, r),
child: const Text('编辑', child: const Text('编辑',
style: TextStyle(fontSize: 12)))), style: TextStyle(fontSize: 12))))),
])) ]))
.toList(), .toList(),
), ),
@@ -804,7 +805,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
// ── 数据导入 Tab ────────────────────────────────────────── // ── 数据导入 Tab ──────────────────────────────────────────
Widget _buildImportTab() { Widget _buildImportTab() {
if (ref.watch(isReadonlyProvider)) { if (WriteGuard.isReadonly(ref)) {
return const Center( return const Center(
child: Text('只读账号无导入权限', child: Text('只读账号无导入权限',
style: TextStyle(color: AppTheme.textSecondary)), style: TextStyle(color: AppTheme.textSecondary)),
+2 -1
View File
@@ -6,6 +6,7 @@ import 'package:intl/intl.dart';
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/foundation.dart' show kIsWeb;
import '../../core/auth/auth_state.dart'; import '../../core/auth/auth_state.dart';
import '../../widgets/write_guard.dart';
import '../../core/config/app_config.dart'; import '../../core/config/app_config.dart';
import '../../core/responsive/responsive.dart'; import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart'; import '../../core/theme/app_theme.dart';
@@ -210,7 +211,7 @@ class _AppShellState extends ConsumerState<AppShell> {
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
_ShopButton(user: user, version: appVersion), _ShopButton(user: user, version: appVersion),
if (ref.watch(isReadonlyProvider)) ...[ if (WriteGuard.isReadonly(ref)) ...[
const SizedBox(width: 8), const SizedBox(width: 8),
Container( Container(
padding: padding:
@@ -22,7 +22,7 @@ import '../../providers/product_provider.dart' show productRepositoryProvider;
import '../../repositories/product_repository.dart'; import '../../repositories/product_repository.dart';
import '../../providers/finance_provider.dart' show financeRepositoryProvider; import '../../providers/finance_provider.dart' show financeRepositoryProvider;
import '../../providers/shop_provider.dart' show shopInfoProvider; import '../../providers/shop_provider.dart' show shopInfoProvider;
import '../../core/auth/auth_state.dart'; import '../../widgets/write_guard.dart';
class StockInListScreen extends ConsumerStatefulWidget { class StockInListScreen extends ConsumerStatefulWidget {
const StockInListScreen({super.key}); const StockInListScreen({super.key});
@@ -281,7 +281,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
ref.read(stockInListProvider.notifier).setPageSize(s), ref.read(stockInListProvider.notifier).setPageSize(s),
toolbar: Row( toolbar: Row(
children: [ children: [
if (showNewButton && !ref.watch(isReadonlyProvider)) if (showNewButton && !WriteGuard.isReadonly(ref))
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () => context.go('/stock-in/new'), onPressed: () => context.go('/stock-in/new'),
icon: const Icon(Icons.add, size: 16), icon: const Icon(Icons.add, size: 16),
@@ -345,7 +345,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
/// 操作按钮列表,表格与移动端卡片共用。 /// 操作按钮列表,表格与移动端卡片共用。
List<Widget> _orderActions(BuildContext context, StockInOrder o) { List<Widget> _orderActions(BuildContext context, StockInOrder o) {
final readonly = ref.watch(isReadonlyProvider); final readonly = WriteGuard.isReadonly(ref);
return [ return [
TextButton( TextButton(
onPressed: () => _showDetail(context, o.id), onPressed: () => _showDetail(context, o.id),
@@ -20,7 +20,7 @@ import '../../providers/inventory_provider.dart';
import '../../providers/tab_state_provider.dart'; import '../../providers/tab_state_provider.dart';
import '../../providers/product_provider.dart'; import '../../providers/product_provider.dart';
import '../../providers/finance_provider.dart' show financeRepositoryProvider; import '../../providers/finance_provider.dart' show financeRepositoryProvider;
import '../../core/auth/auth_state.dart'; import '../../widgets/write_guard.dart';
class StockOutListScreen extends ConsumerStatefulWidget { class StockOutListScreen extends ConsumerStatefulWidget {
const StockOutListScreen({super.key}); const StockOutListScreen({super.key});
@@ -287,7 +287,7 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
ref.read(stockOutListProvider.notifier).setPageSize(s), ref.read(stockOutListProvider.notifier).setPageSize(s),
toolbar: Row( toolbar: Row(
children: [ children: [
if (showNewButton && !ref.watch(isReadonlyProvider)) if (showNewButton && !WriteGuard.isReadonly(ref))
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () => context.go('/stock-out/new'), onPressed: () => context.go('/stock-out/new'),
icon: const Icon(Icons.add, size: 16), icon: const Icon(Icons.add, size: 16),
@@ -351,7 +351,7 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
/// 操作按钮列表,表格与移动端卡片共用。 /// 操作按钮列表,表格与移动端卡片共用。
List<Widget> _orderActions(BuildContext context, StockOutOrder o) { List<Widget> _orderActions(BuildContext context, StockOutOrder o) {
final readonly = ref.watch(isReadonlyProvider); final readonly = WriteGuard.isReadonly(ref);
return [ return [
TextButton( TextButton(
onPressed: () => _showDetail(context, o.id), onPressed: () => _showDetail(context, o.id),