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, - ), - ), - ], - ), - ), - ], - ); - } -}