Files
jiu/client/lib/screens/me/me_screen.dart
T
wangjia b740cc6edf feat(client): iOS 上架合规——首启隐私同意弹窗 + 注销账号入口
个保法/审核 5.1.1:移动端首启全屏隐私同意(链接隐私政策/用户协议,
同意持久化;不同意 Android 退出、iOS 留提示);我的页 iOS 形态新增
「注销账号」(sheet 说明 + 预填 mailto 至 support@51yanmei.com)。
关于页隐私政策/服务条款链接已有,桌面/Web 零改动。

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

276 lines
10 KiB
Dart
Raw 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.
// 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 'package:url_launcher/url_launcher.dart';
import '../../core/auth/auth_state.dart';
import '../../core/config/store_compliance.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')),
// iOS App Store 审核 5.1.1(v):应用内须可发起账号删除
if (isIosAppStoreBuild)
MHubItem(
icon: LucideIcons.userX,
label: '注销账号',
onTap: () => _showDeleteAccountSheet(context, user)),
]),
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)),
]),
);
}
/// 注销账号(iOS App Store 审核 5.1.1(v)):应用内发起 → 邮件人工核验。
/// 措辞保持中性,不涉及任何购买/续费引导。
void _showDeleteAccountSheet(BuildContext context, AuthUser? user) {
showMSheet<void>(
context,
title: '注销账号',
builder: (ctx) {
final t = ctx.tokens;
Widget note(String s) => Padding(
padding: const EdgeInsets.only(top: 4),
child: Text('· $s',
style: TextStyle(
fontSize: AppDims.fsSm, color: t.muted, height: 1.6)),
);
final who = user != null ? '${user.shopNo} · ${user.username}' : '';
return Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('注销将删除您的登录账号,操作不可恢复。',
style: TextStyle(
fontSize: AppDims.fsBody,
fontWeight: FontWeight.w600,
color: t.heading)),
const SizedBox(height: 8),
note('账号与登录凭据将被删除,无法找回'),
note('门店经营数据归属门店:门店内仍有其他用户时数据保留,'
'需整店删除请在申请中说明'),
note('为防止误操作与冒用,注销需人工核验身份,'
'我们将在收到申请后 15 个工作日内完成处理'),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: DsButton('发送注销申请邮件',
variant: DsBtnVariant.primary,
onPressed: () {
final subject = Uri.encodeComponent('账号注销申请($who');
final body = Uri.encodeComponent(
'申请注销账号:$who\n注销原因(选填):\n\n'
'请使用注册时预留的联系方式发送本邮件以便核验。');
launchUrl(Uri.parse(
'mailto:$supportEmail?subject=$subject&body=$body'));
}),
),
const SizedBox(height: 8),
Center(
child: Text('客服邮箱 $supportEmail',
style:
TextStyle(fontSize: AppDims.fsXs, color: t.faint)),
),
],
),
);
},
);
}
}
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();
}),
),
],
);
}
}