Files
wangjia 6238b86dcb feat(client): 登录/注册页照原型重建,ds 真相源组件族统一全部屏
- 登录/注册(login.html/register.html 1:1):两栏卡片+品牌渐变面板+主题小衣服
  pill(onSurface 变体)+记住我(记录并预填最近账号)+原型式 toast 校验;
  登录 fidelity 1.4–2.1% 三主题全绿;注册暂不入闸(少 门店编号/兑换券 字段,
  已记 CONTRACT,screens.mjs 留存根)
- ds 原子补齐:DsToast(.toast 单例)/DsCheck(.check/.agree)/DsButton lg 档/
  DsSelect 替换全部旧 DropdownButton/DsField label 在上/DsInput 后缀与密码形态
- 全屏统一:对话框按钮全 DsButton、盒式输入主题钉死(visualDensity.standard、
  h38、InputDecorationTheme 渗漏修复)、图标全 lucide、JetBrains Mono 三端同源、
  BrandMark 真相源 logo、只读模式写操作全量守卫(WriteGuard+DsToast)
- 出入库列表:版式对齐原型(卡片对齐+搜索框居中)、KPI 近30天滚动、结清后
  失效财务应收应付表;商品编辑抽屉介绍库改搜索下拉、图片双击全屏预览
- 删除旧 UI 死代码:DataTableCard/FormDialog/PageScaffold/SearchChip/
  SelectProductDialog/tabStateProvider
- golden/fidelity:stock-in/out 补注册(9%)、login 入册(8%)、goldens 全量重打;
  修复 pubspec flutter_web_plugins 非法声明(CI pub get 阻断)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-03 09:58:14 +08:00

91 lines
3.3 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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));
},
),
),
],
);
}
}