6238b86dcb
- 登录/注册(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
590 lines
21 KiB
Dart
590 lines
21 KiB
Dart
// widgets/ds/ds_atoms.dart
|
||
// Flutter 组件库(ds=design-system)——一对一镜像原型 atoms.css 的原子。
|
||
// 单一组件源:尺寸/间距/圆角/字号全引 AppDims,颜色全引 context.tokens(禁硬编码)。
|
||
// 屏只组合这些组件,不再自己堆样式(见代码端真相源闸 check_ds_code)。
|
||
import 'package:flutter/material.dart';
|
||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||
import '../../core/theme/context_tokens.dart';
|
||
import '../../core/theme/app_dims.g.dart';
|
||
import 'ds_menu.dart';
|
||
|
||
// ── .btn ──────────────────────────────────────────────────────────────────
|
||
enum DsBtnVariant { ghost, primary, danger, success, accent }
|
||
|
||
/// 原型 .btn:h38 / pad 0 16 / r-md / fs-body / fw600 / gap7,图标 16。
|
||
/// ghost=surface+border / primary=primary+on-primary / danger=danger+白。
|
||
class DsButton extends StatelessWidget {
|
||
final String label;
|
||
final IconData? icon;
|
||
final VoidCallback? onPressed;
|
||
final DsBtnVariant variant;
|
||
final bool small; // .btn.sm:h32 / pad 0 12 / fs-sm
|
||
final bool large; // .btn.lg:h44 / pad 0 22 / fs14(登录/注册主按钮)
|
||
|
||
const DsButton(
|
||
this.label, {
|
||
super.key,
|
||
this.icon,
|
||
this.onPressed,
|
||
this.variant = DsBtnVariant.ghost,
|
||
this.small = false,
|
||
this.large = false,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
final (bg, fg, border) = switch (variant) {
|
||
DsBtnVariant.ghost => (t.surface, t.text, t.border),
|
||
DsBtnVariant.primary => (t.primary, t.onPrimary, t.primary),
|
||
DsBtnVariant.danger => (t.danger, t.onPrimary, t.danger),
|
||
// 语义确认键(审核通过/撤回等),色取 token
|
||
DsBtnVariant.success => (t.success, t.onPrimary, t.success),
|
||
DsBtnVariant.accent => (t.accent, t.onPrimary, t.accent),
|
||
};
|
||
// disabled(onPressed=null)置灰 55%:批量替换 Material 按钮后不能丢禁用反馈
|
||
return Opacity(
|
||
opacity: onPressed == null ? 0.55 : 1,
|
||
child: Material(
|
||
color: bg,
|
||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||
child: InkWell(
|
||
onTap: onPressed,
|
||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||
child: Container(
|
||
height: small ? 32 : (large ? 44 : 38),
|
||
padding: EdgeInsets.symmetric(
|
||
horizontal: small ? 12 : (large ? 22 : 16)),
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||
border: Border.all(color: border),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
if (icon != null) ...[
|
||
Icon(icon, size: 16, color: fg),
|
||
const SizedBox(width: 7),
|
||
],
|
||
// Flexible+ellipsis:宿主给紧约束(如 dev-card 底部 flex:1)时不溢出
|
||
Flexible(
|
||
child: Text(label,
|
||
maxLines: 1,
|
||
softWrap: false,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(
|
||
color: fg,
|
||
fontSize: small
|
||
? AppDims.fsSm
|
||
: (large ? 14 : AppDims.fsBody),
|
||
fontWeight: FontWeight.w600)),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
));
|
||
}
|
||
}
|
||
|
||
// ── .badge ────────────────────────────────────────────────────────────────
|
||
enum DsBadgeTone { ok, danger, warn, info, accent, muted }
|
||
|
||
/// 原型 .badge:h22 / pad 0 9 / r-pill / fs-sm fw600 + 前导圆点(6px, currentColor)。
|
||
class DsBadge extends StatelessWidget {
|
||
final String label;
|
||
final DsBadgeTone tone;
|
||
const DsBadge(this.label, {super.key, this.tone = DsBadgeTone.ok});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
final (bg, fg) = switch (tone) {
|
||
DsBadgeTone.ok => (t.successBg, t.success),
|
||
DsBadgeTone.danger => (t.dangerBg, t.danger),
|
||
DsBadgeTone.warn => (t.warnBg, t.warn),
|
||
DsBadgeTone.info => (t.infoSoft, t.primary),
|
||
DsBadgeTone.accent => (t.accentSoft, t.accent),
|
||
DsBadgeTone.muted => (t.borderSubtle, t.muted),
|
||
};
|
||
return Container(
|
||
height: 22,
|
||
padding: const EdgeInsets.symmetric(horizontal: 9),
|
||
decoration: BoxDecoration(
|
||
color: bg, borderRadius: BorderRadius.circular(AppDims.rPill)),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Container(
|
||
width: 6,
|
||
height: 6,
|
||
decoration: BoxDecoration(color: fg, shape: BoxShape.circle)),
|
||
const SizedBox(width: 5),
|
||
Text(label,
|
||
style: TextStyle(
|
||
color: fg,
|
||
fontSize: AppDims.fsSm,
|
||
fontWeight: FontWeight.w600)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── .chip ─────────────────────────────────────────────────────────────────
|
||
/// 原型 .chip:h34 / pad 0 12 / border r-md / surface / fs-sm / gap7。
|
||
/// 选中值用 .cv(primary fw600);激活(on)边框 primary。配 PopupMenuButton 用。
|
||
class DsChip extends StatelessWidget {
|
||
final String label;
|
||
final String? value; // 选中值(.cv);非空即 on 态
|
||
final VoidCallback? onTap;
|
||
final VoidCallback? onClear; // 选中态下点 × 单独清除该筛选(不触发展开菜单)
|
||
// 纯切换 chip(原型 .chip.on,如 partners 类型筛选):selected 直接给 on 态,
|
||
// caret=false 去掉下拉箭头。
|
||
final bool selected;
|
||
final bool caret;
|
||
const DsChip(
|
||
{super.key,
|
||
required this.label,
|
||
this.value,
|
||
this.onTap,
|
||
this.onClear,
|
||
this.selected = false,
|
||
this.caret = true});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
final on = selected || (value != null && value!.isNotEmpty);
|
||
return InkWell(
|
||
onTap: onTap,
|
||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||
child: Container(
|
||
height: 34,
|
||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||
decoration: BoxDecoration(
|
||
color: t.surface,
|
||
border: Border.all(color: on ? t.primary : t.border),
|
||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(label,
|
||
style: TextStyle(fontSize: AppDims.fsSm, color: t.text)),
|
||
if (value != null && value!.isNotEmpty) ...[
|
||
const SizedBox(width: 7),
|
||
Text(value!,
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsSm,
|
||
color: t.primary,
|
||
fontWeight: FontWeight.w600)),
|
||
],
|
||
// 选中且提供 onClear → 展示可点 ×(清该筛选);否则展示下拉箭头;
|
||
// 纯切换 chip(caret=false)无尾部图标。
|
||
if (on && onClear != null) ...[
|
||
const SizedBox(width: 7),
|
||
GestureDetector(
|
||
behavior: HitTestBehavior.opaque,
|
||
onTap: onClear,
|
||
child: Icon(LucideIcons.x, size: 14, color: t.muted),
|
||
),
|
||
] else if (caret) ...[
|
||
const SizedBox(width: 7),
|
||
Icon(LucideIcons.chevronDown, size: 14, color: t.faint),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── .seg ─────────────────────────────────────────────────────────────────
|
||
/// 原型 .seg:分段 tab(inline-flex / bg / border r-md / pad 3 gap 3)。
|
||
/// button h30 / pad 0 16 / fs-sm fw600 muted;选中=surface 底 + primary 字 + sh-1。
|
||
class DsSeg extends StatelessWidget {
|
||
final List<String> items;
|
||
final int index;
|
||
final ValueChanged<int> onChanged;
|
||
const DsSeg(
|
||
{super.key,
|
||
required this.items,
|
||
required this.index,
|
||
required this.onChanged});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
return Container(
|
||
padding: const EdgeInsets.all(3),
|
||
decoration: BoxDecoration(
|
||
color: t.bg,
|
||
border: Border.all(color: t.border),
|
||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
for (var i = 0; i < items.length; i++) ...[
|
||
if (i > 0) const SizedBox(width: 3),
|
||
InkWell(
|
||
onTap: () => onChanged(i),
|
||
borderRadius: BorderRadius.circular(AppDims.rSm),
|
||
child: Container(
|
||
height: 30,
|
||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||
alignment: Alignment.center,
|
||
decoration: BoxDecoration(
|
||
color: i == index ? t.surface : null,
|
||
borderRadius: BorderRadius.circular(AppDims.rSm),
|
||
// 原型 --sh-1: 0 1px 3px var(--shadow)
|
||
boxShadow: i == index
|
||
? [
|
||
BoxShadow(
|
||
color: t.shadow,
|
||
blurRadius: 3,
|
||
offset: const Offset(0, 1)),
|
||
]
|
||
: null,
|
||
),
|
||
child: Text(items[i],
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsSm,
|
||
fontWeight: FontWeight.w600,
|
||
color: i == index ? t.primary : t.muted)),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ── .searchbox ──────────────────────────────────────────────────────────────
|
||
/// 原型 .searchbox:h34 / bg / border r-md / gap8 / pad 0 11,前导放大镜 14(faint)。
|
||
/// 原型 .field:label(fs-sm muted)在控件上方、间距 6;required 缀红星。
|
||
/// 对话框/面板表单统一用它排字段,禁止 Material labelText 浮动标签。
|
||
class DsField extends StatelessWidget {
|
||
final String label;
|
||
final bool required;
|
||
final Widget child;
|
||
const DsField(this.label,
|
||
{super.key, this.required = false, required Widget input})
|
||
: child = input;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(mainAxisSize: MainAxisSize.min, children: [
|
||
Text(label, style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||
if (required)
|
||
Text(' *',
|
||
style: TextStyle(fontSize: AppDims.fsSm, color: t.danger)),
|
||
]),
|
||
const SizedBox(height: 6),
|
||
child,
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 原型 .check / .agree(login.html / register.html):18×18 盒 + 1.5px 边 +
|
||
/// r-sm;选中 → primary 底/边 + 白勾(13, stroke2.4);文案 fs-body,间距 8。
|
||
/// [label] 用 Widget 以承载富文本(如注册页协议链接);[alignTop] 用于多行文案
|
||
/// (register .agree:align-items:flex-start + 盒 margin-top 1)。
|
||
class DsCheck extends StatelessWidget {
|
||
final bool value;
|
||
final ValueChanged<bool> onChanged;
|
||
final Widget label;
|
||
final bool alignTop;
|
||
const DsCheck({
|
||
super.key,
|
||
required this.value,
|
||
required this.onChanged,
|
||
required this.label,
|
||
this.alignTop = false,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
return MouseRegion(
|
||
cursor: SystemMouseCursors.click,
|
||
child: GestureDetector(
|
||
behavior: HitTestBehavior.opaque,
|
||
onTap: () => onChanged(!value),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment:
|
||
alignTop ? CrossAxisAlignment.start : CrossAxisAlignment.center,
|
||
children: [
|
||
Container(
|
||
width: 18,
|
||
height: 18,
|
||
margin:
|
||
alignTop ? const EdgeInsets.only(top: 1) : EdgeInsets.zero,
|
||
alignment: Alignment.center,
|
||
decoration: BoxDecoration(
|
||
color: value ? t.primary : t.surface,
|
||
border:
|
||
Border.all(color: value ? t.primary : t.border, width: 1.5),
|
||
borderRadius: BorderRadius.circular(AppDims.rSm),
|
||
),
|
||
child: value
|
||
? Icon(LucideIcons.check, size: 13, color: t.onPrimary)
|
||
: null,
|
||
),
|
||
SizedBox(width: alignTop ? 9 : 8),
|
||
Flexible(child: label),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 原型 .input:h38 / surface / 1px border / r-md / pad 0 11 / fs-body;
|
||
/// focus → primary 边;readonly/disabled → bg 底 + muted 字。
|
||
class DsInput extends StatelessWidget {
|
||
final TextEditingController? controller;
|
||
final bool enabled;
|
||
final String? hintText;
|
||
final ValueChanged<String>? onChanged;
|
||
final ValueChanged<String>? onSubmitted;
|
||
final TextInputType? keyboardType;
|
||
final FocusNode? focusNode;
|
||
final bool obscureText; // 密码框
|
||
final Widget? suffix; // 行内后缀(历史下拉箭头 / 密码可见切换)
|
||
const DsInput({
|
||
super.key,
|
||
this.controller,
|
||
this.enabled = true,
|
||
this.hintText,
|
||
this.onChanged,
|
||
this.onSubmitted,
|
||
this.keyboardType,
|
||
this.focusNode,
|
||
this.obscureText = false,
|
||
this.suffix,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
OutlineInputBorder line(Color c) => OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||
borderSide: BorderSide(color: c),
|
||
);
|
||
return SizedBox(
|
||
height: 38,
|
||
child: TextField(
|
||
controller: controller,
|
||
enabled: enabled,
|
||
onChanged: onChanged,
|
||
onSubmitted: onSubmitted,
|
||
keyboardType: keyboardType,
|
||
focusNode: focusNode,
|
||
obscureText: obscureText,
|
||
// letterSpacing 0:M3 默认字距比原型(无字距)宽,数字/拉丁明显
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsBody,
|
||
letterSpacing: 0,
|
||
color: enabled ? t.text : t.muted),
|
||
decoration: InputDecoration(
|
||
isDense: true,
|
||
// 装饰盒锁死 .input 的 38 高(不随 visualDensity 漂移)
|
||
constraints: const BoxConstraints.tightFor(height: 38),
|
||
filled: true,
|
||
fillColor: enabled ? t.surface : t.bg,
|
||
hintText: hintText,
|
||
hintStyle: TextStyle(fontSize: AppDims.fsBody, color: t.faint),
|
||
contentPadding:
|
||
const EdgeInsets.symmetric(horizontal: 11, vertical: 9),
|
||
suffixIcon: suffix == null
|
||
? null
|
||
: Padding(
|
||
padding: const EdgeInsets.only(right: 9), child: suffix),
|
||
suffixIconConstraints:
|
||
const BoxConstraints(minWidth: 0, minHeight: 0),
|
||
enabledBorder: line(t.border),
|
||
disabledBorder: line(t.border),
|
||
focusedBorder: line(t.primary),
|
||
border: line(t.border),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 原型 select:.input 盒式外观(h38/surface/border/r-md/pad 0 11 + 右侧
|
||
/// chevron),点击弹 DsMenu(.menu 弹层,含 ✓ 选中态、锚定坐标已对齐)。
|
||
/// 统一替代 Material DropdownButton——其默认灰底方角弹层不属 ds 体系。
|
||
class DsSelect<T> extends StatelessWidget {
|
||
final T? value;
|
||
final List<(T, String)> options;
|
||
final ValueChanged<T>? onChanged;
|
||
final String hint;
|
||
const DsSelect({
|
||
super.key,
|
||
required this.value,
|
||
required this.options,
|
||
required this.onChanged,
|
||
this.hint = '请选择…',
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
final label =
|
||
options.where((o) => o.$1 == value).map((o) => o.$2).firstOrNull;
|
||
final enabled = onChanged != null;
|
||
return Builder(
|
||
builder: (bctx) => InkWell(
|
||
onTap: enabled
|
||
? () async {
|
||
final picked = await showDsMenu<T>(bctx, items: [
|
||
for (final o in options)
|
||
DsMenuItem(
|
||
value: o.$1, label: o.$2, selected: o.$1 == value),
|
||
]);
|
||
if (picked != null) onChanged!(picked);
|
||
}
|
||
: null,
|
||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||
child: Container(
|
||
height: 38,
|
||
padding: const EdgeInsets.symmetric(horizontal: 11),
|
||
decoration: BoxDecoration(
|
||
color: enabled ? t.surface : t.bg,
|
||
border: Border.all(color: t.border),
|
||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||
),
|
||
child: Row(children: [
|
||
Expanded(
|
||
child: Text(label ?? hint,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(
|
||
fontSize: AppDims.fsBody,
|
||
color: label == null
|
||
? t.faint
|
||
: (enabled ? t.text : t.muted))),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Icon(LucideIcons.chevronDown, size: 14, color: t.muted),
|
||
]),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class DsSearchBox extends StatelessWidget {
|
||
final TextEditingController? controller;
|
||
final String hint;
|
||
final ValueChanged<String>? onChanged;
|
||
final ValueChanged<String>? onSubmitted;
|
||
final double? width;
|
||
const DsSearchBox({
|
||
super.key,
|
||
this.controller,
|
||
this.hint = '',
|
||
this.onChanged,
|
||
this.onSubmitted,
|
||
this.width,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
return SizedBox(
|
||
width: width,
|
||
height: 34,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 11),
|
||
decoration: BoxDecoration(
|
||
color: t.bg,
|
||
border: Border.all(color: t.border),
|
||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Icon(LucideIcons.search, size: 14, color: t.faint),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: TextField(
|
||
controller: controller,
|
||
onChanged: onChanged,
|
||
onSubmitted: onSubmitted,
|
||
style: TextStyle(fontSize: AppDims.fsBody, color: t.text),
|
||
decoration: InputDecoration(
|
||
isCollapsed: true,
|
||
contentPadding: EdgeInsets.zero,
|
||
border: InputBorder.none,
|
||
filled: false,
|
||
enabledBorder: InputBorder.none,
|
||
focusedBorder: InputBorder.none,
|
||
hintText: hint,
|
||
hintStyle:
|
||
TextStyle(fontSize: AppDims.fsBody, color: t.faint),
|
||
),
|
||
),
|
||
),
|
||
// 有输入时显示 × 清除(清空并触发一次空关键词搜索)。
|
||
if (controller != null)
|
||
ValueListenableBuilder<TextEditingValue>(
|
||
valueListenable: controller!,
|
||
builder: (context, value, _) {
|
||
if (value.text.isEmpty) return const SizedBox.shrink();
|
||
return GestureDetector(
|
||
behavior: HitTestBehavior.opaque,
|
||
onTap: () {
|
||
controller!.clear();
|
||
onChanged?.call('');
|
||
onSubmitted?.call('');
|
||
},
|
||
child: Padding(
|
||
padding: const EdgeInsets.only(left: 6),
|
||
child: Icon(LucideIcons.x, size: 14, color: t.muted),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 加载遮罩(reload 时叠在旧内容之上):轻微半透明底 + 屏幕中心进度圈。
|
||
/// 搜索/筛选刷新时用它替代整屏白屏(见列表屏 skipLoadingOnReload)。
|
||
class DsLoadingScrim extends StatelessWidget {
|
||
const DsLoadingScrim({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final t = context.tokens;
|
||
return GestureDetector(
|
||
behavior: HitTestBehavior.opaque, // 加载期间拦截点击,避免误触
|
||
onTap: () {},
|
||
child: ColoredBox(
|
||
color: t.bg.withValues(alpha: 0.55),
|
||
child: Center(
|
||
child: SizedBox(
|
||
width: 34,
|
||
height: 34,
|
||
child: CircularProgressIndicator(strokeWidth: 3, color: t.primary),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|