Files
monisuo/flutter_monisuo/lib/ui/pages/mine/mine_page.dart

765 lines
22 KiB
Dart
Raw Normal View History

import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../providers/auth_provider.dart';
2026-03-30 00:30:42 +08:00
import 'kyc_page.dart';
import '../../../providers/theme_provider.dart';
2026-03-23 00:08:19 +08:00
import '../auth/login_page.dart';
2026-04-04 21:19:29 +08:00
import 'welfare_center_page.dart';
/// 我的页面 - 匹配 .pen 设计稿
class MinePage extends StatefulWidget {
const MinePage({super.key});
@override
State<MinePage> createState() => _MinePageState();
}
class _MinePageState extends State<MinePage>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colorScheme.background,
body: Consumer<AuthProvider>(
builder: (context, auth, _) {
return SingleChildScrollView(
padding: EdgeInsets.fromLTRB(
AppSpacing.md,
AppSpacing.md,
AppSpacing.md,
AppSpacing.xl + AppSpacing.md,
),
child: Column(
children: [
_ProfileCard(user: auth.user),
SizedBox(height: AppSpacing.sm),
_MenuGroup1(
2026-03-30 00:30:42 +08:00
kycStatus: auth.user?.kycStatus ?? 0,
onShowComingSoon: _showComingSoon,
2026-03-30 00:30:42 +08:00
),
SizedBox(height: AppSpacing.sm),
_MenuGroup2(onShowAbout: _showAboutDialog),
SizedBox(height: AppSpacing.lg),
_LogoutButton(onLogout: () => _handleLogout(auth)),
SizedBox(height: AppSpacing.md),
Text(
'System Build v1.0.0',
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant.withOpacity(0.5),
),
),
],
),
);
},
),
);
}
2026-03-23 00:08:26 +08:00
void _showComingSoon(String feature) {
showShadDialog(
context: context,
builder: (context) => ShadDialog.alert(
title: Row(
2026-03-23 00:08:26 +08:00
children: [
Icon(Icons.construction, color: AppColorScheme.warning, size: 20),
SizedBox(width: AppSpacing.sm),
const Text('功能开发中'),
2026-03-23 00:08:26 +08:00
],
),
description: Text('$feature功能正在开发中,敬请期待~'),
actions: [
ShadButton(
child: const Text('知道了'),
onPressed: () => Navigator.of(context).pop(),
),
],
),
);
}
void _showAboutDialog() {
final colorScheme = Theme.of(context).colorScheme;
2026-03-23 00:08:26 +08:00
showShadDialog(
context: context,
builder: (context) => ShadDialog(
title: Row(
children: [
_AvatarCircle(radius: 20, fontSize: 16),
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
2026-03-23 00:08:26 +08:00
const Text('模拟所'),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'虚拟货币模拟交易平台',
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
SizedBox(height: AppSpacing.md),
_InfoRow(icon: Icons.code, text: '版本: 1.0.0'),
SizedBox(height: AppSpacing.sm),
_InfoRow(
icon: Icons.favorite,
text: 'Built with Flutter & Material Design 3'),
2026-03-23 00:08:26 +08:00
],
),
actions: [
ShadButton(
child: const Text('确定'),
onPressed: () => Navigator.of(context).pop(),
),
],
),
);
}
void _handleLogout(AuthProvider auth) {
showShadDialog(
context: context,
builder: (ctx) => ShadDialog.alert(
title: const Text('确认退出'),
description: const Text('确定要退出登录吗?'),
actions: [
ShadButton.outline(
child: const Text('取消'),
onPressed: () => Navigator.of(ctx).pop(),
),
ShadButton.destructive(
child: const Text('退出'),
onPressed: () async {
Navigator.of(ctx).pop();
await auth.logout();
if (ctx.mounted) {
Navigator.of(ctx).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const LoginPage()),
(route) => false,
);
}
},
),
],
),
);
}
}
// ============================================================
// Profile Card
// ============================================================
2026-03-23 00:08:26 +08:00
/// 用户资料卡片 - 头像 + 用户名 + 徽章 + chevron
class _ProfileCard extends StatelessWidget {
final dynamic user;
const _ProfileCard({required this.user});
2026-03-23 00:08:26 +08:00
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Row(
children: [
// Avatar
_AvatarCircle(
radius: 24,
fontSize: 18,
text: user?.avatarText,
),
const SizedBox(width: 12),
// Name + badge column
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user?.username ?? '未登录',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
'普通用户',
style: GoogleFonts.inter(
fontSize: 10,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
// Chevron
Icon(
LucideIcons.chevronRight,
size: 16,
color: colorScheme.onSurfaceVariant,
),
],
),
);
}
2026-03-23 00:08:26 +08:00
}
/// 圆形头像组件
class _AvatarCircle extends StatelessWidget {
2026-03-23 00:08:26 +08:00
final double radius;
final double fontSize;
final String? text;
const _AvatarCircle({
required this.radius,
required this.fontSize,
this.text,
});
2026-03-23 00:08:26 +08:00
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
2026-03-23 00:08:26 +08:00
return CircleAvatar(
radius: radius,
backgroundColor: colorScheme.primary.withOpacity(0.15),
2026-03-23 00:08:26 +08:00
child: Text(
text ?? '',
style: TextStyle(
fontSize: fontSize,
color: colorScheme.primary,
fontWeight: FontWeight.w700,
2026-03-23 00:08:26 +08:00
),
),
2026-03-23 00:08:26 +08:00
);
}
}
// ============================================================
// Menu Group 1 - 福利中心 / 实名认证 / 安全设置 / 消息通知
// ============================================================
2026-03-23 00:08:26 +08:00
class _MenuGroup1 extends StatelessWidget {
final int kycStatus;
final void Function(String) onShowComingSoon;
const _MenuGroup1({
required this.kycStatus,
required this.onShowComingSoon,
});
2026-03-23 00:08:26 +08:00
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
width: double.infinity,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Column(
children: [
// 福利中心
_MenuRow(
icon: LucideIcons.gift,
iconColor: AppColorScheme.darkSecondary, // gold
title: '福利中心',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const WelfareCenterPage()),
);
},
),
_MenuDivider(),
// 实名认证
_MenuRow(
icon: LucideIcons.shieldCheck,
iconColor: AppColorScheme.getUpColor(isDark),
title: '实名认证',
trailing: _KycBadge(kycStatus: kycStatus),
onTap: () {
if (kycStatus == 2) {
_showKycStatusDialog(context);
} else {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const KycPage()),
);
}
},
),
_MenuDivider(),
// 安全设置
_MenuRow(
icon: LucideIcons.lock,
iconColor: colorScheme.onSurfaceVariant,
title: '安全设置',
onTap: () => onShowComingSoon('安全设置'),
),
_MenuDivider(),
// 消息通知
_MenuRow(
icon: LucideIcons.bell,
iconColor: colorScheme.onSurfaceVariant,
title: '消息通知',
trailing: _RedDotIndicator(),
onTap: () => onShowComingSoon('消息通知'),
),
],
),
2026-03-23 00:08:26 +08:00
);
}
}
// ============================================================
// Menu Group 2 - 深色模式 / 系统设置 / 关于我们
// ============================================================
class _MenuGroup2 extends StatelessWidget {
2026-03-23 00:08:26 +08:00
final VoidCallback onShowAbout;
const _MenuGroup2({required this.onShowAbout});
2026-03-23 00:08:26 +08:00
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
width: double.infinity,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Column(
2026-03-23 00:08:26 +08:00
children: [
// 深色模式
_DarkModeRow(),
_MenuDivider(),
// 系统设置
_MenuRow(
icon: LucideIcons.settings,
iconColor: colorScheme.onSurfaceVariant,
title: '系统设置',
onTap: () {
// TODO: 系统设置
},
),
_MenuDivider(),
// 关于我们
_MenuRow(
icon: LucideIcons.info,
iconColor: colorScheme.onSurfaceVariant,
title: '关于我们',
onTap: onShowAbout,
),
2026-03-23 00:08:26 +08:00
],
),
);
}
2026-03-23 00:08:26 +08:00
}
// ============================================================
// Shared menu row components
// ============================================================
2026-03-30 00:30:42 +08:00
/// 单行菜单项icon-in-box + title + trailing (chevron / badge / toggle)
class _MenuRow extends StatelessWidget {
final IconData icon;
final Color iconColor;
final String title;
final Widget? trailing;
final VoidCallback? onTap;
const _MenuRow({
required this.icon,
required this.iconColor,
required this.title,
this.trailing,
this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
// Icon in 36x36 rounded container
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.dark
? Theme.of(context).colorScheme.surfaceContainerHigh
: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Icon(icon, size: 18, color: iconColor),
),
),
const SizedBox(width: 10),
// Title
Expanded(
child: Text(
title,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
// Trailing
if (trailing != null)
trailing!
else
Icon(
LucideIcons.chevronRight,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
),
),
);
}
}
/// Menu group divider
class _MenuDivider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 1,
color: Theme.of(context).colorScheme.outlineVariant.withOpacity(0.15),
margin: const EdgeInsets.only(left: 62),
);
}
}
// ============================================================
// Special trailing widgets
// ============================================================
2026-03-23 00:08:26 +08:00
/// KYC status badge (e.g. "已认证" green badge + chevron)
class _KycBadge extends StatelessWidget {
final int kycStatus;
const _KycBadge({required this.kycStatus});
2026-03-23 00:08:26 +08:00
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final green = AppColorScheme.getUpColor(isDark);
if (kycStatus == 2) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: green.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
'已认证',
style: GoogleFonts.inter(
fontSize: 10,
fontWeight: FontWeight.w500,
color: green,
),
),
),
const SizedBox(width: 8),
Icon(
LucideIcons.chevronRight,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
);
}
if (kycStatus == 1) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: AppColorScheme.warning.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
'审核中',
style: GoogleFonts.inter(
fontSize: 10,
fontWeight: FontWeight.w500,
color: AppColorScheme.warning,
),
),
),
const SizedBox(width: 8),
Icon(
LucideIcons.chevronRight,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
);
}
return Icon(
LucideIcons.chevronRight,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
);
}
}
/// Red dot indicator for notifications + chevron
class _RedDotIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: AppColorScheme.down,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Icon(
LucideIcons.chevronRight,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
);
}
}
/// Dark mode toggle row
class _DarkModeRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final themeProvider = context.watch<ThemeProvider>();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
// Icon in 36x36 rounded container
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainerHigh
: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Icon(
LucideIcons.moon,
size: 18,
color: colorScheme.onSurfaceVariant,
),
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
'深色模式',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
),
),
),
// Toggle switch - matching .pen design (44x24 rounded pill)
GestureDetector(
onTap: () => themeProvider.toggleTheme(),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 44,
height: 24,
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainerHigh
: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: AnimatedAlign(
duration: const Duration(milliseconds: 200),
alignment:
themeProvider.isDarkMode
? Alignment.centerRight
: Alignment.centerLeft,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: colorScheme.onSurface,
shape: BoxShape.circle,
),
),
),
),
),
],
),
);
}
2026-03-23 00:08:26 +08:00
}
// ============================================================
// Logout button
// ============================================================
2026-03-23 00:08:26 +08:00
class _LogoutButton extends StatelessWidget {
final VoidCallback onLogout;
const _LogoutButton({required this.onLogout});
2026-03-23 00:08:26 +08:00
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
2026-03-23 00:08:26 +08:00
return GestureDetector(
onTap: onLogout,
child: Container(
width: double.infinity,
height: 48,
decoration: BoxDecoration(
color: AppColorScheme.down.withOpacity(0.05),
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: AppColorScheme.down.withOpacity(0.15),
),
),
child: Center(
child: Text(
'退出登录',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColorScheme.down,
),
),
),
2026-03-23 00:08:26 +08:00
),
);
}
}
// ============================================================
// Info row (used in about dialog)
// ============================================================
class _InfoRow extends StatelessWidget {
final IconData icon;
final String text;
const _InfoRow({required this.icon, required this.text});
2026-03-23 00:08:26 +08:00
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Row(
children: [
Icon(icon, size: 14, color: colorScheme.onSurfaceVariant),
SizedBox(width: AppSpacing.sm),
Text(
text,
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
],
);
}
}
// ============================================================
// KYC status dialog
// ============================================================
void _showKycStatusDialog(BuildContext context) {
showShadDialog(
context: context,
builder: (ctx) => ShadDialog.alert(
title: Row(
children: [
Icon(Icons.check_circle, color: AppColorScheme.up, size: 20),
SizedBox(width: AppSpacing.sm),
const Text('实名认证'),
],
),
description: const Text('您的实名认证已通过'),
actions: [
ShadButton(
child: const Text('确定'),
onPressed: () => Navigator.of(ctx).pop(),
),
],
),
);
}