import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/auth/auth_state.dart'; import '../core/config/license_copy.dart'; import '../providers/license_provider.dart'; import 'ds/ds_toast.dart'; /// 写操作守卫:按「角色只读」和「授权过期」两种受限场景**区分处理**被包裹的写控件 /// (新增/编辑/删除/审核/提交/结清/导入…): /// /// - **角色只读**(role == 'readonly')→ 隐藏(不显示)。 /// - **授权过期 >7 天**(phase == 'readonly' | 'locked')→ 显示但置灰禁用, /// 点击弹提示说明原因并引导去激活。 /// - **其它**(normal / grace 宽限期)→ 原样显示且可点(宽限期后端仍允许写)。 /// /// 统一入口,避免在各页面散落 `if (!ref.watch(isReadonlyProvider))` 判断。 /// 后端 `middleware.ReadOnly()` / `LicenseGuard()` 仍会对写请求兜底返回 403, /// 本控件只负责「不误导」:只读不显示、过期显示但不可点。 /// /// 用法: /// ```dart /// WriteGuard(child: ElevatedButton(onPressed: _add, child: const Text('新建'))) /// ``` /// 列表 children 里可用 collection-if 配合 [isReadonly] 直接剔除分隔符; /// 过期置灰仍由内层 [WriteGuard] 负责: /// ```dart /// if (!WriteGuard.isReadonly(ref)) ...[WriteGuard(child: button), const SizedBox(width: 8)] /// ``` class WriteGuard extends ConsumerWidget { final Widget child; /// 只读角色时显示的占位控件,默认完全隐藏(不占位)。 final Widget placeholder; const WriteGuard({ super.key, required this.child, this.placeholder = const SizedBox.shrink(), }); /// 当前是否只读角色(写按钮应**隐藏**)。供 collection-if / 复合条件直接调用。 static bool isReadonly(WidgetRef ref) => ref.watch(isReadonlyProvider); /// 当前是否因授权过期 >7 天而应**禁用**写操作(phase readonly/locked)。 static bool licenseBlocked(WidgetRef ref) { final lic = ref.watch(licenseProvider).valueOrNull; return lic != null && lic.isReadOnlyPhase; } @override Widget build(BuildContext context, WidgetRef ref) { // 1) 角色只读:隐藏 if (ref.watch(isReadonlyProvider)) return placeholder; // 2) 授权过期 >7 天:显示但置灰禁用 + 点击弹提示 final lic = ref.watch(licenseProvider).valueOrNull; if (lic != null && lic.isReadOnlyPhase) { return _DisabledByLicense(lic: lic, child: child); } // 3) normal / grace:原样可点 return child; } } /// 授权过期态的写控件包装:视觉置灰 + 拦截点击并提示。 class _DisabledByLicense extends StatelessWidget { final LicenseInfo lic; final Widget child; const _DisabledByLicense({required this.lic, required this.child}); @override Widget build(BuildContext context) { return Stack( children: [ // 置灰且不响应子控件自身的点击 IgnorePointer(child: Opacity(opacity: 0.45, child: child)), // 覆盖一层透明手势层:捕获点击弹出原因说明 Positioned.fill( child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { showDsToast(context, LicenseCopy.writeBlockedToast(lic)); }, ), ), ], ); } }