Merge remote-tracking branch 'origin/main'

# Conflicts:
#	flutter_monisuo/lib/ui/pages/asset/asset_page.dart
#	flutter_monisuo/lib/ui/pages/orders/fund_orders_page.dart
This commit is contained in:
sion
2026-04-05 23:34:18 +08:00
183 changed files with 225563 additions and 3409 deletions

View File

@@ -1,23 +1,20 @@
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 '../../../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 '../../../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';
/// 资产页面 - Material Design 3 风格
/// 资产页面 - Matching .pen design spec (CMcqs)
class AssetPage extends StatefulWidget {
const AssetPage({super.key});
@@ -75,227 +72,54 @@ class _AssetPageState extends State<AssetPage> with AutomaticKeepAliveClientMixi
backgroundColor: colorScheme.surfaceContainerHighest,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: AppSpacing.pagePadding,
padding: const EdgeInsets.fromLTRB(AppSpacing.md, AppSpacing.md + 8, AppSpacing.md, 32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_TabSelector(
tabs: const ['资金账户', '交易账户'],
// Page title: "资产" 22px bold — matching .pen titleFrame padding [16,0,8,0]
Padding(
padding: const EdgeInsets.only(top: 16, bottom: 8),
child: Text(
'资产',
style: GoogleFonts.inter(
fontSize: 22,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
),
const SizedBox(height: AppSpacing.sm),
// Account tab switcher — pill-style matching .pen UE6xC
AccountTabSwitcher(
selectedIndex: _activeTab,
onChanged: (index) => setState(() => _activeTab = index),
),
SizedBox(height: AppSpacing.md),
_activeTab == 0
? _FundAccountCard(provider: provider)
: _TradeAccountCard(
holdings: provider.holdings,
tradeBalance: provider.overview?.tradeBalance,
),
const SizedBox(height: AppSpacing.md),
// Balance card — matching .pen 59637 (cornerRadius lg, stroke, padding 20, gap 12)
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),
onTransfer: () => _navigateToTransfer(context),
),
const SizedBox(height: AppSpacing.md),
// Records link row — matching .pen fLHtq (cornerRadius lg, padding [14,16], stroke)
RecordsLinkRow(
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const FundOrdersPage()),
),
),
const SizedBox(height: AppSpacing.md),
// Holdings section — matching .pen th9BG + 6X6tC
HoldingsSection(holdings: _activeTab == 1 ? provider.holdings : []),
],
),
),
);
},
),
);
}
}
/// 资产总览卡片 - Material Design 3 风格
class _AssetCard extends StatelessWidget {
final dynamic overview;
const _AssetCard({required this.overview});
@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: EdgeInsets.all(AppSpacing.lg + AppSpacing.sm),
decoration: BoxDecoration(
gradient: AppColorScheme.assetCardGradient,
borderRadius: BorderRadius.circular(AppRadius.xl),
boxShadow: [
BoxShadow(
color: colorScheme.primary.withOpacity(isDark ? 0.15 : 0.08),
blurRadius: 20,
),
],
),
child: Column(
children: [
Text(
'PORTFOLIO VALUE',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
color: Colors.white.withOpacity(0.7),
),
),
SizedBox(height: AppSpacing.sm),
Text(
'\$${overview?.totalAsset ?? '0.00'}',
style: GoogleFonts.spaceGrotesk(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(height: AppSpacing.md),
Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.xs + AppSpacing.xs,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.full),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
LucideIcons.trendingUp,
color: Colors.white.withOpacity(0.7),
size: 14,
),
SizedBox(width: AppSpacing.xs),
Text(
'总盈亏: ${overview?.totalProfit ?? '0.00'} USDT',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.7),
),
),
],
),
),
],
),
);
}
}
/// Tab 选择器 - Material Design 3 风格
class _TabSelector extends StatelessWidget {
final List<String> tabs;
final int selectedIndex;
final ValueChanged<int> onChanged;
const _TabSelector({
required this.tabs,
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(
padding: EdgeInsets.all(AppSpacing.xs),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Row(
children: tabs.asMap().entries.map((entry) {
final index = entry.key;
final label = entry.value;
final isSelected = index == selectedIndex;
return Expanded(
child: GestureDetector(
onTap: () => onChanged(index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: EdgeInsets.symmetric(vertical: AppSpacing.sm + AppSpacing.xs),
decoration: BoxDecoration(
color: isSelected ? colorScheme.primary : Colors.transparent,
borderRadius: BorderRadius.circular(AppRadius.md),
boxShadow: isSelected
? [
BoxShadow(
color: colorScheme.primary.withOpacity(isDark ? 0.15 : 0.08),
blurRadius: 10,
),
]
: null,
),
child: Center(
child: Text(
label,
style: TextStyle(
color: isSelected ? colorScheme.onPrimary : colorScheme.onSurfaceVariant,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
),
),
),
);
}).toList(),
),
);
}
}
/// 资金账户卡片 - Glass Panel 风格
class _FundAccountCard extends StatelessWidget {
final AssetProvider provider;
const _FundAccountCard({required this.provider});
@override
Widget build(BuildContext context) {
final fund = provider.fundAccount;
final overview = provider.overview;
final colorScheme = Theme.of(context).colorScheme;
// 优先使用fund数据如果为null则使用overview的fundBalance
final displayBalance = fund?.balance ?? overview?.fundBalance ?? '0.00';
return GlassPanel(
padding: EdgeInsets.all(AppSpacing.lg + AppSpacing.xs),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'USDT 余额',
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const FundOrdersPage()),
),
child: Row(
children: [
Text(
'订单记录',
style: TextStyle(
color: colorScheme.primary,
fontSize: 12,
),
),
Icon(
LucideIcons.chevronRight,
size: 14,
color: colorScheme.primary,
),
],
),
),
],
),
SizedBox(height: AppSpacing.sm),
@@ -1202,57 +1026,13 @@ void _showWithdrawDialog(BuildContext context, String? balance) {
);
}
void _navigateToTransfer(BuildContext context) async {
final result = await Navigator.push<bool>(
context,
MaterialPageRoute(builder: (_) => const TransferPage()),
);
// 如果划转成功,刷新数据
if (result == true && context.mounted) {
context.read<AssetProvider>().refreshAll(force: true);
void _navigateToTransfer(BuildContext context) async {
final result = await Navigator.push<bool>(
context,
MaterialPageRoute(builder: (_) => const TransferPage()),
);
if (result == true && context.mounted) {
context.read<AssetProvider>().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: EdgeInsets.all(AppSpacing.lg),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(title,
style: GoogleFonts.spaceGrotesk(
fontSize: 16,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
)),
if (message != null) ...[
SizedBox(height: AppSpacing.sm),
Text(message,
style: TextStyle(color: colorScheme.onSurfaceVariant),
textAlign: TextAlign.center),
],
SizedBox(height: AppSpacing.lg),
SizedBox(
width: double.infinity,
child: NeonButton(
text: '确定',
type: NeonButtonType.primary,
onPressed: () => Navigator.of(ctx).pop(),
height: 44,
showGlow: false,
),
),
],
),
),
),
);
}

View File

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

View File

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

View File

@@ -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<ShadFormState>();
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<AssetProvider>().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<String, dynamic> 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<AssetProvider>().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<ShadFormState>();
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<AssetProvider>().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,
),
),
],
),
),
),
);
}

View File

@@ -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) => ',',
);
}
}

View File

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

View File

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

View File

@@ -1,16 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../core/theme/app_color_scheme.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../providers/asset_provider.dart';
import '../../../data/models/account_models.dart';
import '../../shared/ui_constants.dart';
import '../../components/neon_glow.dart';
/// 划转页面 - 币安风格
/// 划转页面
class TransferPage extends StatefulWidget {
const TransferPage({super.key});
@@ -20,6 +17,7 @@ class TransferPage extends StatefulWidget {
class _TransferPageState extends State<TransferPage> {
final _amountController = TextEditingController();
final _focusNode = FocusNode();
int _direction = 1; // 1: 资金→交易, 2: 交易→资金
bool _isLoading = false;
@@ -34,9 +32,14 @@ class _TransferPageState extends State<TransferPage> {
@override
void dispose() {
_amountController.dispose();
_focusNode.dispose();
super.dispose();
}
// ============================================
// 数据访问
// ============================================
/// 获取资金账户余额
String get _fundBalance {
final provider = context.read<AssetProvider>();
@@ -64,9 +67,7 @@ class _TransferPageState extends State<TransferPage> {
}
/// 获取当前可用余额(根据方向)
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<TransferPage> {
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<void> _doTransfer() async {
final amount = _amountController.text;
@@ -124,7 +146,6 @@ class _TransferPageState extends State<TransferPage> {
void _setQuickAmount(double percent) {
final available = double.tryParse(_availableBalance) ?? 0;
final amount = available * percent;
// 保留8位小数去除末尾0
_amountController.text = amount.toStringAsFixed(8).replaceAll(RegExp(r'\.?0+$'), '');
}
@@ -135,162 +156,43 @@ class _TransferPageState extends State<TransferPage> {
});
}
// ============================================
// 构建 UI
// ============================================
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final c = _colors;
return Scaffold(
backgroundColor: colorScheme.background,
backgroundColor: c.bgSecondary,
appBar: AppBar(
backgroundColor: Colors.transparent,
backgroundColor: c.surfaceCard,
elevation: 0,
scrolledUnderElevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: colorScheme.onSurface),
icon: Icon(LucideIcons.arrowLeft, color: c.textPrimary, size: 20),
onPressed: () => Navigator.of(context).pop(),
),
title: Text(
'资金划转',
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
'账户划转',
style: _inter(fontSize: 16, fontWeight: FontWeight.w600, color: c.textPrimary),
),
centerTitle: true,
),
body: Consumer<AssetProvider>(
builder: (context, provider, _) {
return SingleChildScrollView(
padding: AppSpacing.pagePadding,
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
child: Column(
children: [
// 第一个卡片位置 - 带动画
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: const Duration(milliseconds: 300),
builder: (context, value, child) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
transitionBuilder: (widget, animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, -1),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: widget,
),
);
},
child: _direction == 1
? _buildAccountCard(
key: const ValueKey('from-card'),
label: '',
accountName: _fromLabel,
balance: _fromBalance,
isDark: isDark,
colorScheme: colorScheme,
)
: _buildAccountCard(
key: const ValueKey('to-card-top'),
label: '',
accountName: _toLabel,
balance: _toBalance,
isDark: isDark,
colorScheme: colorScheme,
),
);
},
),
// 方向切换按钮(固定在中间)
GestureDetector(
onTap: _toggleDirection,
child: Container(
margin: EdgeInsets.symmetric(vertical: AppSpacing.sm),
padding: EdgeInsets.all(AppSpacing.sm + AppSpacing.xs),
decoration: BoxDecoration(
color: colorScheme.primary,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: colorScheme.primary.withOpacity(0.3),
blurRadius: 8,
),
],
),
child: Icon(
Icons.swap_vert,
color: colorScheme.onPrimary,
size: 22,
),
),
),
// 第二个卡片位置 - 带动画
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
transitionBuilder: (widget, animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: widget,
),
);
},
child: _direction == 1
? _buildAccountCard(
key: const ValueKey('to-card'),
label: '',
accountName: _toLabel,
balance: _toBalance,
isDark: isDark,
colorScheme: colorScheme,
)
: _buildAccountCard(
key: const ValueKey('from-card-bottom'),
label: '',
accountName: _fromLabel,
balance: _fromBalance,
isDark: isDark,
colorScheme: colorScheme,
),
),
SizedBox(height: AppSpacing.lg),
// 金额输入卡片
_buildAmountSection(colorScheme, isDark),
SizedBox(height: AppSpacing.lg),
// 确认按钮
SizedBox(
width: double.infinity,
child: NeonButton(
text: _isLoading ? '处理中...' : '确认划转',
icon: _isLoading ? null : LucideIcons.arrowRightLeft,
type: NeonButtonType.primary,
onPressed: _isLoading ? null : _doTransfer,
height: 52,
showGlow: true,
),
),
SizedBox(height: AppSpacing.md),
// 划转说明
_buildTips(colorScheme),
_buildTransferDirectionCard(c),
const SizedBox(height: 24),
_buildAmountSection(c),
const SizedBox(height: 24),
_buildTipsCard(c),
const SizedBox(height: 24),
_buildConfirmButton(c),
],
),
);
@@ -299,280 +201,317 @@ class _TransferPageState extends State<TransferPage> {
);
}
/// 账户卡片
Widget _buildAccountCard({
Key? key,
// ============================================
// Transfer direction card
// ============================================
Widget _buildTransferDirectionCard(_TransferColors c) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: c.surfaceCard,
borderRadius: BorderRadius.circular(AppRadius.xl),
border: Border.all(color: c.borderDefault.withValues(alpha: 0.6)),
),
child: Column(
children: [
// Source account
_animatedSwitcher(
key: 'src-$_direction',
beginOffset: const Offset(0, -1),
child: _buildAccountRow(
label: '',
accountName: _fromLabel,
balance: _fromBalance,
c: c,
),
),
// Swap button
GestureDetector(
onTap: _toggleDirection,
child: Container(
width: 36,
height: 36,
margin: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: c.accentPrimary,
shape: BoxShape.circle,
),
child: Center(
child: Icon(LucideIcons.arrowUpDown, size: 18, color: c.textInverse),
),
),
),
// Destination account
_animatedSwitcher(
key: 'dst-$_direction',
beginOffset: const Offset(0, 1),
child: _buildAccountRow(
label: '',
accountName: _toLabel,
balance: _toBalance,
c: c,
),
),
],
),
);
}
/// 统一的 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<Offset>(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({
required String label,
required String accountName,
required String balance,
required bool isDark,
required ColorScheme colorScheme,
required _TransferColors c,
}) {
return Container(
key: key,
return SizedBox(
width: double.infinity,
padding: EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: isDark ? colorScheme.surfaceContainer : colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: _inter(fontSize: 11, fontWeight: FontWeight.normal, color: c.textMuted)),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm, vertical: AppSpacing.xs),
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: colorScheme.primary,
),
),
),
SizedBox(width: AppSpacing.sm),
Text(
accountName,
style: GoogleFonts.spaceGrotesk(
fontSize: 16,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
],
),
SizedBox(height: AppSpacing.sm),
Row(
children: [
Text(
'可用余额',
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
SizedBox(width: AppSpacing.xs),
Text(
'$balance USDT',
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
],
),
],
),
);
}
/// 金额输入区域
Widget _buildAmountSection(ColorScheme colorScheme, bool isDark) {
return Container(
width: double.infinity,
padding: EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: isDark ? colorScheme.surfaceContainer : colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'划转金额',
style: TextStyle(
fontSize: 13,
color: colorScheme.onSurfaceVariant,
),
),
SizedBox(height: AppSpacing.sm),
// 金额输入行
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: TextField(
controller: _amountController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,8}')),
],
style: GoogleFonts.spaceGrotesk(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
decoration: InputDecoration(
hintText: '0.00',
hintStyle: GoogleFonts.spaceGrotesk(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.onSurfaceVariant.withOpacity(0.3),
),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
),
),
),
Padding(
padding: EdgeInsets.only(bottom: 4),
child: Text(
'USDT',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurfaceVariant,
),
),
),
],
),
SizedBox(height: AppSpacing.sm),
// 快捷按钮行
Row(
children: [
Text(
'可用: ${_availableBalance}',
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
Spacer(),
_buildQuickButton('25%', 0.25, colorScheme),
SizedBox(width: AppSpacing.xs),
_buildQuickButton('50%', 0.50, colorScheme),
SizedBox(width: AppSpacing.xs),
_buildQuickButton('75%', 0.75, colorScheme),
SizedBox(width: AppSpacing.xs),
_buildQuickButton('全部', 1.0, colorScheme),
],
),
if (_direction == 2) ...[
SizedBox(height: AppSpacing.sm),
Container(
padding: EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: AppColorScheme.warning.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Row(
Row(
children: [
Icon(
LucideIcons.triangleAlert,
size: 14,
color: AppColorScheme.warning,
),
SizedBox(width: AppSpacing.xs),
Expanded(
child: Text(
'仅支持 USDT 资产划转到资金账户',
style: TextStyle(
fontSize: 11,
color: AppColorScheme.warning,
),
),
label == '' ? LucideIcons.wallet : LucideIcons.repeat,
size: 18,
color: c.textSecondary,
),
const SizedBox(width: 10),
Text(accountName, style: _inter(fontSize: 14, fontWeight: FontWeight.w600, color: c.textPrimary)),
],
),
Text(
'\u00A5 ${_formatBalance(balance)}',
style: _inter(fontSize: 14, fontWeight: FontWeight.w600, color: c.textPrimary),
),
],
),
],
),
);
}
// ============================================
// Amount input section
// ============================================
Widget _buildAmountSection(_TransferColors c) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label row: "划转金额" + "全部划转"
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('划转金额', style: _inter(fontSize: 14, fontWeight: FontWeight.w500, color: c.textSecondary)),
GestureDetector(
onTap: () => _setQuickAmount(1.0),
child: Text('全部划转', style: _inter(fontSize: 12, fontWeight: FontWeight.w600, color: c.goldAccent)),
),
],
],
),
);
}
/// 快捷百分比按钮
Widget _buildQuickButton(String label, double percent, ColorScheme colorScheme) {
return GestureDetector(
onTap: () => _setQuickAmount(percent),
child: Container(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm, vertical: AppSpacing.xs),
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: colorScheme.primary,
),
),
),
);
}
const SizedBox(height: 12),
/// 划转说明
Widget _buildTips(ColorScheme colorScheme) {
return Container(
padding: EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'划转说明',
style: GoogleFonts.spaceGrotesk(
fontSize: 12,
fontWeight: FontWeight.w600,
color: colorScheme.onSurfaceVariant,
),
),
SizedBox(height: AppSpacing.sm),
_buildTipItem('资金账户用于充提,交易账户用于买卖币种', colorScheme),
_buildTipItem('划转操作即时到账,不可撤销', colorScheme),
_buildTipItem('交易账户只有 USDT 可直接划转到资金账户', colorScheme),
_buildTipItem('其他币种需先卖出换成 USDT 后才能划转', colorScheme),
],
),
);
}
Widget _buildTipItem(String text, ColorScheme colorScheme) {
return Padding(
padding: EdgeInsets.only(bottom: AppSpacing.xs),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 4,
height: 4,
margin: EdgeInsets.only(top: 6, right: AppSpacing.sm),
// Amount input field
GestureDetector(
onTap: () => _focusNode.requestFocus(),
child: Container(
width: double.infinity,
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: colorScheme.onSurfaceVariant,
shape: BoxShape.circle,
color: c.bgTertiary,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: TextField(
controller: _amountController,
focusNode: _focusNode,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,8}')),
],
style: _inter(fontSize: 28, fontWeight: FontWeight.w700, color: c.textPrimary),
decoration: InputDecoration(
hintText: '0.00',
hintStyle: _inter(fontSize: 28, fontWeight: FontWeight.w700, color: c.textMuted),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
),
),
),
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text('USDT', style: _inter(fontSize: 14, fontWeight: FontWeight.normal, color: c.textMuted)),
),
],
),
),
),
const SizedBox(height: 12),
// Percent buttons
Row(
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(),
),
],
);
}
Widget _buildPercentButton(String label, double percent, _TransferColors c) {
return Expanded(
child: GestureDetector(
onTap: () => _setQuickAmount(percent),
child: Container(
height: 36,
decoration: BoxDecoration(
color: c.bgTertiary,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Center(
child: Text(label, style: _inter(fontSize: 13, fontWeight: FontWeight.w500, color: c.textSecondary)),
),
),
),
);
}
// ============================================
// Tips card & Confirm button
// ============================================
Widget _buildTipsCard(_TransferColors c) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: c.profitGreenBg,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Row(
children: [
Icon(LucideIcons.info, size: 16, color: c.profitGreen),
const SizedBox(width: 8),
Expanded(
child: Text(
text,
style: TextStyle(
fontSize: 11,
color: colorScheme.onSurfaceVariant,
),
'划转即时到账,无需手续费',
style: _inter(fontSize: 12, fontWeight: FontWeight.normal, color: c.profitGreen),
),
),
],
),
);
}
Widget _buildConfirmButton(_TransferColors c) {
return SizedBox(
width: double.infinity,
height: 52,
child: GestureDetector(
onTap: _isLoading ? null : _doTransfer,
child: Container(
decoration: BoxDecoration(
color: c.accentPrimary,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Center(
child: _isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(c.textInverse),
),
)
: Text(
'确认划转',
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);
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart';
import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../providers/auth_provider.dart';
import '../main/main_page.dart';
@@ -16,38 +17,52 @@ class LoginPage extends StatefulWidget {
class _LoginPageState extends State<LoginPage> {
final formKey = GlobalKey<ShadFormState>();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
static const _maxFormWidth = 400.0;
static const _logoSize = 64.0;
static const _loadingIndicatorSize = 16.0;
static const _logoCircleSize = 80.0;
static const _inputHeight = 52.0;
static const _buttonHeight = 52.0;
/// 设计稿 radius-lg = 14
static const _designRadiusLg = 14.0;
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: _maxFormWidth),
child: Padding(
padding: EdgeInsets.all(AppSpacing.lg),
child: ShadForm(
key: formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildHeader(theme),
SizedBox(height: AppSpacing.xxl),
_buildUsernameField(),
SizedBox(height: AppSpacing.md),
_buildPasswordField(),
SizedBox(height: AppSpacing.lg),
_buildLoginButton(),
SizedBox(height: AppSpacing.md),
_buildRegisterLink(theme),
],
),
backgroundColor: isDark
? AppColorScheme.darkBackground
: AppColorScheme.lightSurface,
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.xl,
vertical: AppSpacing.xxl,
),
child: ShadForm(
key: formKey,
child: Column(
children: [
// 顶部品牌区域
_buildBrandSection(isDark),
const SizedBox(height: AppSpacing.xxl),
// 表单区域
_buildFormSection(isDark),
const SizedBox(height: AppSpacing.xl),
// 底部注册链接
_buildRegisterRow(isDark),
],
),
),
),
@@ -55,87 +70,258 @@ class _LoginPageState extends State<LoginPage> {
);
}
Widget _buildHeader(ShadThemeData theme) {
// ============================================
// 品牌区域 - Logo + 品牌名 + 标语
// ============================================
Widget _buildBrandSection(bool isDark) {
return Column(
children: [
Icon(
LucideIcons.trendingUp,
size: _logoSize,
color: theme.colorScheme.primary,
// Logo 圆形:渐变 #1F2937 → #374151内含 "M"
Container(
width: _logoCircleSize,
height: _logoCircleSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF1F2937), Color(0xFF374151)],
),
),
alignment: Alignment.center,
child: Text(
'M',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w800,
color: isDark
? AppColorScheme.darkOnSurface
: Colors.white,
),
),
),
SizedBox(height: AppSpacing.lg),
const SizedBox(height: AppSpacing.md),
// 品牌名 "MONISUO"
Text(
'模拟所',
style: theme.textTheme.h1,
'MONISUO',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w800,
letterSpacing: 3,
color: isDark
? AppColorScheme.darkOnSurface
: AppColorScheme.lightOnSurface,
),
textAlign: TextAlign.center,
),
SizedBox(height: AppSpacing.sm),
const SizedBox(height: AppSpacing.md),
// 标语
Text(
'虚拟货币模拟交易平台',
style: theme.textTheme.muted,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
color: isDark
? AppColorScheme.darkOnSurfaceVariant
: AppColorScheme.lightOnSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
);
}
Widget _buildUsernameField() {
return ShadInputFormField(
id: 'username',
label: const Text('用户名'),
placeholder: const Text('请输入用户名'),
leading: const Icon(LucideIcons.user),
validator: _validateUsername,
// ============================================
// 表单区域 - 用户名 + 密码 + 登录按钮
// ============================================
Widget _buildFormSection(bool isDark) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildUsernameField(isDark),
const SizedBox(height: AppSpacing.md),
_buildPasswordField(isDark),
const SizedBox(height: AppSpacing.sm),
_buildLoginButton(isDark),
],
);
}
Widget _buildPasswordField() {
return ShadInputFormField(
id: 'password',
label: const Text('密码'),
placeholder: const Text('请输入密码'),
obscureText: true,
leading: const Icon(LucideIcons.lock),
validator: _validatePassword,
Widget _buildUsernameField(bool isDark) {
final borderColor = isDark
? AppColorScheme.darkOutlineVariant
: AppColorScheme.lightOutlineVariant;
final cardColor = isDark
? AppColorScheme.darkSurfaceContainer
: AppColorScheme.lightSurfaceLowest;
final iconColor = isDark
? AppColorScheme.darkOnSurfaceMuted
: AppColorScheme.lightOnSurfaceMuted;
return SizedBox(
height: _inputHeight,
child: ShadInputFormField(
id: 'username',
placeholder: const Text('请输入用户名'),
leading: Padding(
padding: const EdgeInsets.only(right: AppSpacing.sm),
child: Icon(LucideIcons.user, size: 18, color: iconColor),
),
validator: _validateUsername,
controller: _usernameController,
decoration: ShadDecoration(
border: ShadBorder.all(
color: borderColor,
radius: BorderRadius.circular(_designRadiusLg),
),
),
style: TextStyle(
fontSize: 14,
color: isDark
? AppColorScheme.darkOnSurface
: AppColorScheme.lightOnSurface,
),
),
);
}
Widget _buildLoginButton() {
Widget _buildPasswordField(bool isDark) {
final borderColor = isDark
? AppColorScheme.darkOutlineVariant
: AppColorScheme.lightOutlineVariant;
final iconColor = isDark
? AppColorScheme.darkOnSurfaceMuted
: AppColorScheme.lightOnSurfaceMuted;
return SizedBox(
height: _inputHeight,
child: ShadInputFormField(
id: 'password',
placeholder: const Text('请输入密码'),
obscureText: _obscurePassword,
leading: Padding(
padding: const EdgeInsets.only(right: AppSpacing.sm),
child: Icon(LucideIcons.lock, size: 18, color: iconColor),
),
trailing: GestureDetector(
onTap: () => setState(() => _obscurePassword = !_obscurePassword),
child: Icon(
_obscurePassword ? LucideIcons.eyeOff : LucideIcons.eye,
size: 18,
color: iconColor,
),
),
validator: _validatePassword,
controller: _passwordController,
decoration: ShadDecoration(
border: ShadBorder.all(
color: borderColor,
radius: BorderRadius.circular(_designRadiusLg),
),
),
style: TextStyle(
fontSize: 14,
color: isDark
? AppColorScheme.darkOnSurface
: AppColorScheme.lightOnSurface,
),
),
);
}
Widget _buildLoginButton(bool isDark) {
// 设计稿: accent-primary = light:#1F2937 / dark:#D4AF37
final buttonColor = isDark
? AppColorScheme.darkSecondary
: const Color(0xFF1F2937);
final textColor = isDark
? AppColorScheme.darkBackground
: Colors.white;
return Consumer<AuthProvider>(
builder: (context, auth, _) {
return ShadButton(
onPressed: auth.isLoading ? null : () => _handleLogin(auth),
child: auth.isLoading
? const SizedBox.square(
dimension: _loadingIndicatorSize,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
return SizedBox(
height: _buttonHeight,
child: ShadButton(
onPressed: auth.isLoading ? null : () => _handleLogin(auth),
backgroundColor: buttonColor,
foregroundColor: textColor,
decoration: ShadDecoration(
border: ShadBorder.all(
radius: BorderRadius.circular(_designRadiusLg),
),
),
child: auth.isLoading
? SizedBox.square(
dimension: _loadingIndicatorSize,
child: CircularProgressIndicator(
strokeWidth: 2,
color: textColor,
),
)
: Text(
'登录',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: textColor,
),
),
)
: const Text('登录'),
),
);
},
);
}
Widget _buildRegisterLink(ShadThemeData theme) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'还没有账号?',
style: theme.textTheme.muted,
),
ShadButton.link(
onPressed: _navigateToRegister,
child: const Text('立即注册'),
),
],
// ============================================
// 底部注册链接
// ============================================
Widget _buildRegisterRow(bool isDark) {
// gold-accent: light=#F59E0B / dark=#D4AF37
final goldColor = isDark
? AppColorScheme.darkSecondary
: const Color(0xFFF59E0B);
final secondaryTextColor = isDark
? AppColorScheme.darkOnSurfaceVariant
: AppColorScheme.lightOnSurfaceVariant;
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.xl),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'还没有账户?',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
color: secondaryTextColor,
),
),
const SizedBox(width: AppSpacing.xs),
GestureDetector(
onTap: _navigateToRegister,
child: Text(
'立即注册',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: goldColor,
),
),
),
],
),
);
}
// ============================================
// Validators
// ============================================
String? _validateUsername(String? value) {
if (value == null || value.isEmpty) {
return '请输入用户名';
@@ -156,7 +342,10 @@ class _LoginPageState extends State<LoginPage> {
return null;
}
// ============================================
// Actions
// ============================================
Future<void> _handleLogin(AuthProvider auth) async {
if (!formKey.currentState!.saveAndValidate()) return;
@@ -176,7 +365,6 @@ class _LoginPageState extends State<LoginPage> {
}
void _navigateToMainPage() {
// 使用 Navigator 跳转到主页面,替换当前页面
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const MainPage()),
(route) => false,

View File

@@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../providers/auth_provider.dart';
/// 首页顶栏 - Logo + 搜索/通知/头像
class HeaderBar extends StatelessWidget {
const HeaderBar({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Row(
children: [
// Logo
Text(
'MONISUO',
style: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w700,
letterSpacing: 1,
color: colorScheme.onSurface,
),
),
const Spacer(),
// Search button
_IconButton(
icon: LucideIcons.search,
colorScheme: colorScheme,
onTap: () {},
),
const SizedBox(width: 8),
// Bell button
_IconButton(
icon: LucideIcons.bell,
colorScheme: colorScheme,
onTap: () {},
),
const SizedBox(width: 8),
// Avatar
Consumer<AuthProvider>(
builder: (context, auth, _) {
final username = auth.user?.username ?? '';
final initial = username.isNotEmpty ? username[0].toUpperCase() : '?';
return Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: colorScheme.primary,
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(
initial,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
);
},
),
],
),
);
}
}
class _IconButton extends StatelessWidget {
const _IconButton({
required this.icon,
required this.colorScheme,
required this.onTap,
});
final IconData icon;
final ColorScheme colorScheme;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(16),
),
alignment: Alignment.center,
child: Icon(
icon,
size: 16,
color: colorScheme.onSurfaceVariant,
),
),
);
}
}

View File

@@ -18,6 +18,9 @@ import '../../components/glass_panel.dart';
import '../../components/neon_glow.dart';
import '../main/main_page.dart';
import '../mine/welfare_center_page.dart';
import 'header_bar.dart';
import 'quick_actions_row.dart';
import 'hot_coins_section.dart';
/// 首页
class HomePage extends StatefulWidget {
@@ -102,8 +105,8 @@ class _HomePageState extends State<HomePage>
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 问候
_GreetingSection(),
// Header
HeaderBar(),
SizedBox(height: AppSpacing.md),
// 资产卡片(含总盈利 + 可折叠盈亏日历)
_AssetCard(
@@ -111,6 +114,15 @@ class _HomePageState extends State<HomePage>
onDeposit: _showDeposit,
),
SizedBox(height: AppSpacing.md),
// 快捷操作栏
QuickActionsRow(
onDeposit: _showDeposit,
onWithdraw: () => _navigateToAssetPage(),
onTransfer: () => _navigateToAssetPage(),
onProfit: () {},
onBills: () => _navigateToAssetPage(),
),
SizedBox(height: AppSpacing.md),
// 福利中心入口卡片
_WelfareCard(
totalClaimable: _totalClaimable,
@@ -120,6 +132,9 @@ class _HomePageState extends State<HomePage>
),
),
SizedBox(height: AppSpacing.lg),
// 热门币种
HotCoinsSection(),
SizedBox(height: AppSpacing.lg),
// 持仓
_HoldingsSection(holdings: provider.holdings),
],
@@ -416,40 +431,7 @@ class _HomePageState extends State<HomePage>
}
}
/// 问候区域
class _GreetingSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Consumer<AuthProvider>(
builder: (context, auth, _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'欢迎回来,',
style: TextStyle(
color: colorScheme.onSurfaceVariant,
fontSize: 14,
),
),
SizedBox(height: AppSpacing.xs),
Text(
auth.user?.username ?? '用户',
style: TextStyle(
color: colorScheme.onSurface,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
],
);
},
);
}
}
/// Header 栏:品牌名 + 搜索/通知/头像
/// 资产卡片(含总盈利 + 可折叠盈亏日历)
class _AssetCard extends StatefulWidget {
final AssetOverview? overview;

View File

@@ -0,0 +1,199 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart';
/// 首页热门币种区块
class HotCoinsSection extends StatelessWidget {
const HotCoinsSection({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Column(
children: [
// Title row
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'热门币种',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
Text(
'更多',
style: GoogleFonts.inter(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
const SizedBox(height: 12),
// Card
Container(
decoration: BoxDecoration(
color: colorScheme.surfaceContainer,
border: Border.all(
color: colorScheme.outlineVariant,
width: 1,
),
borderRadius: BorderRadius.circular(AppRadius.xl),
),
child: Column(
children: [
_CoinRow(
symbol: 'BTC',
pair: 'BTC/USDT',
fullName: 'Bitcoin',
price: '68,432.50',
change: '+2.35%',
isUp: true,
colorScheme: colorScheme,
isDark: isDark,
),
Divider(
height: 1,
thickness: 1,
color: colorScheme.outlineVariant.withValues(alpha: 0.15),
),
_CoinRow(
symbol: 'ETH',
pair: 'ETH/USDT',
fullName: 'Ethereum',
price: '3,856.20',
change: '+1.82%',
isUp: true,
colorScheme: colorScheme,
isDark: isDark,
),
Divider(
height: 1,
thickness: 1,
color: colorScheme.outlineVariant.withValues(alpha: 0.15),
),
_CoinRow(
symbol: 'SOL',
pair: 'SOL/USDT',
fullName: 'Solana',
price: '178.65',
change: '-0.94%',
isUp: false,
colorScheme: colorScheme,
isDark: isDark,
),
],
),
),
],
);
}
}
class _CoinRow extends StatelessWidget {
const _CoinRow({
required this.symbol,
required this.pair,
required this.fullName,
required this.price,
required this.change,
required this.isUp,
required this.colorScheme,
required this.isDark,
});
final String symbol;
final String pair;
final String fullName;
final String price;
final String change;
final bool isUp;
final ColorScheme colorScheme;
final bool isDark;
@override
Widget build(BuildContext context) {
final changeColor = isUp
? AppColorScheme.getUpColor(isDark)
: AppColorScheme.down;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Left: avatar + name
Row(
children: [
CircleAvatar(
radius: 18,
backgroundColor: colorScheme.primary.withValues(alpha: 0.1),
child: Text(
symbol,
style: GoogleFonts.inter(
fontSize: 10,
fontWeight: FontWeight.w700,
color: colorScheme.primary,
),
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
pair,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
Text(
fullName,
style: GoogleFonts.inter(
fontSize: 11,
color: colorScheme.onSurfaceVariant,
),
),
],
),
],
),
// Right: price + change
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
price,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
Text(
change,
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w500,
color: changeColor,
),
),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../core/theme/app_spacing.dart';
/// 首页快捷操作栏 - 充值/提现/划转/盈亏/账单
class QuickActionsRow extends StatelessWidget {
const QuickActionsRow({
super.key,
this.onDeposit,
this.onWithdraw,
this.onTransfer,
this.onProfit,
this.onBills,
});
final VoidCallback? onDeposit;
final VoidCallback? onWithdraw;
final VoidCallback? onTransfer;
final VoidCallback? onProfit;
final VoidCallback? onBills;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
decoration: BoxDecoration(
color: colorScheme.surfaceContainer,
border: Border.all(
color: colorScheme.outlineVariant,
width: 1,
),
borderRadius: BorderRadius.circular(AppRadius.xl),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_ActionItem(
icon: LucideIcons.arrowUpRight,
label: '充值',
colorScheme: colorScheme,
onTap: onDeposit,
),
_ActionItem(
icon: LucideIcons.arrowDownLeft,
label: '提现',
colorScheme: colorScheme,
onTap: onWithdraw,
),
_ActionItem(
icon: LucideIcons.repeat,
label: '划转',
colorScheme: colorScheme,
onTap: onTransfer,
),
_ActionItem(
icon: LucideIcons.chartPie,
label: '盈亏',
colorScheme: colorScheme,
onTap: onProfit,
),
_ActionItem(
icon: LucideIcons.fileText,
label: '账单',
colorScheme: colorScheme,
onTap: onBills,
),
],
),
);
}
}
class _ActionItem extends StatelessWidget {
const _ActionItem({
required this.icon,
required this.label,
required this.colorScheme,
required this.onTap,
});
final IconData icon;
final String label;
final ColorScheme colorScheme;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(10),
),
alignment: Alignment.center,
child: Icon(
icon,
size: 18,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 6),
Text(
label,
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w500,
color: colorScheme.onSurfaceVariant,
),
),
],
),
);
}
}

View File

@@ -1,9 +1,11 @@
import 'dart:math';
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 '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_spacing.dart' show AppRadius;
import '../../../data/models/coin.dart';
import '../../../providers/market_provider.dart';
import '../../components/glass_panel.dart';
@@ -53,24 +55,30 @@ class _MarketPageState extends State<MarketPage>
backgroundColor: colorScheme.surfaceContainerHighest,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: AppSpacing.pagePadding,
padding: const EdgeInsets.only(top: 16, left: 16, right: 16, bottom: 32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 上半区BTC + ETH 突出展示
_buildFeaturedSection(provider),
SizedBox(height: AppSpacing.lg),
// 下半区标题
Text(
'代币列表',
style: GoogleFonts.spaceGrotesk(
fontSize: 16,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
// 页面标题 "行情"
Padding(
padding: const EdgeInsets.only(top: 0, bottom: 8),
child: Text(
'行情',
style: GoogleFonts.inter(
fontSize: 22,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
),
SizedBox(height: AppSpacing.md),
// 下半区:代币列表
const SizedBox(height: AppSpacing.md),
// 精选区域BTC + ETH 卡片
_buildFeaturedSection(provider),
const SizedBox(height: AppSpacing.md),
// 分区标题:全部币种 + 更多
_buildSectionHeader(),
const SizedBox(height: AppSpacing.md),
// 币种列表卡片
_buildCoinList(provider),
],
),
@@ -81,7 +89,7 @@ class _MarketPageState extends State<MarketPage>
);
}
/// 上半区BTC + ETH 大卡片
/// 精选区域BTC + ETH 大卡片
Widget _buildFeaturedSection(MarketProvider provider) {
final featured = provider.featuredCoins;
if (featured.isEmpty) return const SizedBox.shrink();
@@ -95,7 +103,7 @@ class _MarketPageState extends State<MarketPage>
Expanded(child: _FeaturedCard(coin: btc))
else
const Expanded(child: SizedBox.shrink()),
SizedBox(width: AppSpacing.md),
const SizedBox(width: 12),
if (eth != null)
Expanded(child: _FeaturedCard(coin: eth))
else
@@ -104,9 +112,37 @@ class _MarketPageState extends State<MarketPage>
);
}
/// 下半区:代币列表
/// 分区标题:全部币种 + 更多
Widget _buildSectionHeader() {
final colorScheme = Theme.of(context).colorScheme;
return 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,
),
),
],
);
}
/// 币种列表
Widget _buildCoinList(MarketProvider provider) {
final coins = provider.otherCoins;
final colorScheme = Theme.of(context).colorScheme;
if (coins.isEmpty) {
return _EmptyState(
@@ -116,12 +152,28 @@ class _MarketPageState extends State<MarketPage>
);
}
return ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: coins.length,
separatorBuilder: (_, __) => SizedBox(height: AppSpacing.sm),
itemBuilder: (context, index) => _CoinListItem(coin: coins[index]),
return Container(
decoration: BoxDecoration(
color: colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
width: 1,
),
),
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: coins.length,
separatorBuilder: (_, __) => Divider(
height: 1,
thickness: 1,
color: colorScheme.outlineVariant.withOpacity(0.5 * 0.15),
indent: 16,
endIndent: 16,
),
itemBuilder: (context, index) => _CoinRow(coin: coins[index]),
),
);
}
@@ -135,13 +187,13 @@ class _MarketPageState extends State<MarketPage>
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(LucideIcons.circleAlert, size: 48, color: colorScheme.error),
SizedBox(height: AppSpacing.md),
const SizedBox(height: AppSpacing.md),
Text(
provider.error ?? '加载失败',
style: TextStyle(color: colorScheme.error),
textAlign: TextAlign.center,
),
SizedBox(height: AppSpacing.md),
const SizedBox(height: AppSpacing.md),
ShadButton(
onPressed: () => provider.refresh(),
child: const Text('重试'),
@@ -153,7 +205,7 @@ class _MarketPageState extends State<MarketPage>
}
}
/// 上半区大卡片BTC / ETH
/// 精选卡片BTC / ETH (130px 高度,含迷你柱状图)
class _FeaturedCard extends StatelessWidget {
final Coin coin;
@@ -168,89 +220,146 @@ class _FeaturedCard extends StatelessWidget {
isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.down;
final changeBgColor = isUp
? AppColorScheme.getUpBackgroundColor(isDark)
: colorScheme.error.withOpacity(0.1);
: AppColorScheme.getDownBackgroundColor(isDark);
return GlassPanel(
padding: EdgeInsets.all(AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 图标 + 币种代码
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.xl),
),
child: Center(
child: Text(
coin.displayIcon,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
),
),
SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
coin.code,
style: GoogleFonts.spaceGrotesk(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
),
],
),
SizedBox(height: AppSpacing.md),
// 当前价格
Text(
'\$${coin.formattedPrice}',
style: GoogleFonts.spaceGrotesk(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
SizedBox(height: AppSpacing.xs),
// 24h 涨跌幅
Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: changeBgColor,
borderRadius: BorderRadius.circular(AppRadius.sm),
border: Border.all(color: changeColor.withOpacity(0.2)),
),
child: Text(
coin.formattedChange,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: changeColor,
padding: const EdgeInsets.all(16),
height: 130,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 第一行:币种名称 + 涨跌徽章
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${coin.code}/USDT',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: changeBgColor,
borderRadius: BorderRadius.circular(4),
),
child: Text(
coin.formattedChange,
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w600,
color: changeColor,
),
),
),
],
),
// 第二行:价格
Text(
'\$${_formatFeaturedPrice(coin)}',
style: GoogleFonts.inter(
fontSize: 24,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
],
),
),
// 第三行:币种全名
Text(
coin.name,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
// 第四行:迷你柱状图
Expanded(
child: _MiniBarChart(isUp: isUp, isDark: isDark, seed: coin.code.hashCode),
),
],
),
);
}
/// 精选卡片使用简短价格格式(带逗号)
String _formatFeaturedPrice(Coin coin) {
if (coin.price >= 1000) {
return _addCommas(coin.price.toStringAsFixed(2));
}
return coin.price.toStringAsFixed(2);
}
String _addCommas(String text) {
final parts = text.split('.');
final intPart = parts[0];
final decPart = parts.length > 1 ? '.${parts[1]}' : '';
final buffer = StringBuffer();
int count = 0;
for (int i = intPart.length - 1; i >= 0; i--) {
if (count > 0 && count % 3 == 0) {
buffer.write(',');
}
buffer.write(intPart[i]);
count++;
}
return '${buffer.toString().split('').reversed.join()}$decPart';
}
}
/// 下半区列表项
class _CoinListItem extends StatelessWidget {
/// 迷你柱状图(模拟价格走势)
class _MiniBarChart extends StatelessWidget {
final bool isUp;
final bool isDark;
final int seed;
const _MiniBarChart({required this.isUp, required this.isDark, required this.seed});
@override
Widget build(BuildContext context) {
final barColor = isUp
? AppColorScheme.getUpColor(isDark)
: AppColorScheme.getDownColor(isDark);
// 生成随机但确定的高度序列
final heights = _generateHeights();
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: heights.map((h) {
return Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 1.5),
child: Container(
height: h,
decoration: BoxDecoration(
color: barColor,
borderRadius: BorderRadius.circular(2),
),
),
),
);
}).toList(),
);
}
List<double> _generateHeights() {
final random = Random(seed);
final base = 8.0;
final range = 16.0;
return List.generate(6, (_) => base + random.nextDouble() * range);
}
}
/// 币种列表行
class _CoinRow extends StatelessWidget {
final Coin coin;
const _CoinListItem({required this.coin});
const _CoinRow({required this.coin});
@override
Widget build(BuildContext context) {
@@ -258,102 +367,72 @@ class _CoinListItem extends StatelessWidget {
final isDark = Theme.of(context).brightness == Brightness.dark;
final isUp = coin.isUp;
final changeColor =
isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.down;
isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.getDownColor(isDark);
final changeBgColor = isUp
? AppColorScheme.getUpBackgroundColor(isDark)
: colorScheme.error.withOpacity(0.1);
: AppColorScheme.getDownBackgroundColor(isDark);
return GestureDetector(
onTap: () => _navigateToTrade(context),
child: GlassPanel(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm + AppSpacing.xs,
),
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
// 币种图标
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.xl),
),
child: Center(
child: Text(
coin.displayIcon,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
),
),
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
// 币种信息
// 头像:圆形字母头像
_CoinAvatar(letter: coin.displayIcon, code: coin.code),
const SizedBox(width: 10),
// 币种信息:交易对 + 全名
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Text(
coin.code,
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
SizedBox(width: AppSpacing.xs),
Text(
'/USDT',
style: TextStyle(
fontSize: 11,
color: colorScheme.onSurfaceVariant,
),
),
],
Text(
'${coin.code}/USDT',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 2),
Text(
coin.name,
style: TextStyle(
fontSize: 12,
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
// 价格和涨跌幅
// 右侧:价格 + 涨跌标签
Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'\$${coin.formattedPrice}',
style: GoogleFonts.spaceGrotesk(
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 2),
Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: 2,
),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: changeBgColor,
borderRadius: BorderRadius.circular(AppRadius.sm),
border: Border.all(color: changeColor.withOpacity(0.2)),
borderRadius: BorderRadius.circular(4),
),
child: Text(
coin.formattedChange,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w500,
color: changeColor,
),
),
@@ -372,6 +451,55 @@ class _CoinListItem extends StatelessWidget {
}
}
/// 币种头像组件
class _CoinAvatar extends StatelessWidget {
final String letter;
final String code;
const _CoinAvatar({required this.letter, required this.code});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
// 从 .pen 设计中的 accent-light 和 accent-primary
final bgColor = colorScheme.primary.withOpacity(isDark ? 0.15 : 0.1);
final textColor = colorScheme.primary;
return Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: bgColor,
shape: BoxShape.circle,
),
child: Center(
child: Text(
_getLetter(),
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w700,
color: textColor,
),
),
),
);
}
String _getLetter() {
const letterMap = {
'SOL': 'S',
'BNB': 'B',
'XRP': 'X',
'DOGE': 'D',
'ADA': 'A',
'DOT': 'D',
};
return letterMap[code] ?? code.substring(0, 1);
}
}
/// 空状态
class _EmptyState extends StatelessWidget {
final IconData icon;
@@ -386,17 +514,17 @@ class _EmptyState extends StatelessWidget {
return Center(
child: Padding(
padding: EdgeInsets.all(AppSpacing.xl),
padding: const EdgeInsets.all(AppSpacing.xl),
child: Column(
children: [
Icon(icon, size: 48, color: colorScheme.onSurfaceVariant),
SizedBox(height: AppSpacing.sm + AppSpacing.xs),
const SizedBox(height: 12),
Text(
message,
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
if (onRetry != null) ...[
SizedBox(height: AppSpacing.md),
const SizedBox(height: AppSpacing.md),
ShadButton(
onPressed: onRetry,
child: const Text('重试'),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<ThemeProvider>();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
// Icon in 36x36 rounded container
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainerHigh
: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Icon(
LucideIcons.moon,
size: 18,
color: colorScheme.onSurfaceVariant,
),
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
'深色模式',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
),
),
),
// Toggle switch - matching .pen design (44x24 rounded pill)
GestureDetector(
onTap: () => themeProvider.toggleTheme(),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 44,
height: 24,
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainerHigh
: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: AnimatedAlign(
duration: const Duration(milliseconds: 200),
alignment:
themeProvider.isDarkMode
? Alignment.centerRight
: Alignment.centerLeft,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: colorScheme.onSurface,
shape: BoxShape.circle,
),
),
),
),
),
],
),
);
}
}

View File

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

View File

@@ -5,31 +5,15 @@ import 'package:google_fonts/google_fonts.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 '../../components/glass_panel.dart';
import '../../components/neon_glow.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';
/// 菜单项数据模型
class _MenuItem {
final IconData icon;
final String title;
final String? subtitle;
final Color? iconColor;
final VoidCallback onTap;
const _MenuItem({
required this.icon,
required this.title,
this.subtitle,
this.iconColor,
required this.onTap,
});
}
/// 我的页面 - Material Design 3 风格
/// 我的页面 - 匹配 .pen 设计稿
class MinePage extends StatefulWidget {
const MinePage({super.key});
@@ -37,7 +21,8 @@ class MinePage extends StatefulWidget {
State<MinePage> createState() => _MinePageState();
}
class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin {
class _MinePageState extends State<MinePage>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@@ -51,26 +36,31 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
body: Consumer<AuthProvider>(
builder: (context, auth, _) {
return SingleChildScrollView(
padding: AppSpacing.pagePadding,
padding: EdgeInsets.fromLTRB(
AppSpacing.md,
AppSpacing.md,
AppSpacing.md,
AppSpacing.xl + AppSpacing.md,
),
child: Column(
children: [
_UserCard(user: auth.user),
SizedBox(height: AppSpacing.md),
_MenuList(
onShowComingSoon: _showComingSoon,
onShowAbout: _showAboutDialog,
ProfileCard(user: auth.user),
SizedBox(height: AppSpacing.sm),
MenuGroup1(
kycStatus: auth.user?.kycStatus ?? 0,
onShowComingSoon: _showComingSoon,
),
SizedBox(height: AppSpacing.xl),
_LogoutButton(onLogout: () => _handleLogout(auth)),
SizedBox(height: AppSpacing.sm),
MenuGroup2(onShowAbout: _showAboutDialog),
SizedBox(height: AppSpacing.lg),
// 版本信息
LogoutButton(onLogout: () => _handleLogout(auth)),
SizedBox(height: AppSpacing.md),
Text(
'System Build v1.0.0-Neo',
style: TextStyle(
fontSize: 10,
color: colorScheme.onSurfaceVariant.withOpacity(0.4),
letterSpacing: 0.3,
'System Build v1.0.0',
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant.withOpacity(0.5),
),
),
],
@@ -110,7 +100,7 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
builder: (context) => ShadDialog(
title: Row(
children: [
_AppLogo(radius: 20, fontSize: 16),
AvatarCircle(radius: 20, fontSize: 16),
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
const Text('模拟所'),
],
@@ -124,9 +114,11 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
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(icon: Icons.favorite, text: 'Built with Flutter & Material Design 3'),
InfoRow(
icon: Icons.favorite,
text: 'Built with Flutter & Material Design 3'),
],
),
actions: [
@@ -168,460 +160,3 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
);
}
}
/// 用户卡片组件 - Material Design 3 风格
class _UserCard extends StatelessWidget {
final dynamic user;
const _UserCard({required this.user});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return GlassPanel(
padding: EdgeInsets.all(AppSpacing.lg + AppSpacing.sm),
child: Row(
children: [
// 头像 - 带霓虹边框
Stack(
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: colorScheme.primary.withOpacity(isDark ? 0.15 : 0.08),
blurRadius: 20,
),
],
),
child: _AppLogo(radius: 36, fontSize: 20, text: user?.avatarText),
),
// 验证徽章
Positioned(
bottom: 0,
right: 0,
child: Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppColorScheme.up,
shape: BoxShape.circle,
border: Border.all(
color: colorScheme.background,
width: 2,
),
),
child: Icon(
Icons.verified,
size: 14,
color: colorScheme.onTertiary,
),
),
),
],
),
SizedBox(width: AppSpacing.md + AppSpacing.xs),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user?.username ?? '未登录',
style: GoogleFonts.spaceGrotesk(
fontSize: 16,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
SizedBox(height: AppSpacing.sm),
// 用户等级标签
Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.full),
border: Border.all(
color: colorScheme.primary.withOpacity(0.2),
),
),
child: Text(
'普通用户',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
color: colorScheme.primary,
),
),
),
],
),
),
Icon(
LucideIcons.chevronRight,
color: colorScheme.onSurfaceVariant,
),
],
),
);
}
}
/// 应用 Logo 组件
class _AppLogo extends StatelessWidget {
final double radius;
final double fontSize;
final String? text;
const _AppLogo({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.2),
child: Text(
text ?? '',
style: TextStyle(
fontSize: fontSize,
color: colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
);
}
}
/// 信息行组件
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,
),
),
],
);
}
}
/// 菜单列表组件 - Glass Panel 风格
class _MenuList extends StatelessWidget {
final void Function(String) onShowComingSoon;
final VoidCallback onShowAbout;
final int kycStatus;
const _MenuList({
required this.onShowComingSoon,
required this.onShowAbout,
required this.kycStatus,
});
@override
Widget build(BuildContext context) {
final themeProvider = context.watch<ThemeProvider>();
final colorScheme = Theme.of(context).colorScheme;
return GlassPanel(
padding: EdgeInsets.zero,
borderRadius: BorderRadius.circular(AppRadius.xxl),
child: Column(
children: [
// 主题切换开关
_ThemeToggleTile(isDarkMode: themeProvider.isDarkMode),
_buildDivider(),
// 菜单项
..._buildMenuItems(context, colorScheme),
],
),
);
}
Widget _buildDivider() {
return Container(
margin: EdgeInsets.only(left: 56),
height: 1,
color: AppColorScheme.glassPanelBorder,
);
}
List<Widget> _buildMenuItems(BuildContext context, ColorScheme colorScheme) {
final items = [
_MenuItem(
icon: LucideIcons.gift,
title: '福利中心',
subtitle: '首充奖励 + 推广奖励',
iconColor: colorScheme.primary,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const WelfareCenterPage()),
);
},
),
_MenuItem(
icon: LucideIcons.userCheck,
title: '实名认证',
subtitle: kycStatus == 2
? '已认证'
: kycStatus == 1
? '审核中'
: '完成实名认证,解锁更多功能',
iconColor: kycStatus == 2 ? AppColorScheme.up : colorScheme.primary,
onTap: () {
if (kycStatus == 2) {
_showKycStatusDialog(context);
} else {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const KycPage()),
);
}
},
),
_MenuItem(
icon: LucideIcons.shield,
title: '安全设置',
subtitle: '密码、二次验证等安全设置',
iconColor: colorScheme.secondary,
onTap: () => onShowComingSoon('安全设置'),
),
_MenuItem(
icon: LucideIcons.bell,
title: '消息通知',
subtitle: '管理消息推送设置',
iconColor: AppColorScheme.up,
onTap: () => onShowComingSoon('消息通知'),
),
_MenuItem(
icon: LucideIcons.settings,
title: '系统设置',
subtitle: '主题、语言等偏好设置',
iconColor: colorScheme.primary,
onTap: () => onShowComingSoon('系统设置'),
),
_MenuItem(
icon: LucideIcons.info,
title: '关于我们',
subtitle: '版本信息与用户协议',
iconColor: colorScheme.onSurfaceVariant,
onTap: onShowAbout,
),
];
return [
for (var i = 0; i < items.length; i++) ...[
_MenuItemTile(item: items[i]),
if (i < items.length - 1) _buildDivider(),
],
];
}
}
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(),
),
],
),
);
}
/// 主题切换组件
class _ThemeToggleTile extends StatelessWidget {
final bool isDarkMode;
const _ThemeToggleTile({required this.isDarkMode});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final themeProvider = context.read<ThemeProvider>();
return InkWell(
onTap: () => themeProvider.toggleTheme(),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm + AppSpacing.xs,
),
child: Row(
children: [
_MenuIcon(
icon: isDarkMode ? LucideIcons.moon : LucideIcons.sun,
color: colorScheme.primary,
),
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'深色模式',
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
),
),
SizedBox(height: AppSpacing.xs / 2),
Text(
isDarkMode ? '当前:深色主题' : '当前:浅色主题',
style: TextStyle(
fontSize: 11,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
Switch(
value: isDarkMode,
onChanged: (_) => themeProvider.toggleTheme(),
activeTrackColor: colorScheme.primary.withOpacity(0.5),
activeColor: colorScheme.primary,
),
],
),
),
);
}
}
/// 菜单项组件
class _MenuItemTile extends StatelessWidget {
final _MenuItem item;
const _MenuItemTile({required this.item});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return InkWell(
onTap: item.onTap,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm + AppSpacing.xs,
),
child: Row(
children: [
_MenuIcon(icon: item.icon, color: item.iconColor),
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
),
),
if (item.subtitle != null) ...[
SizedBox(height: AppSpacing.xs / 2),
Text(
item.subtitle!,
style: TextStyle(
fontSize: 11,
color: colorScheme.onSurfaceVariant,
),
),
],
],
),
),
Icon(
LucideIcons.chevronRight,
size: 18,
color: colorScheme.onSurfaceVariant,
),
],
),
),
);
}
}
/// 菜单图标组件 - Material Design 3 风格
class _MenuIcon extends StatelessWidget {
final IconData icon;
final Color? color;
const _MenuIcon({required this.icon, this.color});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final iconColor = color ?? colorScheme.primary;
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.md + AppSpacing.xs),
border: Border.all(
color: iconColor.withOpacity(0.2),
),
),
child: Icon(icon, size: 20, color: iconColor),
);
}
}
/// 退出登录按钮 - 带霓虹光效
class _LogoutButton extends StatelessWidget {
final VoidCallback onLogout;
const _LogoutButton({required this.onLogout});
@override
Widget build(BuildContext context) {
return NeonButton(
text: 'Logout Terminal',
type: NeonButtonType.error,
icon: Icons.logout,
onPressed: onLogout,
width: double.infinity,
showGlow: true,
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,12 @@
import 'dart:async';
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: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/utils/toast_utils.dart';
import '../../../core/event/app_event_bus.dart';
import '../../../providers/asset_provider.dart';
@@ -21,10 +24,6 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
int _activeTab = 0; // 0=全部, 1=充值, 2=提现
StreamSubscription<AppEvent>? _eventSub;
// 颜色常量
static const upColor = Color(0xFF00C853);
static const downColor = Color(0xFFFF5252);
@override
void initState() {
super.initState();
@@ -52,45 +51,81 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
context.read<AssetProvider>().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 theme = ShadTheme.of(context);
final c = _colors;
return Scaffold(
backgroundColor: theme.colorScheme.background,
backgroundColor: c.background,
appBar: AppBar(
title: const Text('订单记录'),
backgroundColor: theme.colorScheme.background,
leading: IconButton(
icon: const Icon(LucideIcons.arrowLeft, size: 20),
onPressed: () => Navigator.of(context).pop(),
),
title: Text('充提记录', style: _inter(fontSize: 16, fontWeight: FontWeight.w600, color: c.primaryText)),
backgroundColor: c.background,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
),
body: Column(
children: [
_buildTabs(),
_buildFilterTabs(),
Expanded(child: _buildOrderList()),
],
),
);
}
Widget _buildTabs() {
final theme = ShadTheme.of(context);
// ---------------------------------------------------------------------------
// Filter Tabs - pill-style segmented control
// ---------------------------------------------------------------------------
Widget _buildFilterTabs() {
final c = _colors;
return Container(
padding: const EdgeInsets.all(16),
child: Row(
children: [
_buildTab('全部', 0),
const SizedBox(width: 12),
_buildTab('充值', 1),
const SizedBox(width: 12),
_buildTab('提现', 2),
],
return Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Container(
height: 40,
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: c.tabBg,
borderRadius: AppRadius.radiusMd,
),
child: Row(
children: [
_buildPillTab('全部', 0),
_buildPillTab('充值', 1),
_buildPillTab('提现', 2),
],
),
),
);
}
Widget _buildTab(String label, int index) {
final theme = ShadTheme.of(context);
Widget _buildPillTab(String label, int index) {
final c = _colors;
final isActive = _activeTab == index;
return Expanded(
@@ -102,20 +137,17 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
}
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isActive ? theme.colorScheme.primary : theme.colorScheme.card,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isActive ? theme.colorScheme.primary : theme.colorScheme.border,
),
color: isActive ? c.activeTabBg : Colors.transparent,
borderRadius: AppRadius.radiusSm,
),
child: Center(
child: Text(
label,
style: TextStyle(
color: isActive ? Colors.white : theme.colorScheme.mutedForeground,
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
style: _inter(
fontSize: 14,
fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
color: isActive ? c.activeTabText : c.inactiveTabText,
),
),
),
@@ -124,7 +156,12 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
);
}
// ---------------------------------------------------------------------------
// Order List
// ---------------------------------------------------------------------------
Widget _buildOrderList() {
final c = _colors;
return Consumer<AssetProvider>(
builder: (context, provider, _) {
final orders = provider.fundOrders;
@@ -139,16 +176,9 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
LucideIcons.inbox,
size: 64,
color: Colors.grey[400],
),
Icon(LucideIcons.inbox, size: 64, color: c.mutedText),
const SizedBox(height: 16),
Text(
'暂无订单记录',
style: TextStyle(color: Colors.grey[600]),
),
Text('暂无订单记录', style: _inter(fontSize: 14, fontWeight: FontWeight.normal, color: c.secondaryText)),
],
),
);
@@ -157,7 +187,7 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
return RefreshIndicator(
onRefresh: () async => _loadData(),
child: ListView.separated(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
itemCount: orders.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
@@ -169,107 +199,30 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
);
}
// ---------------------------------------------------------------------------
// Order Card
// ---------------------------------------------------------------------------
Widget _buildOrderCard(OrderFund order) {
final theme = ShadTheme.of(context);
final isDeposit = order.isDeposit;
final c = _colors;
return ShadCard(
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: c.cardBg,
borderRadius: AppRadius.radiusLg,
border: Border.all(color: c.borderColor, width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isDeposit
? upColor.withValues(alpha: 0.1)
: downColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
order.typeText,
style: TextStyle(
color: isDeposit ? upColor : downColor,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 8),
_buildStatusBadge(order),
],
),
Text(
order.orderNo,
style: const TextStyle(fontSize: 11, color: Colors.grey),
),
],
),
_buildCardHeader(order),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${isDeposit ? '+' : '-'}${order.amount} USDT',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: isDeposit ? upColor : downColor,
),
),
if (order.canCancel || order.canConfirmPay)
Row(
children: [
if (order.canConfirmPay)
ShadButton.outline(
size: ShadButtonSize.sm,
onPressed: () => _confirmPay(order),
child: const Text('已打款'),
),
if (order.canCancel) ...[
const SizedBox(width: 8),
ShadButton.destructive(
size: ShadButtonSize.sm,
onPressed: () => _cancelOrder(order),
child: const Text('取消'),
),
],
],
),
],
),
_buildAmountRow(order),
const SizedBox(height: 12),
// 显示地址信息
if (order.walletAddress != null) ...[
const Divider(),
_buildDetailRows(order),
if (order.rejectReason != null) ...[
const SizedBox(height: 8),
Row(
children: [
Text(
'${isDeposit ? '充值地址' : '提现地址'}: ',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
Expanded(
child: Text(
order.walletAddress!,
style: const TextStyle(fontSize: 11, fontFamily: 'monospace'),
overflow: TextOverflow.ellipsis,
),
),
GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: order.walletAddress!));
ToastUtils.show('地址已复制');
},
child: Icon(LucideIcons.copy, size: 14, color: Colors.grey[600]),
),
],
),
_buildRejectionReason(order),
],
if (order.withdrawContact != null) ...[
const SizedBox(height: 4),
@@ -331,56 +284,229 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
}
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);
Color bgColor;
Color textColor;
// 根据类型和状态设置颜色
if (order.type == 1) {
// 充值状态
if (order.isDeposit) {
switch (order.status) {
case 1: // 待付款
case 2: // 待确认
bgColor = Colors.orange.withValues(alpha: 0.1);
textColor = Colors.orange;
bgColor = amberBg;
textColor = amberColor;
break;
case 3: // 已完成
bgColor = upColor.withValues(alpha: 0.1);
bgColor = upBg;
textColor = upColor;
break;
default: // 已驳回/已取消
bgColor = downColor.withValues(alpha: 0.1);
bgColor = downBg;
textColor = downColor;
}
} else {
// 提现状态
switch (order.status) {
case 1: // 待审批
bgColor = Colors.orange.withValues(alpha: 0.1);
textColor = Colors.orange;
case 5: // 待财务审核
bgColor = amberBg;
textColor = amberColor;
break;
case 2: // 已完成
bgColor = upColor.withValues(alpha: 0.1);
bgColor = upBg;
textColor = upColor;
break;
default: // 已驳回/已取消
bgColor = downColor.withValues(alpha: 0.1);
bgColor = downBg;
textColor = downColor;
}
}
return _buildBadge(order.statusText, textColor, bgColor);
}
Widget _buildBadge(String text, Color textColor, Color bgColor) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(4),
),
child: Text(
order.statusText,
style: TextStyle(fontSize: 11, color: textColor),
child: Text(text, style: _inter(fontSize: 11, fontWeight: FontWeight.w600, color: textColor)),
);
}
// ---------------------------------------------------------------------------
// Amount Row
// ---------------------------------------------------------------------------
Widget _buildAmountRow(OrderFund order) {
final c = _colors;
return Text(
'${order.isDeposit ? '+' : '-'}${order.amount} USDT',
style: _inter(fontSize: 18, fontWeight: FontWeight.w700, color: c.primaryText),
);
}
// ---------------------------------------------------------------------------
// Detail Rows
// ---------------------------------------------------------------------------
Widget _buildDetailRows(OrderFund order) {
final c = _colors;
return Column(
children: [
_buildDetailRow('订单号', order.orderNo, c),
const SizedBox(height: 6),
if (order.walletAddress != null) ...[
_buildDetailRow(
'网络',
order.remark.isNotEmpty ? order.remark : '-',
c,
),
const SizedBox(height: 6),
_buildDetailRow(
'地址',
_truncateAddress(order.walletAddress!),
c,
trailing: GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: order.walletAddress!));
ToastUtils.show('地址已复制');
},
child: Icon(LucideIcons.copy, size: 14, color: c.mutedText),
),
),
const SizedBox(height: 6),
] else if (order.remark.isNotEmpty) ...[
_buildDetailRow('网络', order.remark, c),
const SizedBox(height: 6),
],
if (order.fee != null && !order.isDeposit) ...[
_buildDetailRow('手续费', '${order.fee}%', c),
const SizedBox(height: 6),
],
_buildDetailRow(
'时间',
_formatTime(order.createTime),
c,
),
],
);
}
Widget _buildDetailRow(
String label,
String value,
_OrderColors c, {
Widget? trailing,
}) {
final valueStyle = _inter(fontSize: 12, fontWeight: FontWeight.normal, color: c.primaryText);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: _inter(fontSize: 12, fontWeight: FontWeight.normal, color: c.mutedText)),
if (trailing != null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(value, style: valueStyle),
const SizedBox(width: 4),
trailing,
],
)
else
Text(value, style: valueStyle),
],
);
}
// ---------------------------------------------------------------------------
// Rejection Reason
// ---------------------------------------------------------------------------
Widget _buildRejectionReason(OrderFund order) {
return Text(
'拒绝原因: ${order.rejectReason}',
style: _inter(fontSize: 12, fontWeight: FontWeight.normal, color: AppColorScheme.getDownColor(_isDark)),
);
}
// ---------------------------------------------------------------------------
// Payable Amount Row (withdrawal)
// ---------------------------------------------------------------------------
Widget _buildPayableRow(OrderFund order) {
final c = _colors;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: c.bgTertiary,
borderRadius: AppRadius.radiusSm,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
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)),
],
),
);
}
// ---------------------------------------------------------------------------
// Action Buttons
// ---------------------------------------------------------------------------
Widget _buildActions(OrderFund order) {
final upColor = AppColorScheme.getUpColor(_isDark);
final downColor = AppColorScheme.getDownColor(_isDark);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (order.canCancel)
GestureDetector(
onTap: () => _cancelOrder(order),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
borderRadius: AppRadius.radiusSm,
border: Border.all(color: downColor, width: 1),
),
child: Text('取消订单', style: _inter(fontSize: 13, fontWeight: FontWeight.w500, color: downColor)),
),
),
if (order.canCancel && order.canConfirmPay)
const SizedBox(width: 12),
if (order.canConfirmPay)
GestureDetector(
onTap: () => _confirmPay(order),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: upColor,
borderRadius: AppRadius.radiusSm,
),
child: Text('已打款', style: _inter(fontSize: 13, fontWeight: FontWeight.w500, color: Colors.white)),
),
),
],
);
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
String _truncateAddress(String address) {
if (address.length > 12) {
return '${address.substring(0, 4)}...${address.substring(address.length - 4)}';
}
return address;
}
String _formatTime(DateTime? time) {
if (time == null) return '-';
return '${time.year}-${time.month.toString().padLeft(2, '0')}-${time.day.toString().padLeft(2, '0')} '
@@ -388,60 +514,92 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
}
void _confirmPay(OrderFund order) async {
final confirmed = await showShadDialog<bool>(
final confirmed = await showShadConfirmDialog(
context: context,
builder: (context) => ShadDialog.alert(
title: const Text('确认已打款'),
description: const Text('确认您已完成向指定地址的转账?'),
actions: [
ShadButton.outline(
child: const Text('取消'),
onPressed: () => Navigator.pop(context, false),
),
ShadButton(
child: const Text('确认'),
onPressed: () => Navigator.pop(context, true),
),
],
),
title: '确认已打款',
description: '确认您已完成向指定地址的转账?',
);
if (confirmed == true && mounted) {
final response = await context.read<AssetProvider>().confirmPay(order.orderNo);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(response.success ? '确认成功,请等待审核' : response.message ?? '确认失败')),
);
BotToast.showText(text: response.success ? '确认成功,请等待审核' : response.message ?? '确认失败');
}
}
}
void _cancelOrder(OrderFund order) async {
final confirmed = await showShadDialog<bool>(
final confirmed = await showShadConfirmDialog(
context: context,
builder: (context) => ShadDialog.alert(
title: const Text('取消订单'),
description: Text('确定要取消订单 ${order.orderNo} 吗?'),
actions: [
ShadButton.outline(
child: const Text('返回'),
onPressed: () => Navigator.pop(context, false),
),
ShadButton.destructive(
child: const Text('确定取消'),
onPressed: () => Navigator.pop(context, true),
),
],
),
title: '取消订单',
description: '确定要取消订单 ${order.orderNo} 吗?',
destructive: true,
);
if (confirmed == true && mounted) {
final response = await context.read<AssetProvider>().cancelOrder(order.orderNo);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(response.success ? '订单已取消' : response.message ?? '取消失败')),
);
BotToast.showText(text: response.success ? '订单已取消' : response.message ?? '取消失败');
}
}
}
Future<bool?> showShadConfirmDialog({
required BuildContext context,
required String title,
required String description,
bool destructive = false,
}) {
return showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(description),
actions: [
TextButton(
child: const Text('取消'),
onPressed: () => Navigator.pop(context, false),
),
TextButton(
child: Text(destructive ? '确定取消' : '确认'),
onPressed: () => Navigator.pop(context, true),
),
],
),
);
}
}
/// 充提订单页面的主题感知颜色集合
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;
}

View File

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

View File

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

View File

@@ -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<Coin> coins;
final ValueChanged<Coin> 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,
)),
],
),
),
],
),
),
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<int> onTradeTypeChanged;
final VoidCallback onAmountChanged;
final ValueChanged<double> 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圆角smbg-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()],
),
),
],
),
],
),
);
}
/// 百分比按钮 - 设计稿圆角smbg-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,
)),
),
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
/// 使用方式: import 'ui/shared/ui_constants.dart';
// 导出颜色系统
export '../../core/constants/app_colors.dart';
export '../../core/theme/app_color_scheme.dart';
// 导出主题配置 (包含 AppTextStyles, AppSpacing, AppRadius, AppBreakpoints)
export '../../core/theme/app_theme.dart';