b740cc6edf
个保法/审核 5.1.1:移动端首启全屏隐私同意(链接隐私政策/用户协议, 同意持久化;不同意 Android 退出、iOS 留提示);我的页 iOS 形态新增 「注销账号」(sheet 说明 + 预填 mailto 至 support@51yanmei.com)。 关于页隐私政策/服务条款链接已有,桌面/Web 零改动。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
276 lines
10 KiB
Dart
276 lines
10 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 '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();
|
||
}),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|