diff --git a/frontend/mining-app/lib/core/router/app_router.dart b/frontend/mining-app/lib/core/router/app_router.dart index fc182814..63e67997 100644 --- a/frontend/mining-app/lib/core/router/app_router.dart +++ b/frontend/mining-app/lib/core/router/app_router.dart @@ -8,6 +8,7 @@ import '../../presentation/pages/auth/change_password_page.dart'; import '../../presentation/pages/home/home_page.dart'; import '../../presentation/pages/contribution/contribution_page.dart'; import '../../presentation/pages/trading/trading_page.dart'; +import '../../presentation/pages/asset/asset_page.dart'; import '../../presentation/pages/profile/profile_page.dart'; import '../../presentation/widgets/main_shell.dart'; import 'routes.dart'; @@ -51,6 +52,10 @@ final appRouterProvider = Provider((ref) { path: Routes.trading, builder: (context, state) => const TradingPage(), ), + GoRoute( + path: Routes.asset, + builder: (context, state) => const AssetPage(), + ), GoRoute( path: Routes.profile, builder: (context, state) => const ProfilePage(), diff --git a/frontend/mining-app/lib/core/router/routes.dart b/frontend/mining-app/lib/core/router/routes.dart index 99859c57..dd954141 100644 --- a/frontend/mining-app/lib/core/router/routes.dart +++ b/frontend/mining-app/lib/core/router/routes.dart @@ -7,6 +7,7 @@ class Routes { static const String home = '/home'; static const String contribution = '/contribution'; static const String trading = '/trading'; + static const String asset = '/asset'; static const String profile = '/profile'; static const String miningRecords = '/mining-records'; static const String contributionRecords = '/contribution-records'; diff --git a/frontend/mining-app/lib/presentation/pages/asset/asset_page.dart b/frontend/mining-app/lib/presentation/pages/asset/asset_page.dart new file mode 100644 index 00000000..58656d64 --- /dev/null +++ b/frontend/mining-app/lib/presentation/pages/asset/asset_page.dart @@ -0,0 +1,789 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../core/constants/app_colors.dart'; +import '../../../core/utils/format_utils.dart'; +import '../../providers/user_providers.dart'; +import '../../providers/mining_providers.dart'; + +class AssetPage extends ConsumerWidget { + const AssetPage({super.key}); + + // 设计色彩 + static const Color _orange = Color(0xFFFF6B00); + static const Color _green = Color(0xFF10B981); + static const Color _grayText = Color(0xFF6B7280); + static const Color _darkText = Color(0xFF1F2937); + static const Color _bgGray = Color(0xFFF3F4F6); + static const Color _lightGray = Color(0xFFF9FAFB); + static const Color _riverBed = Color(0xFF4B5563); + static const Color _serenade = Color(0xFFFFF7ED); + static const Color _feta = Color(0xFFF0FDF4); + static const Color _scandal = Color(0xFFDCFCE7); + static const Color _jewel = Color(0xFF15803D); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final user = ref.watch(userNotifierProvider); + final accountSequence = user.accountSequence ?? ''; + final accountAsync = ref.watch(shareAccountProvider(accountSequence)); + + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: RefreshIndicator( + onRefresh: () async { + ref.invalidate(shareAccountProvider(accountSequence)); + }, + child: SingleChildScrollView( + child: Column( + children: [ + // 顶部导航栏 + _buildAppBar(context, user), + // 内容 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + const SizedBox(height: 8), + // 总资产卡片 + accountAsync.when( + data: (account) => _buildTotalAssetCard(account), + loading: () => _buildLoadingCard(), + error: (_, __) => _buildErrorCard('资产加载失败'), + ), + const SizedBox(height: 24), + // 快捷操作按钮 + _buildQuickActions(), + const SizedBox(height: 24), + // 资产列表 + accountAsync.when( + data: (account) => _buildAssetList(account), + loading: () => _buildLoadingCard(), + error: (_, __) => const SizedBox.shrink(), + ), + const SizedBox(height: 24), + // 收益统计 + _buildEarningsCard(), + const SizedBox(height: 24), + // 账户列表 + accountAsync.when( + data: (account) => _buildAccountList(account), + loading: () => _buildLoadingCard(), + error: (_, __) => const SizedBox.shrink(), + ), + const SizedBox(height: 100), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildAppBar(BuildContext context, user) { + return Container( + color: _bgGray.withOpacity(0.9), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + child: Row( + children: [ + // 头像 + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(18), + border: Border.all(color: _green, width: 2), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Center( + child: Text( + user.nickname?.substring(0, 1) ?? 'U', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: _darkText, + ), + ), + ), + ), + ), + const Spacer(), + // 标题 + const Text( + '我的资产', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF111827), + ), + ), + const Spacer(), + // 设置按钮 + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(18), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: const Icon(Icons.settings_outlined, size: 20, color: _grayText), + ), + const SizedBox(width: 12), + // 通知按钮 + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(18), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Stack( + children: [ + const Center( + child: Icon(Icons.notifications_outlined, size: 20, color: _grayText), + ), + Positioned( + right: 8, + top: 8, + child: Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildTotalAssetCard(account) { + final totalAsset = account?.tradingBalance ?? '88888.88'; + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 30, + offset: const Offset(0, 8), + ), + ], + ), + child: Stack( + children: [ + // 背景装饰圆 + Positioned( + right: -20, + top: -40, + child: Container( + width: 128, + height: 128, + decoration: BoxDecoration( + color: _serenade, + borderRadius: BorderRadius.circular(64), + ), + ), + ), + // 顶部渐变条 + Positioned( + top: 0, + left: 0, + right: 0, + child: Container( + height: 4, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFFFF6B00), Color(0xFFFDBA74)], + ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + ), + ), + // 内容 + Padding( + padding: const EdgeInsets.fromLTRB(24, 20, 24, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题行 + Row( + children: [ + const Text( + '总资产估值', + style: TextStyle( + fontSize: 14, + color: _grayText, + ), + ), + const SizedBox(width: 8), + Icon( + Icons.visibility_outlined, + size: 14, + color: _grayText.withOpacity(0.5), + ), + ], + ), + const SizedBox(height: 8), + // 金额 + Text( + '¥ ${formatAmount(totalAsset)}', + style: const TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + color: _orange, + letterSpacing: -0.75, + ), + ), + const SizedBox(height: 4), + // USDT估值 + const Text( + '≈ 12,345.67 USDT', + style: TextStyle( + fontSize: 14, + color: Color(0xFF9CA3AF), + ), + ), + const SizedBox(height: 12), + // 今日收益 + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: _feta, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.trending_up, size: 14, color: _green), + const SizedBox(width: 6), + Text( + '+¥ 156.78 今日', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: _green, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildQuickActions() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildQuickActionItem(Icons.add, '接收', _orange), + _buildQuickActionItem(Icons.remove, '发送', _orange), + _buildQuickActionItem(Icons.swap_horiz, '划转', _orange), + _buildQuickActionItem(Icons.download, '提现', _orange), + ], + ); + } + + Widget _buildQuickActionItem(IconData icon, String label, Color color) { + return Column( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: _serenade, + borderRadius: BorderRadius.circular(16), + ), + child: Icon(icon, color: color, size: 24), + ), + const SizedBox(height: 8), + Text( + label, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: _riverBed, + ), + ), + ], + ); + } + + Widget _buildAssetList(account) { + return Column( + children: [ + // 积分股 + _buildAssetItem( + icon: Icons.trending_up, + iconColor: _orange, + iconBgColor: _serenade, + title: '积分股', + amount: account?.miningBalance ?? '123456.78', + valueInCny: '¥15,234.56', + tag: '含倍数资产: 246,913.56', + growthText: '每秒 +0.0015', + ), + const SizedBox(height: 16), + // 绿积分 + _buildAssetItem( + icon: Icons.eco, + iconColor: _green, + iconBgColor: _feta, + title: '绿积分', + amount: account?.tradingBalance ?? '88888.88', + valueInCny: '¥10,986.54', + badge: '可提现', + badgeColor: _jewel, + badgeBgColor: _scandal, + ), + const SizedBox(height: 16), + // 待分配积分股 + _buildAssetItem( + icon: Icons.hourglass_empty, + iconColor: _orange, + iconBgColor: _serenade, + title: '待分配积分股', + amount: '1,234.56', + subtitle: '次日开始参与分配', + ), + ], + ); + } + + Widget _buildAssetItem({ + required IconData icon, + required Color iconColor, + required Color iconBgColor, + required String title, + required String amount, + String? valueInCny, + String? tag, + String? growthText, + String? badge, + Color? badgeColor, + Color? badgeBgColor, + String? subtitle, + }) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: Row( + children: [ + // 图标 + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: iconBgColor, + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: iconColor, size: 24), + ), + const SizedBox(width: 12), + // 内容 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题行 + Row( + children: [ + Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF111827), + ), + ), + if (badge != null) ...[ + const SizedBox(width: 7), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: badgeBgColor ?? _scandal, + borderRadius: BorderRadius.circular(9999), + ), + child: Text( + badge, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w500, + color: badgeColor ?? _jewel, + ), + ), + ), + ], + ], + ), + const SizedBox(height: 2), + // 数量 + Text( + formatAmount(amount), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF111827), + ), + ), + // 估值 + if (valueInCny != null) + Text( + '≈ $valueInCny', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF9CA3AF), + ), + ), + // 副标题 + if (subtitle != null) + Padding( + padding: const EdgeInsets.only(top: 3), + child: Text( + subtitle, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF9CA3AF), + ), + ), + ), + // 标签行 + if (tag != null || growthText != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + children: [ + if (tag != null) + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: _serenade, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + tag, + style: const TextStyle( + fontSize: 10, + color: _orange, + ), + ), + ), + if (growthText != null) ...[ + const SizedBox(width: 8), + Row( + children: [ + Icon(Icons.bolt, size: 12, color: _green), + Text( + growthText, + style: TextStyle( + fontSize: 10, + color: _green, + ), + ), + ], + ), + ], + ], + ), + ), + ], + ), + ), + // 箭头 + Icon(Icons.chevron_right, size: 14, color: _grayText.withOpacity(0.5)), + ], + ), + ); + } + + Widget _buildEarningsCard() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: Column( + children: [ + // 标题 + Row( + children: [ + Container( + width: 4, + height: 20, + decoration: BoxDecoration( + color: _orange, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 8), + const Text( + '收益统计', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: _darkText, + ), + ), + ], + ), + const SizedBox(height: 16), + // 统计数据 + Row( + children: [ + _buildEarningsItem('累计收益', '12,345.67', _orange), + Container( + width: 1, + height: 40, + color: _serenade, + ), + _buildEarningsItem('今日收益', '+156.78', _green), + Container( + width: 1, + height: 40, + color: _serenade, + ), + _buildEarningsItem('昨日收益', '143.21', const Color(0xFF9CA3AF)), + ], + ), + ], + ), + ); + } + + Widget _buildEarningsItem(String label, String value, Color valueColor) { + return Expanded( + child: Column( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 12, + color: _grayText, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 4), + Text( + value, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: valueColor, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + Widget _buildAccountList(account) { + return Column( + children: [ + // 交易账户 + _buildAccountItem( + icon: Icons.account_balance_wallet, + iconColor: _orange, + title: '交易账户', + balance: account?.tradingBalance ?? '5678.90', + unit: '绿积分', + status: '正常', + statusColor: _green, + statusBgColor: _feta, + ), + const SizedBox(height: 16), + // 提现账户 + _buildAccountItem( + icon: Icons.savings, + iconColor: _orange, + title: '提现账户', + balance: '1,234.56', + unit: '绿积分', + status: '已绑定', + statusColor: const Color(0xFF9CA3AF), + statusBgColor: Colors.white, + statusBorder: true, + ), + ], + ); + } + + Widget _buildAccountItem({ + required IconData icon, + required Color iconColor, + required String title, + required String balance, + required String unit, + required String status, + required Color statusColor, + required Color statusBgColor, + bool statusBorder = false, + }) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: Row( + children: [ + // 图标 + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: _serenade, + borderRadius: BorderRadius.circular(18), + ), + child: Icon(icon, color: iconColor, size: 20), + ), + const SizedBox(width: 12), + // 内容 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFF111827), + ), + ), + const SizedBox(height: 2), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: '$balance ', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: _orange, + ), + ), + TextSpan( + text: unit, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF9CA3AF), + ), + ), + ], + ), + ), + ], + ), + ), + // 状态标签 + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: statusBgColor, + borderRadius: BorderRadius.circular(9999), + border: statusBorder ? Border.all(color: const Color(0xFFE5E7EB)) : null, + ), + child: Text( + status, + style: TextStyle( + fontSize: 10, + color: statusColor, + ), + ), + ), + const SizedBox(width: 8), + // 箭头 + Icon(Icons.chevron_right, size: 14, color: _grayText.withOpacity(0.5)), + ], + ), + ); + } + + Widget _buildLoadingCard() { + return Container( + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: const Center(child: CircularProgressIndicator()), + ); + } + + Widget _buildErrorCard(String message) { + return Container( + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Center( + child: Column( + children: [ + const Icon(Icons.error_outline, size: 48, color: AppColors.error), + const SizedBox(height: 8), + Text(message), + ], + ), + ), + ); + } +} diff --git a/frontend/mining-app/lib/presentation/pages/auth/login_page.dart b/frontend/mining-app/lib/presentation/pages/auth/login_page.dart index 54128d16..7a19b7e6 100644 --- a/frontend/mining-app/lib/presentation/pages/auth/login_page.dart +++ b/frontend/mining-app/lib/presentation/pages/auth/login_page.dart @@ -85,7 +85,7 @@ class _LoginPageState extends ConsumerState { } if (mounted) { - context.go(Routes.home); + context.go(Routes.contribution); } } catch (e) { if (mounted) { diff --git a/frontend/mining-app/lib/presentation/pages/auth/register_page.dart b/frontend/mining-app/lib/presentation/pages/auth/register_page.dart index 453757f9..c386eef1 100644 --- a/frontend/mining-app/lib/presentation/pages/auth/register_page.dart +++ b/frontend/mining-app/lib/presentation/pages/auth/register_page.dart @@ -79,7 +79,7 @@ class _RegisterPageState extends ConsumerState { await ref.read(userNotifierProvider.notifier).register(phone, password, smsCode); if (mounted) { - context.go(Routes.home); + context.go(Routes.contribution); } } catch (e) { if (mounted) { diff --git a/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart b/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart index 310bc3c9..49ba8b41 100644 --- a/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart +++ b/frontend/mining-app/lib/presentation/pages/contribution/contribution_page.dart @@ -8,6 +8,14 @@ import '../../providers/contribution_providers.dart'; class ContributionPage extends ConsumerWidget { const ContributionPage({super.key}); + // 设计色彩 + static const Color _orange = Color(0xFFFF6B00); + static const Color _green = Color(0xFF22C55E); + static const Color _grayText = Color(0xFF6B7280); + static const Color _darkText = Color(0xFF1F2937); + static const Color _bgGray = Color(0xFFF3F4F6); + static const Color _lightGray = Color(0xFFF9FAFB); + @override Widget build(BuildContext context, WidgetRef ref) { final user = ref.watch(userNotifierProvider); @@ -15,66 +23,62 @@ class ContributionPage extends ConsumerWidget { final contributionAsync = ref.watch(contributionProvider(accountSequence)); return Scaffold( - appBar: AppBar( - title: const Text('我的算力'), - backgroundColor: AppColors.primary, - foregroundColor: Colors.white, - ), - body: RefreshIndicator( - onRefresh: () async { - ref.invalidate(contributionProvider(accountSequence)); - }, - child: contributionAsync.when( - data: (contribution) { - if (contribution == null) { - return const Center(child: Text('暂无数据')); - } - return SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.all(16), + backgroundColor: const Color(0xFFF5F5F5), + body: SafeArea( + child: RefreshIndicator( + onRefresh: () async { + ref.invalidate(contributionProvider(accountSequence)); + }, + child: contributionAsync.when( + data: (contribution) { + return CustomScrollView( + slivers: [ + // 顶部导航栏 + SliverToBoxAdapter(child: _buildAppBar(context)), + // 内容 + SliverPadding( + padding: const EdgeInsets.all(16), + sliver: SliverList( + delegate: SliverChildListDelegate([ + // 总贡献值卡片 + _buildTotalContributionCard(contribution), + const SizedBox(height: 16), + // 三栏统计 + _buildThreeColumnStats(contribution), + const SizedBox(height: 16), + // 今日预估收益 + _buildTodayEstimateCard(contribution), + const SizedBox(height: 16), + // 贡献值明细 + _buildContributionDetailCard(contribution), + const SizedBox(height: 16), + // 团队层级统计 + _buildTeamStatsCard(contribution), + const SizedBox(height: 16), + // 贡献值失效倒计时 + _buildExpirationCard(contribution), + const SizedBox(height: 24), + ]), + ), + ), + ], + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, _) => Center( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ - // 总算力卡片 - _buildTotalCard(contribution.effectiveContribution), - - const SizedBox(height: 24), - - // 算力构成 - const Text( - '算力构成', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + const Icon(Icons.error_outline, size: 48, color: AppColors.error), + const SizedBox(height: 16), + Text('加载失败: $error'), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => ref.invalidate(contributionProvider(accountSequence)), + child: const Text('重试'), ), - const SizedBox(height: 12), - _buildBreakdownCard(contribution), - - const SizedBox(height: 24), - - // 解锁状态 - const Text( - '解锁状态', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 12), - _buildUnlockStatus(contribution), ], ), - ); - }, - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, _) => Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.error_outline, size: 48, color: AppColors.error), - const SizedBox(height: 16), - Text('加载失败: $error'), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => ref.invalidate(contributionProvider(accountSequence)), - child: const Text('重试'), - ), - ], ), ), ), @@ -82,31 +86,113 @@ class ContributionPage extends ConsumerWidget { ); } - Widget _buildTotalCard(String effectiveContribution) { + Widget _buildAppBar(BuildContext context) { return Container( - width: double.infinity, - padding: const EdgeInsets.all(24), + color: _lightGray, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + // Logo + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: _orange.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.eco, color: _orange, size: 20), + ), + const SizedBox(width: 8), + const Text( + '榴莲生态', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: _darkText, + letterSpacing: 0.45, + ), + ), + const Spacer(), + // 客服 + IconButton( + icon: const Icon(Icons.headset_mic_outlined, color: _grayText), + onPressed: () {}, + ), + // 通知(带红点) + Stack( + children: [ + IconButton( + icon: const Icon(Icons.notifications_outlined, color: _grayText), + onPressed: () {}, + ), + Positioned( + right: 10, + top: 10, + child: Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildTotalContributionCard(contribution) { + final total = contribution?.effectiveContribution ?? '0'; + return Container( + padding: const EdgeInsets.all(20), decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [AppColors.primary, Color(0xFF16A34A)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), + color: Colors.white, borderRadius: BorderRadius.circular(16), ), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '有效算力', - style: TextStyle(color: Colors.white70, fontSize: 14), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '总贡献值', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: _grayText), + ), + Icon(Icons.visibility_outlined, color: _grayText.withOpacity(0.5), size: 18), + ], ), const SizedBox(height: 8), Text( - formatAmount(effectiveContribution), + formatAmount(total), style: const TextStyle( - color: Colors.white, - fontSize: 42, + fontSize: 30, fontWeight: FontWeight.bold, + color: _orange, + letterSpacing: -0.75, + ), + ), + const SizedBox(height: 12), + // 有效期标签 + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: _lightGray, + borderRadius: BorderRadius.circular(999), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.info_outline, size: 14, color: _grayText.withOpacity(0.7)), + const SizedBox(width: 6), + Text( + '贡献值有效期: 剩余 730 天', + style: TextStyle(fontSize: 12, color: _grayText.withOpacity(0.9)), + ), + ], ), ), ], @@ -114,38 +200,39 @@ class ContributionPage extends ConsumerWidget { ); } - Widget _buildBreakdownCard(contribution) { - return Card( - child: Padding( - padding: const EdgeInsets.all(16), + Widget _buildThreeColumnStats(contribution) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + _buildStatColumn('个人贡献值', contribution?.personalContribution ?? '0', false), + _buildStatColumn('团队贡献值', contribution?.teamLevelContribution ?? '0', true), + _buildStatColumn('省市公司', contribution?.systemContribution ?? '0', true), + ], + ), + ); + } + + Widget _buildStatColumn(String label, String value, bool showLeftBorder) { + return Expanded( + child: Container( + decoration: showLeftBorder + ? const BoxDecoration( + border: Border(left: BorderSide(color: Color(0xFFE5E7EB), width: 1)), + ) + : null, + padding: const EdgeInsets.symmetric(horizontal: 4), child: Column( children: [ - _buildBreakdownRow( - '个人算力', - contribution.personalContribution, - '70%', - AppColors.primary, - ), - const Divider(), - _buildBreakdownRow( - '系统算力', - contribution.systemContribution, - '15%', - AppColors.secondary, - ), - const Divider(), - _buildBreakdownRow( - '团队层级', - contribution.teamLevelContribution, - '7.5%', - AppColors.warning, - ), - const Divider(), - _buildBreakdownRow( - '团队奖励', - contribution.teamBonusContribution, - '7.5%', - Colors.purple, + Text(label, style: const TextStyle(fontSize: 12, color: _grayText)), + const SizedBox(height: 4), + Text( + formatAmount(value), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _darkText), ), ], ), @@ -153,70 +240,191 @@ class ContributionPage extends ConsumerWidget { ); } - Widget _buildBreakdownRow(String label, String value, String rate, Color color) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), + Widget _buildTodayEstimateCard(contribution) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), child: Row( children: [ + // 图标 Container( - width: 4, - height: 24, + width: 40, + height: 40, decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(2), + color: _green.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), ), + child: const Icon(Icons.trending_up, color: _green, size: 24), ), const SizedBox(width: 12), + // 文字说明 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: const TextStyle(fontSize: 14)), + const Text( + '今日预估收益', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: _grayText), + ), Text( - '占比 $rate', - style: const TextStyle(color: AppColors.textMuted, fontSize: 12), + '基于当前贡献值占比计算', + style: TextStyle(fontSize: 12, color: _grayText.withOpacity(0.7)), ), ], ), ), - Text( - formatAmount(value), - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + // 收益数值 + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: const [ + Text.rich( + TextSpan( + children: [ + TextSpan( + text: '+156.78 ', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: _green), + ), + TextSpan( + text: '积分', + style: TextStyle(fontSize: 12, color: _green), + ), + ], + ), + ), + Text('股', style: TextStyle(fontSize: 12, color: _green)), + ], ), ], ), ); } - Widget _buildUnlockStatus(contribution) { - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - _buildUnlockRow( - '已解锁层级', - '${contribution.unlockedLevelDepth}/15 层', - contribution.unlockedLevelDepth / 15, - ), - const SizedBox(height: 16), - _buildUnlockRow( - '已解锁奖励档', - '${contribution.unlockedBonusTiers}/3 档', - contribution.unlockedBonusTiers / 3, - ), - const SizedBox(height: 16), - Row( - children: [ - const Icon(Icons.info_outline, size: 16, color: AppColors.textMuted), - const SizedBox(width: 8), - Expanded( - child: Text( - '直推认种人数: ${contribution.directReferralAdoptedCount}', - style: const TextStyle(color: AppColors.textMuted, fontSize: 12), - ), + Widget _buildContributionDetailCard(contribution) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + // 标题行 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '贡献值明细', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: _darkText), + ), + GestureDetector( + onTap: () {}, + child: Row( + children: const [ + Text('查看全部', style: TextStyle(fontSize: 12, color: _orange)), + Icon(Icons.chevron_right, size: 14, color: _orange), + ], ), - ], + ), + ], + ), + const SizedBox(height: 16), + // 明细列表 + _buildDetailRow('认种榴莲树', '2024-01-15 14:30', '+22,617.00'), + const Divider(height: 24), + _buildDetailRow('团队奖励(5级)', '2024-01-15 09:12', '+1,130.85'), + const Divider(height: 24), + _buildDetailRow('直推奖励', '2024-01-14 18:45', '+565.43'), + ], + ), + ); + } + + Widget _buildDetailRow(String title, String time, String amount) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: _darkText)), + const SizedBox(height: 2), + Text(time, style: const TextStyle(fontSize: 12, color: _grayText)), + ], + ), + Text( + amount, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: _green), + ), + ], + ); + } + + Widget _buildTeamStatsCard(contribution) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '团队层级统计', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: _darkText), + ), + const SizedBox(height: 16), + // 第一行 + Row( + children: [ + _buildTeamStatItem('直推人数', '${contribution?.directReferralAdoptedCount ?? 0}', '人'), + const SizedBox(width: 16), + _buildTeamStatItem('团队总人数', '128', '人'), + ], + ), + const SizedBox(height: 16), + // 第二行 + Row( + children: [ + _buildTeamStatItem('已解锁层级', '${contribution?.unlockedLevelDepth ?? 0}', '级'), + const SizedBox(width: 16), + _buildTeamStatItem('团队认种总数', '456', '棵'), + ], + ), + ], + ), + ); + } + + Widget _buildTeamStatItem(String label, String value, String unit) { + return Expanded( + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: _bgGray, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: const TextStyle(fontSize: 12, color: _grayText)), + const SizedBox(height: 4), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: '$value ', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: _orange), + ), + TextSpan( + text: unit, + style: const TextStyle(fontSize: 12, color: _grayText), + ), + ], + ), ), ], ), @@ -224,24 +432,59 @@ class ContributionPage extends ConsumerWidget { ); } - Widget _buildUnlockRow(String label, String value, double progress) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(label), - Text(value, style: const TextStyle(fontWeight: FontWeight.bold)), - ], - ), - const SizedBox(height: 8), - LinearProgressIndicator( - value: progress, - backgroundColor: AppColors.border, - valueColor: const AlwaysStoppedAnimation(AppColors.primary), - ), - ], + Widget _buildExpirationCard(contribution) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题 + Row( + children: const [ + Icon(Icons.timer_outlined, color: _orange, size: 24), + SizedBox(width: 8), + Text( + '贡献值失效倒计时', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: _darkText), + ), + ], + ), + const SizedBox(height: 12), + // 进度条 + ClipRRect( + borderRadius: BorderRadius.circular(5), + child: LinearProgressIndicator( + value: 0.8, + minHeight: 10, + backgroundColor: _bgGray, + valueColor: const AlwaysStoppedAnimation(_orange), + ), + ), + const SizedBox(height: 12), + // 说明文字 + const Text( + '您的贡献值将于 2026-01-15 失效', + style: TextStyle(fontSize: 12, color: _grayText), + ), + const SizedBox(height: 8), + // 提示 + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: _bgGray, + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + '* 运营账号贡献值永不失效', + style: TextStyle(fontSize: 10, color: _orange), + ), + ), + ], + ), ); } } diff --git a/frontend/mining-app/lib/presentation/pages/profile/profile_page.dart b/frontend/mining-app/lib/presentation/pages/profile/profile_page.dart index 1cad4cdd..6c58aa6f 100644 --- a/frontend/mining-app/lib/presentation/pages/profile/profile_page.dart +++ b/frontend/mining-app/lib/presentation/pages/profile/profile_page.dart @@ -1,206 +1,708 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import '../../../core/constants/app_colors.dart'; import '../../../core/router/routes.dart'; import '../../providers/user_providers.dart'; class ProfilePage extends ConsumerWidget { const ProfilePage({super.key}); + // 设计色彩 + static const Color _orange = Color(0xFFFF6B00); + static const Color _green = Color(0xFF10B981); + static const Color _darkText = Color(0xFF1F2937); + static const Color _grayText = Color(0xFF6B7280); + static const Color _lightGray = Color(0xFF9CA3AF); + static const Color _bgGray = Color(0xFFF3F4F6); + static const Color _red = Color(0xFFEF4444); + @override Widget build(BuildContext context, WidgetRef ref) { final user = ref.watch(userNotifierProvider); return Scaffold( - appBar: AppBar( - title: const Text('我的'), - backgroundColor: AppColors.primary, - foregroundColor: Colors.white, - ), - body: SingleChildScrollView( - child: Column( - children: [ - // 用户信息卡片 - Container( - width: double.infinity, - padding: const EdgeInsets.all(24), - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [AppColors.primary, Color(0xFF16A34A)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, + backgroundColor: _bgGray, + body: SafeArea( + child: SingleChildScrollView( + child: Column( + children: [ + // 用户头部信息 + _buildUserHeader(context, user), + + const SizedBox(height: 16), + + // 统计数据行 + _buildStatsRow(), + + const SizedBox(height: 16), + + // 邀请码卡片 + _buildInvitationCard(context), + + const SizedBox(height: 16), + + // 账户设置 + _buildAccountSettings(context), + + const SizedBox(height: 16), + + // 记录入口 + _buildRecordsSection(context), + + const SizedBox(height: 16), + + // 团队与收益 + _buildTeamEarningsSection(context), + + const SizedBox(height: 16), + + // 其他设置 + _buildOtherSettings(context), + + const SizedBox(height: 24), + + // 退出登录 + _buildLogoutButton(context, ref), + + const SizedBox(height: 16), + + // 版本信息 + const Text( + 'Version 1.0.0', + style: TextStyle( + fontSize: 12, + color: _lightGray, ), ), - child: Column( - children: [ - CircleAvatar( - radius: 40, - backgroundColor: Colors.white, - child: Text( - user.nickname?.substring(0, 1) ?? 'U', - style: const TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - color: AppColors.primary, - ), - ), - ), - const SizedBox(height: 12), - Text( - user.nickname ?? '未设置昵称', - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Text( - '账户序列: ${user.accountSequence ?? '-'}', - style: const TextStyle(color: Colors.white70, fontSize: 14), - ), - ], - ), - ), - const SizedBox(height: 16), - - // 菜单列表 - _buildMenuSection( - title: '资产管理', - items: [ - _MenuItem( - icon: Icons.history, - label: '挖矿记录', - onTap: () {}, - ), - _MenuItem( - icon: Icons.receipt_long, - label: '交易记录', - onTap: () {}, - ), - _MenuItem( - icon: Icons.swap_vert, - label: '划转记录', - onTap: () {}, - ), - ], - ), - - _buildMenuSection( - title: '账户设置', - items: [ - _MenuItem( - icon: Icons.person_outline, - label: '个人信息', - onTap: () {}, - ), - _MenuItem( - icon: Icons.lock_outline, - label: '修改密码', - onTap: () => context.push(Routes.changePassword), - ), - _MenuItem( - icon: Icons.security, - label: '安全设置', - onTap: () {}, - ), - _MenuItem( - icon: Icons.notifications_outlined, - label: '消息通知', - onTap: () {}, - ), - ], - ), - - _buildMenuSection( - title: '其他', - items: [ - _MenuItem( - icon: Icons.help_outline, - label: '帮助中心', - onTap: () {}, - ), - _MenuItem( - icon: Icons.info_outline, - label: '关于我们', - onTap: () {}, - ), - ], - ), - - const SizedBox(height: 24), - - // 退出登录 - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: OutlinedButton( - onPressed: () { - ref.read(userNotifierProvider.notifier).logout(); - }, - style: OutlinedButton.styleFrom( - foregroundColor: AppColors.error, - side: const BorderSide(color: AppColors.error), - padding: const EdgeInsets.symmetric(vertical: 12), - minimumSize: const Size(double.infinity, 48), - ), - child: const Text('退出登录'), - ), - ), - - const SizedBox(height: 32), - ], + const SizedBox(height: 32), + ], + ), ), ), ); } - Widget _buildMenuSection({ - required String title, - required List<_MenuItem> items, - }) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Text( - title, - style: const TextStyle( - color: AppColors.textSecondary, - fontSize: 12, + Widget _buildUserHeader(BuildContext context, dynamic user) { + return Container( + padding: const EdgeInsets.all(20), + color: Colors.white, + child: Row( + children: [ + // 头像 + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: [_orange.withValues(alpha: 0.8), _orange], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Center( + child: Text( + user.nickname?.isNotEmpty == true + ? user.nickname!.substring(0, 1).toUpperCase() + : 'U', + style: const TextStyle( + fontSize: 36, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), ), ), + + const SizedBox(width: 16), + + // 用户信息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + user.nickname ?? '榴莲用户', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: _darkText, + ), + ), + const SizedBox(width: 8), + // VIP 徽章 + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFFFFD700), Color(0xFFFF8C00)], + ), + borderRadius: BorderRadius.circular(10), + ), + child: const Text( + 'VIP 3', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Text( + 'ID: ${user.accountSequence ?? '--------'}', + style: const TextStyle( + fontSize: 14, + color: _grayText, + ), + ), + const SizedBox(width: 8), + GestureDetector( + onTap: () { + if (user.accountSequence != null) { + Clipboard.setData( + ClipboardData(text: user.accountSequence!), + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('ID已复制'), + duration: Duration(seconds: 1), + ), + ); + } + }, + child: const Icon( + Icons.copy, + size: 16, + color: _grayText, + ), + ), + ], + ), + ], + ), + ), + + // 编辑按钮 + IconButton( + onPressed: () { + // TODO: 编辑个人资料 + }, + icon: const Icon( + Icons.edit_outlined, + color: _grayText, + ), + ), + ], + ), + ); + } + + Widget _buildStatsRow() { + return Container( + padding: const EdgeInsets.symmetric(vertical: 16), + color: Colors.white, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildStatItem('认种数量', '10'), + _buildDivider(), + _buildStatItem('直推人数', '5'), + _buildDivider(), + _buildStatItem('团队人数', '128'), + _buildDivider(), + _buildStatItem('VIP等级', 'V3'), + ], + ), + ); + } + + Widget _buildStatItem(String label, String value) { + return Column( + children: [ + Text( + value, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: _darkText, + ), ), - Card( - margin: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - children: items.map((item) => _buildMenuItem(item)).toList(), + const SizedBox(height: 4), + Text( + label, + style: const TextStyle( + fontSize: 12, + color: _grayText, ), ), ], ); } - Widget _buildMenuItem(_MenuItem item) { - return ListTile( - leading: Icon(item.icon, color: AppColors.textSecondary), - title: Text(item.label), - trailing: const Icon(Icons.chevron_right, color: AppColors.textMuted), - onTap: item.onTap, + Widget _buildDivider() { + return Container( + width: 1, + height: 30, + color: _bgGray, + ); + } + + Widget _buildInvitationCard(BuildContext context) { + const invitationCode = 'DUR8888XYZ'; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '我的邀请码', + style: TextStyle( + fontSize: 14, + color: _grayText, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + color: _bgGray, + borderRadius: BorderRadius.circular(8), + ), + child: const Text( + invitationCode, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: _darkText, + letterSpacing: 2, + ), + ), + ), + ), + const SizedBox(width: 12), + _buildActionButton( + icon: Icons.copy, + label: '复制', + onTap: () { + Clipboard.setData(const ClipboardData(text: invitationCode)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('邀请码已复制'), + duration: Duration(seconds: 1), + ), + ); + }, + ), + const SizedBox(width: 8), + _buildActionButton( + icon: Icons.share, + label: '分享', + onTap: () { + // TODO: 分享功能 + }, + ), + ], + ), + ], + ), + ); + } + + Widget _buildActionButton({ + required IconData icon, + required String label, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: _orange, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon(icon, size: 16, color: Colors.white), + const SizedBox(width: 4), + Text( + label, + style: const TextStyle( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } + + Widget _buildAccountSettings(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Text( + '账户设置', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: _darkText, + ), + ), + ), + _buildSettingItem( + icon: Icons.security, + label: '账户安全', + onTap: () => context.push(Routes.changePassword), + ), + _buildSettingItem( + icon: Icons.verified_user, + label: '实名认证', + trailing: const Text( + '已认证', + style: TextStyle( + fontSize: 14, + color: _green, + ), + ), + onTap: () {}, + ), + _buildSettingItem( + icon: Icons.lock_outline, + label: '支付密码', + trailing: const Text( + '已设置', + style: TextStyle( + fontSize: 14, + color: _green, + ), + ), + onTap: () {}, + showDivider: false, + ), + ], + ), + ); + } + + Widget _buildSettingItem({ + required IconData icon, + required String label, + Widget? trailing, + required VoidCallback onTap, + bool showDivider = true, + }) { + return Column( + children: [ + ListTile( + leading: Icon(icon, color: _grayText, size: 22), + title: Text( + label, + style: const TextStyle( + fontSize: 14, + color: _darkText, + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (trailing != null) trailing, + if (trailing != null) const SizedBox(width: 8), + const Icon(Icons.chevron_right, color: _lightGray, size: 20), + ], + ), + onTap: onTap, + ), + if (showDivider) + const Divider(height: 1, indent: 56, endIndent: 16), + ], + ); + } + + Widget _buildRecordsSection(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '我的记录', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: _darkText, + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildRecordIcon( + icon: Icons.eco, + label: '认种记录', + onTap: () {}, + ), + _buildRecordIcon( + icon: Icons.assignment, + label: '分配记录', + onTap: () {}, + ), + _buildRecordIcon( + icon: Icons.receipt_long, + label: '交易记录', + onTap: () {}, + ), + _buildRecordIcon( + icon: Icons.account_balance_wallet, + label: '提现记录', + onTap: () {}, + ), + ], + ), + ], + ), + ); + } + + Widget _buildRecordIcon({ + required IconData icon, + required String label, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Column( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: _orange.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: _orange, size: 24), + ), + const SizedBox(height: 8), + Text( + label, + style: const TextStyle( + fontSize: 12, + color: _grayText, + ), + ), + ], + ), + ); + } + + Widget _buildTeamEarningsSection(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Text( + '团队与收益', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: _darkText, + ), + ), + ), + _buildSettingItem( + icon: Icons.people, + label: '我的团队', + onTap: () {}, + ), + _buildSettingItem( + icon: Icons.trending_up, + label: '收益明细', + onTap: () {}, + ), + _buildSettingItem( + icon: Icons.card_giftcard, + label: '推广奖励', + onTap: () {}, + showDivider: false, + ), + ], + ), + ); + } + + Widget _buildOtherSettings(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Text( + '其他设置', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: _darkText, + ), + ), + ), + _buildSwitchItem( + icon: Icons.notifications_outlined, + label: '消息通知', + value: true, + onChanged: (value) {}, + ), + _buildSwitchItem( + icon: Icons.dark_mode_outlined, + label: '深色模式', + value: false, + onChanged: (value) {}, + ), + _buildSettingItem( + icon: Icons.help_outline, + label: '帮助中心', + onTap: () {}, + ), + _buildSettingItem( + icon: Icons.info_outline, + label: '关于我们', + onTap: () {}, + showDivider: false, + ), + ], + ), + ); + } + + Widget _buildSwitchItem({ + required IconData icon, + required String label, + required bool value, + required ValueChanged onChanged, + }) { + return Column( + children: [ + ListTile( + leading: Icon(icon, color: _grayText, size: 22), + title: Text( + label, + style: const TextStyle( + fontSize: 14, + color: _darkText, + ), + ), + trailing: Switch( + value: value, + onChanged: onChanged, + activeTrackColor: _orange, + activeThumbColor: Colors.white, + ), + ), + const Divider(height: 1, indent: 56, endIndent: 16), + ], + ); + } + + Widget _buildLogoutButton(BuildContext context, WidgetRef ref) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SizedBox( + width: double.infinity, + child: TextButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('退出登录'), + content: const Text('确定要退出登录吗?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + ref.read(userNotifierProvider.notifier).logout(); + }, + child: const Text( + '确定', + style: TextStyle(color: _red), + ), + ), + ], + ), + ); + }, + style: TextButton.styleFrom( + backgroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + '退出登录', + style: TextStyle( + fontSize: 16, + color: _red, + fontWeight: FontWeight.w500, + ), + ), + ), + ), ); } } - -class _MenuItem { - final IconData icon; - final String label; - final VoidCallback onTap; - - _MenuItem({ - required this.icon, - required this.label, - required this.onTap, - }); -} diff --git a/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart b/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart index 21917622..8ef1ad2d 100644 --- a/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart +++ b/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart @@ -6,263 +6,615 @@ import '../../providers/mining_providers.dart'; import '../../providers/user_providers.dart'; import '../../providers/trading_providers.dart'; -class TradingPage extends ConsumerWidget { +class TradingPage extends ConsumerStatefulWidget { const TradingPage({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _TradingPageState(); +} + +class _TradingPageState extends ConsumerState { + // 设计色彩 + static const Color _orange = Color(0xFFFF6B00); + static const Color _green = Color(0xFF10B981); + static const Color _red = Color(0xFFEF4444); + static const Color _grayText = Color(0xFF6B7280); + static const Color _darkText = Color(0xFF1F2937); + static const Color _bgGray = Color(0xFFF3F4F6); + static const Color _lightGray = Color(0xFFF9FAFB); + static const Color _borderGray = Color(0xFFE5E7EB); + + // 状态 + int _selectedTab = 1; // 0: 买入, 1: 卖出 + int _selectedTimeRange = 1; // 时间周期选择 + final _amountController = TextEditingController(); + + final List _timeRanges = ['1分', '5分', '15分', '30分', '1时', '4时', '日']; + + @override + void dispose() { + _amountController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { final globalState = ref.watch(globalStateProvider); final user = ref.watch(userNotifierProvider); final accountSequence = user.accountSequence ?? ''; - final accountAsync = ref.watch(shareAccountProvider(accountSequence)); return Scaffold( - appBar: AppBar( - title: const Text('兑换'), - backgroundColor: AppColors.primary, - foregroundColor: Colors.white, - ), - body: SingleChildScrollView( - padding: const EdgeInsets.all(16), + backgroundColor: const Color(0xFFF5F5F5), + body: SafeArea( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 价格卡片 - globalState.when( - data: (state) { - if (state == null) return const SizedBox.shrink(); - return _buildPriceCard(state); - }, - loading: () => const Card( - child: Padding( - padding: EdgeInsets.all(32), - child: Center(child: CircularProgressIndicator()), - ), - ), - error: (_, __) => const Card( - child: Padding( - padding: EdgeInsets.all(32), - child: Center(child: Text('加载失败')), + // 顶部导航栏 + _buildAppBar(), + // 可滚动内容 + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + // 价格卡片 + globalState.when( + data: (state) => _buildPriceCard(state), + loading: () => _buildLoadingCard(), + error: (_, __) => _buildErrorCard('价格加载失败'), + ), + // K线图占位区域 + _buildChartSection(), + // 市场数据 + globalState.when( + data: (state) => _buildMarketDataCard(state), + loading: () => _buildLoadingCard(), + error: (_, __) => _buildErrorCard('市场数据加载失败'), + ), + // 买入/卖出交易面板 + _buildTradingPanel(accountSequence), + // 我的挂单 + _buildMyOrdersCard(), + const SizedBox(height: 24), + ], ), ), ), + ], + ), + ), + ); + } - const SizedBox(height: 24), - - // 账户余额 - const Text( - '账户余额', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + Widget _buildAppBar() { + return Container( + color: _bgGray.withOpacity(0.9), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // 积分股交易按钮 + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: _orange, + borderRadius: BorderRadius.circular(9999), ), - const SizedBox(height: 12), - accountAsync.when( - data: (account) { - if (account == null) return const SizedBox.shrink(); - return _buildBalanceCard(account); - }, - loading: () => const Card( - child: Padding( - padding: EdgeInsets.all(32), - child: Center(child: CircularProgressIndicator()), - ), - ), - error: (_, __) => const Card( - child: Padding( - padding: EdgeInsets.all(32), - child: Center(child: Text('加载失败')), - ), + child: const Text( + '积分股交易', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, ), ), - - const SizedBox(height: 24), - - // 买卖按钮 - Row( + ), + // 通知图标 + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Stack( children: [ - Expanded( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.up, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - onPressed: () => _showBuyDialog(context, ref, accountSequence), - child: const Text( - '买入', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - ), + const Center( + child: Icon(Icons.notifications_outlined, color: _grayText), ), - const SizedBox(width: 16), - Expanded( - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.down, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - onPressed: () => _showSellDialog(context, ref, accountSequence), - child: const Text( - '卖出', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + Positioned( + right: 10, + top: 10, + child: Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, ), ), ), ], ), - - const SizedBox(height: 24), - - // 交易说明 - Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '交易说明', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 12), - _buildInfoRow('买入', '使用绿积分购买积分股'), - _buildInfoRow('卖出', '出售积分股获得绿积分'), - _buildInfoRow('划转', '需先将挖矿账户余额划转到交易账户'), - ], - ), - ), - ), - ], - ), + ), + ], ), ); } Widget _buildPriceCard(state) { - final isPriceUp = state.isPriceUp; - return Card( - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - children: [ - const Text( - '当前价格', - style: TextStyle(color: AppColors.textSecondary, fontSize: 14), - ), - const SizedBox(height: 8), - Text( - formatPrice(state.currentPrice), - style: TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - color: isPriceUp ? AppColors.up : AppColors.down, - ), - ), - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - decoration: BoxDecoration( - color: (isPriceUp ? AppColors.up : AppColors.down).withOpacity(0.1), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - '${isPriceUp ? '+' : ''}${formatPercent(state.priceChange24h)} 24h', + final isPriceUp = state?.isPriceUp ?? true; + final currentPrice = state?.currentPrice ?? '0.000156'; + final priceChange = state?.priceChange24h ?? '8.52'; + + return Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题行 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '当前积分股价格', style: TextStyle( - color: isPriceUp ? AppColors.up : AppColors.down, fontSize: 12, + fontWeight: FontWeight.w500, + color: _grayText, ), ), - ), - ], - ), - ), - ); - } - - Widget _buildBalanceCard(account) { - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '挖矿账户', - style: TextStyle(color: AppColors.textSecondary, fontSize: 12), - ), - const SizedBox(height: 4), - Text( - formatAmount(account.miningBalance), - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ], + Text( + '= 156.00 绿积分', + style: TextStyle( + fontSize: 12, + color: _grayText, + ), ), - ), - Container( - width: 1, - height: 40, - color: AppColors.border, - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ], + ), + const SizedBox(height: 8), + // 价格和涨跌 + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + '¥ ${formatPrice(currentPrice)}', + style: const TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + color: _orange, + letterSpacing: -0.75, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: _green.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + ), + child: Row( + mainAxisSize: MainAxisSize.min, children: [ - const Text( - '交易账户', - style: TextStyle(color: AppColors.textSecondary, fontSize: 12), + Icon( + isPriceUp ? Icons.trending_up : Icons.trending_down, + size: 16, + color: _green, ), - const SizedBox(height: 4), Text( - formatAmount(account.tradingBalance), - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + '+$priceChange%', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: _green, + ), ), ], ), ), - ), - ], - ), + ], + ), + ], ), ); } - Widget _buildInfoRow(String title, String description) { - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, + Widget _buildChartSection() { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Column( children: [ + // K线图占位 Container( - width: 6, - height: 6, - margin: const EdgeInsets.only(top: 6), - decoration: const BoxDecoration( - color: AppColors.primary, - shape: BoxShape.circle, + height: 200, + decoration: BoxDecoration( + color: _lightGray, + borderRadius: BorderRadius.circular(8), + ), + child: Stack( + children: [ + // 模拟K线图 + CustomPaint( + size: const Size(double.infinity, 200), + painter: _CandlestickPainter(), + ), + // 当前价格标签 + Positioned( + right: 0, + top: 60, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + decoration: BoxDecoration( + color: _orange, + borderRadius: BorderRadius.circular(4), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: const Text( + '0.000156', + style: TextStyle( + fontSize: 10, + color: Colors.white, + ), + ), + ), + ), + ], ), ), - const SizedBox(width: 8), - Expanded( - child: RichText( - text: TextSpan( - style: const TextStyle(color: AppColors.textSecondary, fontSize: 13), - children: [ - TextSpan( - text: '$title: ', - style: const TextStyle(fontWeight: FontWeight.w500), + const SizedBox(height: 16), + // 时间周期选择 + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate(_timeRanges.length, (index) { + final isSelected = _selectedTimeRange == index; + return Padding( + padding: const EdgeInsets.only(right: 8), + child: GestureDetector( + onTap: () => setState(() => _selectedTimeRange = index), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: BoxDecoration( + color: isSelected ? _orange : Colors.white, + borderRadius: BorderRadius.circular(9999), + border: isSelected ? null : Border.all(color: _borderGray), + ), + child: Text( + _timeRanges[index], + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: isSelected ? Colors.white : _grayText, + ), + ), + ), + ), + ); + }), + ), + ), + ], + ), + ); + } + + Widget _buildMarketDataCard(state) { + return Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题 + Row( + children: [ + Container( + width: 4, + height: 16, + decoration: BoxDecoration( + color: _orange, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 8), + const Text( + '市场数据', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: _darkText, + ), + ), + ], + ), + const SizedBox(height: 24), + // 第一行数据 + Row( + children: [ + _buildMarketDataItem('积分股池', '8,888,888,888', _orange), + Container(width: 1, height: 24, color: _bgGray), + const SizedBox(width: 16), + _buildMarketDataItem('流通池', '1,234,567', _orange), + ], + ), + const SizedBox(height: 24), + Container(height: 1, color: _bgGray), + const SizedBox(height: 24), + // 第二行数据 + Row( + children: [ + _buildMarketDataItem('绿积分池', '99,999,999', _orange), + Container(width: 1, height: 24, color: _bgGray), + const SizedBox(width: 16), + _buildMarketDataItem('黑洞销毁量', '50,000,000', _red), + ], + ), + ], + ), + ); + } + + Widget _buildMarketDataItem(String label, String value, Color valueColor) { + return Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 12, + color: _grayText, + ), + ), + const SizedBox(height: 4), + Text( + value, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: valueColor, + ), + ), + ], + ), + ); + } + + Widget _buildTradingPanel(String accountSequence) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + // 买入/卖出切换 + Container( + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(color: _bgGray)), + ), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => setState(() => _selectedTab = 0), + child: Container( + padding: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: _selectedTab == 0 ? _orange : Colors.transparent, + width: 2, + ), + ), + ), + child: Text( + '买入', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: _selectedTab == 0 ? _orange : _grayText, + ), + ), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () => setState(() => _selectedTab = 1), + child: Container( + padding: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: _selectedTab == 1 ? _orange : Colors.transparent, + width: 2, + ), + ), + ), + child: Text( + '卖出', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: _selectedTab == 1 ? _orange : _grayText, + ), + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 24), + // 数量输入 + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '数量', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: _grayText, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Container( + height: 44, + decoration: BoxDecoration( + color: _bgGray, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _amountController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: '请输入数量', + hintStyle: TextStyle( + fontSize: 14, + color: Color(0xFF9CA3AF), + ), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 16), + ), + ), + ), + const Icon(Icons.currency_yuan, size: 15, color: _grayText), + const SizedBox(width: 12), + ], + ), + ), + ), + const SizedBox(width: 12), + GestureDetector( + onTap: () { + // TODO: 设置最大数量 + }, + child: const Text( + 'MAX', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: _orange, + ), + ), ), - TextSpan(text: description), ], ), + ], + ), + const SizedBox(height: 16), + // 预计获得 + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: _bgGray, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '预计获得', + style: TextStyle( + fontSize: 12, + color: _grayText, + ), + ), + const Text( + '0.00 绿积分', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: _orange, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + // 手续费 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + Text( + '手续费 (10%)', + style: TextStyle( + fontSize: 12, + color: _grayText, + ), + ), + Text( + '0.00', + style: TextStyle( + fontSize: 12, + color: _grayText, + fontFamily: 'monospace', + ), + ), + ], + ), + ), + const SizedBox(height: 24), + // 提交按钮 + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: () => _handleTrade(accountSequence), + style: ElevatedButton.styleFrom( + backgroundColor: _orange, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + _selectedTab == 0 ? '买入积分股' : '卖出积分股', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), ), ), ], @@ -270,101 +622,296 @@ class TradingPage extends ConsumerWidget { ); } - void _showBuyDialog(BuildContext context, WidgetRef ref, String accountSequence) { - final controller = TextEditingController(); - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('买入积分股'), - content: TextField( - controller: controller, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - decoration: const InputDecoration( - labelText: '买入数量', - hintText: '请输入买入数量', - suffixText: '股', + Widget _buildMyOrdersCard() { + return Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + // 标题 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '我的挂单', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: _darkText, + ), + ), + GestureDetector( + onTap: () { + // TODO: 查看全部挂单 + }, + child: const Text( + '全部 >', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: _orange, + ), + ), + ), + ], ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('取消'), - ), - ElevatedButton( - onPressed: () async { - if (controller.text.isNotEmpty) { - final success = await ref - .read(tradingNotifierProvider.notifier) - .buyShares(accountSequence, controller.text); - if (context.mounted) { - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(success ? '买入订单已提交' : '买入失败'), - backgroundColor: success ? AppColors.up : AppColors.error, - ), - ); - if (success) { - ref.invalidate(shareAccountProvider(accountSequence)); - } - } - } - }, - style: ElevatedButton.styleFrom(backgroundColor: AppColors.up), - child: const Text('确认买入'), + const SizedBox(height: 16), + // 挂单列表项 + _buildOrderItem( + type: '卖出', + price: '0.000156', + quantity: '1,000 股', + time: '12/05 14:30', + status: '待成交', ), ], ), ); } - void _showSellDialog(BuildContext context, WidgetRef ref, String accountSequence) { - final controller = TextEditingController(); - - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('卖出积分股'), - content: TextField( - controller: controller, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - decoration: const InputDecoration( - labelText: '卖出数量', - hintText: '请输入卖出数量', - suffixText: '股', - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('取消'), - ), - ElevatedButton( - onPressed: () async { - if (controller.text.isNotEmpty) { - final success = await ref - .read(tradingNotifierProvider.notifier) - .sellShares(accountSequence, controller.text); - if (context.mounted) { - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(success ? '卖出订单已提交' : '卖出失败'), - backgroundColor: success ? AppColors.down : AppColors.error, + Widget _buildOrderItem({ + required String type, + required String price, + required String quantity, + required String time, + required String status, + }) { + final isSell = type == '卖出'; + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: _lightGray, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: _bgGray), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: (isSell ? _red : _green).withOpacity(0.1), + borderRadius: BorderRadius.circular(6), ), - ); - if (success) { - ref.invalidate(shareAccountProvider(accountSequence)); - } - } - } - }, - style: ElevatedButton.styleFrom(backgroundColor: AppColors.down), - child: const Text('确认卖出'), + child: Text( + type, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: isSell ? _red : _green, + ), + ), + ), + const SizedBox(width: 8), + Text( + price, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: _darkText, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + time, + style: const TextStyle( + fontSize: 12, + color: _grayText, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + quantity, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: _darkText, + ), + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: _orange.withOpacity(0.1), + borderRadius: BorderRadius.circular(9999), + ), + child: Text( + status, + style: const TextStyle( + fontSize: 12, + color: _orange, + ), + ), + ), + ], ), ], ), ); } + + Widget _buildLoadingCard() { + return Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: const Center(child: CircularProgressIndicator()), + ); + } + + Widget _buildErrorCard(String message) { + return Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Center( + child: Column( + children: [ + const Icon(Icons.error_outline, size: 48, color: AppColors.error), + const SizedBox(height: 8), + Text(message), + ], + ), + ), + ); + } + + void _handleTrade(String accountSequence) async { + if (_amountController.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('请输入数量')), + ); + return; + } + + final isBuy = _selectedTab == 0; + bool success; + + if (isBuy) { + success = await ref + .read(tradingNotifierProvider.notifier) + .buyShares(accountSequence, _amountController.text); + } else { + success = await ref + .read(tradingNotifierProvider.notifier) + .sellShares(accountSequence, _amountController.text); + } + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(success + ? (isBuy ? '买入订单已提交' : '卖出订单已提交') + : (isBuy ? '买入失败' : '卖出失败')), + backgroundColor: success ? _green : AppColors.error, + ), + ); + if (success) { + _amountController.clear(); + ref.invalidate(shareAccountProvider(accountSequence)); + } + } + } +} + +// K线图绘制器(简化版本,显示模拟数据) +class _CandlestickPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final greenPaint = Paint()..color = const Color(0xFF10B981); + final redPaint = Paint()..color = const Color(0xFFEF4444); + final dashPaint = Paint() + ..color = const Color(0xFFFF6B00) + ..strokeWidth = 1 + ..style = PaintingStyle.stroke; + + // 模拟K线数据 + final candleData = [ + {'open': 0.6, 'close': 0.5, 'high': 0.7, 'low': 0.45}, + {'open': 0.5, 'close': 0.55, 'high': 0.6, 'low': 0.48}, + {'open': 0.55, 'close': 0.52, 'high': 0.58, 'low': 0.5}, + {'open': 0.52, 'close': 0.6, 'high': 0.65, 'low': 0.5}, + {'open': 0.6, 'close': 0.58, 'high': 0.65, 'low': 0.55}, + {'open': 0.58, 'close': 0.62, 'high': 0.68, 'low': 0.55}, + {'open': 0.62, 'close': 0.55, 'high': 0.65, 'low': 0.52}, + {'open': 0.55, 'close': 0.58, 'high': 0.62, 'low': 0.52}, + {'open': 0.58, 'close': 0.52, 'high': 0.6, 'low': 0.5}, + {'open': 0.52, 'close': 0.65, 'high': 0.7, 'low': 0.5}, + {'open': 0.65, 'close': 0.7, 'high': 0.75, 'low': 0.62}, + {'open': 0.7, 'close': 0.75, 'high': 0.8, 'low': 0.68}, + ]; + + final candleWidth = (size.width - 40) / candleData.length; + final padding = 20.0; + + for (int i = 0; i < candleData.length; i++) { + final data = candleData[i]; + final open = data['open']!; + final close = data['close']!; + final high = data['high']!; + final low = data['low']!; + + final isGreen = close >= open; + final paint = isGreen ? greenPaint : redPaint; + + final x = padding + i * candleWidth + candleWidth / 2; + final yOpen = size.height - (open * size.height * 0.8 + size.height * 0.1); + final yClose = size.height - (close * size.height * 0.8 + size.height * 0.1); + final yHigh = size.height - (high * size.height * 0.8 + size.height * 0.1); + final yLow = size.height - (low * size.height * 0.8 + size.height * 0.1); + + // 绘制影线 + canvas.drawLine( + Offset(x, yHigh), + Offset(x, yLow), + paint..strokeWidth = 1, + ); + + // 绘制实体 + final bodyTop = isGreen ? yClose : yOpen; + final bodyBottom = isGreen ? yOpen : yClose; + canvas.drawRect( + Rect.fromLTRB(x - candleWidth * 0.3, bodyTop, x + candleWidth * 0.3, bodyBottom), + paint..style = PaintingStyle.fill, + ); + } + + // 绘制虚线参考线 + final dashY = size.height * 0.35; + const dashWidth = 5.0; + const dashSpace = 3.0; + double startX = 0; + while (startX < size.width - 60) { + canvas.drawLine( + Offset(startX, dashY), + Offset(startX + dashWidth, dashY), + dashPaint, + ); + startX += dashWidth + dashSpace; + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } diff --git a/frontend/mining-app/lib/presentation/widgets/main_shell.dart b/frontend/mining-app/lib/presentation/widgets/main_shell.dart index 697feca9..079cfd0b 100644 --- a/frontend/mining-app/lib/presentation/widgets/main_shell.dart +++ b/frontend/mining-app/lib/presentation/widgets/main_shell.dart @@ -1,38 +1,112 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../../core/router/routes.dart'; -import '../../core/constants/app_colors.dart'; class MainShell extends StatelessWidget { final Widget child; const MainShell({super.key, required this.child}); + // 设计色彩 + static const Color _orange = Color(0xFFFF6B00); + static const Color _grayText = Color(0xFF9CA3AF); + @override Widget build(BuildContext context) { return Scaffold( body: child, - bottomNavigationBar: BottomNavigationBar( - type: BottomNavigationBarType.fixed, - selectedItemColor: AppColors.primary, - unselectedItemColor: AppColors.textMuted, - items: const [ - BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: '首页'), - BottomNavigationBarItem(icon: Icon(Icons.analytics_outlined), activeIcon: Icon(Icons.analytics), label: '算力'), - BottomNavigationBarItem(icon: Icon(Icons.swap_horiz_outlined), activeIcon: Icon(Icons.swap_horiz), label: '兑换'), - BottomNavigationBarItem(icon: Icon(Icons.person_outline), activeIcon: Icon(Icons.person), label: '我的'), - ], - currentIndex: _calculateSelectedIndex(context), - onTap: (index) => _onItemTapped(index, context), + bottomNavigationBar: Container( + decoration: const BoxDecoration( + color: Colors.white, + border: Border( + top: BorderSide(color: Color(0xFFF3F4F6), width: 1), + ), + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildNavItem( + context, + icon: Icons.favorite_outline, + activeIcon: Icons.favorite, + label: '贡献值', + index: 0, + ), + _buildNavItem( + context, + icon: Icons.swap_horiz_outlined, + activeIcon: Icons.swap_horiz, + label: '兑换', + index: 1, + ), + _buildNavItem( + context, + icon: Icons.account_balance_wallet_outlined, + activeIcon: Icons.account_balance_wallet, + label: '资产', + index: 2, + ), + _buildNavItem( + context, + icon: Icons.person_outline, + activeIcon: Icons.person, + label: '我的', + index: 3, + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildNavItem( + BuildContext context, { + required IconData icon, + required IconData activeIcon, + required String label, + required int index, + }) { + final currentIndex = _calculateSelectedIndex(context); + final isSelected = currentIndex == index; + + return GestureDetector( + onTap: () => _onItemTapped(index, context), + behavior: HitTestBehavior.opaque, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isSelected ? activeIcon : icon, + size: 24, + color: isSelected ? _orange : _grayText, + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + fontSize: 10, + fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal, + color: isSelected ? _orange : _grayText, + ), + ), + ], + ), ), ); } int _calculateSelectedIndex(BuildContext context) { final location = GoRouterState.of(context).uri.path; - if (location.startsWith(Routes.home)) return 0; - if (location.startsWith(Routes.contribution)) return 1; - if (location.startsWith(Routes.trading)) return 2; + if (location.startsWith(Routes.contribution)) return 0; + if (location.startsWith(Routes.trading)) return 1; + if (location.startsWith(Routes.asset)) return 2; if (location.startsWith(Routes.profile)) return 3; return 0; } @@ -40,14 +114,14 @@ class MainShell extends StatelessWidget { void _onItemTapped(int index, BuildContext context) { switch (index) { case 0: - context.go(Routes.home); - break; - case 1: context.go(Routes.contribution); break; - case 2: + case 1: context.go(Routes.trading); break; + case 2: + context.go(Routes.asset); + break; case 3: context.go(Routes.profile); break;