Files
jiu/client/lib/screens/me/me_screen.dart
T
wangjia be9afcf68d feat(client): 移动壳基建——底部 5 tab + 我的 hub + 统一底部 sheet + 移动组件四件套
对齐移动原型体系(m-*.html)的 Stage 0 共享基建,桌面形态零改动:

- 导航:router 新增 branch 9(/me 我的 + /me/license 授权屏);窄屏 tab 根
  (库存/入库/出库/财务/我的)出底部 MTabBar,二级屏隐藏 tabbar + 顶栏返回箭头
  (PopScope 拦系统返回键防退 app);窄屏 Drawer/汉堡删除;窄屏顶栏对齐 m-top
  (标题 + 主题/通知铃/头像→我的)
- sheet:showMSheet(grip/标题/86vh/键盘避让)+ showAdaptiveSheet;六个共享
  弹层呈现函数加窄屏 sheet 分支(订单/往来/财务往来/商品编辑抽屉 + 退单/登记收支
  dialog 的 asSheet 形态)
- 徽章:DsBadge/StatusPill/StatusBadge 加图标变体(默认圆点零漂移)+
  status_icon_map 转录原型 BADGE_ICON 28 词
- 组件:MKpiGrid(2×2 可点 KPI)/ MSearchRow(搜索+状态钮+详搜钮)/
  MHubGroup(hub 入口组)/ showThemeSheet(主题外观 sheet)
- 授权:_LicensePanel/_SettingsCard 抽取为公共 LicensePanel/SettingsCard,
  窄屏授权跳转 /me/license
- golden:app_shell_mobile + me 各三主题(390×844@2x);全量 259 测试绿,
  check_ds_code / check-l1-sync 闸过

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 11:56:04 +08:00

209 lines
7.0 KiB
Dart

// screens/me/me_screen.dart — 「我的」hub(镜像原型 m-me.html,移动愿景导航中枢)。
// 结构:用户头卡(54 渐变圆头像 + 真名 + 店名·账号 + 角色徽章)
// + 经营管理组(往来单位/基础数据/库存盘点)
// + 系统组(用户管理/授权管理/设备管理/系统设置/主题外观/关于我们)
// + 退出登录(确认 sheet)。窄屏底部 tab「我的」入口;桌面侧栏不含此屏。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../core/auth/auth_state.dart';
import '../../core/theme/app_dims.g.dart';
import '../../core/theme/context_tokens.dart';
import '../../providers/shop_provider.dart';
import '../../widgets/ds/ds_atoms.dart';
import '../../widgets/ds/m_hub.dart';
import '../../widgets/ds/m_sheet.dart';
import '../../widgets/ds/status_icon_map.dart';
import '../../widgets/theme_sheet.dart';
String _roleLabel(String role) {
switch (role) {
case 'superadmin':
return '超级管理员';
case 'admin':
return '管理员';
case 'readonly':
return '只读';
default:
return '操作员';
}
}
class MeScreen extends ConsumerWidget {
const MeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = context.tokens;
final user = ref.watch(authStateProvider).user;
final shopName =
ref.watch(shopInfoProvider).valueOrNull?.name ?? user?.shopNo ?? '';
return Container(
color: t.bg,
child: ListView(
padding: const EdgeInsets.all(14),
children: [
if (user != null) _header(t, user, shopName),
MHubGroup(label: '经营管理', items: [
MHubItem(
icon: LucideIcons.users,
label: '往来单位',
onTap: () => context.go('/partners')),
MHubItem(
icon: LucideIcons.layoutGrid,
label: '基础数据',
onTap: () => context.go('/products')),
MHubItem(
icon: LucideIcons.clipboardCheck,
label: '库存盘点',
onTap: () => context.go('/inventory/check')),
]),
MHubGroup(label: '系统', items: [
MHubItem(
icon: LucideIcons.userPlus,
label: '用户管理',
onTap: () => context.go('/settings/users')),
MHubItem(
icon: LucideIcons.trendingUp,
label: '授权管理',
onTap: () => context.go('/me/license')),
MHubItem(
icon: LucideIcons.monitorSmartphone,
label: '设备管理',
onTap: () => context.go('/devices')),
MHubItem(
icon: LucideIcons.settings,
label: '系统设置',
onTap: () => context.go('/settings')),
MHubItem(
icon: LucideIcons.shirt,
label: '主题外观',
onTap: () => showThemeSheet(context)),
MHubItem(
icon: LucideIcons.info,
label: '关于我们',
onTap: () => context.go('/about')),
]),
const SizedBox(height: 18),
_LogoutButton(onConfirm: () {
ref.read(authStateProvider.notifier).logout();
context.go('/login');
}),
],
),
);
}
/// 用户头卡(原型 .me-hd):surface 卡 + 54 渐变圆头像 + 名/店·账号 + 角色徽章。
Widget _header(dynamic t, AuthUser user, String shopName) {
final name = user.realName.isNotEmpty ? user.realName : user.username;
final initial = name.isNotEmpty ? name.characters.first : '';
final roleLabel = _roleLabel(user.role);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: t.surface,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rLg),
),
child: Row(children: [
Container(
width: 54,
height: 54,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [t.primary, t.primaryDark],
),
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(initial,
style: TextStyle(
color: t.onPrimary,
fontSize: 22,
fontWeight: FontWeight.w800)),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(name,
style: TextStyle(
fontSize: AppDims.fsH2,
fontWeight: FontWeight.w700,
color: t.heading)),
const SizedBox(height: 3),
Text('$shopName · ${user.username}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
],
),
),
DsBadge(roleLabel,
tone: DsBadgeTone.info, icon: statusIcon(roleLabel)),
]),
);
}
}
class _LogoutButton extends StatelessWidget {
final VoidCallback onConfirm;
const _LogoutButton({required this.onConfirm});
@override
Widget build(BuildContext context) {
final t = context.tokens;
// Material 自带(InkWell 水波 + golden 直挂无 Scaffold 场景)
return Material(
color: t.surface,
borderRadius: BorderRadius.circular(AppDims.rMd),
child: InkWell(
onTap: () => _confirm(context),
borderRadius: BorderRadius.circular(AppDims.rMd),
child: Container(
height: 44,
alignment: Alignment.center,
decoration: BoxDecoration(
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Text('退出登录',
style: TextStyle(
fontSize: AppDims.fsBody,
fontWeight: FontWeight.w600,
color: t.danger)),
),
),
);
}
void _confirm(BuildContext context) {
showMSheet<void>(
context,
title: '退出登录',
builder: (ctx) => Text('确认退出当前账号?',
style: TextStyle(
fontSize: AppDims.fsBody, color: ctx.tokens.text)),
actions: [
Builder(
builder: (ctx) =>
DsButton('取消', onPressed: () => Navigator.of(ctx).pop())),
Builder(
builder: (ctx) => DsButton('退出登录',
variant: DsBtnVariant.danger, onPressed: () {
Navigator.of(ctx).pop();
onConfirm();
}),
),
],
);
}
}