feat(client): 设置/用户/设备/关于窄屏对齐移动原型(hub 形态/卡片流/sheet/无打印)
- 系统设置窄屏改 hub 三组(门店/偏好/数据):门店信息/编号规则/默认仓库走 底部 sheet,主题外观接 showThemeSheet,?tab= 深链窄屏忽略;桌面 subnav 不动 - 用户管理窄屏:m-section + 头像卡片流 + 角色图标徽章 + FAB 新增; 点卡开详情 sheet(drow + 重置密码/启停/编辑),表单复用为 sheet 形态 - 设备管理窄屏:会话卡流(本机标注/在线图标徽章)+ 外设卡流(mc-foot 脚注), 「+ 添加设备」在区块标题行右侧(m-pill 选择 sheet),打印模板窄屏不渲染 - 关于我们窄屏:品牌 hero + hub 链接组 + 产品信息 drow 卡 + 时间线 pill 标签 + 意见反馈 sheet(类型 pill/描述/联系方式);授权信息不显示(已迁 /me/license) - golden:四屏窄屏基准独立成 *_mobile_golden_test.dart(390×844 @2x 三主题), 桌面 golden 零漂移 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
// head + KPI 4 卡(无图标)+ toolbar(搜索 + 角色筛选)+ 表格(头像/角色徽章/
|
||||
// 启停)+ 新增/编辑弹窗(角色 2×2 单选卡 + 状态开关 + 重置密码)。
|
||||
// 角色口径按后端四级:superadmin/admin/operator/readonly(原型三处不一致已拉平)。
|
||||
// 窄屏(原型 m-users.html):隐藏页头/KPI/toolbar,m-section + 头像卡片流 +
|
||||
// FAB 新增;点卡开详情 sheet(drow + 重置密码/启停/编辑),表单走 sheet 形态。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
@@ -18,7 +20,8 @@ import '../../widgets/ds/ds_atoms.dart';
|
||||
import '../../widgets/ds/ds_kpi.dart';
|
||||
import '../../widgets/ds/ds_menu.dart';
|
||||
import '../../widgets/ds/ds_table.dart';
|
||||
import '../../widgets/mobile_list_card.dart';
|
||||
import '../../widgets/ds/m_sheet.dart';
|
||||
import '../../widgets/ds/status_icon_map.dart';
|
||||
import '../../widgets/write_guard.dart';
|
||||
import '../../core/theme/app_fonts.dart';
|
||||
import '../../widgets/ds/ds_toast.dart';
|
||||
@@ -108,6 +111,8 @@ class _UsersScreenState extends ConsumerState<UsersScreen> {
|
||||
),
|
||||
),
|
||||
data: (users) {
|
||||
// ── 窄屏(原型 m-users.html):m-section + 头像卡片流 + FAB 新增 ──
|
||||
if (mobile) return _mobileBody(t, users);
|
||||
final kw = _searchCtrl.text.trim().toLowerCase();
|
||||
final filtered = users.where((u) {
|
||||
if (_roleFilter != '全部角色' && u.roleLabel != _roleFilter) {
|
||||
@@ -171,7 +176,6 @@ class _UsersScreenState extends ConsumerState<UsersScreen> {
|
||||
pagerInfoText: '共 ${filtered.length} 名成员',
|
||||
emptyText: '没有匹配的成员',
|
||||
toolbar: _toolbar(mobile),
|
||||
mobileCards: filtered.map(_userCard).toList(),
|
||||
columns: const [
|
||||
DsColumn('user', '用户'),
|
||||
DsColumn('role', '角色'),
|
||||
@@ -350,19 +354,260 @@ class _UsersScreenState extends ConsumerState<UsersScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _userCard(AppUser u) {
|
||||
return MobileListCard(
|
||||
title: Text(u.realName?.isNotEmpty == true ? u.realName! : u.username),
|
||||
subtitle: Text(u.username),
|
||||
trailing: userRoleBadge(u.role),
|
||||
fields: [
|
||||
MobileCardField('状态', u.isActive ? '启用' : '停用'),
|
||||
MobileCardField('最近登录', _fmtLast(u.lastLoginAt)),
|
||||
],
|
||||
onTap: () => _openUser(u),
|
||||
// ── 窄屏(原型 m-users.html)────────────────────────────────
|
||||
Widget _mobileBody(dynamic t, List<AppUser> users) {
|
||||
return Scaffold(
|
||||
backgroundColor: t.bg,
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(14),
|
||||
children: [
|
||||
// .m-section
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(2, 2, 2, 8),
|
||||
child: Text('成员 · 共 ${users.length}',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsSm,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: .4,
|
||||
color: t.muted)),
|
||||
),
|
||||
if (users.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
child: Text('暂无成员',
|
||||
style: TextStyle(fontSize: AppDims.fsBody, color: t.muted)),
|
||||
),
|
||||
for (final u in users) _mUserCard(t, u),
|
||||
],
|
||||
),
|
||||
// 原型 .m-fab:52px primary 圆形悬浮新增(不走 M3 FAB 主题壳)
|
||||
floatingActionButton: WriteGuard(
|
||||
child: Material(
|
||||
color: t.primary,
|
||||
shape: const CircleBorder(),
|
||||
child: InkWell(
|
||||
onTap: () => _openUser(),
|
||||
customBorder: const CircleBorder(),
|
||||
child: SizedBox(
|
||||
width: 52,
|
||||
height: 52,
|
||||
child: Icon(LucideIcons.plus, size: 24, color: t.onPrimary),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 角色徽章(图标变体,原型 badge.bi:管理员=shield / 普通=user / 只读=eye)。
|
||||
DsBadge _roleBadgeIc(String role) {
|
||||
// 图标词表按原型三级(icons.js BADGE_ICON 登记词),四级角色归并映射
|
||||
final protoWord = switch (role) {
|
||||
'superadmin' || 'admin' => '管理员',
|
||||
'readonly' => '只读',
|
||||
_ => '普通',
|
||||
};
|
||||
final tone = switch (role) {
|
||||
'superadmin' => DsBadgeTone.danger,
|
||||
'admin' => DsBadgeTone.info,
|
||||
'operator' => DsBadgeTone.ok,
|
||||
'readonly' => DsBadgeTone.warn,
|
||||
_ => DsBadgeTone.muted,
|
||||
};
|
||||
final label = switch (role) {
|
||||
'superadmin' => '超级管理员',
|
||||
'admin' => '管理员',
|
||||
'operator' => '操作员',
|
||||
'readonly' => '只读',
|
||||
_ => role,
|
||||
};
|
||||
return DsBadge(label, tone: tone, icon: statusIcon(protoWord));
|
||||
}
|
||||
|
||||
/// 原型 .m-card:40 圆头像(.uav) + 姓名 + 账号(mono) + 角色徽章。
|
||||
Widget _mUserCard(dynamic t, AppUser u) {
|
||||
final name = u.realName?.isNotEmpty == true ? u.realName! : u.username;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Material(
|
||||
color: t.surface,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(color: t.border),
|
||||
borderRadius: BorderRadius.circular(AppDims.rLg),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () => _openUserSheet(u),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 13, 14, 13),
|
||||
child: Row(children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignment: Alignment.center,
|
||||
decoration:
|
||||
BoxDecoration(color: t.brand50, shape: BoxShape.circle),
|
||||
child: Text(name.characters.first,
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsBody,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: t.primary)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsBody,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: t.heading)),
|
||||
const SizedBox(height: 3),
|
||||
Text(u.username,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsXs,
|
||||
color: t.muted,
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_roleBadgeIc(u.role),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 成员详情 sheet(原型 openU:drow 键值 + 重置密码/启停/编辑)。
|
||||
void _openUserSheet(AppUser u) {
|
||||
final name = u.realName?.isNotEmpty == true ? u.realName! : u.username;
|
||||
showMSheet<void>(
|
||||
context,
|
||||
title: name,
|
||||
builder: (ctx) {
|
||||
final t = ctx.tokens;
|
||||
Widget drow(String label, Widget value, {bool last = false}) =>
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 11),
|
||||
decoration: BoxDecoration(
|
||||
border: last
|
||||
? null
|
||||
: Border(bottom: BorderSide(color: t.borderSubtle)),
|
||||
),
|
||||
child: Row(children: [
|
||||
Text(label,
|
||||
style: TextStyle(fontSize: AppDims.fsBody, color: t.muted)),
|
||||
const Spacer(),
|
||||
value,
|
||||
]),
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
drow(
|
||||
'账号',
|
||||
Text(u.username,
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsBody,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback,
|
||||
color: t.text))),
|
||||
drow('角色', _roleBadgeIc(u.role)),
|
||||
drow(
|
||||
'状态',
|
||||
DsBadge(u.isActive ? '启用' : '停用',
|
||||
tone: u.isActive ? DsBadgeTone.ok : DsBadgeTone.muted,
|
||||
icon: statusIcon(u.isActive ? '启用' : '停用'))),
|
||||
drow(
|
||||
'最近登录',
|
||||
Text(_fmtLast(u.lastLoginAt),
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsBody,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: t.text)),
|
||||
last: true),
|
||||
const SizedBox(height: 16),
|
||||
WriteGuard(
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: DsButton('重置密码', onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
_promptResetPassword(u);
|
||||
}),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DsButton(u.isActive ? '停用' : '启用', onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
_toggleActive(u);
|
||||
}),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DsButton('编辑', variant: DsBtnVariant.primary,
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
_openUser(u);
|
||||
}),
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 重置密码(详情 sheet 入口;提示框与表单内「重置密码」同口径)。
|
||||
Future<void> _promptResetPassword(AppUser u) async {
|
||||
final ctrl = TextEditingController();
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text('重置密码 · ${u.username}'),
|
||||
content: SizedBox(
|
||||
width: ctx.dialogWidth(320),
|
||||
child: DsField('新密码',
|
||||
input: TextField(
|
||||
controller: ctrl,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(hintText: '至少 8 位'),
|
||||
)),
|
||||
),
|
||||
actions: [
|
||||
DsButton('取消', onPressed: () => Navigator.of(ctx).pop(false)),
|
||||
DsButton('重置',
|
||||
variant: DsBtnVariant.primary,
|
||||
onPressed: () => Navigator.of(ctx).pop(true)),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok != true || !mounted) return;
|
||||
if (ctrl.text.length < 8) {
|
||||
showDsToast(context, '密码至少 8 位');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ref.read(userListProvider.notifier).resetPassword(u.id, ctrl.text);
|
||||
if (mounted) {
|
||||
showDsToast(context, '密码已重置 ✓', bg: context.tokens.success);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showDsToast(context, '重置失败:$e', bg: context.tokens.danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toggleActive(AppUser u) async {
|
||||
try {
|
||||
await ref
|
||||
@@ -376,6 +621,19 @@ class _UsersScreenState extends ConsumerState<UsersScreen> {
|
||||
}
|
||||
|
||||
void _openUser([AppUser? user]) {
|
||||
// 窄屏:表单走底部 sheet(原型 m-users FAB → 表单弹层)
|
||||
if (context.isMobile) {
|
||||
showMSheet<void>(
|
||||
context,
|
||||
title: user == null ? '新增用户' : '编辑用户',
|
||||
builder: (_) => _UserFormDialog(
|
||||
user: user,
|
||||
sheet: true,
|
||||
onSaved: () => ref.read(userListProvider.notifier).reload(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
showAppDialog(
|
||||
context: context,
|
||||
builder: (ctx) => _UserFormDialog(
|
||||
@@ -387,10 +645,12 @@ class _UsersScreenState extends ConsumerState<UsersScreen> {
|
||||
}
|
||||
|
||||
// ── 新增/编辑用户弹窗(原型 #ov:mgrid 2 列 + 角色 2×2 rcard + 状态 switch)──
|
||||
// [sheet]=true 时以 sheet 内容形态渲染(无 Dialog 壳、标题由 showMSheet 提供)。
|
||||
class _UserFormDialog extends ConsumerStatefulWidget {
|
||||
final AppUser? user;
|
||||
final VoidCallback onSaved;
|
||||
const _UserFormDialog({this.user, required this.onSaved});
|
||||
final bool sheet;
|
||||
const _UserFormDialog({this.user, required this.onSaved, this.sheet = false});
|
||||
|
||||
@override
|
||||
ConsumerState<_UserFormDialog> createState() => _UserFormDialogState();
|
||||
@@ -507,118 +767,120 @@ class _UserFormDialogState extends ConsumerState<_UserFormDialog> {
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.tokens;
|
||||
final isEdit = widget.user != null;
|
||||
final form = Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// sheet 形态标题由 showMSheet 头部提供
|
||||
if (!widget.sheet) ...[
|
||||
Text(isEdit ? '编辑用户' : '新增用户',
|
||||
style: const TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 18),
|
||||
],
|
||||
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Expanded(
|
||||
child: DsField('姓名',
|
||||
required: true,
|
||||
input: TextFormField(
|
||||
controller: _nameCtrl,
|
||||
decoration: const InputDecoration(hintText: '例如 李采购'),
|
||||
validator: (v) =>
|
||||
(v == null || v.trim().isEmpty) ? '不能为空' : null,
|
||||
)),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: DsField('登录账号',
|
||||
required: true,
|
||||
input: TextFormField(
|
||||
controller: _userCtrl,
|
||||
decoration: const InputDecoration(hintText: '字母 / 数字'),
|
||||
validator: (v) =>
|
||||
(v == null || v.trim().isEmpty) ? '不能为空' : null,
|
||||
)),
|
||||
),
|
||||
]),
|
||||
if (!isEdit) ...[
|
||||
const SizedBox(height: 14),
|
||||
DsField('初始密码',
|
||||
required: true,
|
||||
input: TextFormField(
|
||||
controller: _passCtrl,
|
||||
obscureText: true,
|
||||
decoration:
|
||||
const InputDecoration(hintText: '至少 8 位,成员首次登录可改'),
|
||||
validator: (v) =>
|
||||
(v == null || v.length < 8) ? '至少 8 位' : null,
|
||||
)),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
Text('角色',
|
||||
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||||
const SizedBox(height: 8),
|
||||
// 原型 .roles 2×2 单选卡
|
||||
Column(children: [
|
||||
for (var row = 0; row < 2; row++)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: row == 0 ? 10 : 0),
|
||||
child: Row(children: [
|
||||
for (var col = 0; col < 2; col++) ...[
|
||||
if (col > 0) const SizedBox(width: 10),
|
||||
Expanded(child: _roleCard(_roles[row * 2 + col])),
|
||||
],
|
||||
]),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 16),
|
||||
Row(children: [
|
||||
Text('账号状态',
|
||||
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||||
const SizedBox(width: 12),
|
||||
Switch(
|
||||
value: _active,
|
||||
onChanged: (v) => setState(() => _active = v),
|
||||
),
|
||||
Text(_active ? '启用' : '停用',
|
||||
style: TextStyle(fontSize: AppDims.fsBody, color: t.text)),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
Row(children: [
|
||||
if (isEdit)
|
||||
TextButton(
|
||||
onPressed: _resetPassword,
|
||||
child: const Text('重置密码'),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: _saving ? null : _save,
|
||||
child: _saving
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white)) // ds-ignore: primary 底白字加载圈
|
||||
: const Text('保存'),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
if (widget.sheet) return form;
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: context.dialogWidth(520),
|
||||
padding: const EdgeInsets.all(22),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(isEdit ? '编辑用户' : '新增用户',
|
||||
style: const TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 18),
|
||||
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Expanded(
|
||||
child: DsField('姓名',
|
||||
required: true,
|
||||
input: TextFormField(
|
||||
controller: _nameCtrl,
|
||||
decoration: const InputDecoration(hintText: '例如 李采购'),
|
||||
validator: (v) =>
|
||||
(v == null || v.trim().isEmpty) ? '不能为空' : null,
|
||||
)),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: DsField('登录账号',
|
||||
required: true,
|
||||
input: TextFormField(
|
||||
controller: _userCtrl,
|
||||
decoration:
|
||||
const InputDecoration(hintText: '字母 / 数字'),
|
||||
validator: (v) =>
|
||||
(v == null || v.trim().isEmpty) ? '不能为空' : null,
|
||||
)),
|
||||
),
|
||||
]),
|
||||
if (!isEdit) ...[
|
||||
const SizedBox(height: 14),
|
||||
DsField('初始密码',
|
||||
required: true,
|
||||
input: TextFormField(
|
||||
controller: _passCtrl,
|
||||
obscureText: true,
|
||||
decoration:
|
||||
const InputDecoration(hintText: '至少 8 位,成员首次登录可改'),
|
||||
validator: (v) =>
|
||||
(v == null || v.length < 8) ? '至少 8 位' : null,
|
||||
)),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
Text('角色',
|
||||
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||||
const SizedBox(height: 8),
|
||||
// 原型 .roles 2×2 单选卡
|
||||
Column(children: [
|
||||
for (var row = 0; row < 2; row++)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: row == 0 ? 10 : 0),
|
||||
child: Row(children: [
|
||||
for (var col = 0; col < 2; col++) ...[
|
||||
if (col > 0) const SizedBox(width: 10),
|
||||
Expanded(child: _roleCard(_roles[row * 2 + col])),
|
||||
],
|
||||
]),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 16),
|
||||
Row(children: [
|
||||
Text('账号状态',
|
||||
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||||
const SizedBox(width: 12),
|
||||
Switch(
|
||||
value: _active,
|
||||
onChanged: (v) => setState(() => _active = v),
|
||||
),
|
||||
Text(_active ? '启用' : '停用',
|
||||
style:
|
||||
TextStyle(fontSize: AppDims.fsBody, color: t.text)),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
Row(children: [
|
||||
if (isEdit)
|
||||
TextButton(
|
||||
onPressed: _resetPassword,
|
||||
child: const Text('重置密码'),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: _saving ? null : _save,
|
||||
child: _saving
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color:
|
||||
Colors.white)) // ds-ignore: primary 底白字加载圈
|
||||
: const Text('保存'),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: form,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user