From 02099d2a6ace87d36058321567ae1a02a38841ab Mon Sep 17 00:00:00 2001 From: sion123 <450702724@qq.com> Date: Sun, 5 Apr 2026 22:38:56 +0800 Subject: [PATCH] docs: relocate skills system documentation and refactor asset page components Move skills system documentation from bottom to top of CLAUDE.md for better organization. Refactor Flutter asset page by extracting UI components into separate files and updating import structure for improved modularity. --- CLAUDE.md | 7 +- .../lib/ui/pages/asset/asset_page.dart | 1159 +---------------- .../components/account_tab_switcher.dart | 95 ++ .../asset/components/action_buttons_row.dart | 113 ++ .../pages/asset/components/asset_dialogs.dart | 602 +++++++++ .../pages/asset/components/balance_card.dart | 84 ++ .../asset/components/holdings_section.dart | 208 +++ .../asset/components/records_link_row.dart | 49 + .../lib/ui/pages/asset/transfer_page.dart | 410 +++--- .../mine/components/about_dialog_helpers.dart | 29 + .../pages/mine/components/avatar_circle.dart | 36 + .../pages/mine/components/logout_button.dart | 38 + .../ui/pages/mine/components/menu_group1.dart | 105 ++ .../ui/pages/mine/components/menu_group2.dart | 44 + .../mine/components/menu_group_container.dart | 33 + .../ui/pages/mine/components/menu_row.dart | 88 ++ .../components/menu_trailing_widgets.dart | 196 +++ .../pages/mine/components/profile_card.dart | 73 ++ .../lib/ui/pages/mine/mine_page.dart | 628 +-------- .../ui/pages/mine/welfare_center_page.dart | 556 ++++---- .../lib/ui/pages/orders/fund_orders_page.dart | 344 ++--- .../pages/trade/components/amount_input.dart | 113 ++ .../pages/trade/components/coin_avatar.dart | 32 + .../pages/trade/components/coin_selector.dart | 234 ++++ .../trade/components/confirm_dialog.dart | 114 ++ .../trade/components/placeholder_card.dart | 41 + .../ui/pages/trade/components/price_card.dart | 88 ++ .../pages/trade/components/trade_button.dart | 64 + .../trade/components/trade_form_card.dart | 247 ++++ .../lib/ui/pages/trade/trade_page.dart | 917 +------------ 30 files changed, 3317 insertions(+), 3430 deletions(-) create mode 100644 flutter_monisuo/lib/ui/pages/asset/components/account_tab_switcher.dart create mode 100644 flutter_monisuo/lib/ui/pages/asset/components/action_buttons_row.dart create mode 100644 flutter_monisuo/lib/ui/pages/asset/components/asset_dialogs.dart create mode 100644 flutter_monisuo/lib/ui/pages/asset/components/balance_card.dart create mode 100644 flutter_monisuo/lib/ui/pages/asset/components/holdings_section.dart create mode 100644 flutter_monisuo/lib/ui/pages/asset/components/records_link_row.dart create mode 100644 flutter_monisuo/lib/ui/pages/mine/components/about_dialog_helpers.dart create mode 100644 flutter_monisuo/lib/ui/pages/mine/components/avatar_circle.dart create mode 100644 flutter_monisuo/lib/ui/pages/mine/components/logout_button.dart create mode 100644 flutter_monisuo/lib/ui/pages/mine/components/menu_group1.dart create mode 100644 flutter_monisuo/lib/ui/pages/mine/components/menu_group2.dart create mode 100644 flutter_monisuo/lib/ui/pages/mine/components/menu_group_container.dart create mode 100644 flutter_monisuo/lib/ui/pages/mine/components/menu_row.dart create mode 100644 flutter_monisuo/lib/ui/pages/mine/components/menu_trailing_widgets.dart create mode 100644 flutter_monisuo/lib/ui/pages/mine/components/profile_card.dart create mode 100644 flutter_monisuo/lib/ui/pages/trade/components/amount_input.dart create mode 100644 flutter_monisuo/lib/ui/pages/trade/components/coin_avatar.dart create mode 100644 flutter_monisuo/lib/ui/pages/trade/components/coin_selector.dart create mode 100644 flutter_monisuo/lib/ui/pages/trade/components/confirm_dialog.dart create mode 100644 flutter_monisuo/lib/ui/pages/trade/components/placeholder_card.dart create mode 100644 flutter_monisuo/lib/ui/pages/trade/components/price_card.dart create mode 100644 flutter_monisuo/lib/ui/pages/trade/components/trade_button.dart create mode 100644 flutter_monisuo/lib/ui/pages/trade/components/trade_form_card.dart diff --git a/CLAUDE.md b/CLAUDE.md index 594dd56..fab063b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,6 +7,10 @@ 模拟所 (Monisuo) — 虚拟货币模拟交易平台。全栈 monorepo,三个子系统共用一个 MySQL 数据库。 +## 技能系统 + +当处理任务时,先扫描 `.agents/skills/` 目录下是否有相关技能。如果技能可能适用,先用 Read 工具读取对应的 `SKILL.md`,然后严格遵循其指引执行。技能优先级高于默认行为,但低于用户的显式指令。 + ## 构建与运行命令 ### Java 后端 (Maven, Java 8, Spring Boot 2.2.4) @@ -83,9 +87,6 @@ deploy/deploy_server.sh backend # 仅部署后端 ### 数据库核心表 `sys_user`、`sys_admin`、`coin`(`price_type`: 1=实时, 2=管理定价)、`account_fund`、`account_trade`(唯一索引 `user_id+coin_code`)、`order_trade`、`order_fund`(充提订单,状态驱动的审批流)、`account_flow`、`sys_config`、`user_favorite`、`cold_wallet`。 -## 技能系统 - -当处理任务时,先扫描 `.agents/skills/` 目录下是否有相关技能。如果技能可能适用,先用 Read 工具读取对应的 `SKILL.md`,然后严格遵循其指引执行。技能优先级高于默认行为,但低于用户的显式指令。 ## 代码规范 diff --git a/flutter_monisuo/lib/ui/pages/asset/asset_page.dart b/flutter_monisuo/lib/ui/pages/asset/asset_page.dart index fce4a0b..14e9778 100644 --- a/flutter_monisuo/lib/ui/pages/asset/asset_page.dart +++ b/flutter_monisuo/lib/ui/pages/asset/asset_page.dart @@ -1,21 +1,16 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:shadcn_ui/shadcn_ui.dart'; -import 'package:bot_toast/bot_toast.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 'package:provider/provider.dart'; import '../../../core/theme/app_spacing.dart'; -import '../../../core/utils/toast_utils.dart'; import '../../../core/event/app_event_bus.dart'; -import '../../../data/models/account_models.dart'; import '../../../providers/asset_provider.dart'; -import '../../../providers/auth_provider.dart'; -import '../../shared/ui_constants.dart'; -import '../../components/glass_panel.dart'; -import '../../components/neon_glow.dart'; +import 'components/account_tab_switcher.dart'; +import 'components/action_buttons_row.dart'; +import 'components/asset_dialogs.dart'; +import 'components/balance_card.dart'; +import 'components/holdings_section.dart'; +import 'components/records_link_row.dart'; import '../orders/fund_orders_page.dart'; import 'transfer_page.dart'; @@ -95,26 +90,26 @@ class _AssetPageState extends State with AutomaticKeepAliveClientMixi ), const SizedBox(height: AppSpacing.sm), // Account tab switcher — pill-style matching .pen UE6xC - _AccountTabSwitcher( + AccountTabSwitcher( selectedIndex: _activeTab, onChanged: (index) => setState(() => _activeTab = index), ), const SizedBox(height: AppSpacing.md), // Balance card — matching .pen 59637 (cornerRadius lg, stroke, padding 20, gap 12) - _BalanceCard( + BalanceCard( provider: provider, activeTab: _activeTab, ), const SizedBox(height: AppSpacing.md), // Action buttons row — matching .pen pIpHe (gap 12) - _ActionButtonsRow( - onDeposit: () => _showDepositDialog(context), - onWithdraw: () => _showWithdrawDialog(context, provider.fundAccount?.balance), + ActionButtonsRow( + onDeposit: () => showDepositDialog(context), + onWithdraw: () => showWithdrawDialog(context, provider.fundAccount?.balance), onTransfer: () => _navigateToTransfer(context), ), const SizedBox(height: AppSpacing.md), // Records link row — matching .pen fLHtq (cornerRadius lg, padding [14,16], stroke) - _RecordsLinkRow( + RecordsLinkRow( onTap: () => Navigator.push( context, MaterialPageRoute(builder: (_) => const FundOrdersPage()), @@ -122,7 +117,7 @@ class _AssetPageState extends State with AutomaticKeepAliveClientMixi ), const SizedBox(height: AppSpacing.md), // Holdings section — matching .pen th9BG + 6X6tC - _HoldingsSection(holdings: _activeTab == 1 ? provider.holdings : []), + HoldingsSection(holdings: _activeTab == 1 ? provider.holdings : []), ], ), ), @@ -131,1128 +126,14 @@ class _AssetPageState extends State with AutomaticKeepAliveClientMixi ), ); } -} -// ============================================ -// Account Tab Switcher — .pen node UE6xC -// height: 40, padding: 3, cornerRadius: md, fill: $bg-tertiary -// activeTab: fill $bg-primary, cornerRadius sm, shadow blur 3, color #0000000D, offset y 1 -// activeTabText: 14px, fontWeight 600, fill $text-primary -// inactiveTabText: 14px, fontWeight 500, fill $text-secondary -// ============================================ - -class _AccountTabSwitcher extends StatelessWidget { - final int selectedIndex; - final ValueChanged onChanged; - - const _AccountTabSwitcher({ - required this.selectedIndex, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; - - return Container( - height: 40, - padding: const EdgeInsets.all(3), - decoration: BoxDecoration( - color: isDark ? colorScheme.surfaceContainerHighest : colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(AppRadius.md), - ), - child: Row( - children: [ - _buildTab( - context: context, - label: '资金账户', - isSelected: selectedIndex == 0, - onTap: () => onChanged(0), - isDark: isDark, - ), - _buildTab( - context: context, - label: '交易账户', - isSelected: selectedIndex == 1, - onTap: () => onChanged(1), - isDark: isDark, - ), - ], - ), + void _navigateToTransfer(BuildContext context) async { + final result = await Navigator.push( + context, + MaterialPageRoute(builder: (_) => const TransferPage()), ); - } - - Widget _buildTab({ - required BuildContext context, - required String label, - required bool isSelected, - required VoidCallback onTap, - required bool isDark, - }) { - final colorScheme = Theme.of(context).colorScheme; - - return Expanded( - child: GestureDetector( - onTap: onTap, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - decoration: BoxDecoration( - color: isSelected - ? colorScheme.surface - : Colors.transparent, - borderRadius: BorderRadius.circular(AppRadius.sm), - boxShadow: isSelected - ? [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 3, - offset: const Offset(0, 1), - ), - ] - : null, - ), - alignment: Alignment.center, - child: Text( - label, - style: GoogleFonts.inter( - fontSize: 14, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, - color: isSelected ? colorScheme.onSurface : colorScheme.onSurfaceVariant, - ), - ), - ), - ), - ); - } -} - -// ============================================ -// Balance Card — .pen node 59637 -// cornerRadius: lg, fill: $surface-card, padding: 20, stroke: $border-default 1px, gap: 12 -// balLabel: "USDT 余额" 12px normal $text-secondary -// balAmount: "25,680.50" 28px w700 $text-primary -// balSubRow: "≈ $25,680.50 USD" 12px normal $text-muted -// ============================================ - -class _BalanceCard extends StatelessWidget { - final AssetProvider provider; - final int activeTab; - - const _BalanceCard({ - required this.provider, - required this.activeTab, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; - - final displayBalance = activeTab == 0 - ? (provider.fundAccount?.balance ?? provider.overview?.fundBalance ?? '0.00') - : _calculateTradeTotal(); - - return GlassPanel( - padding: const EdgeInsets.all(20), - borderRadius: BorderRadius.circular(AppRadius.lg), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'USDT 余额', - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.normal, - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 12), - Text( - _formatBalance(displayBalance), - style: GoogleFonts.inter( - fontSize: 28, - fontWeight: FontWeight.w700, - color: colorScheme.onSurface, - ), - ), - const SizedBox(height: 12), - Text( - '\u2248 \$${_formatBalance(displayBalance)} USD', - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.normal, - color: isDark ? AppColorScheme.darkOnSurfaceMuted : colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ); - } - - String _calculateTradeTotal() { - double total = 0; - for (var h in provider.holdings) { - total += double.tryParse(h.currentValue?.toString() ?? '0') ?? 0; + if (result == true && context.mounted) { + context.read().refreshAll(force: true); } - return total.toStringAsFixed(2); - } - - String _formatBalance(String balance) { - final d = double.tryParse(balance) ?? 0; - return d.toStringAsFixed(2).replaceAllMapped( - RegExp(r'\B(?=(\d{3})+(?!\d))'), - (Match m) => ',', - ); } } - -// ============================================ -// Action Buttons Row — .pen node pIpHe -// gap: 12, three buttons evenly distributed -// Each button: circle 48x48 fill $bg-tertiary, cornerRadius 24 -// icon: 20px $accent-primary (lucide: arrow-up-right / arrow-down-left / repeat) -// label: 12px w500 $text-secondary -// ============================================ - -class _ActionButtonsRow extends StatelessWidget { - final VoidCallback onDeposit; - final VoidCallback onWithdraw; - final VoidCallback onTransfer; - - const _ActionButtonsRow({ - required this.onDeposit, - required this.onWithdraw, - required this.onTransfer, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; - final accentColor = isDark ? colorScheme.secondary : colorScheme.primary; - final bgColor = isDark ? colorScheme.surfaceContainerHighest : colorScheme.surfaceContainerHigh; - - return Row( - children: [ - _ActionButton( - icon: LucideIcons.arrowUpRight, - label: '充值', - accentColor: accentColor, - bgColor: bgColor, - onTap: onDeposit, - ), - const SizedBox(width: 12), - _ActionButton( - icon: LucideIcons.arrowDownLeft, - label: '提现', - accentColor: accentColor, - bgColor: bgColor, - onTap: onWithdraw, - ), - const SizedBox(width: 12), - _ActionButton( - icon: LucideIcons.repeat, - label: '划转', - accentColor: accentColor, - bgColor: bgColor, - onTap: onTransfer, - ), - ], - ); - } -} - -/// Single action button — matching .pen btn1/btn2/btn3 -class _ActionButton extends StatelessWidget { - final IconData icon; - final String label; - final Color accentColor; - final Color bgColor; - final VoidCallback onTap; - - const _ActionButton({ - required this.icon, - required this.label, - required this.accentColor, - required this.bgColor, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - return Expanded( - child: GestureDetector( - onTap: onTap, - child: Column( - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: bgColor, - shape: BoxShape.circle, - ), - alignment: Alignment.center, - child: Icon( - icon, - size: 20, - color: accentColor, - ), - ), - const SizedBox(height: 6), - Text( - label, - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.w500, - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ); - } -} - -// ============================================ -// Records Link Row — .pen node fLHtq -// cornerRadius: lg, fill: $surface-card, padding: [14, 16], stroke: $border-default 1px -// recordsText: "充提记录" 14px w500 $text-primary -// recordsChevron: lucide chevron-right 16px $text-muted -// ============================================ - -class _RecordsLinkRow extends StatelessWidget { - final VoidCallback onTap; - - const _RecordsLinkRow({required this.onTap}); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; - final mutedColor = isDark ? AppColorScheme.darkOnSurfaceMuted : colorScheme.onSurfaceVariant; - - return GestureDetector( - onTap: onTap, - child: GlassPanel( - padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: 14), - borderRadius: BorderRadius.circular(AppRadius.lg), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '充提记录', - style: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w500, - color: colorScheme.onSurface, - ), - ), - Icon( - LucideIcons.chevronRight, - size: 16, - color: mutedColor, - ), - ], - ), - ), - ); - } -} - -// ============================================ -// Holdings Section — .pen nodes th9BG (header) + 6X6tC (card) -// Holdings Header: "交易账户持仓" 16px w600 $text-primary | "查看全部 >" 12px normal $text-secondary -// Holdings Card: cornerRadius lg, fill $surface-card, stroke $border-default 1px -// Each row: padding [14, 16], justifyContent space_between -// Left: avatar (36x36, cornerRadius 18, fill $accent-light) + coin info (gap 2) -// AvatarText: coinCode first letter, 14px w700 $accent-primary -// CoinName: 14px w600 $text-primary -// CoinAmt: 12px normal $text-secondary -// Right: value + pnl (gap 2, alignItems end) -// Value: 13px w500 $text-primary -// Pnl: 12px w500, $profit-green / $loss-red -// Divider: $border-default height 1 opacity 0.5 -// ============================================ - -class _HoldingsSection extends StatelessWidget { - final List holdings; - - const _HoldingsSection({required this.holdings}); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - return Column( - children: [ - // Header row: "交易账户持仓" + "查看全部 >" - Padding( - padding: const EdgeInsets.only(bottom: AppSpacing.sm), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '交易账户持仓', - style: GoogleFonts.inter( - fontSize: 16, - fontWeight: FontWeight.w600, - color: colorScheme.onSurface, - ), - ), - Text( - '查看全部 >', - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.normal, - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - // Holdings card — uses real provider.holdings data - if (holdings.isEmpty) - Padding( - padding: const EdgeInsets.all(AppSpacing.xl), - child: Text( - '暂无持仓', - style: GoogleFonts.inter( - fontSize: 13, - color: colorScheme.onSurfaceVariant, - ), - ), - ) - else - GlassPanel( - padding: EdgeInsets.zero, - borderRadius: BorderRadius.circular(AppRadius.lg), - child: Column( - children: List.generate(holdings.length, (index) { - final h = holdings[index] as AccountTrade; - final isProfit = h.profitRate >= 0; - return Column( - children: [ - _HoldingRow( - coinCode: h.coinCode, - quantity: double.tryParse(h.quantity)?.toStringAsFixed(4) ?? h.quantity, - value: '${double.tryParse(h.currentValue)?.toStringAsFixed(2) ?? h.currentValue} USDT', - profitRate: '${isProfit ? '+' : ''}${h.profitRate.toStringAsFixed(2)}%', - isProfit: isProfit, - ), - if (index < holdings.length - 1) const _HoldingDivider(), - ], - ); - }), - ), - ), - ], - ); - } -} - -/// Divider between holding rows — .pen node BCCbR / yejhE -/// fill: $border-default, height: 1, opacity: 0.5 -class _HoldingDivider extends StatelessWidget { - const _HoldingDivider(); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return Container( - height: 1, - margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), - color: colorScheme.outlineVariant.withValues(alpha: 0.5), - ); - } -} - -/// Holding row — matching .pen nodes dAt4j / eK6vq / jiSUK -/// padding [14, 16], space_between layout -/// Left: avatar circle (36x36, radius 18, fill $accent-light) + coin info (gap 2) -/// Right: value + pnl (gap 2, align end) -class _HoldingRow extends StatelessWidget { - final String coinCode; - final String quantity; - final String value; - final String profitRate; - final bool isProfit; - - const _HoldingRow({ - required this.coinCode, - required this.quantity, - required this.value, - required this.profitRate, - required this.isProfit, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; - final accentColor = isDark ? colorScheme.secondary : colorScheme.primary; - final accentBgColor = accentColor.withValues(alpha: 0.1); - final profitColor = isProfit ? AppColorScheme.getUpColor(isDark) : AppColorScheme.getDownColor(isDark); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: 14), - child: Row( - children: [ - // Avatar circle with first letter — .pen SJNDJ/EjSIN/3GQ5M - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: accentBgColor, - borderRadius: BorderRadius.circular(18), - ), - alignment: Alignment.center, - child: Text( - coinCode.substring(0, 1), - style: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w700, - color: accentColor, - ), - ), - ), - const SizedBox(width: 10), - // Coin name + quantity — .pen fivxJ/Kxv3d/5CsoQ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - coinCode, - style: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w600, - color: colorScheme.onSurface, - ), - ), - const SizedBox(height: 2), - Text( - quantity, - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.normal, - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - // Value + profit rate — .pen vYJsU/2nLAg/IlWck - Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - value, - style: GoogleFonts.inter( - fontSize: 13, - fontWeight: FontWeight.w500, - color: colorScheme.onSurface, - ), - ), - const SizedBox(height: 2), - Text( - profitRate, - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.w500, - color: profitColor, - ), - ), - ], - ), - ], - ), - ); - } -} - -// ============================================ -// Dialogs — kept from original with style updates -// ============================================ - -void _showDepositDialog(BuildContext context) { - final amountController = TextEditingController(); - final formKey = GlobalKey(); - final colorScheme = Theme.of(context).colorScheme; - - showShadDialog( - context: context, - builder: (ctx) => Dialog( - backgroundColor: Colors.transparent, - child: GlassPanel( - borderRadius: BorderRadius.circular(AppRadius.lg), - padding: const EdgeInsets.all(AppSpacing.lg), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '充值', - style: GoogleFonts.inter( - fontSize: 16, - fontWeight: FontWeight.w700, - color: colorScheme.onSurface, - ), - ), - const SizedBox(height: AppSpacing.xs), - Text( - 'Asset: USDT', - style: GoogleFonts.inter( - fontSize: 12, - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - Container( - padding: const EdgeInsets.all(AppSpacing.sm), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(AppRadius.md), - ), - child: Icon( - LucideIcons.wallet, - color: colorScheme.secondary, - ), - ), - ], - ), - const SizedBox(height: AppSpacing.lg), - ShadForm( - key: formKey, - child: ShadInputFormField( - id: 'amount', - controller: amountController, - label: const Text('充值金额'), - placeholder: const Text('最低 1000 USDT'), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - validator: (v) { - if (v == null || v.isEmpty) return '请输入金额'; - final n = double.tryParse(v); - if (n == null || n <= 0) return '请输入有效金额'; - if (n < 1000) return '单笔最低充值1000 USDT'; - return null; - }, - ), - ), - const SizedBox(height: AppSpacing.lg), - Row( - children: [ - Expanded( - child: NeonButton( - text: '取消', - type: NeonButtonType.outline, - onPressed: () => Navigator.of(ctx).pop(), - height: 48, - showGlow: false, - ), - ), - const SizedBox(width: AppSpacing.sm), - Expanded( - child: NeonButton( - text: '下一步', - type: NeonButtonType.primary, - onPressed: () async { - if (formKey.currentState!.saveAndValidate()) { - Navigator.of(ctx).pop(); - final response = await context.read().deposit( - amount: amountController.text, - ); - if (context.mounted) { - if (response.success && response.data != null) { - _showDepositResultDialog(context, response.data!); - } else { - _showResultDialog(context, '申请失败', response.message); - } - } - } - }, - height: 48, - showGlow: true, - ), - ), - ], - ), - ], - ), - ), - ), - ); -} - -void _showDepositResultDialog(BuildContext context, Map data) { - final orderNo = data['orderNo'] as String? ?? ''; - final amount = data['amount']?.toString() ?? '0.00'; - final walletAddress = data['walletAddress'] as String? ?? ''; - final walletNetwork = data['walletNetwork'] as String? ?? 'TRC20'; - final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; - - showShadDialog( - context: context, - builder: (ctx) => Dialog( - backgroundColor: Colors.transparent, - child: GlassPanel( - borderRadius: BorderRadius.circular(AppRadius.lg), - padding: const EdgeInsets.all(AppSpacing.lg), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - NeonIcon( - icon: Icons.check_circle, - color: AppColorScheme.getUpColor(isDark), - size: 24, - ), - const SizedBox(width: AppSpacing.sm), - Text( - '充值申请成功', - style: GoogleFonts.inter( - fontSize: 16, - fontWeight: FontWeight.w700, - color: colorScheme.onSurface, - ), - ), - ], - ), - const SizedBox(height: AppSpacing.lg), - _InfoRow(label: '订单号', value: orderNo), - const SizedBox(height: AppSpacing.sm), - _InfoRow(label: '充值金额', value: '$amount USDT', isBold: true), - const SizedBox(height: AppSpacing.lg), - Text( - '请向以下地址转账:', - style: GoogleFonts.inter( - fontSize: 12, - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: AppSpacing.sm), - _WalletAddressCard(address: walletAddress, network: walletNetwork), - const SizedBox(height: AppSpacing.md), - Container( - padding: const EdgeInsets.all(AppSpacing.sm), - decoration: BoxDecoration( - color: AppColorScheme.warning.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all( - color: AppColorScheme.warning.withValues(alpha: 0.2), - ), - ), - child: Row( - children: [ - Icon(Icons.info_outline, size: 16, color: AppColorScheme.warning), - const SizedBox(width: AppSpacing.sm), - Expanded( - child: Text( - '转账完成后请点击"已打款"按钮确认', - style: GoogleFonts.inter(fontSize: 12, color: AppColorScheme.warning), - ), - ), - ], - ), - ), - const SizedBox(height: AppSpacing.lg), - Row( - children: [ - Expanded( - child: NeonButton( - text: '稍后确认', - type: NeonButtonType.outline, - onPressed: () => Navigator.of(ctx).pop(), - height: 44, - showGlow: false, - ), - ), - const SizedBox(width: AppSpacing.sm), - Expanded( - child: NeonButton( - text: '已打款', - type: NeonButtonType.primary, - onPressed: () async { - Navigator.of(ctx).pop(); - final response = await context.read().confirmPay(orderNo); - if (context.mounted) { - _showResultDialog( - context, - response.success ? '确认成功' : '确认失败', - response.success ? '请等待管理员审核' : response.message, - ); - } - }, - height: 44, - showGlow: true, - ), - ), - ], - ), - ], - ), - ), - ), - ); -} - -class _InfoRow extends StatelessWidget { - final String label; - final String value; - final bool isBold; - - const _InfoRow({required this.label, required this.value, this.isBold = false}); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: GoogleFonts.inter( - fontSize: 12, - color: colorScheme.onSurfaceVariant, - ), - ), - Text( - value, - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: isBold ? FontWeight.bold : FontWeight.normal, - color: colorScheme.onSurface, - ), - ), - ], - ); - } -} - -class _WalletAddressCard extends StatelessWidget { - final String address; - final String network; - - const _WalletAddressCard({required this.address, required this.network}); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - return Container( - padding: const EdgeInsets.all(AppSpacing.md), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all( - color: colorScheme.outlineVariant.withValues(alpha: 0.3), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - address, - style: const TextStyle( - fontFamily: 'monospace', - fontSize: 12, - ), - ), - ), - GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: address)); - ToastUtils.show('地址已复制到剪贴板'); - }, - child: Container( - padding: const EdgeInsets.all(AppSpacing.xs), - decoration: BoxDecoration( - color: colorScheme.primary.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(AppRadius.sm), - ), - child: Icon( - LucideIcons.copy, - size: 16, - color: colorScheme.primary, - ), - ), - ), - ], - ), - const SizedBox(height: AppSpacing.sm), - Text( - '网络: $network', - style: GoogleFonts.inter( - fontSize: 11, - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ); - } -} - -void _showWithdrawDialog(BuildContext context, String? balance) { - final amountController = TextEditingController(); - final addressController = TextEditingController(); - final contactController = TextEditingController(); - final formKey = GlobalKey(); - final colorScheme = Theme.of(context).colorScheme; - - showShadDialog( - context: context, - builder: (ctx) => Dialog( - backgroundColor: Colors.transparent, - child: GlassPanel( - borderRadius: BorderRadius.circular(AppRadius.lg), - padding: const EdgeInsets.all(AppSpacing.lg), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(AppSpacing.sm), - decoration: BoxDecoration( - color: colorScheme.primary.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(AppRadius.md), - ), - child: Icon( - LucideIcons.wallet, - color: colorScheme.primary, - ), - ), - const SizedBox(width: AppSpacing.sm), - Text( - '提现', - style: GoogleFonts.inter( - fontSize: 16, - fontWeight: FontWeight.w700, - color: colorScheme.onSurface, - ), - ), - ], - ), - const SizedBox(height: AppSpacing.xs), - Text( - '安全地将您的资产转移到外部钱包地址', - style: GoogleFonts.inter( - fontSize: 12, - color: colorScheme.onSurfaceVariant, - ), - ), - if (balance != null) ...[ - const SizedBox(height: AppSpacing.md), - Container( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.sm, - ), - decoration: BoxDecoration( - color: AppColorScheme.up.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(AppRadius.full), - border: Border.all( - color: AppColorScheme.up.withValues(alpha: 0.2), - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '可用余额: ', - style: GoogleFonts.inter( - fontSize: 10, - color: colorScheme.onSurfaceVariant, - ), - ), - Text( - '$balance USDT', - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.bold, - color: AppColorScheme.up, - ), - ), - ], - ), - ), - ], - const SizedBox(height: AppSpacing.lg), - ShadForm( - key: formKey, - child: Column( - children: [ - ShadInputFormField( - id: 'amount', - controller: amountController, - label: const Text('提现金额'), - placeholder: const Text('请输入提现金额(USDT)'), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - validator: Validators.amount, - ), - const SizedBox(height: AppSpacing.md), - ShadInputFormField( - id: 'address', - controller: addressController, - label: const Text('目标地址'), - placeholder: const Text('请输入提现地址'), - validator: (v) => Validators.required(v, '提现地址'), - ), - const SizedBox(height: AppSpacing.md), - ShadInputFormField( - id: 'contact', - controller: contactController, - label: const Text('联系方式(可选)'), - placeholder: const Text('联系方式'), - ), - ], - ), - ), - const SizedBox(height: AppSpacing.lg), - Row( - children: [ - Expanded( - child: NeonButton( - text: '取消', - type: NeonButtonType.outline, - onPressed: () => Navigator.of(ctx).pop(), - height: 44, - showGlow: false, - ), - ), - const SizedBox(width: AppSpacing.sm), - Expanded( - child: NeonButton( - text: '提交', - type: NeonButtonType.primary, - onPressed: () async { - if (formKey.currentState!.saveAndValidate()) { - Navigator.of(ctx).pop(); - final response = await context.read().withdraw( - amount: amountController.text, - withdrawAddress: addressController.text, - withdrawContact: contactController.text.isNotEmpty - ? contactController.text - : null, - ); - if (context.mounted) { - _showResultDialog( - context, - response.success ? '申请成功' : '申请失败', - response.success ? '请等待管理员审批' : response.message, - ); - } - } - }, - height: 44, - showGlow: true, - ), - ), - ], - ), - const SizedBox(height: AppSpacing.md), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.verified_user, - size: 12, - color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5), - ), - const SizedBox(width: AppSpacing.xs), - Text( - 'End-to-End Encrypted Transaction', - style: GoogleFonts.inter( - fontSize: 10, - color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5), - ), - ), - ], - ), - ], - ), - ), - ), - ), - ); -} - -void _navigateToTransfer(BuildContext context) async { - final result = await Navigator.push( - context, - MaterialPageRoute(builder: (_) => const TransferPage()), - ); - if (result == true && context.mounted) { - context.read().refreshAll(force: true); - } -} - -void _showResultDialog(BuildContext context, String title, String? message) { - final colorScheme = Theme.of(context).colorScheme; - - showShadDialog( - context: context, - builder: (ctx) => Dialog( - backgroundColor: Colors.transparent, - child: GlassPanel( - borderRadius: BorderRadius.circular(AppRadius.lg), - padding: const EdgeInsets.all(AppSpacing.lg), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - title, - style: GoogleFonts.inter( - fontSize: 16, - fontWeight: FontWeight.w700, - color: colorScheme.onSurface, - ), - ), - if (message != null) ...[ - const SizedBox(height: AppSpacing.sm), - Text( - message, - style: GoogleFonts.inter(color: colorScheme.onSurfaceVariant), - textAlign: TextAlign.center, - ), - ], - const SizedBox(height: AppSpacing.lg), - SizedBox( - width: double.infinity, - child: NeonButton( - text: '确定', - type: NeonButtonType.primary, - onPressed: () => Navigator.of(ctx).pop(), - height: 44, - showGlow: false, - ), - ), - ], - ), - ), - ), - ); -} diff --git a/flutter_monisuo/lib/ui/pages/asset/components/account_tab_switcher.dart b/flutter_monisuo/lib/ui/pages/asset/components/account_tab_switcher.dart new file mode 100644 index 0000000..8fba218 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/asset/components/account_tab_switcher.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../../../core/theme/app_spacing.dart'; + +/// 账户标签切换器 — .pen node UE6xC +/// height: 40, padding: 3, cornerRadius: md, fill: $bg-tertiary +/// activeTab: fill $bg-primary, cornerRadius sm, shadow blur 3, color #0000000D, offset y 1 +/// activeTabText: 14px, fontWeight 600, fill $text-primary +/// inactiveTabText: 14px, fontWeight 500, fill $text-secondary +class AccountTabSwitcher extends StatelessWidget { + final int selectedIndex; + final ValueChanged onChanged; + + const AccountTabSwitcher({ + super.key, + required this.selectedIndex, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Container( + height: 40, + padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + color: isDark ? colorScheme.surfaceContainerHighest : colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(AppRadius.md), + ), + child: Row( + children: [ + _buildTab( + context: context, + label: '资金账户', + isSelected: selectedIndex == 0, + onTap: () => onChanged(0), + isDark: isDark, + ), + _buildTab( + context: context, + label: '交易账户', + isSelected: selectedIndex == 1, + onTap: () => onChanged(1), + isDark: isDark, + ), + ], + ), + ); + } + + Widget _buildTab({ + required BuildContext context, + required String label, + required bool isSelected, + required VoidCallback onTap, + required bool isDark, + }) { + final colorScheme = Theme.of(context).colorScheme; + + return Expanded( + child: GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + color: isSelected + ? colorScheme.surface + : Colors.transparent, + borderRadius: BorderRadius.circular(AppRadius.sm), + boxShadow: isSelected + ? [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 3, + offset: const Offset(0, 1), + ), + ] + : null, + ), + alignment: Alignment.center, + child: Text( + label, + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, + color: isSelected ? colorScheme.onSurface : colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/asset/components/action_buttons_row.dart b/flutter_monisuo/lib/ui/pages/asset/components/action_buttons_row.dart new file mode 100644 index 0000000..adfafb5 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/asset/components/action_buttons_row.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; + +/// 操作按钮行 — .pen node pIpHe +/// gap: 12, three buttons evenly distributed +/// Each button: circle 48x48 fill $bg-tertiary, cornerRadius 24 +/// icon: 20px $accent-primary (lucide: arrow-up-right / arrow-down-left / repeat) +/// label: 12px w500 $text-secondary +class ActionButtonsRow extends StatelessWidget { + final VoidCallback onDeposit; + final VoidCallback onWithdraw; + final VoidCallback onTransfer; + + const ActionButtonsRow({ + super.key, + required this.onDeposit, + required this.onWithdraw, + required this.onTransfer, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + final accentColor = isDark ? colorScheme.secondary : colorScheme.primary; + final bgColor = isDark ? colorScheme.surfaceContainerHighest : colorScheme.surfaceContainerHigh; + + return Row( + children: [ + ActionButton( + icon: LucideIcons.arrowUpRight, + label: '充值', + accentColor: accentColor, + bgColor: bgColor, + onTap: onDeposit, + ), + const SizedBox(width: 12), + ActionButton( + icon: LucideIcons.arrowDownLeft, + label: '提现', + accentColor: accentColor, + bgColor: bgColor, + onTap: onWithdraw, + ), + const SizedBox(width: 12), + ActionButton( + icon: LucideIcons.repeat, + label: '划转', + accentColor: accentColor, + bgColor: bgColor, + onTap: onTransfer, + ), + ], + ); + } +} + +/// 单个操作按钮 — matching .pen btn1/btn2/btn3 +class ActionButton extends StatelessWidget { + final IconData icon; + final String label; + final Color accentColor; + final Color bgColor; + final VoidCallback onTap; + + const ActionButton({ + super.key, + required this.icon, + required this.label, + required this.accentColor, + required this.bgColor, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Expanded( + child: GestureDetector( + onTap: onTap, + child: Column( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: bgColor, + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Icon( + icon, + size: 20, + color: accentColor, + ), + ), + const SizedBox(height: 6), + Text( + label, + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w500, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/asset/components/asset_dialogs.dart b/flutter_monisuo/lib/ui/pages/asset/components/asset_dialogs.dart new file mode 100644 index 0000000..fc353c5 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/asset/components/asset_dialogs.dart @@ -0,0 +1,602 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; +import 'package:provider/provider.dart'; +import '../../../../core/theme/app_color_scheme.dart'; +import '../../../../core/theme/app_spacing.dart'; +import '../../../../core/utils/toast_utils.dart'; +import '../../../../providers/asset_provider.dart'; +import '../../../components/glass_panel.dart'; +import '../../../components/neon_glow.dart'; +import '../../../shared/ui_constants.dart'; + +// ============================================ +// Dialog helpers — shared sub-widgets +// ============================================ + +/// 信息行 — 用于对话框中显示 label/value 键值对 +class InfoRow extends StatelessWidget { + final String label; + final String value; + final bool isBold; + + const InfoRow({ + super.key, + required this.label, + required this.value, + this.isBold = false, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: GoogleFonts.inter( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + Text( + value, + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal, + color: colorScheme.onSurface, + ), + ), + ], + ); + } +} + +/// 钱包地址卡片 — 用于充值结果对话框中展示钱包地址 +class WalletAddressCard extends StatelessWidget { + final String address; + final String network; + + const WalletAddressCard({ + super.key, + required this.address, + required this.network, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Container( + padding: const EdgeInsets.all(AppSpacing.md), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(AppRadius.md), + border: Border.all( + color: colorScheme.outlineVariant.withValues(alpha: 0.3), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + address, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 12, + ), + ), + ), + GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: address)); + ToastUtils.show('地址已复制到剪贴板'); + }, + child: Container( + padding: const EdgeInsets.all(AppSpacing.xs), + decoration: BoxDecoration( + color: colorScheme.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + child: Icon( + LucideIcons.copy, + size: 16, + color: colorScheme.primary, + ), + ), + ), + ], + ), + const SizedBox(height: AppSpacing.sm), + Text( + '网络: $network', + style: GoogleFonts.inter( + fontSize: 11, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ); + } +} + +// ============================================ +// Dialog functions — kept from original with style updates +// ============================================ + +/// 充值对话框 +void showDepositDialog(BuildContext context) { + final amountController = TextEditingController(); + final formKey = GlobalKey(); + final colorScheme = Theme.of(context).colorScheme; + + showShadDialog( + context: context, + builder: (ctx) => Dialog( + backgroundColor: Colors.transparent, + child: GlassPanel( + borderRadius: BorderRadius.circular(AppRadius.lg), + padding: const EdgeInsets.all(AppSpacing.lg), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '充值', + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w700, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: AppSpacing.xs), + Text( + 'Asset: USDT', + style: GoogleFonts.inter( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + Container( + padding: const EdgeInsets.all(AppSpacing.sm), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(AppRadius.md), + ), + child: Icon( + LucideIcons.wallet, + color: colorScheme.secondary, + ), + ), + ], + ), + const SizedBox(height: AppSpacing.lg), + ShadForm( + key: formKey, + child: ShadInputFormField( + id: 'amount', + controller: amountController, + label: const Text('充值金额'), + placeholder: const Text('最低 1000 USDT'), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + validator: (v) { + if (v == null || v.isEmpty) return '请输入金额'; + final n = double.tryParse(v); + if (n == null || n <= 0) return '请输入有效金额'; + if (n < 1000) return '单笔最低充值1000 USDT'; + return null; + }, + ), + ), + const SizedBox(height: AppSpacing.lg), + Row( + children: [ + Expanded( + child: NeonButton( + text: '取消', + type: NeonButtonType.outline, + onPressed: () => Navigator.of(ctx).pop(), + height: 48, + showGlow: false, + ), + ), + const SizedBox(width: AppSpacing.sm), + Expanded( + child: NeonButton( + text: '下一步', + type: NeonButtonType.primary, + onPressed: () async { + if (formKey.currentState!.saveAndValidate()) { + Navigator.of(ctx).pop(); + final response = await context.read().deposit( + amount: amountController.text, + ); + if (context.mounted) { + if (response.success && response.data != null) { + showDepositResultDialog(context, response.data!); + } else { + showResultDialog(context, '申请失败', response.message); + } + } + } + }, + height: 48, + showGlow: true, + ), + ), + ], + ), + ], + ), + ), + ), + ); +} + +/// 充值结果对话框 — 展示钱包地址和确认打款 +void showDepositResultDialog(BuildContext context, Map data) { + final orderNo = data['orderNo'] as String? ?? ''; + final amount = data['amount']?.toString() ?? '0.00'; + final walletAddress = data['walletAddress'] as String? ?? ''; + final walletNetwork = data['walletNetwork'] as String? ?? 'TRC20'; + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + + showShadDialog( + context: context, + builder: (ctx) => Dialog( + backgroundColor: Colors.transparent, + child: GlassPanel( + borderRadius: BorderRadius.circular(AppRadius.lg), + padding: const EdgeInsets.all(AppSpacing.lg), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + NeonIcon( + icon: Icons.check_circle, + color: AppColorScheme.getUpColor(isDark), + size: 24, + ), + const SizedBox(width: AppSpacing.sm), + Text( + '充值申请成功', + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w700, + color: colorScheme.onSurface, + ), + ), + ], + ), + const SizedBox(height: AppSpacing.lg), + InfoRow(label: '订单号', value: orderNo), + const SizedBox(height: AppSpacing.sm), + InfoRow(label: '充值金额', value: '$amount USDT', isBold: true), + const SizedBox(height: AppSpacing.lg), + Text( + '请向以下地址转账:', + style: GoogleFonts.inter( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: AppSpacing.sm), + WalletAddressCard(address: walletAddress, network: walletNetwork), + const SizedBox(height: AppSpacing.md), + Container( + padding: const EdgeInsets.all(AppSpacing.sm), + decoration: BoxDecoration( + color: AppColorScheme.warning.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppRadius.md), + border: Border.all( + color: AppColorScheme.warning.withValues(alpha: 0.2), + ), + ), + child: Row( + children: [ + Icon(Icons.info_outline, size: 16, color: AppColorScheme.warning), + const SizedBox(width: AppSpacing.sm), + Expanded( + child: Text( + '转账完成后请点击"已打款"按钮确认', + style: GoogleFonts.inter(fontSize: 12, color: AppColorScheme.warning), + ), + ), + ], + ), + ), + const SizedBox(height: AppSpacing.lg), + Row( + children: [ + Expanded( + child: NeonButton( + text: '稍后确认', + type: NeonButtonType.outline, + onPressed: () => Navigator.of(ctx).pop(), + height: 44, + showGlow: false, + ), + ), + const SizedBox(width: AppSpacing.sm), + Expanded( + child: NeonButton( + text: '已打款', + type: NeonButtonType.primary, + onPressed: () async { + Navigator.of(ctx).pop(); + final response = await context.read().confirmPay(orderNo); + if (context.mounted) { + showResultDialog( + context, + response.success ? '确认成功' : '确认失败', + response.success ? '请等待管理员审核' : response.message, + ); + } + }, + height: 44, + showGlow: true, + ), + ), + ], + ), + ], + ), + ), + ), + ); +} + +/// 提现对话框 +void showWithdrawDialog(BuildContext context, String? balance) { + final amountController = TextEditingController(); + final addressController = TextEditingController(); + final contactController = TextEditingController(); + final formKey = GlobalKey(); + final colorScheme = Theme.of(context).colorScheme; + + showShadDialog( + context: context, + builder: (ctx) => Dialog( + backgroundColor: Colors.transparent, + child: GlassPanel( + borderRadius: BorderRadius.circular(AppRadius.lg), + padding: const EdgeInsets.all(AppSpacing.lg), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(AppSpacing.sm), + decoration: BoxDecoration( + color: colorScheme.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppRadius.md), + ), + child: Icon( + LucideIcons.wallet, + color: colorScheme.primary, + ), + ), + const SizedBox(width: AppSpacing.sm), + Text( + '提现', + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w700, + color: colorScheme.onSurface, + ), + ), + ], + ), + const SizedBox(height: AppSpacing.xs), + Text( + '安全地将您的资产转移到外部钱包地址', + style: GoogleFonts.inter( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + if (balance != null) ...[ + const SizedBox(height: AppSpacing.md), + Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ), + decoration: BoxDecoration( + color: AppColorScheme.up.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppRadius.full), + border: Border.all( + color: AppColorScheme.up.withValues(alpha: 0.2), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '可用余额: ', + style: GoogleFonts.inter( + fontSize: 10, + color: colorScheme.onSurfaceVariant, + ), + ), + Text( + '$balance USDT', + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.bold, + color: AppColorScheme.up, + ), + ), + ], + ), + ), + ], + const SizedBox(height: AppSpacing.lg), + ShadForm( + key: formKey, + child: Column( + children: [ + ShadInputFormField( + id: 'amount', + controller: amountController, + label: const Text('提现金额'), + placeholder: const Text('请输入提现金额(USDT)'), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + validator: Validators.amount, + ), + const SizedBox(height: AppSpacing.md), + ShadInputFormField( + id: 'address', + controller: addressController, + label: const Text('目标地址'), + placeholder: const Text('请输入提现地址'), + validator: (v) => Validators.required(v, '提现地址'), + ), + const SizedBox(height: AppSpacing.md), + ShadInputFormField( + id: 'contact', + controller: contactController, + label: const Text('联系方式(可选)'), + placeholder: const Text('联系方式'), + ), + ], + ), + ), + const SizedBox(height: AppSpacing.lg), + Row( + children: [ + Expanded( + child: NeonButton( + text: '取消', + type: NeonButtonType.outline, + onPressed: () => Navigator.of(ctx).pop(), + height: 44, + showGlow: false, + ), + ), + const SizedBox(width: AppSpacing.sm), + Expanded( + child: NeonButton( + text: '提交', + type: NeonButtonType.primary, + onPressed: () async { + if (formKey.currentState!.saveAndValidate()) { + Navigator.of(ctx).pop(); + final response = await context.read().withdraw( + amount: amountController.text, + withdrawAddress: addressController.text, + withdrawContact: contactController.text.isNotEmpty + ? contactController.text + : null, + ); + if (context.mounted) { + showResultDialog( + context, + response.success ? '申请成功' : '申请失败', + response.success ? '请等待管理员审批' : response.message, + ); + } + } + }, + height: 44, + showGlow: true, + ), + ), + ], + ), + const SizedBox(height: AppSpacing.md), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.verified_user, + size: 12, + color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5), + ), + const SizedBox(width: AppSpacing.xs), + Text( + 'End-to-End Encrypted Transaction', + style: GoogleFonts.inter( + fontSize: 10, + color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); +} + +/// 通用结果对话框 — 展示操作成功/失败信息 +void showResultDialog(BuildContext context, String title, String? message) { + final colorScheme = Theme.of(context).colorScheme; + + showShadDialog( + context: context, + builder: (ctx) => Dialog( + backgroundColor: Colors.transparent, + child: GlassPanel( + borderRadius: BorderRadius.circular(AppRadius.lg), + padding: const EdgeInsets.all(AppSpacing.lg), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w700, + color: colorScheme.onSurface, + ), + ), + if (message != null) ...[ + const SizedBox(height: AppSpacing.sm), + Text( + message, + style: GoogleFonts.inter(color: colorScheme.onSurfaceVariant), + textAlign: TextAlign.center, + ), + ], + const SizedBox(height: AppSpacing.lg), + SizedBox( + width: double.infinity, + child: NeonButton( + text: '确定', + type: NeonButtonType.primary, + onPressed: () => Navigator.of(ctx).pop(), + height: 44, + showGlow: false, + ), + ), + ], + ), + ), + ), + ); +} diff --git a/flutter_monisuo/lib/ui/pages/asset/components/balance_card.dart b/flutter_monisuo/lib/ui/pages/asset/components/balance_card.dart new file mode 100644 index 0000000..61d2c63 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/asset/components/balance_card.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../../../core/theme/app_color_scheme.dart'; +import '../../../../core/theme/app_spacing.dart'; +import '../../../../providers/asset_provider.dart'; +import '../../../components/glass_panel.dart'; + +/// 余额卡片 — .pen node 59637 +/// cornerRadius: lg, fill: $surface-card, padding: 20, stroke: $border-default 1px, gap: 12 +/// balLabel: "USDT 余额" 12px normal $text-secondary +/// balAmount: "25,680.50" 28px w700 $text-primary +/// balSubRow: "≈ $25,680.50 USD" 12px normal $text-muted +class BalanceCard extends StatelessWidget { + final AssetProvider provider; + final int activeTab; + + const BalanceCard({ + super.key, + required this.provider, + required this.activeTab, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + + final displayBalance = activeTab == 0 + ? (provider.fundAccount?.balance ?? provider.overview?.fundBalance ?? '0.00') + : _calculateTradeTotal(); + + return GlassPanel( + padding: const EdgeInsets.all(20), + borderRadius: BorderRadius.circular(AppRadius.lg), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'USDT 余额', + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.normal, + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 12), + Text( + _formatBalance(displayBalance), + style: GoogleFonts.inter( + fontSize: 28, + fontWeight: FontWeight.w700, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 12), + Text( + '\u2248 \$${_formatBalance(displayBalance)} USD', + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.normal, + color: isDark ? AppColorScheme.darkOnSurfaceMuted : colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ); + } + + String _calculateTradeTotal() { + double total = 0; + for (var h in provider.holdings) { + total += double.tryParse(h.currentValue?.toString() ?? '0') ?? 0; + } + return total.toStringAsFixed(2); + } + + String _formatBalance(String balance) { + final d = double.tryParse(balance) ?? 0; + return d.toStringAsFixed(2).replaceAllMapped( + RegExp(r'\B(?=(\d{3})+(?!\d))'), + (Match m) => ',', + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/asset/components/holdings_section.dart b/flutter_monisuo/lib/ui/pages/asset/components/holdings_section.dart new file mode 100644 index 0000000..9598b67 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/asset/components/holdings_section.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../../../core/theme/app_color_scheme.dart'; +import '../../../../core/theme/app_spacing.dart'; +import '../../../../data/models/account_models.dart'; +import '../../../components/glass_panel.dart'; + +/// 持仓区域 — .pen nodes th9BG (header) + 6X6tC (card) +/// Holdings Header: "交易账户持仓" 16px w600 $text-primary | "查看全部 >" 12px normal $text-secondary +/// Holdings Card: cornerRadius lg, fill $surface-card, stroke $border-default 1px +class HoldingsSection extends StatelessWidget { + final List holdings; + + const HoldingsSection({super.key, required this.holdings}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Column( + children: [ + // Header row: "交易账户持仓" + "查看全部 >" + Padding( + padding: const EdgeInsets.only(bottom: AppSpacing.sm), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '交易账户持仓', + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + Text( + '查看全部 >', + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.normal, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + // Holdings card — uses real provider.holdings data + if (holdings.isEmpty) + Padding( + padding: const EdgeInsets.all(AppSpacing.xl), + child: Text( + '暂无持仓', + style: GoogleFonts.inter( + fontSize: 13, + color: colorScheme.onSurfaceVariant, + ), + ), + ) + else + GlassPanel( + padding: EdgeInsets.zero, + borderRadius: BorderRadius.circular(AppRadius.lg), + child: Column( + children: List.generate(holdings.length, (index) { + final h = holdings[index] as AccountTrade; + final isProfit = h.profitRate >= 0; + return Column( + children: [ + HoldingRow( + coinCode: h.coinCode, + quantity: double.tryParse(h.quantity)?.toStringAsFixed(4) ?? h.quantity, + value: '${double.tryParse(h.currentValue)?.toStringAsFixed(2) ?? h.currentValue} USDT', + profitRate: '${isProfit ? '+' : ''}${h.profitRate.toStringAsFixed(2)}%', + isProfit: isProfit, + ), + if (index < holdings.length - 1) const HoldingDivider(), + ], + ); + }), + ), + ), + ], + ); + } +} + +/// 持仓行分隔线 — .pen node BCCbR / yejhE +/// fill: $border-default, height: 1, opacity: 0.5 +class HoldingDivider extends StatelessWidget { + const HoldingDivider({super.key}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Container( + height: 1, + margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), + color: colorScheme.outlineVariant.withValues(alpha: 0.5), + ); + } +} + +/// 持仓行 — matching .pen nodes dAt4j / eK6vq / jiSUK +/// padding [14, 16], space_between layout +/// Left: avatar circle (36x36, radius 18, fill $accent-light) + coin info (gap 2) +/// Right: value + pnl (gap 2, align end) +class HoldingRow extends StatelessWidget { + final String coinCode; + final String quantity; + final String value; + final String profitRate; + final bool isProfit; + + const HoldingRow({ + super.key, + required this.coinCode, + required this.quantity, + required this.value, + required this.profitRate, + required this.isProfit, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + final accentColor = isDark ? colorScheme.secondary : colorScheme.primary; + final accentBgColor = accentColor.withValues(alpha: 0.1); + final profitColor = isProfit ? AppColorScheme.getUpColor(isDark) : AppColorScheme.getDownColor(isDark); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: 14), + child: Row( + children: [ + // Avatar circle with first letter — .pen SJNDJ/EjSIN/3GQ5M + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: accentBgColor, + borderRadius: BorderRadius.circular(18), + ), + alignment: Alignment.center, + child: Text( + coinCode.substring(0, 1), + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w700, + color: accentColor, + ), + ), + ), + const SizedBox(width: 10), + // Coin name + quantity — .pen fivxJ/Kxv3d/5CsoQ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + coinCode, + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 2), + Text( + quantity, + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.normal, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + // Value + profit rate — .pen vYJsU/2nLAg/IlWck + Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + value, + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w500, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 2), + Text( + profitRate, + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w500, + color: profitColor, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/asset/components/records_link_row.dart b/flutter_monisuo/lib/ui/pages/asset/components/records_link_row.dart new file mode 100644 index 0000000..40f3596 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/asset/components/records_link_row.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.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 '../../../components/glass_panel.dart'; + +/// 充提记录链接行 — .pen node fLHtq +/// cornerRadius: lg, fill: $surface-card, padding: [14, 16], stroke: $border-default 1px +/// recordsText: "充提记录" 14px w500 $text-primary +/// recordsChevron: lucide chevron-right 16px $text-muted +class RecordsLinkRow extends StatelessWidget { + final VoidCallback onTap; + + const RecordsLinkRow({super.key, required this.onTap}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + final mutedColor = isDark ? AppColorScheme.darkOnSurfaceMuted : colorScheme.onSurfaceVariant; + + return GestureDetector( + onTap: onTap, + child: GlassPanel( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: 14), + borderRadius: BorderRadius.circular(AppRadius.lg), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '充提记录', + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w500, + color: colorScheme.onSurface, + ), + ), + Icon( + LucideIcons.chevronRight, + size: 16, + color: mutedColor, + ), + ], + ), + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/asset/transfer_page.dart b/flutter_monisuo/lib/ui/pages/asset/transfer_page.dart index 124f5d9..963f82f 100644 --- a/flutter_monisuo/lib/ui/pages/asset/transfer_page.dart +++ b/flutter_monisuo/lib/ui/pages/asset/transfer_page.dart @@ -3,7 +3,6 @@ import 'package:flutter/services.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/asset_provider.dart'; import '../../../data/models/account_models.dart'; @@ -37,6 +36,10 @@ class _TransferPageState extends State { super.dispose(); } + // ============================================ + // 数据访问 + // ============================================ + /// 获取资金账户余额 String get _fundBalance { final provider = context.read(); @@ -64,9 +67,7 @@ class _TransferPageState extends State { } /// 获取当前可用余额(根据方向) - String get _availableBalance { - return _direction == 1 ? _fundBalance : _tradeUsdtBalance; - } + String get _availableBalance => _direction == 1 ? _fundBalance : _tradeUsdtBalance; /// 从账户名 String get _fromLabel => _direction == 1 ? '资金账户' : '交易账户'; @@ -74,6 +75,27 @@ class _TransferPageState extends State { String get _fromBalance => _direction == 1 ? _fundBalance : _tradeUsdtBalance; String get _toBalance => _direction == 1 ? _tradeUsdtBalance : _fundBalance; + // ============================================ + // 主题辅助 + // ============================================ + + bool get _isDark => Theme.of(context).brightness == Brightness.dark; + + /// 一次性获取所有主题感知颜色 + _TransferColors get _colors => _TransferColors(_isDark); + + TextStyle _inter({ + required double fontSize, + required FontWeight fontWeight, + required Color color, + }) { + return GoogleFonts.inter(fontSize: fontSize, fontWeight: fontWeight, color: color); + } + + // ============================================ + // 业务逻辑 + // ============================================ + /// 执行划转 Future _doTransfer() async { final amount = _amountController.text; @@ -134,42 +156,27 @@ class _TransferPageState extends State { }); } + // ============================================ + // 构建 UI + // ============================================ + @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; - - // Theme-aware colors matching .pen design tokens - final bgSecondary = isDark ? const Color(0xFF0B1120) : const Color(0xFFF8FAFC); - final surfaceCard = isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF); - final bgTertiary = isDark ? const Color(0xFF1E293B) : const Color(0xFFF1F5F9); - final borderDefault = isDark ? const Color(0xFF334155) : const Color(0xFFE2E8F0); - final textPrimary = isDark ? const Color(0xFFF8FAFC) : const Color(0xFF0F172A); - final textSecondary = isDark ? const Color(0xFF94A3B8) : const Color(0xFF475569); - final textMuted = isDark ? const Color(0xFF64748B) : const Color(0xFF94A3B8); - final textInverse = isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF); - final accentPrimary = isDark ? const Color(0xFFD4AF37) : const Color(0xFF1F2937); - final goldAccent = isDark ? const Color(0xFFD4AF37) : const Color(0xFFF59E0B); - final profitGreen = isDark ? const Color(0xFF4ADE80) : const Color(0xFF16A34A); - final profitGreenBg = isDark ? const Color(0xFF052E16) : const Color(0xFFF0FDF4); + final c = _colors; return Scaffold( - backgroundColor: bgSecondary, + backgroundColor: c.bgSecondary, appBar: AppBar( - backgroundColor: isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF), + backgroundColor: c.surfaceCard, elevation: 0, scrolledUnderElevation: 0, leading: IconButton( - icon: Icon(LucideIcons.arrowLeft, color: textPrimary, size: 20), + icon: Icon(LucideIcons.arrowLeft, color: c.textPrimary, size: 20), onPressed: () => Navigator.of(context).pop(), ), title: Text( '账户划转', - style: GoogleFonts.inter( - fontSize: 16, - fontWeight: FontWeight.w600, - color: textPrimary, - ), + style: _inter(fontSize: 16, fontWeight: FontWeight.w600, color: c.textPrimary), ), centerTitle: true, ), @@ -179,46 +186,13 @@ class _TransferPageState extends State { padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), child: Column( children: [ - // --- Transfer Direction Card --- - _buildTransferDirectionCard( - colorScheme: colorScheme, - isDark: isDark, - surfaceCard: surfaceCard, - borderDefault: borderDefault, - textPrimary: textPrimary, - textSecondary: textSecondary, - textMuted: textMuted, - textInverse: textInverse, - accentPrimary: accentPrimary, - ), - + _buildTransferDirectionCard(c), const SizedBox(height: 24), - - // --- Amount Section --- - _buildAmountSection( - isDark: isDark, - bgTertiary: bgTertiary, - textPrimary: textPrimary, - textSecondary: textSecondary, - textMuted: textMuted, - goldAccent: goldAccent, - ), - + _buildAmountSection(c), const SizedBox(height: 24), - - // --- Tips Card --- - _buildTipsCard( - profitGreen: profitGreen, - profitGreenBg: profitGreenBg, - ), - + _buildTipsCard(c), const SizedBox(height: 24), - - // --- Confirm Button --- - _buildConfirmButton( - accentPrimary: accentPrimary, - textInverse: textInverse, - ), + _buildConfirmButton(c), ], ), ); @@ -227,51 +201,30 @@ class _TransferPageState extends State { ); } - /// Transfer direction card with source, swap, destination - Widget _buildTransferDirectionCard({ - required ColorScheme colorScheme, - required bool isDark, - required Color surfaceCard, - required Color borderDefault, - required Color textPrimary, - required Color textSecondary, - required Color textMuted, - required Color textInverse, - required Color accentPrimary, - }) { + // ============================================ + // Transfer direction card + // ============================================ + + Widget _buildTransferDirectionCard(_TransferColors c) { return Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: surfaceCard, + color: c.surfaceCard, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: borderDefault.withOpacity(0.6)), + border: Border.all(color: c.borderDefault.withValues(alpha: 0.6)), ), child: Column( children: [ // Source account - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - switchInCurve: Curves.easeInOut, - switchOutCurve: Curves.easeInOut, - transitionBuilder: (widget, animation) { - return SlideTransition( - position: Tween( - begin: const Offset(0, -1), - end: Offset.zero, - ).animate(animation), - child: FadeTransition(opacity: animation, child: widget), - ); - }, + _animatedSwitcher( + key: 'src-$_direction', + beginOffset: const Offset(0, -1), child: _buildAccountRow( - key: ValueKey('src-$_direction'), label: '从', accountName: _fromLabel, balance: _fromBalance, - isDark: isDark, - textMuted: textMuted, - textPrimary: textPrimary, - textSecondary: textSecondary, + c: c, ), ), @@ -283,42 +236,24 @@ class _TransferPageState extends State { height: 36, margin: const EdgeInsets.symmetric(vertical: 16), decoration: BoxDecoration( - color: accentPrimary, + color: c.accentPrimary, shape: BoxShape.circle, ), child: Center( - child: Icon( - LucideIcons.arrowUpDown, - size: 18, - color: textInverse, - ), + child: Icon(LucideIcons.arrowUpDown, size: 18, color: c.textInverse), ), ), ), // Destination account - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - switchInCurve: Curves.easeInOut, - switchOutCurve: Curves.easeInOut, - transitionBuilder: (widget, animation) { - return SlideTransition( - position: Tween( - begin: const Offset(0, 1), - end: Offset.zero, - ).animate(animation), - child: FadeTransition(opacity: animation, child: widget), - ); - }, + _animatedSwitcher( + key: 'dst-$_direction', + beginOffset: const Offset(0, 1), child: _buildAccountRow( - key: ValueKey('dst-$_direction'), label: '到', accountName: _toLabel, balance: _toBalance, - isDark: isDark, - textMuted: textMuted, - textPrimary: textPrimary, - textSecondary: textSecondary, + c: c, ), ), ], @@ -326,64 +261,57 @@ class _TransferPageState extends State { ); } + /// 统一的 AnimatedSwitcher 构造 + Widget _animatedSwitcher({ + required String key, + required Offset beginOffset, + required Widget child, + }) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + transitionBuilder: (widget, animation) { + return SlideTransition( + position: Tween(begin: beginOffset, end: Offset.zero).animate(animation), + child: FadeTransition(opacity: animation, child: widget), + ); + }, + child: KeyedSubtree(key: ValueKey(key), child: child), + ); + } + /// Single account row inside the direction card Widget _buildAccountRow({ - Key? key, required String label, required String accountName, required String balance, - required bool isDark, - required Color textMuted, - required Color textPrimary, - required Color textSecondary, + required _TransferColors c, }) { - return Container( - key: key, + return SizedBox( width: double.infinity, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Label row - Text( - label, - style: GoogleFonts.inter( - fontSize: 11, - fontWeight: FontWeight.normal, - color: textMuted, - ), - ), + Text(label, style: _inter(fontSize: 11, fontWeight: FontWeight.normal, color: c.textMuted)), const SizedBox(height: 8), - // Account name + balance row Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // Account name with icon Row( children: [ Icon( label == '从' ? LucideIcons.wallet : LucideIcons.repeat, size: 18, - color: textSecondary, + color: c.textSecondary, ), const SizedBox(width: 10), - Text( - accountName, - style: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w600, - color: textPrimary, - ), - ), + Text(accountName, style: _inter(fontSize: 14, fontWeight: FontWeight.w600, color: c.textPrimary)), ], ), - // Balance Text( '\u00A5 ${_formatBalance(balance)}', - style: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w600, - color: textPrimary, - ), + style: _inter(fontSize: 14, fontWeight: FontWeight.w600, color: c.textPrimary), ), ], ), @@ -392,22 +320,11 @@ class _TransferPageState extends State { ); } - /// Format balance for display - String _formatBalance(String balance) { - final val = double.tryParse(balance); - if (val == null) return '0.00'; - return val.toStringAsFixed(2); - } + // ============================================ + // Amount input section + // ============================================ - /// Amount input section - Widget _buildAmountSection({ - required bool isDark, - required Color bgTertiary, - required Color textPrimary, - required Color textSecondary, - required Color textMuted, - required Color goldAccent, - }) { + Widget _buildAmountSection(_TransferColors c) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -415,24 +332,10 @@ class _TransferPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - '划转金额', - style: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w500, - color: textSecondary, - ), - ), + Text('划转金额', style: _inter(fontSize: 14, fontWeight: FontWeight.w500, color: c.textSecondary)), GestureDetector( onTap: () => _setQuickAmount(1.0), - child: Text( - '全部划转', - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.w600, - color: goldAccent, - ), - ), + child: Text('全部划转', style: _inter(fontSize: 12, fontWeight: FontWeight.w600, color: c.goldAccent)), ), ], ), @@ -446,13 +349,12 @@ class _TransferPageState extends State { height: 56, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( - color: bgTertiary, + color: c.bgTertiary, borderRadius: BorderRadius.circular(AppRadius.lg), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // Input Expanded( child: TextField( controller: _amountController, @@ -461,35 +363,19 @@ class _TransferPageState extends State { inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,8}')), ], - style: GoogleFonts.inter( - fontSize: 28, - fontWeight: FontWeight.w700, - color: textPrimary, - ), + style: _inter(fontSize: 28, fontWeight: FontWeight.w700, color: c.textPrimary), decoration: InputDecoration( hintText: '0.00', - hintStyle: GoogleFonts.inter( - fontSize: 28, - fontWeight: FontWeight.w700, - color: textMuted, - ), + hintStyle: _inter(fontSize: 28, fontWeight: FontWeight.w700, color: c.textMuted), border: InputBorder.none, contentPadding: EdgeInsets.zero, isDense: true, ), ), ), - // Suffix Padding( padding: const EdgeInsets.only(left: 8), - child: Text( - 'USDT', - style: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.normal, - color: textMuted, - ), - ), + child: Text('USDT', style: _inter(fontSize: 14, fontWeight: FontWeight.normal, color: c.textMuted)), ), ], ), @@ -499,74 +385,58 @@ class _TransferPageState extends State { // Percent buttons Row( - children: [ - _buildPercentButton('25%', 0.25, isDark, bgTertiary, textSecondary), - const SizedBox(width: 8), - _buildPercentButton('50%', 0.50, isDark, bgTertiary, textSecondary), - const SizedBox(width: 8), - _buildPercentButton('75%', 0.75, isDark, bgTertiary, textSecondary), - const SizedBox(width: 8), - _buildPercentButton('100%', 1.0, isDark, bgTertiary, textSecondary), - ], + children: [0.25, 0.50, 0.75, 1.0].asMap().entries.map((entry) { + final index = entry.key; + final percent = entry.value; + final label = '${(percent * 100).toInt()}%'; + return Padding( + padding: EdgeInsets.only(left: index > 0 ? 8 : 0), + child: _buildPercentButton(label, percent, c), + ); + }).toList(), ), ], ); } - /// Percent quick button - Widget _buildPercentButton(String label, double percent, bool isDark, Color bgTertiary, Color textSecondary) { + Widget _buildPercentButton(String label, double percent, _TransferColors c) { return Expanded( child: GestureDetector( onTap: () => _setQuickAmount(percent), child: Container( height: 36, decoration: BoxDecoration( - color: bgTertiary, + color: c.bgTertiary, borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Center( - child: Text( - label, - style: GoogleFonts.inter( - fontSize: 13, - fontWeight: FontWeight.w500, - color: textSecondary, - ), - ), + child: Text(label, style: _inter(fontSize: 13, fontWeight: FontWeight.w500, color: c.textSecondary)), ), ), ), ); } - /// Tips card with green background - Widget _buildTipsCard({ - required Color profitGreen, - required Color profitGreenBg, - }) { + // ============================================ + // Tips card & Confirm button + // ============================================ + + Widget _buildTipsCard(_TransferColors c) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( - color: profitGreenBg, + color: c.profitGreenBg, borderRadius: BorderRadius.circular(AppRadius.lg), ), child: Row( children: [ - Icon( - LucideIcons.info, - size: 16, - color: profitGreen, - ), + Icon(LucideIcons.info, size: 16, color: c.profitGreen), const SizedBox(width: 8), Expanded( child: Text( '划转即时到账,无需手续费', - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.normal, - color: profitGreen, - ), + style: _inter(fontSize: 12, fontWeight: FontWeight.normal, color: c.profitGreen), ), ), ], @@ -574,11 +444,7 @@ class _TransferPageState extends State { ); } - /// Confirm button - Widget _buildConfirmButton({ - required Color accentPrimary, - required Color textInverse, - }) { + Widget _buildConfirmButton(_TransferColors c) { return SizedBox( width: double.infinity, height: 52, @@ -586,7 +452,7 @@ class _TransferPageState extends State { onTap: _isLoading ? null : _doTransfer, child: Container( decoration: BoxDecoration( - color: accentPrimary, + color: c.accentPrimary, borderRadius: BorderRadius.circular(AppRadius.lg), ), child: Center( @@ -596,20 +462,56 @@ class _TransferPageState extends State { height: 20, child: CircularProgressIndicator( strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(textInverse), + valueColor: AlwaysStoppedAnimation(c.textInverse), ), ) : Text( '确认划转', - style: GoogleFonts.inter( - fontSize: 16, - fontWeight: FontWeight.w700, - color: textInverse, - ), + style: _inter(fontSize: 16, fontWeight: FontWeight.w700, color: c.textInverse), ), ), ), ), ); } + + // ============================================ + // Helpers + // ============================================ + + String _formatBalance(String balance) { + final val = double.tryParse(balance); + if (val == null) return '0.00'; + return val.toStringAsFixed(2); + } +} + +/// 主题感知颜色集合,避免在 build() 中重复定义大量局部变量 +class _TransferColors { + final Color bgSecondary; + final Color surfaceCard; + final Color bgTertiary; + final Color borderDefault; + final Color textPrimary; + final Color textSecondary; + final Color textMuted; + final Color textInverse; + final Color accentPrimary; + final Color goldAccent; + final Color profitGreen; + final Color profitGreenBg; + + _TransferColors(bool isDark) + : bgSecondary = isDark ? const Color(0xFF0B1120) : const Color(0xFFF8FAFC), + surfaceCard = isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF), + bgTertiary = isDark ? const Color(0xFF1E293B) : const Color(0xFFF1F5F9), + borderDefault = isDark ? const Color(0xFF334155) : const Color(0xFFE2E8F0), + textPrimary = isDark ? const Color(0xFFF8FAFC) : const Color(0xFF0F172A), + textSecondary = isDark ? const Color(0xFF94A3B8) : const Color(0xFF475569), + textMuted = isDark ? const Color(0xFF64748B) : const Color(0xFF94A3B8), + textInverse = isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF), + accentPrimary = isDark ? const Color(0xFFD4AF37) : const Color(0xFF1F2937), + goldAccent = isDark ? const Color(0xFFD4AF37) : const Color(0xFFF59E0B), + profitGreen = isDark ? const Color(0xFF4ADE80) : const Color(0xFF16A34A), + profitGreenBg = isDark ? const Color(0xFF052E16) : const Color(0xFFF0FDF4); } diff --git a/flutter_monisuo/lib/ui/pages/mine/components/about_dialog_helpers.dart b/flutter_monisuo/lib/ui/pages/mine/components/about_dialog_helpers.dart new file mode 100644 index 0000000..f2a54a2 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/mine/components/about_dialog_helpers.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import '../../../../core/theme/app_spacing.dart'; + +/// 信息行组件(用于关于对话框) +class InfoRow extends StatelessWidget { + final IconData icon; + final String text; + + const InfoRow({super.key, required this.icon, required this.text}); + + @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, + ), + ), + ], + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/mine/components/avatar_circle.dart b/flutter_monisuo/lib/ui/pages/mine/components/avatar_circle.dart new file mode 100644 index 0000000..715dda4 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/mine/components/avatar_circle.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +/// 圆形头像组件 +/// +/// 显示用户首字母或默认比特币符号。通过 [radius] 控制大小, +/// [fontSize] 控制文字大小,[text] 可传入用户头像文字。 +class AvatarCircle extends StatelessWidget { + final double radius; + final double fontSize; + final String? text; + + const AvatarCircle({ + super.key, + required this.radius, + required this.fontSize, + this.text, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return CircleAvatar( + radius: radius, + backgroundColor: colorScheme.primary.withOpacity(0.15), + child: Text( + text ?? '₿', + style: TextStyle( + fontSize: fontSize, + color: colorScheme.primary, + fontWeight: FontWeight.w700, + ), + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/mine/components/logout_button.dart b/flutter_monisuo/lib/ui/pages/mine/components/logout_button.dart new file mode 100644 index 0000000..d7fe3ee --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/mine/components/logout_button.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../../../core/theme/app_color_scheme.dart'; +import '../../../../core/theme/app_spacing.dart'; + +/// 退出登录按钮 +class LogoutButton extends StatelessWidget { + final VoidCallback onLogout; + const LogoutButton({super.key, required this.onLogout}); + + @override + Widget build(BuildContext context) { + 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, + ), + ), + ), + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/mine/components/menu_group1.dart b/flutter_monisuo/lib/ui/pages/mine/components/menu_group1.dart new file mode 100644 index 0000000..010c6e7 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/mine/components/menu_group1.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; +import '../../../../core/theme/app_color_scheme.dart'; +import '../../../../core/theme/app_spacing.dart'; +import '../kyc_page.dart'; +import '../welfare_center_page.dart'; +import 'menu_group_container.dart'; +import 'menu_row.dart'; +import 'menu_trailing_widgets.dart'; + +/// 菜单分组1 - 福利中心 / 实名认证 / 安全设置 / 消息通知 +class MenuGroup1 extends StatelessWidget { + final int kycStatus; + final void Function(String) onShowComingSoon; + + const MenuGroup1({ + super.key, + required this.kycStatus, + required this.onShowComingSoon, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + + return MenuGroupContainer( + child: Column( + children: [ + // 福利中心 + MenuRow( + icon: LucideIcons.gift, + iconColor: AppColorScheme.darkSecondary, // gold + title: '福利中心', + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const WelfareCenterPage()), + ); + }, + ), + const 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()), + ); + } + }, + ), + const MenuDivider(), + // 安全设置 + MenuRow( + icon: LucideIcons.lock, + iconColor: colorScheme.onSurfaceVariant, + title: '安全设置', + onTap: () => onShowComingSoon('安全设置'), + ), + const MenuDivider(), + // 消息通知 + MenuRow( + icon: LucideIcons.bell, + iconColor: colorScheme.onSurfaceVariant, + title: '消息通知', + trailing: const RedDotIndicator(), + onTap: () => onShowComingSoon('消息通知'), + ), + ], + ), + ); + } +} + +/// 显示 KYC 认证状态对话框 +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(), + ), + ], + ), + ); +} diff --git a/flutter_monisuo/lib/ui/pages/mine/components/menu_group2.dart b/flutter_monisuo/lib/ui/pages/mine/components/menu_group2.dart new file mode 100644 index 0000000..3b2a79a --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/mine/components/menu_group2.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; +import 'menu_group_container.dart'; +import 'menu_row.dart'; +import 'menu_trailing_widgets.dart'; + +/// 菜单分组2 - 深色模式 / 系统设置 / 关于我们 +class MenuGroup2 extends StatelessWidget { + final VoidCallback onShowAbout; + + const MenuGroup2({super.key, required this.onShowAbout}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return MenuGroupContainer( + child: Column( + children: [ + // 深色模式 + const DarkModeRow(), + const MenuDivider(), + // 系统设置 + MenuRow( + icon: LucideIcons.settings, + iconColor: colorScheme.onSurfaceVariant, + title: '系统设置', + onTap: () { + // TODO: 系统设置 + }, + ), + const MenuDivider(), + // 关于我们 + MenuRow( + icon: LucideIcons.info, + iconColor: colorScheme.onSurfaceVariant, + title: '关于我们', + onTap: onShowAbout, + ), + ], + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/mine/components/menu_group_container.dart b/flutter_monisuo/lib/ui/pages/mine/components/menu_group_container.dart new file mode 100644 index 0000000..02f38a9 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/mine/components/menu_group_container.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import '../../../../core/theme/app_spacing.dart'; + +/// 菜单分组容器 - 统一的圆角卡片样式 +/// +/// 所有菜单分组共享相同的容器样式:背景色、圆角、边框。 +/// 通过 [child] 传入菜单项 Column。 +class MenuGroupContainer extends StatelessWidget { + final Widget child; + + const MenuGroupContainer({super.key, required this.child}); + + @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: child, + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/mine/components/menu_row.dart b/flutter_monisuo/lib/ui/pages/mine/components/menu_row.dart new file mode 100644 index 0000000..e3e2113 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/mine/components/menu_row.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; + +/// 单行菜单项:icon-in-box + title + trailing (chevron / badge / toggle) +/// +/// 通用菜单行组件,[icon] 和 [iconColor] 控制左侧图标, +/// [title] 为菜单文字,[trailing] 为右侧自定义内容(默认显示 chevron), +/// [onTap] 为点击回调。 +class MenuRow extends StatelessWidget { + final IconData icon; + final Color iconColor; + final String title; + final Widget? trailing; + final VoidCallback? onTap; + + const MenuRow({ + super.key, + 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, + ), + ], + ), + ), + ); + } +} + +/// 菜单组内分割线 +class MenuDivider extends StatelessWidget { + const MenuDivider({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + height: 1, + color: Theme.of(context).colorScheme.outlineVariant.withOpacity(0.15), + margin: const EdgeInsets.only(left: 62), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/mine/components/menu_trailing_widgets.dart b/flutter_monisuo/lib/ui/pages/mine/components/menu_trailing_widgets.dart new file mode 100644 index 0000000..ff97ec8 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/mine/components/menu_trailing_widgets.dart @@ -0,0 +1,196 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; +import 'package:provider/provider.dart'; +import '../../../../core/theme/app_color_scheme.dart'; +import '../../../../core/theme/app_spacing.dart'; +import '../../../../providers/theme_provider.dart'; + +/// KYC 状态徽章 (e.g. "已认证" green badge + chevron) +/// +/// 根据 [kycStatus] 显示不同状态: +/// - 2: 已认证(绿色) +/// - 1: 审核中(橙色) +/// - 其他: 仅显示 chevron +class KycBadge extends StatelessWidget { + final int kycStatus; + const KycBadge({super.key, required this.kycStatus}); + + @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, + ); + } +} + +/// 红点指示器 - 消息通知 + chevron +class RedDotIndicator extends StatelessWidget { + const RedDotIndicator({super.key}); + + @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, + ), + ], + ); + } +} + +/// 深色模式切换行 +class DarkModeRow extends StatelessWidget { + const DarkModeRow({super.key}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + final themeProvider = context.watch(); + + 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, + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/mine/components/profile_card.dart b/flutter_monisuo/lib/ui/pages/mine/components/profile_card.dart new file mode 100644 index 0000000..b6b3e24 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/mine/components/profile_card.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; +import '../../../../core/theme/app_spacing.dart'; +import 'avatar_circle.dart'; + +/// 用户资料卡片 - 头像 + 用户名 + 徽章 + chevron +class ProfileCard extends StatelessWidget { + final dynamic user; + const ProfileCard({super.key, required this.user}); + + @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, + ), + ], + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/mine/mine_page.dart b/flutter_monisuo/lib/ui/pages/mine/mine_page.dart index e39bee2..582b9ec 100644 --- a/flutter_monisuo/lib/ui/pages/mine/mine_page.dart +++ b/flutter_monisuo/lib/ui/pages/mine/mine_page.dart @@ -2,14 +2,16 @@ 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'; -import 'kyc_page.dart'; -import '../../../providers/theme_provider.dart'; import '../auth/login_page.dart'; -import 'welfare_center_page.dart'; +import 'components/about_dialog_helpers.dart'; +import 'components/avatar_circle.dart'; +import 'components/logout_button.dart'; +import 'components/menu_group1.dart'; +import 'components/menu_group2.dart'; +import 'components/profile_card.dart'; /// 我的页面 - 匹配 .pen 设计稿 class MinePage extends StatefulWidget { @@ -42,16 +44,16 @@ class _MinePageState extends State ), child: Column( children: [ - _ProfileCard(user: auth.user), + ProfileCard(user: auth.user), SizedBox(height: AppSpacing.sm), - _MenuGroup1( + MenuGroup1( kycStatus: auth.user?.kycStatus ?? 0, onShowComingSoon: _showComingSoon, ), SizedBox(height: AppSpacing.sm), - _MenuGroup2(onShowAbout: _showAboutDialog), + MenuGroup2(onShowAbout: _showAboutDialog), SizedBox(height: AppSpacing.lg), - _LogoutButton(onLogout: () => _handleLogout(auth)), + LogoutButton(onLogout: () => _handleLogout(auth)), SizedBox(height: AppSpacing.md), Text( 'System Build v1.0.0', @@ -98,7 +100,7 @@ class _MinePageState extends State builder: (context) => ShadDialog( title: Row( children: [ - _AvatarCircle(radius: 20, fontSize: 16), + AvatarCircle(radius: 20, fontSize: 16), SizedBox(width: AppSpacing.sm + AppSpacing.xs), const Text('模拟所'), ], @@ -112,9 +114,9 @@ class _MinePageState extends State style: TextStyle(color: colorScheme.onSurfaceVariant), ), SizedBox(height: AppSpacing.md), - _InfoRow(icon: Icons.code, text: '版本: 1.0.0'), + InfoRow(icon: Icons.code, text: '版本: 1.0.0'), SizedBox(height: AppSpacing.sm), - _InfoRow( + InfoRow( icon: Icons.favorite, text: 'Built with Flutter & Material Design 3'), ], @@ -158,607 +160,3 @@ class _MinePageState extends State ); } } - -// ============================================================ -// Profile Card -// ============================================================ - -/// 用户资料卡片 - 头像 + 用户名 + 徽章 + chevron -class _ProfileCard extends StatelessWidget { - final dynamic user; - const _ProfileCard({required this.user}); - - @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, - ), - ], - ), - ); - } -} - -/// 圆形头像组件 -class _AvatarCircle extends StatelessWidget { - final double radius; - final double fontSize; - final String? text; - - const _AvatarCircle({ - required this.radius, - required this.fontSize, - this.text, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; - - return CircleAvatar( - radius: radius, - backgroundColor: colorScheme.primary.withOpacity(0.15), - child: Text( - text ?? '₿', - style: TextStyle( - fontSize: fontSize, - color: colorScheme.primary, - fontWeight: FontWeight.w700, - ), - ), - ); - } -} - -// ============================================================ -// Menu Group 1 - 福利中心 / 实名认证 / 安全设置 / 消息通知 -// ============================================================ - -class _MenuGroup1 extends StatelessWidget { - final int kycStatus; - final void Function(String) onShowComingSoon; - - const _MenuGroup1({ - required this.kycStatus, - required this.onShowComingSoon, - }); - - @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('消息通知'), - ), - ], - ), - ); - } -} - -// ============================================================ -// Menu Group 2 - 深色模式 / 系统设置 / 关于我们 -// ============================================================ - -class _MenuGroup2 extends StatelessWidget { - final VoidCallback onShowAbout; - - const _MenuGroup2({required this.onShowAbout}); - - @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: [ - // 深色模式 - _DarkModeRow(), - _MenuDivider(), - // 系统设置 - _MenuRow( - icon: LucideIcons.settings, - iconColor: colorScheme.onSurfaceVariant, - title: '系统设置', - onTap: () { - // TODO: 系统设置 - }, - ), - _MenuDivider(), - // 关于我们 - _MenuRow( - icon: LucideIcons.info, - iconColor: colorScheme.onSurfaceVariant, - title: '关于我们', - onTap: onShowAbout, - ), - ], - ), - ); - } -} - -// ============================================================ -// Shared menu row components -// ============================================================ - -/// 单行菜单项: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 -// ============================================================ - -/// KYC status badge (e.g. "已认证" green badge + chevron) -class _KycBadge extends StatelessWidget { - final int kycStatus; - const _KycBadge({required this.kycStatus}); - - @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(); - - 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, - ), - ), - ), - ), - ), - ], - ), - ); - } -} - -// ============================================================ -// Logout button -// ============================================================ - -class _LogoutButton extends StatelessWidget { - final VoidCallback onLogout; - const _LogoutButton({required this.onLogout}); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - 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, - ), - ), - ), - ), - ); - } -} - -// ============================================================ -// Info row (used in about dialog) -// ============================================================ - -class _InfoRow extends StatelessWidget { - final IconData icon; - final String text; - - const _InfoRow({required this.icon, required this.text}); - - @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(), - ), - ], - ), - ); -} diff --git a/flutter_monisuo/lib/ui/pages/mine/welfare_center_page.dart b/flutter_monisuo/lib/ui/pages/mine/welfare_center_page.dart index d12b8b8..84d4caf 100644 --- a/flutter_monisuo/lib/ui/pages/mine/welfare_center_page.dart +++ b/flutter_monisuo/lib/ui/pages/mine/welfare_center_page.dart @@ -9,7 +9,6 @@ import '../../../core/utils/toast_utils.dart'; import '../../../core/event/app_event_bus.dart'; import '../../../data/services/bonus_service.dart'; import '../../../providers/asset_provider.dart'; -import '../../../providers/auth_provider.dart'; /// 福利中心页面 class WelfareCenterPage extends StatefulWidget { @@ -48,64 +47,144 @@ class _WelfareCenterPageState extends State { // 主题感知颜色辅助 // ============================================ - /// 金色强调色 ($gold-accent) - Color _goldAccent(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return isDark ? AppColorScheme.darkSecondary : const Color(0xFFF59E0B); - } + bool get _isDark => Theme.of(context).brightness == Brightness.dark; - /// 金色强调色带透明度 - Color _goldAccentWithOpacity(BuildContext context, double opacity) { - return _goldAccent(context).withOpacity(opacity); - } + /// 金色强调色 ($gold-accent) + Color get _goldAccent => + _isDark ? AppColorScheme.darkSecondary : const Color(0xFFF59E0B); /// 盈利绿色 ($profit-green) - Color _profitGreen(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return isDark ? const Color(0xFF4ADE80) : const Color(0xFF16A34A); - } + Color get _profitGreen => + _isDark ? const Color(0xFF4ADE80) : const Color(0xFF16A34A); /// 盈利绿色背景 ($profit-green-bg) - Color _profitGreenBg(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return isDark ? const Color(0xFF052E16) : const Color(0xFFF0FDF4); - } + Color get _profitGreenBg => + _isDark ? const Color(0xFF052E16) : const Color(0xFFF0FDF4); /// 文字静默色 ($text-muted) - Color _textMuted(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return isDark ? const Color(0xFF64748B) : const Color(0xFF94A3B8); - } + Color get _textMuted => + _isDark ? const Color(0xFF64748B) : const Color(0xFF94A3B8); /// 第三级背景色 ($bg-tertiary) - Color _bgTertiary(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return isDark ? AppColorScheme.darkSurfaceContainerHigh : const Color(0xFFF1F5F9); - } + Color get _bgTertiary => + _isDark ? AppColorScheme.darkSurfaceContainerHigh : const Color(0xFFF1F5F9); /// 卡片表面色 ($surface-card) - Color _surfaceCard(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return isDark ? AppColorScheme.darkSurfaceContainer : Colors.white; - } + Color get _surfaceCard => + _isDark ? AppColorScheme.darkSurfaceContainer : Colors.white; /// 反色文字 ($text-inverse) - Color _textInverse(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return isDark ? const Color(0xFF0F172A) : Colors.white; + Color get _textInverse => + _isDark ? const Color(0xFF0F172A) : Colors.white; + + // ============================================ + // 文本样式辅助 + // ============================================ + + TextStyle _inter({ + required double fontSize, + required FontWeight fontWeight, + required Color color, + }) { + return GoogleFonts.inter( + fontSize: fontSize, + fontWeight: fontWeight, + color: color, + ); + } + + // ============================================ + // 容器样式辅助 + // ============================================ + + /// 标准卡片容器 + BoxDecoration _cardDecoration({Color? borderColor}) { + final scheme = Theme.of(context).colorScheme; + return BoxDecoration( + color: _surfaceCard, + borderRadius: BorderRadius.circular(AppRadius.lg), + border: Border.all( + color: borderColor ?? scheme.outlineVariant.withValues(alpha: 0.15), + ), + ); + } + + /// 金色渐变卡片容器 + BoxDecoration _goldGradientDecoration() { + return BoxDecoration( + borderRadius: BorderRadius.circular(AppRadius.xl), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + _goldAccent.withValues(alpha: 0.15), + _surfaceCard, + ], + ), + border: Border.all( + color: _goldAccent.withValues(alpha: 0.3), + width: 1, + ), + ); + } + + /// 状态胶囊标签 + Widget _statusBadge(String text, Color textColor, Color bgColor) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + child: Text( + text, + style: _inter(fontSize: 11, fontWeight: FontWeight.w600, color: textColor), + ), + ); + } + + /// 全宽按钮 + Widget _fullWidthButton({ + required String text, + required Color backgroundColor, + required Color foregroundColor, + required VoidCallback? onPressed, + Color? disabledBackgroundColor, + }) { + return SizedBox( + width: double.infinity, + height: 44, + child: ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.lg), + ), + disabledBackgroundColor: + disabledBackgroundColor ?? backgroundColor.withValues(alpha: 0.3), + disabledForegroundColor: foregroundColor.withValues(alpha: 0.7), + ), + child: Text( + text, + style: _inter(fontSize: 14, fontWeight: FontWeight.w700, color: foregroundColor), + ), + ), + ); } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; return Scaffold( - backgroundColor: isDark + backgroundColor: _isDark ? AppColorScheme.darkBackground : const Color(0xFFF8FAFC), appBar: AppBar( - backgroundColor: isDark + backgroundColor: _isDark ? AppColorScheme.darkBackground : Colors.white, elevation: 0, @@ -114,7 +193,7 @@ class _WelfareCenterPageState extends State { titleSpacing: 0, title: Text( '福利中心', - style: GoogleFonts.inter( + style: _inter( fontSize: 17, fontWeight: FontWeight.w600, color: colorScheme.onSurface, @@ -128,32 +207,25 @@ class _WelfareCenterPageState extends State { body: _isLoading ? Center( child: CircularProgressIndicator( - color: _goldAccent(context), + color: _goldAccent, strokeWidth: 2.5, ), ) : RefreshIndicator( onRefresh: _loadData, - color: _goldAccent(context), + color: _goldAccent, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(16, 8, 16, 32), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 推广码卡片(金色渐变边框) _buildReferralCodeCard(context), const SizedBox(height: 16), - - // 新人福利卡片 _buildNewUserBonusCard(context), const SizedBox(height: 16), - - // 推广奖励列表 _buildReferralRewardsSection(context), const SizedBox(height: 16), - - // 奖励规则 _buildRulesCard(context), ], ), @@ -169,37 +241,22 @@ class _WelfareCenterPageState extends State { Widget _buildReferralCodeCard(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final referralCode = _welfareData?['referralCode'] as String? ?? ''; - final gold = _goldAccent(context); return Container( width: double.infinity, padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(AppRadius.xl), - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - _goldAccentWithOpacity(context, 0.15), - _surfaceCard(context), - ], - ), - border: Border.all( - color: _goldAccentWithOpacity(context, 0.3), - width: 1, - ), - ), + decoration: _goldGradientDecoration(), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Row: gift icon + 标题 Row( children: [ - Icon(LucideIcons.gift, color: gold, size: 24), + Icon(LucideIcons.gift, color: _goldAccent, size: 24), const SizedBox(width: 10), Text( '我的邀请码', - style: GoogleFonts.inter( + style: _inter( fontSize: 16, fontWeight: FontWeight.w700, color: colorScheme.onSurface, @@ -208,20 +265,15 @@ class _WelfareCenterPageState extends State { ], ), const SizedBox(height: 16), - - // 邀请码 Text( referralCode.isEmpty ? '暂无邀请码' : referralCode, - style: GoogleFonts.inter( + style: _inter( fontSize: 24, fontWeight: FontWeight.w800, - letterSpacing: 2, - color: gold, - ), + color: _goldAccent, + ).copyWith(letterSpacing: 2), ), const SizedBox(height: 16), - - // 复制邀请码按钮 SizedBox( width: double.infinity, height: 40, @@ -233,20 +285,17 @@ class _WelfareCenterPageState extends State { ToastUtils.show('邀请码已复制'); }, style: ElevatedButton.styleFrom( - backgroundColor: gold, - foregroundColor: _textInverse(context), + backgroundColor: _goldAccent, + foregroundColor: _textInverse, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.lg), ), - disabledBackgroundColor: gold.withOpacity(0.4), + disabledBackgroundColor: _goldAccent.withValues(alpha: 0.4), ), child: Text( '复制邀请码', - style: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w600, - ), + style: _inter(fontSize: 14, fontWeight: FontWeight.w600, color: _textInverse), ), ), ), @@ -265,10 +314,8 @@ class _WelfareCenterPageState extends State { final eligible = newUserBonus?['eligible'] as bool? ?? false; final claimed = newUserBonus?['claimed'] as bool? ?? false; final deposited = newUserBonus?['deposited'] as bool? ?? false; - final green = _profitGreen(context); - final greenBg = _profitGreenBg(context); - // 状态标签 + // 状态判定 String badgeText; bool showAvailableBadge; String buttonText; @@ -298,13 +345,7 @@ class _WelfareCenterPageState extends State { return Container( width: double.infinity, padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: _surfaceCard(context), - borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all( - color: colorScheme.outlineVariant.withOpacity(0.15), - ), - ), + decoration: _cardDecoration(), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -314,78 +355,40 @@ class _WelfareCenterPageState extends State { children: [ Text( '新人福利', - style: GoogleFonts.inter( + style: _inter( fontSize: 16, fontWeight: FontWeight.w700, color: colorScheme.onSurface, ), ), if (showAvailableBadge) - Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - decoration: BoxDecoration( - color: greenBg, - borderRadius: BorderRadius.circular(AppRadius.sm), - ), - child: Text( - badgeText, - style: GoogleFonts.inter( - fontSize: 11, - fontWeight: FontWeight.w600, - color: green, - ), - ), - ), + _statusBadge(badgeText, _profitGreen, _profitGreenBg), ], ), const SizedBox(height: 12), - - // 金额 Text( '+100 USDT', - style: GoogleFonts.inter( + style: _inter( fontSize: 28, fontWeight: FontWeight.w800, - color: claimed ? colorScheme.onSurfaceVariant : green, + color: claimed ? colorScheme.onSurfaceVariant : _profitGreen, ), ), const SizedBox(height: 8), - - // 描述 Text( description, - style: GoogleFonts.inter( + style: _inter( fontSize: 13, fontWeight: FontWeight.normal, color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 16), - - // 领取按钮 - SizedBox( - width: double.infinity, - height: 44, - child: ElevatedButton( - onPressed: canClaim ? () => _claimNewUserBonus() : null, - style: ElevatedButton.styleFrom( - backgroundColor: green, - foregroundColor: Colors.white, - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.lg), - ), - disabledBackgroundColor: green.withOpacity(0.3), - disabledForegroundColor: Colors.white70, - ), - child: Text( - buttonText, - style: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w700, - ), - ), - ), + _fullWidthButton( + text: buttonText, + backgroundColor: _profitGreen, + foregroundColor: Colors.white, + onPressed: canClaim ? () => _claimNewUserBonus() : null, ), ], ), @@ -405,40 +408,29 @@ class _WelfareCenterPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Section Header - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '推广奖励', - style: GoogleFonts.inter( - fontSize: 16, - fontWeight: FontWeight.w600, - color: colorScheme.onSurface, - ), - ), - const SizedBox(height: 4), - Text( - '每邀请一位好友充值达标,奖励100 USDT', - style: GoogleFonts.inter( - fontSize: 11, - fontWeight: FontWeight.normal, - color: _textMuted(context), - ), - ), - ], + Text( + '推广奖励', + style: _inter( + fontSize: 16, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + Text( + '每邀请一位好友充值达标,奖励100 USDT', + style: _inter( + fontSize: 11, + fontWeight: FontWeight.normal, + color: _textMuted, + ), ), const SizedBox(height: 12), // 推广列表卡片 Container( width: double.infinity, - decoration: BoxDecoration( - color: _surfaceCard(context), - borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all( - color: colorScheme.outlineVariant.withOpacity(0.15), - ), - ), + decoration: _cardDecoration(), child: referralRewards.isEmpty ? _buildEmptyReferralList(context) : _buildReferralListItems(context, referralRewards), @@ -457,13 +449,14 @@ class _WelfareCenterPageState extends State { Icon( LucideIcons.users, size: 36, - color: _textMuted(context).withOpacity(0.4), + color: _textMuted.withValues(alpha: 0.4), ), const SizedBox(height: 8), Text( '暂无推广用户', - style: GoogleFonts.inter( + style: _inter( fontSize: 13, + fontWeight: FontWeight.normal, color: colorScheme.onSurfaceVariant, ), ), @@ -475,9 +468,6 @@ class _WelfareCenterPageState extends State { Widget _buildReferralListItems(BuildContext context, List referralRewards) { final colorScheme = Theme.of(context).colorScheme; - final gold = _goldAccent(context); - final green = _profitGreen(context); - final greenBg = _profitGreenBg(context); return Column( children: List.generate(referralRewards.length, (index) { @@ -488,92 +478,17 @@ class _WelfareCenterPageState extends State { final milestones = data['milestones'] as List? ?? []; final isLast = index == referralRewards.length - 1; - // 判断状态 - bool hasClaimable = claimableCount > 0; - bool hasAnyMilestone = milestones.isNotEmpty; - bool allClaimed = milestones.isNotEmpty && - milestones.every((m) => (m as Map)['claimed'] == true); - // 进度计算 - double progress = 0; - if (milestones.isNotEmpty) { - int earnedCount = milestones.where((m) { - final milestone = m as Map; - return milestone['earned'] as bool? ?? false; - }).length; - progress = earnedCount / milestones.length; - } else { - // 无里程碑数据时,根据充值金额估算 - final deposit = double.tryParse(totalDeposit) ?? 0; - progress = (deposit / 1000).clamp(0.0, 1.0); - } - - // 状态颜色和文字 - Color progressColor; - Color statusTextColor; - String statusText; - Widget? actionWidget; - - if (hasClaimable) { - // 可领取 (achieved, green) - progressColor = green; - statusTextColor = green; - statusText = ''; - actionWidget = GestureDetector( - onTap: () => _claimReferralBonus(data['userId'] as int, milestones.isNotEmpty ? (milestones.firstWhere( - (m) => (m as Map)['claimable'] == true, - orElse: () => milestones.first, - ) as Map)['milestone'] as int? ?? 1 : 1), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), - decoration: BoxDecoration( - color: greenBg, - borderRadius: BorderRadius.circular(AppRadius.sm), - ), - child: Text( - '领取', - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.w600, - color: green, - ), - ), - ), - ); - } else if (progress > 0) { - // 进行中 (amber / gold) - progressColor = gold; - statusTextColor = const Color(0xFFD97706); - statusText = ''; - actionWidget = Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), - decoration: BoxDecoration( - color: const Color(0xFFFEF3C7), - borderRadius: BorderRadius.circular(AppRadius.sm), - ), - child: Text( - '进行中', - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.w600, - color: statusTextColor, - ), - ), - ); - } else { - // 待达标 (gray) - progressColor = _bgTertiary(context); - statusTextColor = _textMuted(context); - statusText = ''; - actionWidget = Text( - '待达标', - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.w500, - color: _textMuted(context), - ), - ); - } + final progress = _computeProgress(milestones, totalDeposit); + // 操作按钮 + final actionWidget = _buildReferralAction( + data: data, + claimableCount: claimableCount, + milestones: milestones, + progress: progress, + ); + // 进度条颜色 + final progressColor = _referralProgressColor(claimableCount, progress); return Column( children: [ @@ -581,48 +496,21 @@ class _WelfareCenterPageState extends State { padding: const EdgeInsets.all(16), child: Column( children: [ - // Top Row: avatar + name + deposit + action Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ - // Avatar - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: _bgTertiary(context), - shape: BoxShape.circle, - ), - child: Center( - child: Text( - username.isNotEmpty ? username[0].toUpperCase() : '?', - style: GoogleFonts.inter( - fontSize: 13, - fontWeight: FontWeight.w500, - color: colorScheme.onSurfaceVariant, - ), - ), - ), - ), + _buildAvatar(username), const SizedBox(width: 10), Text( username, - style: GoogleFonts.inter( - fontSize: 13, - fontWeight: FontWeight.w500, - color: colorScheme.onSurface, - ), + style: _inter(fontSize: 13, fontWeight: FontWeight.w500, color: colorScheme.onSurface), ), const SizedBox(width: 10), Text( '充值: \u00A5$totalDeposit', - style: GoogleFonts.inter( - fontSize: 11, - fontWeight: FontWeight.normal, - color: colorScheme.onSurfaceVariant, - ), + style: _inter(fontSize: 11, fontWeight: FontWeight.normal, color: colorScheme.onSurfaceVariant), ), ], ), @@ -630,15 +518,13 @@ class _WelfareCenterPageState extends State { ], ), const SizedBox(height: 10), - - // Progress bar ClipRRect( borderRadius: BorderRadius.circular(3), child: SizedBox( height: 6, child: LinearProgressIndicator( value: progress, - backgroundColor: _bgTertiary(context), + backgroundColor: _bgTertiary, valueColor: AlwaysStoppedAnimation(progressColor), minHeight: 6, ), @@ -651,7 +537,7 @@ class _WelfareCenterPageState extends State { Divider( height: 1, thickness: 1, - color: colorScheme.outlineVariant.withOpacity(0.15), + color: colorScheme.outlineVariant.withValues(alpha: 0.15), ), ], ); @@ -659,6 +545,77 @@ class _WelfareCenterPageState extends State { ); } + Widget _buildAvatar(String username) { + final colorScheme = Theme.of(context).colorScheme; + return Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: _bgTertiary, + shape: BoxShape.circle, + ), + child: Center( + child: Text( + username.isNotEmpty ? username[0].toUpperCase() : '?', + style: _inter( + fontSize: 13, + fontWeight: FontWeight.w500, + color: colorScheme.onSurfaceVariant, + ), + ), + ), + ); + } + + /// 计算推荐奖励进度 + double _computeProgress(List milestones, String totalDeposit) { + if (milestones.isNotEmpty) { + int earnedCount = milestones.where((m) { + final milestone = m as Map; + return milestone['earned'] as bool? ?? false; + }).length; + return earnedCount / milestones.length; + } + final deposit = double.tryParse(totalDeposit) ?? 0; + return (deposit / 1000).clamp(0.0, 1.0); + } + + /// 根据状态获取进度条颜色 + Color _referralProgressColor(int claimableCount, double progress) { + if (claimableCount > 0) return _profitGreen; + if (progress > 0) return _goldAccent; + return _bgTertiary; + } + + /// 构建推荐奖励的操作按钮 + Widget? _buildReferralAction({ + required Map data, + required int claimableCount, + required List milestones, + required double progress, + }) { + if (claimableCount > 0) { + final int milestoneValue = milestones.isNotEmpty + ? (milestones.firstWhere( + (m) => (m as Map)['claimable'] == true, + orElse: () => milestones.first, + ) as Map)['milestone'] as int? ?? 1 + : 1; + + return GestureDetector( + onTap: () => _claimReferralBonus(data['userId'] as int, milestoneValue), + child: _statusBadge('领取', _profitGreen, _profitGreenBg), + ); + } + if (progress > 0) { + return _statusBadge('进行中', const Color(0xFFD97706), const Color(0xFFFEF3C7)); + } + return Text( + '待达标', + style: _inter(fontSize: 12, fontWeight: FontWeight.w500, color: _textMuted), + ); + } + // ============================================ // 奖励规则卡片 // ============================================ @@ -670,7 +627,7 @@ class _WelfareCenterPageState extends State { width: double.infinity, padding: const EdgeInsets.fromLTRB(20, 16, 20, 16), decoration: BoxDecoration( - color: _bgTertiary(context), + color: _bgTertiary, borderRadius: BorderRadius.circular(AppRadius.lg), ), child: Column( @@ -678,32 +635,33 @@ class _WelfareCenterPageState extends State { children: [ Text( '奖励规则', - style: GoogleFonts.inter( + style: _inter( fontSize: 13, fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), const SizedBox(height: 8), - _buildRuleItem('新用户注册完成实名认证奖励 100 USDT', context), - _buildRuleItem('邀请好友充值每达 1000 USDT,双方各获得 100 USDT', context), - _buildRuleItem('奖励直接发放至资金账户', context), + _buildRuleItem('新用户注册完成实名认证奖励 100 USDT'), + _buildRuleItem('邀请好友充值每达 1000 USDT,双方各获得 100 USDT'), + _buildRuleItem('奖励直接发放至资金账户'), ], ), ); } - Widget _buildRuleItem(String text, BuildContext context) { + Widget _buildRuleItem(String text) { + final ruleTextColor = _isDark + ? AppColorScheme.darkOnSurfaceVariant + : const Color(0xFF475569); return Padding( padding: const EdgeInsets.symmetric(vertical: 3), child: Text( '\u2022 $text', - style: GoogleFonts.inter( + style: _inter( fontSize: 12, fontWeight: FontWeight.normal, - color: Theme.of(context).brightness == Brightness.dark - ? AppColorScheme.darkOnSurfaceVariant - : const Color(0xFF475569), + color: ruleTextColor, ), ), ); diff --git a/flutter_monisuo/lib/ui/pages/orders/fund_orders_page.dart b/flutter_monisuo/lib/ui/pages/orders/fund_orders_page.dart index 6eeebf7..ae605fb 100644 --- a/flutter_monisuo/lib/ui/pages/orders/fund_orders_page.dart +++ b/flutter_monisuo/lib/ui/pages/orders/fund_orders_page.dart @@ -7,7 +7,6 @@ import 'package:bot_toast/bot_toast.dart'; import 'package:provider/provider.dart'; import '../../../core/theme/app_color_scheme.dart'; import '../../../core/theme/app_spacing.dart'; -import '../../../core/theme/app_theme.dart' show AppRadius; import '../../../core/utils/toast_utils.dart'; import '../../../core/event/app_event_bus.dart'; import '../../../providers/asset_provider.dart'; @@ -52,36 +51,48 @@ class _FundOrdersPageState extends State { context.read().loadFundOrders(type: type); } + // ============================================ + // 主题辅助 + // ============================================ + + bool get _isDark => Theme.of(context).brightness == Brightness.dark; + + /// 一次性获取所有主题感知颜色 + _OrderColors get _colors => _OrderColors(_isDark); + + TextStyle _inter({ + required double fontSize, + required FontWeight fontWeight, + required Color color, + }) { + return GoogleFonts.inter(fontSize: fontSize, fontWeight: fontWeight, color: color); + } + + // ============================================ + // 构建 UI + // ============================================ + @override Widget build(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - final bgColor = isDark - ? AppColorScheme.darkBackground - : AppColorScheme.lightBackground; + final c = _colors; return Scaffold( - backgroundColor: bgColor, + backgroundColor: c.background, appBar: AppBar( leading: IconButton( icon: const Icon(LucideIcons.arrowLeft, size: 20), onPressed: () => Navigator.of(context).pop(), ), - title: Text( - '充提记录', - style: GoogleFonts.inter( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - backgroundColor: bgColor, + title: Text('充提记录', style: _inter(fontSize: 16, fontWeight: FontWeight.w600, color: c.primaryText)), + backgroundColor: c.background, elevation: 0, scrolledUnderElevation: 0, centerTitle: true, ), body: Column( children: [ - _buildFilterTabs(context, isDark), - Expanded(child: _buildOrderList(context, isDark)), + _buildFilterTabs(), + Expanded(child: _buildOrderList()), ], ), ); @@ -90,13 +101,8 @@ class _FundOrdersPageState extends State { // --------------------------------------------------------------------------- // Filter Tabs - pill-style segmented control // --------------------------------------------------------------------------- - Widget _buildFilterTabs(BuildContext context, bool isDark) { - final bgColor = isDark - ? AppColorScheme.darkSurfaceContainerHigh - : AppColorScheme.lightSurfaceHigh; - final activeBgColor = isDark - ? AppColorScheme.darkOnSurface - : Colors.white; + Widget _buildFilterTabs() { + final c = _colors; return Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), @@ -104,33 +110,23 @@ class _FundOrdersPageState extends State { height: 40, padding: const EdgeInsets.all(3), decoration: BoxDecoration( - color: bgColor, + color: c.tabBg, borderRadius: AppRadius.radiusMd, ), child: Row( children: [ - _buildPillTab('全部', 0, activeBgColor, isDark), - _buildPillTab('充值', 1, activeBgColor, isDark), - _buildPillTab('提现', 2, activeBgColor, isDark), + _buildPillTab('全部', 0), + _buildPillTab('充值', 1), + _buildPillTab('提现', 2), ], ), ), ); } - Widget _buildPillTab( - String label, - int index, - Color activeBgColor, - bool isDark, - ) { + Widget _buildPillTab(String label, int index) { + final c = _colors; final isActive = _activeTab == index; - final activeTextColor = isDark - ? AppColorScheme.darkBackground - : AppColorScheme.lightOnSurface; - final inactiveTextColor = isDark - ? AppColorScheme.darkOnSurfaceVariant - : AppColorScheme.lightOnSurfaceVariant; return Expanded( child: GestureDetector( @@ -142,16 +138,16 @@ class _FundOrdersPageState extends State { }, child: Container( decoration: BoxDecoration( - color: isActive ? activeBgColor : Colors.transparent, + color: isActive ? c.activeTabBg : Colors.transparent, borderRadius: AppRadius.radiusSm, ), child: Center( child: Text( label, - style: GoogleFonts.inter( + style: _inter( fontSize: 14, fontWeight: isActive ? FontWeight.w600 : FontWeight.w500, - color: isActive ? activeTextColor : inactiveTextColor, + color: isActive ? c.activeTabText : c.inactiveTabText, ), ), ), @@ -163,7 +159,9 @@ class _FundOrdersPageState extends State { // --------------------------------------------------------------------------- // Order List // --------------------------------------------------------------------------- - Widget _buildOrderList(BuildContext context, bool isDark) { + Widget _buildOrderList() { + final c = _colors; + return Consumer( builder: (context, provider, _) { final orders = provider.fundOrders; @@ -178,23 +176,9 @@ class _FundOrdersPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - LucideIcons.inbox, - size: 64, - color: isDark - ? AppColorScheme.darkOnSurfaceMuted - : AppColorScheme.lightOnSurfaceMuted, - ), + Icon(LucideIcons.inbox, size: 64, color: c.mutedText), const SizedBox(height: 16), - Text( - '暂无订单记录', - style: GoogleFonts.inter( - fontSize: 14, - color: isDark - ? AppColorScheme.darkOnSurfaceVariant - : AppColorScheme.lightOnSurfaceVariant, - ), - ), + Text('暂无订单记录', style: _inter(fontSize: 14, fontWeight: FontWeight.normal, color: c.secondaryText)), ], ), ); @@ -207,7 +191,7 @@ class _FundOrdersPageState extends State { itemCount: orders.length, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (context, index) { - return _buildOrderCard(orders[index], isDark); + return _buildOrderCard(orders[index]); }, ), ); @@ -218,52 +202,35 @@ class _FundOrdersPageState extends State { // --------------------------------------------------------------------------- // Order Card // --------------------------------------------------------------------------- - Widget _buildOrderCard(OrderFund order, bool isDark) { - final cardBg = isDark - ? AppColorScheme.darkSurfaceContainer - : AppColorScheme.lightSurfaceLowest; - final borderColor = isDark - ? AppColorScheme.darkOutlineVariant.withValues(alpha: 0.15) - : AppColorScheme.lightOutlineVariant.withValues(alpha: 0.5); - final primaryText = isDark - ? AppColorScheme.darkOnSurface - : AppColorScheme.lightOnSurface; - final mutedText = isDark - ? AppColorScheme.darkOnSurfaceMuted - : AppColorScheme.lightOnSurfaceMuted; + Widget _buildOrderCard(OrderFund order) { + final c = _colors; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: cardBg, + color: c.cardBg, borderRadius: AppRadius.radiusLg, - border: Border.all(color: borderColor, width: 1), + border: Border.all(color: c.borderColor, width: 1), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header: type badge + status badge - _buildCardHeader(order, isDark), + _buildCardHeader(order), const SizedBox(height: 12), - // Amount - _buildAmountRow(order, primaryText), + _buildAmountRow(order), const SizedBox(height: 12), - // Detail rows - _buildDetailRows(order, primaryText, mutedText), - // Rejection reason + _buildDetailRows(order), if (order.rejectReason != null) ...[ const SizedBox(height: 8), _buildRejectionReason(order), ], - // Payable amount (withdrawal with fee) if (order.receivableAmount != null && !order.isDeposit) ...[ const SizedBox(height: 8), - _buildPayableRow(order, isDark, primaryText), + _buildPayableRow(order), ], - // Action buttons if (order.canCancel || order.canConfirmPay) ...[ const SizedBox(height: 12), - _buildActions(order, isDark), + _buildActions(order), ], ], ), @@ -273,30 +240,28 @@ class _FundOrdersPageState extends State { // --------------------------------------------------------------------------- // Card Header - type badge + status badge // --------------------------------------------------------------------------- - Widget _buildCardHeader(OrderFund order, bool isDark) { - final upColor = AppColorScheme.getUpColor(isDark); - final downColor = AppColorScheme.getDownColor(isDark); - final upBg = AppColorScheme.getUpBackgroundColor(isDark, opacity: 0.12); - final downBg = AppColorScheme.getDownBackgroundColor(isDark, opacity: 0.12); + Widget _buildCardHeader(OrderFund order) { + final upColor = AppColorScheme.getUpColor(_isDark); + final downColor = AppColorScheme.getDownColor(_isDark); + final upBg = AppColorScheme.getUpBackgroundColor(_isDark, opacity: 0.12); + final downBg = AppColorScheme.getDownBackgroundColor(_isDark, opacity: 0.12); final typeColor = order.isDeposit ? upColor : downColor; final typeBg = order.isDeposit ? upBg : downBg; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // Type badge (充值 / 提现) _buildBadge(order.typeText, typeColor, typeBg), - // Status badge - _buildStatusBadge(order, isDark), + _buildStatusBadge(order), ], ); } - Widget _buildStatusBadge(OrderFund order, bool isDark) { - final upColor = AppColorScheme.getUpColor(isDark); - final downColor = AppColorScheme.getDownColor(isDark); - final upBg = AppColorScheme.getUpBackgroundColor(isDark, opacity: 0.12); - final downBg = AppColorScheme.getDownBackgroundColor(isDark, opacity: 0.12); + Widget _buildStatusBadge(OrderFund order) { + final upColor = AppColorScheme.getUpColor(_isDark); + final downColor = AppColorScheme.getDownColor(_isDark); + final upBg = AppColorScheme.getUpBackgroundColor(_isDark, opacity: 0.12); + final downBg = AppColorScheme.getDownBackgroundColor(_isDark, opacity: 0.12); const amberColor = Color(0xFFD97706); const amberBg = Color(0xFFFEF3C7); @@ -345,86 +310,63 @@ class _FundOrdersPageState extends State { color: bgColor, borderRadius: BorderRadius.circular(4), ), - child: Text( - text, - style: GoogleFonts.inter( - fontSize: 11, - fontWeight: FontWeight.w600, - color: textColor, - ), - ), + child: Text(text, style: _inter(fontSize: 11, fontWeight: FontWeight.w600, color: textColor)), ); } // --------------------------------------------------------------------------- // Amount Row // --------------------------------------------------------------------------- - Widget _buildAmountRow(OrderFund order, Color primaryText) { + Widget _buildAmountRow(OrderFund order) { + final c = _colors; return Text( '${order.isDeposit ? '+' : '-'}${order.amount} USDT', - style: GoogleFonts.inter( - fontSize: 18, - fontWeight: FontWeight.w700, - color: primaryText, - ), + style: _inter(fontSize: 18, fontWeight: FontWeight.w700, color: c.primaryText), ); } // --------------------------------------------------------------------------- // Detail Rows // --------------------------------------------------------------------------- - Widget _buildDetailRows( - OrderFund order, - Color primaryText, - Color mutedText, - ) { + Widget _buildDetailRows(OrderFund order) { + final c = _colors; + return Column( children: [ - // Order number - _buildDetailRow('订单号', order.orderNo, primaryText, mutedText), + _buildDetailRow('订单号', order.orderNo, c), const SizedBox(height: 6), - // Network / wallet address if (order.walletAddress != null) ...[ _buildDetailRow( '网络', order.remark.isNotEmpty ? order.remark : '-', - primaryText, - mutedText, + c, ), const SizedBox(height: 6), _buildDetailRow( '地址', _truncateAddress(order.walletAddress!), - primaryText, - mutedText, + c, trailing: GestureDetector( onTap: () { Clipboard.setData(ClipboardData(text: order.walletAddress!)); ToastUtils.show('地址已复制'); }, - child: Icon( - LucideIcons.copy, - size: 14, - color: mutedText, - ), + child: Icon(LucideIcons.copy, size: 14, color: c.mutedText), ), ), const SizedBox(height: 6), ] else if (order.remark.isNotEmpty) ...[ - _buildDetailRow('网络', order.remark, primaryText, mutedText), + _buildDetailRow('网络', order.remark, c), const SizedBox(height: 6), ], - // Fee (withdrawal) if (order.fee != null && !order.isDeposit) ...[ - _buildDetailRow('手续费', '${order.fee}%', primaryText, mutedText), + _buildDetailRow('手续费', '${order.fee}%', c), const SizedBox(height: 6), ], - // Time _buildDetailRow( '时间', _formatTime(order.createTime), - primaryText, - mutedText, + c, ), ], ); @@ -433,46 +375,26 @@ class _FundOrdersPageState extends State { Widget _buildDetailRow( String label, String value, - Color primaryText, - Color mutedText, { + _OrderColors c, { Widget? trailing, }) { + final valueStyle = _inter(fontSize: 12, fontWeight: FontWeight.normal, color: c.primaryText); + return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - label, - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.normal, - color: mutedText, - ), - ), + Text(label, style: _inter(fontSize: 12, fontWeight: FontWeight.normal, color: c.mutedText)), if (trailing != null) Row( mainAxisSize: MainAxisSize.min, children: [ - Text( - value, - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.normal, - color: primaryText, - ), - ), + Text(value, style: valueStyle), const SizedBox(width: 4), trailing, ], ) else - Text( - value, - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.normal, - color: primaryText, - ), - ), + Text(value, style: valueStyle), ], ); } @@ -481,57 +403,29 @@ class _FundOrdersPageState extends State { // Rejection Reason // --------------------------------------------------------------------------- Widget _buildRejectionReason(OrderFund order) { - final isDark = Theme.of(context).brightness == Brightness.dark; return Text( '拒绝原因: ${order.rejectReason}', - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.normal, - color: AppColorScheme.getDownColor(isDark), - ), + style: _inter(fontSize: 12, fontWeight: FontWeight.normal, color: AppColorScheme.getDownColor(_isDark)), ); } // --------------------------------------------------------------------------- // Payable Amount Row (withdrawal) // --------------------------------------------------------------------------- - Widget _buildPayableRow( - OrderFund order, - bool isDark, - Color primaryText, - ) { - final bgTertiary = isDark - ? AppColorScheme.darkSurfaceContainerHigh - : AppColorScheme.lightSurfaceHigh; - final secondaryText = isDark - ? AppColorScheme.darkOnSurfaceVariant - : AppColorScheme.lightOnSurfaceVariant; + Widget _buildPayableRow(OrderFund order) { + final c = _colors; return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: bgTertiary, + color: c.bgTertiary, borderRadius: AppRadius.radiusSm, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - '应付金额', - style: GoogleFonts.inter( - fontSize: 13, - fontWeight: FontWeight.w500, - color: secondaryText, - ), - ), - Text( - '${order.receivableAmount} USDT', - style: GoogleFonts.inter( - fontSize: 13, - fontWeight: FontWeight.w600, - color: primaryText, - ), - ), + Text('应付金额', style: _inter(fontSize: 13, fontWeight: FontWeight.w500, color: c.secondaryText)), + Text('${order.receivableAmount} USDT', style: _inter(fontSize: 13, fontWeight: FontWeight.w600, color: c.primaryText)), ], ), ); @@ -540,9 +434,9 @@ class _FundOrdersPageState extends State { // --------------------------------------------------------------------------- // Action Buttons // --------------------------------------------------------------------------- - Widget _buildActions(OrderFund order, bool isDark) { - final upColor = AppColorScheme.getUpColor(isDark); - final downColor = AppColorScheme.getDownColor(isDark); + Widget _buildActions(OrderFund order) { + final upColor = AppColorScheme.getUpColor(_isDark); + final downColor = AppColorScheme.getDownColor(_isDark); return Row( mainAxisAlignment: MainAxisAlignment.end, @@ -556,14 +450,7 @@ class _FundOrdersPageState extends State { borderRadius: AppRadius.radiusSm, border: Border.all(color: downColor, width: 1), ), - child: Text( - '取消订单', - style: GoogleFonts.inter( - fontSize: 13, - fontWeight: FontWeight.w500, - color: downColor, - ), - ), + child: Text('取消订单', style: _inter(fontSize: 13, fontWeight: FontWeight.w500, color: downColor)), ), ), if (order.canCancel && order.canConfirmPay) @@ -577,14 +464,7 @@ class _FundOrdersPageState extends State { color: upColor, borderRadius: AppRadius.radiusSm, ), - child: Text( - '已打款', - style: GoogleFonts.inter( - fontSize: 13, - fontWeight: FontWeight.w500, - color: Colors.white, - ), - ), + child: Text('已打款', style: _inter(fontSize: 13, fontWeight: FontWeight.w500, color: Colors.white)), ), ), ], @@ -663,3 +543,37 @@ class _FundOrdersPageState extends State { ); } } + +/// 充提订单页面的主题感知颜色集合 +class _OrderColors { + final Color background; + final Color cardBg; + final Color borderColor; + final Color bgTertiary; + final Color primaryText; + final Color secondaryText; + final Color mutedText; + final Color tabBg; + final Color activeTabBg; + final Color activeTabText; + final Color inactiveTabText; + + _OrderColors(bool isDark) + : background = isDark ? AppColorScheme.darkBackground : AppColorScheme.lightBackground, + cardBg = isDark ? AppColorScheme.darkSurfaceContainer : AppColorScheme.lightSurfaceLowest, + borderColor = isDark + ? AppColorScheme.darkOutlineVariant.withValues(alpha: 0.15) + : AppColorScheme.lightOutlineVariant.withValues(alpha: 0.5), + bgTertiary = isDark + ? AppColorScheme.darkSurfaceContainerHigh + : AppColorScheme.lightSurfaceHigh, + primaryText = isDark ? AppColorScheme.darkOnSurface : AppColorScheme.lightOnSurface, + secondaryText = isDark + ? AppColorScheme.darkOnSurfaceVariant + : AppColorScheme.lightOnSurfaceVariant, + mutedText = isDark ? AppColorScheme.darkOnSurfaceMuted : AppColorScheme.lightOnSurfaceMuted, + tabBg = isDark ? AppColorScheme.darkSurfaceContainerHigh : AppColorScheme.lightSurfaceHigh, + activeTabBg = isDark ? AppColorScheme.darkOnSurface : Colors.white, + activeTabText = isDark ? AppColorScheme.darkBackground : AppColorScheme.lightOnSurface, + inactiveTabText = isDark ? AppColorScheme.darkOnSurfaceVariant : AppColorScheme.lightOnSurfaceVariant; +} diff --git a/flutter_monisuo/lib/ui/pages/trade/components/amount_input.dart b/flutter_monisuo/lib/ui/pages/trade/components/amount_input.dart new file mode 100644 index 0000000..6482bb6 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/trade/components/amount_input.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../../../core/theme/app_color_scheme.dart'; +import '../../../../core/theme/app_spacing.dart'; + +/// 金额输入框组件(含超额提示) +/// +/// 设计稿:bg-tertiary,圆角md,高48。 +/// 输入金额超过可用 USDT 余额时显示警告提示。 +class AmountInput extends StatefulWidget { + final TextEditingController amountController; + final String maxAmount; + final bool isBuy; + final Color actionColor; + final VoidCallback onChanged; + + const AmountInput({ + super.key, + required this.amountController, + required this.maxAmount, + required this.isBuy, + required this.actionColor, + required this.onChanged, + }); + + @override + State createState() => _AmountInputState(); +} + +class _AmountInputState extends State { + bool _isExceeded = false; + + void _checkLimit() { + final input = double.tryParse(widget.amountController.text) ?? 0; + final max = double.tryParse(widget.maxAmount) ?? 0; + final exceeded = widget.isBuy && input > max && max > 0 && input > 0; + if (exceeded != _isExceeded) { + setState(() => _isExceeded = exceeded); + } + widget.onChanged(); + } + + @override + void initState() { + super.initState(); + widget.amountController.addListener(_checkLimit); + } + + @override + void dispose() { + widget.amountController.removeListener(_checkLimit); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final warningColor = AppColorScheme.warning; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 48, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest.withOpacity(0.3), + borderRadius: BorderRadius.circular(AppRadius.md), + ), + child: TextField( + controller: widget.amountController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + onChanged: (_) => _checkLimit(), + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.normal, + color: colorScheme.onSurface, + fontFeatures: [FontFeature.tabularFigures()], + ), + decoration: InputDecoration( + hintText: '请输入金额', + hintStyle: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.normal, + color: colorScheme.onSurfaceVariant.withOpacity(0.5), + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + ), + ), + ), + ), + if (_isExceeded) + Padding( + padding: EdgeInsets.only(top: AppSpacing.xs), + child: Row( + children: [ + Icon(Icons.error_outline, size: 13, color: warningColor), + SizedBox(width: 4), + Text( + '超出可用USDT余额', + style: GoogleFonts.inter( + fontSize: 11, + color: warningColor, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/trade/components/coin_avatar.dart b/flutter_monisuo/lib/ui/pages/trade/components/coin_avatar.dart new file mode 100644 index 0000000..5d7bfec --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/trade/components/coin_avatar.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import '../../../../core/theme/app_spacing.dart'; + +/// 币种头像组件 +/// +/// 显示币种图标或首字母的圆形头像,带主题色边框和背景。 +class CoinAvatar extends StatelessWidget { + final String? icon; + const CoinAvatar({super.key, this.icon}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(AppRadius.md), + border: Border.all(color: colorScheme.primary.withOpacity(0.2)), + ), + child: Center( + child: Text(icon ?? '?', + style: TextStyle( + fontSize: 20, + color: colorScheme.primary, + fontWeight: FontWeight.bold, + )), + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/trade/components/coin_selector.dart b/flutter_monisuo/lib/ui/pages/trade/components/coin_selector.dart new file mode 100644 index 0000000..5feddae --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/trade/components/coin_selector.dart @@ -0,0 +1,234 @@ +import 'package:flutter/material.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 '../../../../data/models/coin.dart'; +import 'coin_avatar.dart'; + +/// 币种选择器组件 +/// +/// 显示当前选中的币种交易对,点击弹出底部弹窗选择币种。 +/// 卡片背景 + 圆角lg + border + padding:16 +/// 横向布局:coinInfo(竖向 pair+name) + chevronDown +class CoinSelector extends StatelessWidget { + final Coin? selectedCoin; + final List coins; + final ValueChanged onCoinSelected; + + const CoinSelector({ + super.key, + required this.selectedCoin, + required this.coins, + required this.onCoinSelected, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + + return GestureDetector( + onTap: () => _showCoinPicker(context), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(AppSpacing.md), + decoration: BoxDecoration( + color: isDark + ? colorScheme.surfaceContainer + : colorScheme.surfaceContainerLowest, + borderRadius: BorderRadius.circular(AppRadius.lg), + border: Border.all( + color: colorScheme.outlineVariant.withOpacity(0.15), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // 币种信息:交易对 + 名称 + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + selectedCoin != null + ? '${selectedCoin!.code}/USDT' + : '选择币种', + style: GoogleFonts.inter( + fontSize: 18, + fontWeight: FontWeight.w700, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 2), + Text( + selectedCoin?.name ?? '点击选择交易对', + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.normal, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + // 下拉箭头 + Icon(LucideIcons.chevronDown, + size: 16, color: colorScheme.onSurfaceVariant), + ], + ), + ), + ); + } + + void _showCoinPicker(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (ctx) => Container( + height: MediaQuery.of(ctx).size.height * 0.65, + decoration: BoxDecoration( + color: isDark + ? colorScheme.surface + : colorScheme.surfaceContainerLowest, + borderRadius: + BorderRadius.vertical(top: Radius.circular(AppRadius.xxl)), + ), + child: Column( + children: [ + // 拖动指示器 + Container( + margin: EdgeInsets.only(top: AppSpacing.sm), + width: 40, + height: 4, + decoration: BoxDecoration( + color: colorScheme.onSurfaceVariant.withOpacity(0.3), + borderRadius: BorderRadius.circular(2), + ), + ), + // 标题栏 + Padding( + padding: EdgeInsets.all(AppSpacing.lg), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('选择币种', + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + )), + GestureDetector( + onTap: () => Navigator.of(ctx).pop(), + child: Icon(LucideIcons.x, + color: colorScheme.onSurfaceVariant), + ), + ], + ), + ), + Divider(height: 1, color: colorScheme.outlineVariant.withOpacity(0.2)), + // 币种列表 + Expanded( + child: ListView.builder( + padding: EdgeInsets.symmetric(vertical: AppSpacing.sm), + itemCount: coins.length, + itemBuilder: (listCtx, index) => + _buildCoinItem(coins[index], context, listCtx), + ), + ), + ], + ), + ), + ); + } + + Widget _buildCoinItem( + Coin coin, BuildContext context, BuildContext sheetContext) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + final isSelected = selectedCoin?.code == coin.code; + final changeColor = coin.isUp + ? AppColorScheme.getUpColor(isDark) + : AppColorScheme.getDownColor(isDark); + + return GestureDetector( + onTap: () { + Navigator.of(sheetContext).pop(); + onCoinSelected(coin); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: AppSpacing.lg, vertical: AppSpacing.md), + color: + isSelected ? colorScheme.primary.withOpacity(0.1) : Colors.transparent, + child: Row( + children: [ + CoinAvatar(icon: coin.displayIcon), + SizedBox(width: AppSpacing.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 第一行:币种代码 + USDT + 价格 + 涨跌幅 + Row( + children: [ + Text(coin.code, + style: GoogleFonts.inter( + fontSize: 15, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + )), + SizedBox(width: AppSpacing.xs), + Text('/USDT', + style: GoogleFonts.inter( + fontSize: 11, + color: colorScheme.onSurfaceVariant, + )), + const Spacer(), + Text('\$${coin.formattedPrice}', + style: GoogleFonts.inter( + fontSize: 13, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + )), + SizedBox(width: AppSpacing.sm), + // 涨跌幅徽章 + Container( + padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: changeColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + child: Text(coin.formattedChange, + style: GoogleFonts.inter( + fontSize: 11, + color: changeColor, + fontWeight: FontWeight.w600, + )), + ), + if (isSelected) ...[ + SizedBox(width: AppSpacing.sm), + Icon(LucideIcons.check, + size: 16, color: colorScheme.primary), + ], + ], + ), + SizedBox(height: 3), + // 第二行:币种名称 + Text(coin.name, + style: GoogleFonts.inter( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + )), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/trade/components/confirm_dialog.dart b/flutter_monisuo/lib/ui/pages/trade/components/confirm_dialog.dart new file mode 100644 index 0000000..3b51e98 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/trade/components/confirm_dialog.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../../../core/theme/app_color_scheme.dart'; +import '../../../../core/theme/app_spacing.dart'; +import '../../../components/glass_panel.dart'; +import '../../../components/neon_glow.dart'; + +/// 交易确认对话框 +/// +/// 显示交易详情(交易对、委托价格、交易金额、交易数量), +/// 用户确认后执行交易。 +class ConfirmDialog extends StatelessWidget { + final bool isBuy; + final String coinCode; + final String price; + final String quantity; + final String amount; + + const ConfirmDialog({ + super.key, + required this.isBuy, + required this.coinCode, + required this.price, + required this.quantity, + required this.amount, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + final actionColor = isBuy + ? AppColorScheme.getUpColor(isDark) + : AppColorScheme.getDownColor(isDark); + + return Dialog( + backgroundColor: Colors.transparent, + child: GlassPanel( + borderRadius: BorderRadius.circular(AppRadius.lg), + padding: EdgeInsets.all(AppSpacing.lg), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Text( + '确认${isBuy ? '买入' : '卖出'}', + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + ), + SizedBox(height: AppSpacing.lg), + _dialogRow('交易对', '$coinCode/USDT', colorScheme), + SizedBox(height: AppSpacing.sm), + _dialogRow('委托价格', '$price USDT', colorScheme), + SizedBox(height: AppSpacing.sm), + _dialogRow('交易金额', '$amount USDT', colorScheme, + valueColor: actionColor), + SizedBox(height: AppSpacing.sm), + _dialogRow('交易数量', '$quantity $coinCode', colorScheme), + SizedBox(height: AppSpacing.lg), + Row( + children: [ + Expanded( + child: NeonButton( + text: '取消', + type: NeonButtonType.outline, + onPressed: () => Navigator.of(context).pop(false), + height: 44, + showGlow: false, + ), + ), + SizedBox(width: AppSpacing.sm), + Expanded( + child: NeonButton( + text: '确认${isBuy ? '买入' : '卖出'}', + type: isBuy ? NeonButtonType.tertiary : NeonButtonType.error, + onPressed: () => Navigator.of(context).pop(true), + height: 44, + showGlow: true, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _dialogRow(String label, String value, ColorScheme colorScheme, + {Color? valueColor}) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.normal, + color: colorScheme.onSurfaceVariant, + )), + Text(value, + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w600, + color: valueColor ?? colorScheme.onSurface, + )), + ], + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/trade/components/placeholder_card.dart b/flutter_monisuo/lib/ui/pages/trade/components/placeholder_card.dart new file mode 100644 index 0000000..109d6f2 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/trade/components/placeholder_card.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../../../core/theme/app_spacing.dart'; + +/// 占位卡片组件 +/// +/// 当未选择币种时显示的占位提示卡片。 +class PlaceholderCard extends StatelessWidget { + final String message; + final ColorScheme colorScheme; + const PlaceholderCard({ + super.key, + required this.message, + required this.colorScheme, + }); + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + return Container( + width: double.infinity, + padding: const EdgeInsets.all(AppSpacing.xl), + decoration: BoxDecoration( + color: isDark + ? colorScheme.surfaceContainer + : colorScheme.surfaceContainerLowest, + borderRadius: BorderRadius.circular(AppRadius.lg), + border: Border.all( + color: colorScheme.outlineVariant.withOpacity(0.15), + ), + ), + child: Center( + child: Text(message, + style: GoogleFonts.inter( + color: colorScheme.onSurfaceVariant, + fontSize: 14, + )), + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/trade/components/price_card.dart b/flutter_monisuo/lib/ui/pages/trade/components/price_card.dart new file mode 100644 index 0000000..2fdabc1 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/trade/components/price_card.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../../../core/theme/app_color_scheme.dart'; +import '../../../../core/theme/app_spacing.dart'; +import '../../../../data/models/coin.dart'; + +/// 价格卡片组件 +/// +/// 显示当前币种价格和 24h 涨跌幅。 +/// 布局:大号价格(32px bold) + 涨跌幅徽章(圆角sm,涨绿背景) + "24h 变化" 副标题。 +class PriceCard extends StatelessWidget { + final Coin coin; + const PriceCard({super.key, required this.coin}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + final isUp = coin.isUp; + final changeColor = + isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.getDownColor(isDark); + final changeBgColor = isUp + ? AppColorScheme.getUpBackgroundColor(isDark) + : AppColorScheme.getDownBackgroundColor(isDark); + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: isDark + ? colorScheme.surfaceContainer + : colorScheme.surfaceContainerLowest, + borderRadius: BorderRadius.circular(AppRadius.lg), + border: Border.all( + color: colorScheme.outlineVariant.withOpacity(0.15), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 价格行:大号价格 + 涨跌幅徽章 + Row( + children: [ + Text( + coin.formattedPrice, + style: GoogleFonts.inter( + fontSize: 32, + fontWeight: FontWeight.w700, + color: colorScheme.onSurface, + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + const SizedBox(width: AppSpacing.sm), + // 涨跌幅徽章 - 圆角sm,涨绿背景 + Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.sm, vertical: AppSpacing.xs), + decoration: BoxDecoration( + color: changeBgColor, + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + child: Text( + coin.formattedChange, + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + color: changeColor, + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + ), + ], + ), + const SizedBox(height: AppSpacing.sm), + // 副标题 + Text( + '24h 变化', + style: GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.normal, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/trade/components/trade_button.dart b/flutter_monisuo/lib/ui/pages/trade/components/trade_button.dart new file mode 100644 index 0000000..9b00160 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/trade/components/trade_button.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../../../core/theme/app_color_scheme.dart'; +import '../../../../core/theme/app_spacing.dart'; + +/// 交易按钮组件 +/// +/// CTA 买入/卖出按钮。profit-green底 / sell-red底,圆角lg,高48,白字16px bold。 +class TradeButton extends StatelessWidget { + final bool isBuy; + final String? coinCode; + final bool enabled; + final bool isLoading; + final VoidCallback onPressed; + + const TradeButton({ + super.key, + required this.isBuy, + required this.coinCode, + required this.enabled, + required this.isLoading, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final fillColor = + isBuy ? AppColorScheme.buyButtonFill : AppColorScheme.sellButtonFill; + + return GestureDetector( + onTap: enabled ? onPressed : null, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 48, + decoration: BoxDecoration( + color: enabled ? fillColor : colorScheme.onSurface.withOpacity(0.08), + borderRadius: BorderRadius.circular(AppRadius.lg), + ), + child: Center( + child: isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : Text( + '${isBuy ? '买入' : '卖出'} ${coinCode ?? ""}', + style: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w700, + color: enabled + ? Colors.white + : colorScheme.onSurface.withOpacity(0.3), + ), + ), + ), + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/trade/components/trade_form_card.dart b/flutter_monisuo/lib/ui/pages/trade/components/trade_form_card.dart new file mode 100644 index 0000000..df34c84 --- /dev/null +++ b/flutter_monisuo/lib/ui/pages/trade/components/trade_form_card.dart @@ -0,0 +1,247 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../../../core/theme/app_color_scheme.dart'; +import '../../../../core/theme/app_spacing.dart'; +import '../../../../data/models/coin.dart'; +import 'amount_input.dart'; + +/// 交易表单卡片组件 +/// +/// 包含买入/卖出切换、金额输入、可用余额、快捷比例按钮、计算数量行。 +/// card背景 + 圆角lg + border + padding:20 + gap:16 +class TradeFormCard extends StatelessWidget { + final int tradeType; + final Coin? selectedCoin; + final TextEditingController amountController; + final String availableUsdt; + final String availableCoinQty; + final String calculatedQuantity; + final String maxAmount; + final ValueChanged onTradeTypeChanged; + final VoidCallback onAmountChanged; + final ValueChanged onFillPercent; + + const TradeFormCard({ + super.key, + required this.tradeType, + required this.selectedCoin, + required this.amountController, + required this.availableUsdt, + required this.availableCoinQty, + required this.calculatedQuantity, + required this.maxAmount, + required this.onTradeTypeChanged, + required this.onAmountChanged, + required this.onFillPercent, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + final isBuy = tradeType == 0; + final actionColor = isBuy + ? AppColorScheme.getUpColor(isDark) + : AppColorScheme.getDownColor(isDark); + + // 设计稿中 card 背景色 + final cardBgColor = isDark + ? colorScheme.surfaceContainer + : colorScheme.surfaceContainerLowest; + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: cardBgColor, + borderRadius: BorderRadius.circular(AppRadius.lg), + border: Border.all( + color: colorScheme.outlineVariant.withOpacity(0.15), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ---- 买入/卖出切换 ---- + // 设计稿:ClipRRect + 圆角md,两等宽按钮 + ClipRRect( + borderRadius: BorderRadius.circular(AppRadius.md), + child: Row( + children: [ + // 买入按钮 + Expanded( + child: GestureDetector( + onTap: () => onTradeTypeChanged(0), + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + height: 40, + decoration: BoxDecoration( + color: isBuy + ? AppColorScheme.buyButtonFill + : cardBgColor, + border: isBuy + ? null + : Border.all( + color: colorScheme.outlineVariant.withOpacity(0.15)), + ), + child: Center( + child: Text( + '买入', + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isBuy + ? Colors.white + : colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ), + ), + // 卖出按钮 + Expanded( + child: GestureDetector( + onTap: () => onTradeTypeChanged(1), + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + height: 40, + decoration: BoxDecoration( + color: !isBuy + ? AppColorScheme.sellButtonFill + : cardBgColor, + border: !isBuy + ? null + : Border.all( + color: colorScheme.outlineVariant.withOpacity(0.15)), + ), + child: Center( + child: Text( + '卖出', + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w600, + color: !isBuy + ? Colors.white + : colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: AppSpacing.md + AppSpacing.sm), + + // ---- 交易金额 label 行 ---- + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('交易金额', + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.normal, + color: colorScheme.onSurfaceVariant, + )), + Text('USDT', + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + )), + ], + ), + const SizedBox(height: AppSpacing.sm), + + // ---- 金额输入框 ---- + AmountInput( + amountController: amountController, + maxAmount: maxAmount, + isBuy: isBuy, + actionColor: actionColor, + onChanged: onAmountChanged, + ), + const SizedBox(height: AppSpacing.sm), + + // ---- 可用余额 ---- + Text( + isBuy + ? '可用: $availableUsdt USDT' + : '可用: $availableCoinQty ${selectedCoin?.code ?? ""}', + style: GoogleFonts.inter( + fontSize: 11, + fontWeight: FontWeight.normal, + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: AppSpacing.md), + + // ---- 快捷比例按钮 25% 50% 75% 100% ---- + // 设计稿:gap:8,圆角sm,bg-tertiary,高32 + Row( + children: [ + _buildPctButton('25%', 0.25, colorScheme), + const SizedBox(width: AppSpacing.sm), + _buildPctButton('50%', 0.5, colorScheme), + const SizedBox(width: AppSpacing.sm), + _buildPctButton('75%', 0.75, colorScheme), + const SizedBox(width: AppSpacing.sm), + _buildPctButton('100%', 1.0, colorScheme), + ], + ), + const SizedBox(height: AppSpacing.md + AppSpacing.sm), + + // ---- 计算数量行 ---- + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('交易数量', + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.normal, + color: colorScheme.onSurfaceVariant, + )), + Text( + '$calculatedQuantity ${selectedCoin?.code ?? ''}', + style: GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + ], + ), + ], + ), + ); + } + + /// 百分比按钮 - 设计稿:圆角sm,bg-tertiary,高32 + Widget _buildPctButton(String label, double pct, ColorScheme colorScheme) { + return Expanded( + child: GestureDetector( + onTap: () => onFillPercent(pct), + child: Container( + height: 32, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest.withOpacity(0.5), + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + child: Center( + child: Text(label, + style: GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w500, + color: colorScheme.onSurfaceVariant, + )), + ), + ), + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/trade/trade_page.dart b/flutter_monisuo/lib/ui/pages/trade/trade_page.dart index 743d725..e84d742 100644 --- a/flutter_monisuo/lib/ui/pages/trade/trade_page.dart +++ b/flutter_monisuo/lib/ui/pages/trade/trade_page.dart @@ -1,16 +1,19 @@ 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 '../../../data/models/coin.dart'; import '../../../providers/market_provider.dart'; import '../../../providers/asset_provider.dart'; import '../../../data/services/trade_service.dart'; -import '../../components/glass_panel.dart'; import '../../components/neon_glow.dart'; +import 'components/coin_selector.dart'; +import 'components/price_card.dart'; +import 'components/placeholder_card.dart'; +import 'components/trade_form_card.dart'; +import 'components/trade_button.dart'; +import 'components/confirm_dialog.dart'; /// 交易页面 /// @@ -124,7 +127,7 @@ class _TradePageState extends State child: Column( children: [ // 币种选择器卡片 - _CoinSelector( + CoinSelector( selectedCoin: _selectedCoin, coins: market.allCoins .where((c) => @@ -143,16 +146,16 @@ class _TradePageState extends State // 价格卡片 if (_selectedCoin != null) - _PriceCard(coin: _selectedCoin!) + PriceCard(coin: _selectedCoin!) else - _PlaceholderCard( + PlaceholderCard( message: '请先选择交易币种', colorScheme: colorScheme, ), const SizedBox(height: AppSpacing.md), // 交易表单卡片(内含买入/卖出切换 + 表单) - _TradeFormCard( + TradeFormCard( tradeType: _tradeType, selectedCoin: _selectedCoin, amountController: _amountController, @@ -173,7 +176,7 @@ class _TradePageState extends State SizedBox( width: double.infinity, height: 48, - child: _TradeButton( + child: TradeButton( isBuy: _tradeType == 0, coinCode: _selectedCoin?.code, enabled: _canTrade() && !_isSubmitting, @@ -217,7 +220,7 @@ class _TradePageState extends State final confirmed = await showDialog( context: context, - builder: (ctx) => _ConfirmDialog( + builder: (ctx) => ConfirmDialog( isBuy: isBuy, coinCode: coinCode, price: price, @@ -288,899 +291,3 @@ class _TradePageState extends State ); } } - -/// 确认对话框 -class _ConfirmDialog extends StatelessWidget { - final bool isBuy; - final String coinCode; - final String price; - final String quantity; - final String amount; - - const _ConfirmDialog({ - required this.isBuy, - required this.coinCode, - required this.price, - required this.quantity, - required this.amount, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; - final actionColor = isBuy - ? AppColorScheme.getUpColor(isDark) - : AppColorScheme.getDownColor(isDark); - - return Dialog( - backgroundColor: Colors.transparent, - child: GlassPanel( - borderRadius: BorderRadius.circular(AppRadius.lg), - padding: EdgeInsets.all(AppSpacing.lg), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: Text( - '确认${isBuy ? '买入' : '卖出'}', - style: GoogleFonts.inter( - fontSize: 16, - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - ), - ), - SizedBox(height: AppSpacing.lg), - _dialogRow('交易对', '$coinCode/USDT', colorScheme), - SizedBox(height: AppSpacing.sm), - _dialogRow('委托价格', '$price USDT', colorScheme), - SizedBox(height: AppSpacing.sm), - _dialogRow('交易金额', '$amount USDT', colorScheme, - valueColor: actionColor), - SizedBox(height: AppSpacing.sm), - _dialogRow('交易数量', '$quantity $coinCode', colorScheme), - SizedBox(height: AppSpacing.lg), - Row( - children: [ - Expanded( - child: NeonButton( - text: '取消', - type: NeonButtonType.outline, - onPressed: () => Navigator.of(context).pop(false), - height: 44, - showGlow: false, - ), - ), - SizedBox(width: AppSpacing.sm), - Expanded( - child: NeonButton( - text: '确认${isBuy ? '买入' : '卖出'}', - type: isBuy ? NeonButtonType.tertiary : NeonButtonType.error, - onPressed: () => Navigator.of(context).pop(true), - height: 44, - showGlow: true, - ), - ), - ], - ), - ], - ), - ), - ); - } - - Widget _dialogRow(String label, String value, ColorScheme colorScheme, - {Color? valueColor}) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(label, - style: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.normal, - color: colorScheme.onSurfaceVariant, - )), - Text(value, - style: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w600, - color: valueColor ?? colorScheme.onSurface, - )), - ], - ); - } -} - -// ============================================ -// 币种选择器 - 设计稿 Coin Selector Card -// card背景 + 圆角lg + border + padding:16 -// 横向布局:coinInfo(竖向 pair+name) + chevronDown -// ============================================ - -class _CoinSelector extends StatelessWidget { - final Coin? selectedCoin; - final List coins; - final ValueChanged onCoinSelected; - - const _CoinSelector({ - required this.selectedCoin, - required this.coins, - required this.onCoinSelected, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; - - return GestureDetector( - onTap: () => _showCoinPicker(context), - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(AppSpacing.md), - decoration: BoxDecoration( - color: isDark - ? colorScheme.surfaceContainer - : colorScheme.surfaceContainerLowest, - borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all( - color: colorScheme.outlineVariant.withOpacity(0.15), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // 币种信息:交易对 + 名称 - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - selectedCoin != null - ? '${selectedCoin!.code}/USDT' - : '选择币种', - style: GoogleFonts.inter( - fontSize: 18, - fontWeight: FontWeight.w700, - color: colorScheme.onSurface, - ), - ), - const SizedBox(height: 2), - Text( - selectedCoin?.name ?? '点击选择交易对', - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.normal, - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - // 下拉箭头 - Icon(LucideIcons.chevronDown, - size: 16, color: colorScheme.onSurfaceVariant), - ], - ), - ), - ); - } - - void _showCoinPicker(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; - - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - isScrollControlled: true, - builder: (ctx) => Container( - height: MediaQuery.of(ctx).size.height * 0.65, - decoration: BoxDecoration( - color: isDark - ? colorScheme.surface - : colorScheme.surfaceContainerLowest, - borderRadius: - BorderRadius.vertical(top: Radius.circular(AppRadius.xxl)), - ), - child: Column( - children: [ - // 拖动指示器 - Container( - margin: EdgeInsets.only(top: AppSpacing.sm), - width: 40, - height: 4, - decoration: BoxDecoration( - color: colorScheme.onSurfaceVariant.withOpacity(0.3), - borderRadius: BorderRadius.circular(2), - ), - ), - // 标题栏 - Padding( - padding: EdgeInsets.all(AppSpacing.lg), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('选择币种', - style: GoogleFonts.inter( - fontSize: 16, - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - )), - GestureDetector( - onTap: () => Navigator.of(ctx).pop(), - child: Icon(LucideIcons.x, - color: colorScheme.onSurfaceVariant), - ), - ], - ), - ), - Divider(height: 1, color: colorScheme.outlineVariant.withOpacity(0.2)), - // 币种列表 - Expanded( - child: ListView.builder( - padding: EdgeInsets.symmetric(vertical: AppSpacing.sm), - itemCount: coins.length, - itemBuilder: (listCtx, index) => - _buildCoinItem(coins[index], context, listCtx), - ), - ), - ], - ), - ), - ); - } - - Widget _buildCoinItem( - Coin coin, BuildContext context, BuildContext sheetContext) { - final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; - final isSelected = selectedCoin?.code == coin.code; - final changeColor = coin.isUp - ? AppColorScheme.getUpColor(isDark) - : AppColorScheme.getDownColor(isDark); - - return GestureDetector( - onTap: () { - Navigator.of(sheetContext).pop(); - onCoinSelected(coin); - }, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: AppSpacing.lg, vertical: AppSpacing.md), - color: - isSelected ? colorScheme.primary.withOpacity(0.1) : Colors.transparent, - child: Row( - children: [ - _CoinAvatar(icon: coin.displayIcon), - SizedBox(width: AppSpacing.md), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 第一行:币种代码 + USDT + 价格 + 涨跌幅 - Row( - children: [ - Text(coin.code, - style: GoogleFonts.inter( - fontSize: 15, - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - )), - SizedBox(width: AppSpacing.xs), - Text('/USDT', - style: GoogleFonts.inter( - fontSize: 11, - color: colorScheme.onSurfaceVariant, - )), - const Spacer(), - Text('\$${coin.formattedPrice}', - style: GoogleFonts.inter( - fontSize: 13, - fontWeight: FontWeight.w600, - color: colorScheme.onSurface, - )), - SizedBox(width: AppSpacing.sm), - // 涨跌幅徽章 - Container( - padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: changeColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(AppRadius.sm), - ), - child: Text(coin.formattedChange, - style: GoogleFonts.inter( - fontSize: 11, - color: changeColor, - fontWeight: FontWeight.w600, - )), - ), - if (isSelected) ...[ - SizedBox(width: AppSpacing.sm), - Icon(LucideIcons.check, - size: 16, color: colorScheme.primary), - ], - ], - ), - SizedBox(height: 3), - // 第二行:币种名称 - Text(coin.name, - style: GoogleFonts.inter( - fontSize: 12, - color: colorScheme.onSurfaceVariant, - )), - ], - ), - ), - ], - ), - ), - ); - } -} - -/// 币种头像 -class _CoinAvatar extends StatelessWidget { - final String? icon; - const _CoinAvatar({this.icon}); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: colorScheme.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: colorScheme.primary.withOpacity(0.2)), - ), - child: Center( - child: Text(icon ?? '?', - style: TextStyle( - fontSize: 20, - color: colorScheme.primary, - fontWeight: FontWeight.bold, - )), - ), - ); - } -} - -// ============================================ -// 价格卡片 - 设计稿 Price Card -// card背景 + 圆角lg + border + padding:20 + gap:8 -// 竖向布局: -// priceRow: 大号价格(32px bold) + 涨跌幅徽章(圆角sm,涨绿背景) -// subtitle: "24h 变化" -// ============================================ - -class _PriceCard extends StatelessWidget { - final Coin coin; - const _PriceCard({required this.coin}); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; - final isUp = coin.isUp; - final changeColor = - isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.getDownColor(isDark); - final changeBgColor = isUp - ? AppColorScheme.getUpBackgroundColor(isDark) - : AppColorScheme.getDownBackgroundColor(isDark); - - return Container( - width: double.infinity, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: isDark - ? colorScheme.surfaceContainer - : colorScheme.surfaceContainerLowest, - borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all( - color: colorScheme.outlineVariant.withOpacity(0.15), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 价格行:大号价格 + 涨跌幅徽章 - Row( - children: [ - Text( - coin.formattedPrice, - style: GoogleFonts.inter( - fontSize: 32, - fontWeight: FontWeight.w700, - color: colorScheme.onSurface, - fontFeatures: [FontFeature.tabularFigures()], - ), - ), - const SizedBox(width: AppSpacing.sm), - // 涨跌幅徽章 - 圆角sm,涨绿背景 - Container( - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.sm, vertical: AppSpacing.xs), - decoration: BoxDecoration( - color: changeBgColor, - borderRadius: BorderRadius.circular(AppRadius.sm), - ), - child: Text( - coin.formattedChange, - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.w600, - color: changeColor, - fontFeatures: [FontFeature.tabularFigures()], - ), - ), - ), - ], - ), - const SizedBox(height: AppSpacing.sm), - // 副标题 - Text( - '24h 变化', - style: GoogleFonts.inter( - fontSize: 11, - fontWeight: FontWeight.normal, - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ); - } -} - -/// 占位卡片 -class _PlaceholderCard extends StatelessWidget { - final String message; - final ColorScheme colorScheme; - const _PlaceholderCard({required this.message, required this.colorScheme}); - - @override - Widget build(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return Container( - width: double.infinity, - padding: const EdgeInsets.all(AppSpacing.xl), - decoration: BoxDecoration( - color: isDark - ? colorScheme.surfaceContainer - : colorScheme.surfaceContainerLowest, - borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all( - color: colorScheme.outlineVariant.withOpacity(0.15), - ), - ), - child: Center( - child: Text(message, - style: GoogleFonts.inter( - color: colorScheme.onSurfaceVariant, - fontSize: 14, - )), - ), - ); - } -} - -// ============================================ -// 交易表单卡片 - 设计稿 Trade Form Card -// card背景 + 圆角lg + border + padding:20 + gap:16 -// 竖向布局: -// Buy/Sell Toggle(圆角md,clip,横向两等宽按钮) -// 金额label行("交易金额" + "USDT") -// 输入框(bg-tertiary,圆角md,高48) -// 可用余额文字 -// 快捷比例按钮行(25% 50% 75% 100%,gap:8) -// 计算数量行 -// ============================================ - -class _TradeFormCard extends StatelessWidget { - final int tradeType; - final Coin? selectedCoin; - final TextEditingController amountController; - final String availableUsdt; - final String availableCoinQty; - final String calculatedQuantity; - final String maxAmount; - final ValueChanged onTradeTypeChanged; - final VoidCallback onAmountChanged; - final ValueChanged onFillPercent; - - const _TradeFormCard({ - required this.tradeType, - required this.selectedCoin, - required this.amountController, - required this.availableUsdt, - required this.availableCoinQty, - required this.calculatedQuantity, - required this.maxAmount, - required this.onTradeTypeChanged, - required this.onAmountChanged, - required this.onFillPercent, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final isDark = Theme.of(context).brightness == Brightness.dark; - final isBuy = tradeType == 0; - final actionColor = isBuy - ? AppColorScheme.getUpColor(isDark) - : AppColorScheme.getDownColor(isDark); - - // 设计稿中 card 背景色 - final cardBgColor = isDark - ? colorScheme.surfaceContainer - : colorScheme.surfaceContainerLowest; - - return Container( - width: double.infinity, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: cardBgColor, - borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all( - color: colorScheme.outlineVariant.withOpacity(0.15), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // ---- 买入/卖出切换 ---- - // 设计稿:ClipRRect + 圆角md,两等宽按钮 - ClipRRect( - borderRadius: BorderRadius.circular(AppRadius.md), - child: Row( - children: [ - // 买入按钮 - Expanded( - child: GestureDetector( - onTap: () => onTradeTypeChanged(0), - child: AnimatedContainer( - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOut, - height: 40, - decoration: BoxDecoration( - color: isBuy - ? AppColorScheme.buyButtonFill - : cardBgColor, - border: isBuy - ? null - : Border.all( - color: colorScheme.outlineVariant.withOpacity(0.15)), - ), - child: Center( - child: Text( - '买入', - style: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w600, - color: isBuy - ? Colors.white - : colorScheme.onSurfaceVariant, - ), - ), - ), - ), - ), - ), - // 卖出按钮 - Expanded( - child: GestureDetector( - onTap: () => onTradeTypeChanged(1), - child: AnimatedContainer( - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOut, - height: 40, - decoration: BoxDecoration( - color: !isBuy - ? AppColorScheme.sellButtonFill - : cardBgColor, - border: !isBuy - ? null - : Border.all( - color: colorScheme.outlineVariant.withOpacity(0.15)), - ), - child: Center( - child: Text( - '卖出', - style: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w600, - color: !isBuy - ? Colors.white - : colorScheme.onSurfaceVariant, - ), - ), - ), - ), - ), - ), - ], - ), - ), - const SizedBox(height: AppSpacing.md + AppSpacing.sm), - - // ---- 交易金额 label 行 ---- - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('交易金额', - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.normal, - color: colorScheme.onSurfaceVariant, - )), - Text('USDT', - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.w600, - color: colorScheme.onSurface, - )), - ], - ), - const SizedBox(height: AppSpacing.sm), - - // ---- 金额输入框 ---- - _AmountInput( - amountController: amountController, - maxAmount: maxAmount, - isBuy: isBuy, - actionColor: actionColor, - onChanged: onAmountChanged, - ), - const SizedBox(height: AppSpacing.sm), - - // ---- 可用余额 ---- - Text( - isBuy - ? '可用: $availableUsdt USDT' - : '可用: $availableCoinQty ${selectedCoin?.code ?? ""}', - style: GoogleFonts.inter( - fontSize: 11, - fontWeight: FontWeight.normal, - color: colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: AppSpacing.md), - - // ---- 快捷比例按钮 25% 50% 75% 100% ---- - // 设计稿:gap:8,圆角sm,bg-tertiary,高32 - Row( - children: [ - _buildPctButton('25%', 0.25, colorScheme), - const SizedBox(width: AppSpacing.sm), - _buildPctButton('50%', 0.5, colorScheme), - const SizedBox(width: AppSpacing.sm), - _buildPctButton('75%', 0.75, colorScheme), - const SizedBox(width: AppSpacing.sm), - _buildPctButton('100%', 1.0, colorScheme), - ], - ), - const SizedBox(height: AppSpacing.md + AppSpacing.sm), - - // ---- 计算数量行 ---- - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('交易数量', - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.normal, - color: colorScheme.onSurfaceVariant, - )), - Text( - '$calculatedQuantity ${selectedCoin?.code ?? ''}', - style: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.w600, - color: colorScheme.onSurface, - fontFeatures: [FontFeature.tabularFigures()], - ), - ), - ], - ), - ], - ), - ); - } - - /// 百分比按钮 - 设计稿:圆角sm,bg-tertiary,高32 - Widget _buildPctButton(String label, double pct, ColorScheme colorScheme) { - return Expanded( - child: GestureDetector( - onTap: () => onFillPercent(pct), - child: Container( - height: 32, - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest.withOpacity(0.5), - borderRadius: BorderRadius.circular(AppRadius.sm), - ), - child: Center( - child: Text(label, - style: GoogleFonts.inter( - fontSize: 12, - fontWeight: FontWeight.w500, - color: colorScheme.onSurfaceVariant, - )), - ), - ), - ), - ); - } -} - -// ============================================ -// CTA 交易按钮 - 设计稿 Buy Button -// profit-green底 / sell-red底,圆角lg,高48,白字16px bold -// ============================================ - -class _TradeButton extends StatelessWidget { - final bool isBuy; - final String? coinCode; - final bool enabled; - final bool isLoading; - final VoidCallback onPressed; - - const _TradeButton({ - required this.isBuy, - required this.coinCode, - required this.enabled, - required this.isLoading, - required this.onPressed, - }); - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final fillColor = - isBuy ? AppColorScheme.buyButtonFill : AppColorScheme.sellButtonFill; - - return GestureDetector( - onTap: enabled ? onPressed : null, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - height: 48, - decoration: BoxDecoration( - color: enabled ? fillColor : colorScheme.onSurface.withOpacity(0.08), - borderRadius: BorderRadius.circular(AppRadius.lg), - ), - child: Center( - child: isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : Text( - '${isBuy ? '买入' : '卖出'} ${coinCode ?? ""}', - style: GoogleFonts.inter( - fontSize: 16, - fontWeight: FontWeight.w700, - color: enabled - ? Colors.white - : colorScheme.onSurface.withOpacity(0.3), - ), - ), - ), - ), - ); - } -} - -// ============================================ -// 金额输入框(含超额提示) -// 设计稿:bg-tertiary,圆角md,高48 -// ============================================ - -class _AmountInput extends StatefulWidget { - final TextEditingController amountController; - final String maxAmount; - final bool isBuy; - final Color actionColor; - final VoidCallback onChanged; - - const _AmountInput({ - required this.amountController, - required this.maxAmount, - required this.isBuy, - required this.actionColor, - required this.onChanged, - }); - - @override - State<_AmountInput> createState() => _AmountInputState(); -} - -class _AmountInputState extends State<_AmountInput> { - bool _isExceeded = false; - - void _checkLimit() { - final input = double.tryParse(widget.amountController.text) ?? 0; - final max = double.tryParse(widget.maxAmount) ?? 0; - final exceeded = widget.isBuy && input > max && max > 0 && input > 0; - if (exceeded != _isExceeded) { - setState(() => _isExceeded = exceeded); - } - widget.onChanged(); - } - - @override - void initState() { - super.initState(); - widget.amountController.addListener(_checkLimit); - } - - @override - void dispose() { - widget.amountController.removeListener(_checkLimit); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final warningColor = AppColorScheme.warning; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 48, - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest.withOpacity(0.3), - borderRadius: BorderRadius.circular(AppRadius.md), - ), - child: TextField( - controller: widget.amountController, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - onChanged: (_) => _checkLimit(), - style: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.normal, - color: colorScheme.onSurface, - fontFeatures: [FontFeature.tabularFigures()], - ), - decoration: InputDecoration( - hintText: '请输入金额', - hintStyle: GoogleFonts.inter( - fontSize: 14, - fontWeight: FontWeight.normal, - color: colorScheme.onSurfaceVariant.withOpacity(0.5), - ), - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric( - horizontal: AppSpacing.md, - ), - ), - ), - ), - if (_isExceeded) - Padding( - padding: EdgeInsets.only(top: AppSpacing.xs), - child: Row( - children: [ - Icon(Icons.error_outline, size: 13, color: warningColor), - SizedBox(width: 4), - Text( - '超出可用USDT余额', - style: GoogleFonts.inter( - fontSize: 11, - color: warningColor, - ), - ), - ], - ), - ), - ], - ); - } -}