docs: relocate skills system documentation and refactor asset page components

Move skills system documentation from bottom to top of CLAUDE.md for better organization. Refactor Flutter asset page by extracting UI components into separate files and updating import structure for improved modularity.
This commit is contained in:
2026-04-05 22:38:56 +08:00
parent d8cd38c4de
commit 02099d2a6a
30 changed files with 3317 additions and 3430 deletions

View File

@@ -7,6 +7,10 @@
模拟所 (Monisuo) — 虚拟货币模拟交易平台。全栈 monorepo三个子系统共用一个 MySQL 数据库。
## 技能系统
当处理任务时,先扫描 `.agents/skills/` 目录下是否有相关技能。如果技能可能适用,先用 Read 工具读取对应的 `SKILL.md`,然后严格遵循其指引执行。技能优先级高于默认行为,但低于用户的显式指令。
## 构建与运行命令
### Java 后端 (Maven, Java 8, Spring Boot 2.2.4)
@@ -83,9 +87,6 @@ deploy/deploy_server.sh backend # 仅部署后端
### 数据库核心表
`sys_user``sys_admin``coin``price_type`: 1=实时, 2=管理定价)、`account_fund``account_trade`(唯一索引 `user_id+coin_code`)、`order_trade``order_fund`(充提订单,状态驱动的审批流)、`account_flow``sys_config``user_favorite``cold_wallet`
## 技能系统
当处理任务时,先扫描 `.agents/skills/` 目录下是否有相关技能。如果技能可能适用,先用 Read 工具读取对应的 `SKILL.md`,然后严格遵循其指引执行。技能优先级高于默认行为,但低于用户的显式指令。
## 代码规范

File diff suppressed because it is too large Load Diff

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

@@ -3,7 +3,6 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../providers/asset_provider.dart';
import '../../../data/models/account_models.dart';
@@ -37,6 +36,10 @@ class _TransferPageState extends State<TransferPage> {
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;
@@ -134,42 +156,27 @@ 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;
// Theme-aware colors matching .pen design tokens
final bgSecondary = isDark ? const Color(0xFF0B1120) : const Color(0xFFF8FAFC);
final surfaceCard = isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF);
final bgTertiary = isDark ? const Color(0xFF1E293B) : const Color(0xFFF1F5F9);
final borderDefault = isDark ? const Color(0xFF334155) : const Color(0xFFE2E8F0);
final textPrimary = isDark ? const Color(0xFFF8FAFC) : const Color(0xFF0F172A);
final textSecondary = isDark ? const Color(0xFF94A3B8) : const Color(0xFF475569);
final textMuted = isDark ? const Color(0xFF64748B) : const Color(0xFF94A3B8);
final textInverse = isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF);
final accentPrimary = isDark ? const Color(0xFFD4AF37) : const Color(0xFF1F2937);
final goldAccent = isDark ? const Color(0xFFD4AF37) : const Color(0xFFF59E0B);
final profitGreen = isDark ? const Color(0xFF4ADE80) : const Color(0xFF16A34A);
final profitGreenBg = isDark ? const Color(0xFF052E16) : const Color(0xFFF0FDF4);
final c = _colors;
return Scaffold(
backgroundColor: bgSecondary,
backgroundColor: c.bgSecondary,
appBar: AppBar(
backgroundColor: isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF),
backgroundColor: c.surfaceCard,
elevation: 0,
scrolledUnderElevation: 0,
leading: IconButton(
icon: Icon(LucideIcons.arrowLeft, color: textPrimary, size: 20),
icon: Icon(LucideIcons.arrowLeft, color: c.textPrimary, size: 20),
onPressed: () => Navigator.of(context).pop(),
),
title: Text(
'账户划转',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: textPrimary,
),
style: _inter(fontSize: 16, fontWeight: FontWeight.w600, color: c.textPrimary),
),
centerTitle: true,
),
@@ -179,46 +186,13 @@ class _TransferPageState extends State<TransferPage> {
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
child: Column(
children: [
// --- Transfer Direction Card ---
_buildTransferDirectionCard(
colorScheme: colorScheme,
isDark: isDark,
surfaceCard: surfaceCard,
borderDefault: borderDefault,
textPrimary: textPrimary,
textSecondary: textSecondary,
textMuted: textMuted,
textInverse: textInverse,
accentPrimary: accentPrimary,
),
_buildTransferDirectionCard(c),
const SizedBox(height: 24),
// --- Amount Section ---
_buildAmountSection(
isDark: isDark,
bgTertiary: bgTertiary,
textPrimary: textPrimary,
textSecondary: textSecondary,
textMuted: textMuted,
goldAccent: goldAccent,
),
_buildAmountSection(c),
const SizedBox(height: 24),
// --- Tips Card ---
_buildTipsCard(
profitGreen: profitGreen,
profitGreenBg: profitGreenBg,
),
_buildTipsCard(c),
const SizedBox(height: 24),
// --- Confirm Button ---
_buildConfirmButton(
accentPrimary: accentPrimary,
textInverse: textInverse,
),
_buildConfirmButton(c),
],
),
);
@@ -227,51 +201,30 @@ class _TransferPageState extends State<TransferPage> {
);
}
/// Transfer direction card with source, swap, destination
Widget _buildTransferDirectionCard({
required ColorScheme colorScheme,
required bool isDark,
required Color surfaceCard,
required Color borderDefault,
required Color textPrimary,
required Color textSecondary,
required Color textMuted,
required Color textInverse,
required Color accentPrimary,
}) {
// ============================================
// Transfer direction card
// ============================================
Widget _buildTransferDirectionCard(_TransferColors c) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: surfaceCard,
color: c.surfaceCard,
borderRadius: BorderRadius.circular(AppRadius.xl),
border: Border.all(color: borderDefault.withOpacity(0.6)),
border: Border.all(color: c.borderDefault.withValues(alpha: 0.6)),
),
child: Column(
children: [
// Source account
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
transitionBuilder: (widget, animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, -1),
end: Offset.zero,
).animate(animation),
child: FadeTransition(opacity: animation, child: widget),
);
},
_animatedSwitcher(
key: 'src-$_direction',
beginOffset: const Offset(0, -1),
child: _buildAccountRow(
key: ValueKey('src-$_direction'),
label: '',
accountName: _fromLabel,
balance: _fromBalance,
isDark: isDark,
textMuted: textMuted,
textPrimary: textPrimary,
textSecondary: textSecondary,
c: c,
),
),
@@ -283,42 +236,24 @@ class _TransferPageState extends State<TransferPage> {
height: 36,
margin: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: accentPrimary,
color: c.accentPrimary,
shape: BoxShape.circle,
),
child: Center(
child: Icon(
LucideIcons.arrowUpDown,
size: 18,
color: textInverse,
),
child: Icon(LucideIcons.arrowUpDown, size: 18, color: c.textInverse),
),
),
),
// Destination account
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
transitionBuilder: (widget, animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(animation),
child: FadeTransition(opacity: animation, child: widget),
);
},
_animatedSwitcher(
key: 'dst-$_direction',
beginOffset: const Offset(0, 1),
child: _buildAccountRow(
key: ValueKey('dst-$_direction'),
label: '',
accountName: _toLabel,
balance: _toBalance,
isDark: isDark,
textMuted: textMuted,
textPrimary: textPrimary,
textSecondary: textSecondary,
c: c,
),
),
],
@@ -326,64 +261,57 @@ class _TransferPageState extends State<TransferPage> {
);
}
/// 统一的 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({
Key? key,
required String label,
required String accountName,
required String balance,
required bool isDark,
required Color textMuted,
required Color textPrimary,
required Color textSecondary,
required _TransferColors c,
}) {
return Container(
key: key,
return SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label row
Text(
label,
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.normal,
color: textMuted,
),
),
Text(label, style: _inter(fontSize: 11, fontWeight: FontWeight.normal, color: c.textMuted)),
const SizedBox(height: 8),
// Account name + balance row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Account name with icon
Row(
children: [
Icon(
label == '' ? LucideIcons.wallet : LucideIcons.repeat,
size: 18,
color: textSecondary,
color: c.textSecondary,
),
const SizedBox(width: 10),
Text(
accountName,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: textPrimary,
),
),
Text(accountName, style: _inter(fontSize: 14, fontWeight: FontWeight.w600, color: c.textPrimary)),
],
),
// Balance
Text(
'\u00A5 ${_formatBalance(balance)}',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: textPrimary,
),
style: _inter(fontSize: 14, fontWeight: FontWeight.w600, color: c.textPrimary),
),
],
),
@@ -392,22 +320,11 @@ class _TransferPageState extends State<TransferPage> {
);
}
/// Format balance for display
String _formatBalance(String balance) {
final val = double.tryParse(balance);
if (val == null) return '0.00';
return val.toStringAsFixed(2);
}
// ============================================
// Amount input section
// ============================================
/// Amount input section
Widget _buildAmountSection({
required bool isDark,
required Color bgTertiary,
required Color textPrimary,
required Color textSecondary,
required Color textMuted,
required Color goldAccent,
}) {
Widget _buildAmountSection(_TransferColors c) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -415,24 +332,10 @@ class _TransferPageState extends State<TransferPage> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'划转金额',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: textSecondary,
),
),
Text('划转金额', style: _inter(fontSize: 14, fontWeight: FontWeight.w500, color: c.textSecondary)),
GestureDetector(
onTap: () => _setQuickAmount(1.0),
child: Text(
'全部划转',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
color: goldAccent,
),
),
child: Text('全部划转', style: _inter(fontSize: 12, fontWeight: FontWeight.w600, color: c.goldAccent)),
),
],
),
@@ -446,13 +349,12 @@ class _TransferPageState extends State<TransferPage> {
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: bgTertiary,
color: c.bgTertiary,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Input
Expanded(
child: TextField(
controller: _amountController,
@@ -461,35 +363,19 @@ class _TransferPageState extends State<TransferPage> {
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,8}')),
],
style: GoogleFonts.inter(
fontSize: 28,
fontWeight: FontWeight.w700,
color: textPrimary,
),
style: _inter(fontSize: 28, fontWeight: FontWeight.w700, color: c.textPrimary),
decoration: InputDecoration(
hintText: '0.00',
hintStyle: GoogleFonts.inter(
fontSize: 28,
fontWeight: FontWeight.w700,
color: textMuted,
),
hintStyle: _inter(fontSize: 28, fontWeight: FontWeight.w700, color: c.textMuted),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
),
),
),
// Suffix
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
'USDT',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.normal,
color: textMuted,
),
),
child: Text('USDT', style: _inter(fontSize: 14, fontWeight: FontWeight.normal, color: c.textMuted)),
),
],
),
@@ -499,74 +385,58 @@ class _TransferPageState extends State<TransferPage> {
// Percent buttons
Row(
children: [
_buildPercentButton('25%', 0.25, isDark, bgTertiary, textSecondary),
const SizedBox(width: 8),
_buildPercentButton('50%', 0.50, isDark, bgTertiary, textSecondary),
const SizedBox(width: 8),
_buildPercentButton('75%', 0.75, isDark, bgTertiary, textSecondary),
const SizedBox(width: 8),
_buildPercentButton('100%', 1.0, isDark, bgTertiary, textSecondary),
],
children: [0.25, 0.50, 0.75, 1.0].asMap().entries.map((entry) {
final index = entry.key;
final percent = entry.value;
final label = '${(percent * 100).toInt()}%';
return Padding(
padding: EdgeInsets.only(left: index > 0 ? 8 : 0),
child: _buildPercentButton(label, percent, c),
);
}).toList(),
),
],
);
}
/// Percent quick button
Widget _buildPercentButton(String label, double percent, bool isDark, Color bgTertiary, Color textSecondary) {
Widget _buildPercentButton(String label, double percent, _TransferColors c) {
return Expanded(
child: GestureDetector(
onTap: () => _setQuickAmount(percent),
child: Container(
height: 36,
decoration: BoxDecoration(
color: bgTertiary,
color: c.bgTertiary,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Center(
child: Text(
label,
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: textSecondary,
),
),
child: Text(label, style: _inter(fontSize: 13, fontWeight: FontWeight.w500, color: c.textSecondary)),
),
),
),
);
}
/// Tips card with green background
Widget _buildTipsCard({
required Color profitGreen,
required Color profitGreenBg,
}) {
// ============================================
// Tips card & Confirm button
// ============================================
Widget _buildTipsCard(_TransferColors c) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: profitGreenBg,
color: c.profitGreenBg,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Row(
children: [
Icon(
LucideIcons.info,
size: 16,
color: profitGreen,
),
Icon(LucideIcons.info, size: 16, color: c.profitGreen),
const SizedBox(width: 8),
Expanded(
child: Text(
'划转即时到账,无需手续费',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: profitGreen,
),
style: _inter(fontSize: 12, fontWeight: FontWeight.normal, color: c.profitGreen),
),
),
],
@@ -574,11 +444,7 @@ class _TransferPageState extends State<TransferPage> {
);
}
/// Confirm button
Widget _buildConfirmButton({
required Color accentPrimary,
required Color textInverse,
}) {
Widget _buildConfirmButton(_TransferColors c) {
return SizedBox(
width: double.infinity,
height: 52,
@@ -586,7 +452,7 @@ class _TransferPageState extends State<TransferPage> {
onTap: _isLoading ? null : _doTransfer,
child: Container(
decoration: BoxDecoration(
color: accentPrimary,
color: c.accentPrimary,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Center(
@@ -596,20 +462,56 @@ class _TransferPageState extends State<TransferPage> {
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(textInverse),
valueColor: AlwaysStoppedAnimation<Color>(c.textInverse),
),
)
: Text(
'确认划转',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w700,
color: textInverse,
),
style: _inter(fontSize: 16, fontWeight: FontWeight.w700, color: c.textInverse),
),
),
),
),
);
}
// ============================================
// Helpers
// ============================================
String _formatBalance(String balance) {
final val = double.tryParse(balance);
if (val == null) return '0.00';
return val.toStringAsFixed(2);
}
}
/// 主题感知颜色集合,避免在 build() 中重复定义大量局部变量
class _TransferColors {
final Color bgSecondary;
final Color surfaceCard;
final Color bgTertiary;
final Color borderDefault;
final Color textPrimary;
final Color textSecondary;
final Color textMuted;
final Color textInverse;
final Color accentPrimary;
final Color goldAccent;
final Color profitGreen;
final Color profitGreenBg;
_TransferColors(bool isDark)
: bgSecondary = isDark ? const Color(0xFF0B1120) : const Color(0xFFF8FAFC),
surfaceCard = isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF),
bgTertiary = isDark ? const Color(0xFF1E293B) : const Color(0xFFF1F5F9),
borderDefault = isDark ? const Color(0xFF334155) : const Color(0xFFE2E8F0),
textPrimary = isDark ? const Color(0xFFF8FAFC) : const Color(0xFF0F172A),
textSecondary = isDark ? const Color(0xFF94A3B8) : const Color(0xFF475569),
textMuted = isDark ? const Color(0xFF64748B) : const Color(0xFF94A3B8),
textInverse = isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF),
accentPrimary = isDark ? const Color(0xFFD4AF37) : const Color(0xFF1F2937),
goldAccent = isDark ? const Color(0xFFD4AF37) : const Color(0xFFF59E0B),
profitGreen = isDark ? const Color(0xFF4ADE80) : const Color(0xFF16A34A),
profitGreenBg = isDark ? const Color(0xFF052E16) : const Color(0xFFF0FDF4);
}

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

@@ -2,14 +2,16 @@ import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../providers/auth_provider.dart';
import 'kyc_page.dart';
import '../../../providers/theme_provider.dart';
import '../auth/login_page.dart';
import 'welfare_center_page.dart';
import 'components/about_dialog_helpers.dart';
import 'components/avatar_circle.dart';
import 'components/logout_button.dart';
import 'components/menu_group1.dart';
import 'components/menu_group2.dart';
import 'components/profile_card.dart';
/// 我的页面 - 匹配 .pen 设计稿
class MinePage extends StatefulWidget {
@@ -42,16 +44,16 @@ class _MinePageState extends State<MinePage>
),
child: Column(
children: [
_ProfileCard(user: auth.user),
ProfileCard(user: auth.user),
SizedBox(height: AppSpacing.sm),
_MenuGroup1(
MenuGroup1(
kycStatus: auth.user?.kycStatus ?? 0,
onShowComingSoon: _showComingSoon,
),
SizedBox(height: AppSpacing.sm),
_MenuGroup2(onShowAbout: _showAboutDialog),
MenuGroup2(onShowAbout: _showAboutDialog),
SizedBox(height: AppSpacing.lg),
_LogoutButton(onLogout: () => _handleLogout(auth)),
LogoutButton(onLogout: () => _handleLogout(auth)),
SizedBox(height: AppSpacing.md),
Text(
'System Build v1.0.0',
@@ -98,7 +100,7 @@ class _MinePageState extends State<MinePage>
builder: (context) => ShadDialog(
title: Row(
children: [
_AvatarCircle(radius: 20, fontSize: 16),
AvatarCircle(radius: 20, fontSize: 16),
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
const Text('模拟所'),
],
@@ -112,9 +114,9 @@ class _MinePageState extends State<MinePage>
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
SizedBox(height: AppSpacing.md),
_InfoRow(icon: Icons.code, text: '版本: 1.0.0'),
InfoRow(icon: Icons.code, text: '版本: 1.0.0'),
SizedBox(height: AppSpacing.sm),
_InfoRow(
InfoRow(
icon: Icons.favorite,
text: 'Built with Flutter & Material Design 3'),
],
@@ -158,607 +160,3 @@ class _MinePageState extends State<MinePage>
);
}
}
// ============================================================
// Profile Card
// ============================================================
/// 用户资料卡片 - 头像 + 用户名 + 徽章 + chevron
class _ProfileCard extends StatelessWidget {
final dynamic user;
const _ProfileCard({required this.user});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Row(
children: [
// Avatar
_AvatarCircle(
radius: 24,
fontSize: 18,
text: user?.avatarText,
),
const SizedBox(width: 12),
// Name + badge column
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user?.username ?? '未登录',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
'普通用户',
style: GoogleFonts.inter(
fontSize: 10,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
// Chevron
Icon(
LucideIcons.chevronRight,
size: 16,
color: colorScheme.onSurfaceVariant,
),
],
),
);
}
}
/// 圆形头像组件
class _AvatarCircle extends StatelessWidget {
final double radius;
final double fontSize;
final String? text;
const _AvatarCircle({
required this.radius,
required this.fontSize,
this.text,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return CircleAvatar(
radius: radius,
backgroundColor: colorScheme.primary.withOpacity(0.15),
child: Text(
text ?? '',
style: TextStyle(
fontSize: fontSize,
color: colorScheme.primary,
fontWeight: FontWeight.w700,
),
),
);
}
}
// ============================================================
// Menu Group 1 - 福利中心 / 实名认证 / 安全设置 / 消息通知
// ============================================================
class _MenuGroup1 extends StatelessWidget {
final int kycStatus;
final void Function(String) onShowComingSoon;
const _MenuGroup1({
required this.kycStatus,
required this.onShowComingSoon,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
width: double.infinity,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Column(
children: [
// 福利中心
_MenuRow(
icon: LucideIcons.gift,
iconColor: AppColorScheme.darkSecondary, // gold
title: '福利中心',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const WelfareCenterPage()),
);
},
),
_MenuDivider(),
// 实名认证
_MenuRow(
icon: LucideIcons.shieldCheck,
iconColor: AppColorScheme.getUpColor(isDark),
title: '实名认证',
trailing: _KycBadge(kycStatus: kycStatus),
onTap: () {
if (kycStatus == 2) {
_showKycStatusDialog(context);
} else {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const KycPage()),
);
}
},
),
_MenuDivider(),
// 安全设置
_MenuRow(
icon: LucideIcons.lock,
iconColor: colorScheme.onSurfaceVariant,
title: '安全设置',
onTap: () => onShowComingSoon('安全设置'),
),
_MenuDivider(),
// 消息通知
_MenuRow(
icon: LucideIcons.bell,
iconColor: colorScheme.onSurfaceVariant,
title: '消息通知',
trailing: _RedDotIndicator(),
onTap: () => onShowComingSoon('消息通知'),
),
],
),
);
}
}
// ============================================================
// Menu Group 2 - 深色模式 / 系统设置 / 关于我们
// ============================================================
class _MenuGroup2 extends StatelessWidget {
final VoidCallback onShowAbout;
const _MenuGroup2({required this.onShowAbout});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
width: double.infinity,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Column(
children: [
// 深色模式
_DarkModeRow(),
_MenuDivider(),
// 系统设置
_MenuRow(
icon: LucideIcons.settings,
iconColor: colorScheme.onSurfaceVariant,
title: '系统设置',
onTap: () {
// TODO: 系统设置
},
),
_MenuDivider(),
// 关于我们
_MenuRow(
icon: LucideIcons.info,
iconColor: colorScheme.onSurfaceVariant,
title: '关于我们',
onTap: onShowAbout,
),
],
),
);
}
}
// ============================================================
// Shared menu row components
// ============================================================
/// 单行菜单项icon-in-box + title + trailing (chevron / badge / toggle)
class _MenuRow extends StatelessWidget {
final IconData icon;
final Color iconColor;
final String title;
final Widget? trailing;
final VoidCallback? onTap;
const _MenuRow({
required this.icon,
required this.iconColor,
required this.title,
this.trailing,
this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
// Icon in 36x36 rounded container
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.dark
? Theme.of(context).colorScheme.surfaceContainerHigh
: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Icon(icon, size: 18, color: iconColor),
),
),
const SizedBox(width: 10),
// Title
Expanded(
child: Text(
title,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
// Trailing
if (trailing != null)
trailing!
else
Icon(
LucideIcons.chevronRight,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
),
),
);
}
}
/// Menu group divider
class _MenuDivider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 1,
color: Theme.of(context).colorScheme.outlineVariant.withOpacity(0.15),
margin: const EdgeInsets.only(left: 62),
);
}
}
// ============================================================
// Special trailing widgets
// ============================================================
/// KYC status badge (e.g. "已认证" green badge + chevron)
class _KycBadge extends StatelessWidget {
final int kycStatus;
const _KycBadge({required this.kycStatus});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final green = AppColorScheme.getUpColor(isDark);
if (kycStatus == 2) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: green.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
'已认证',
style: GoogleFonts.inter(
fontSize: 10,
fontWeight: FontWeight.w500,
color: green,
),
),
),
const SizedBox(width: 8),
Icon(
LucideIcons.chevronRight,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
);
}
if (kycStatus == 1) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: AppColorScheme.warning.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
'审核中',
style: GoogleFonts.inter(
fontSize: 10,
fontWeight: FontWeight.w500,
color: AppColorScheme.warning,
),
),
),
const SizedBox(width: 8),
Icon(
LucideIcons.chevronRight,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
);
}
return Icon(
LucideIcons.chevronRight,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
);
}
}
/// Red dot indicator for notifications + chevron
class _RedDotIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: AppColorScheme.down,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Icon(
LucideIcons.chevronRight,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
);
}
}
/// Dark mode toggle row
class _DarkModeRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final themeProvider = context.watch<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,
),
),
),
),
),
],
),
);
}
}
// ============================================================
// Logout button
// ============================================================
class _LogoutButton extends StatelessWidget {
final VoidCallback onLogout;
const _LogoutButton({required this.onLogout});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return GestureDetector(
onTap: onLogout,
child: Container(
width: double.infinity,
height: 48,
decoration: BoxDecoration(
color: AppColorScheme.down.withOpacity(0.05),
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: AppColorScheme.down.withOpacity(0.15),
),
),
child: Center(
child: Text(
'退出登录',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColorScheme.down,
),
),
),
),
);
}
}
// ============================================================
// Info row (used in about dialog)
// ============================================================
class _InfoRow extends StatelessWidget {
final IconData icon;
final String text;
const _InfoRow({required this.icon, required this.text});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Row(
children: [
Icon(icon, size: 14, color: colorScheme.onSurfaceVariant),
SizedBox(width: AppSpacing.sm),
Text(
text,
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
],
);
}
}
// ============================================================
// KYC status dialog
// ============================================================
void _showKycStatusDialog(BuildContext context) {
showShadDialog(
context: context,
builder: (ctx) => ShadDialog.alert(
title: Row(
children: [
Icon(Icons.check_circle, color: AppColorScheme.up, size: 20),
SizedBox(width: AppSpacing.sm),
const Text('实名认证'),
],
),
description: const Text('您的实名认证已通过'),
actions: [
ShadButton(
child: const Text('确定'),
onPressed: () => Navigator.of(ctx).pop(),
),
],
),
);
}

View File

@@ -9,7 +9,6 @@ import '../../../core/utils/toast_utils.dart';
import '../../../core/event/app_event_bus.dart';
import '../../../data/services/bonus_service.dart';
import '../../../providers/asset_provider.dart';
import '../../../providers/auth_provider.dart';
/// 福利中心页面
class WelfareCenterPage extends StatefulWidget {
@@ -48,64 +47,144 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
// 主题感知颜色辅助
// ============================================
/// 金色强调色 ($gold-accent)
Color _goldAccent(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return isDark ? AppColorScheme.darkSecondary : const Color(0xFFF59E0B);
}
bool get _isDark => Theme.of(context).brightness == Brightness.dark;
/// 金色强调色带透明度
Color _goldAccentWithOpacity(BuildContext context, double opacity) {
return _goldAccent(context).withOpacity(opacity);
}
/// 金色强调色 ($gold-accent)
Color get _goldAccent =>
_isDark ? AppColorScheme.darkSecondary : const Color(0xFFF59E0B);
/// 盈利绿色 ($profit-green)
Color _profitGreen(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return isDark ? const Color(0xFF4ADE80) : const Color(0xFF16A34A);
}
Color get _profitGreen =>
_isDark ? const Color(0xFF4ADE80) : const Color(0xFF16A34A);
/// 盈利绿色背景 ($profit-green-bg)
Color _profitGreenBg(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return isDark ? const Color(0xFF052E16) : const Color(0xFFF0FDF4);
}
Color get _profitGreenBg =>
_isDark ? const Color(0xFF052E16) : const Color(0xFFF0FDF4);
/// 文字静默色 ($text-muted)
Color _textMuted(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return isDark ? const Color(0xFF64748B) : const Color(0xFF94A3B8);
}
Color get _textMuted =>
_isDark ? const Color(0xFF64748B) : const Color(0xFF94A3B8);
/// 第三级背景色 ($bg-tertiary)
Color _bgTertiary(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return isDark ? AppColorScheme.darkSurfaceContainerHigh : const Color(0xFFF1F5F9);
}
Color get _bgTertiary =>
_isDark ? AppColorScheme.darkSurfaceContainerHigh : const Color(0xFFF1F5F9);
/// 卡片表面色 ($surface-card)
Color _surfaceCard(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return isDark ? AppColorScheme.darkSurfaceContainer : Colors.white;
}
Color get _surfaceCard =>
_isDark ? AppColorScheme.darkSurfaceContainer : Colors.white;
/// 反色文字 ($text-inverse)
Color _textInverse(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return isDark ? const Color(0xFF0F172A) : Colors.white;
Color get _textInverse =>
_isDark ? const Color(0xFF0F172A) : Colors.white;
// ============================================
// 文本样式辅助
// ============================================
TextStyle _inter({
required double fontSize,
required FontWeight fontWeight,
required Color color,
}) {
return GoogleFonts.inter(
fontSize: fontSize,
fontWeight: fontWeight,
color: color,
);
}
// ============================================
// 容器样式辅助
// ============================================
/// 标准卡片容器
BoxDecoration _cardDecoration({Color? borderColor}) {
final scheme = Theme.of(context).colorScheme;
return BoxDecoration(
color: _surfaceCard,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: borderColor ?? scheme.outlineVariant.withValues(alpha: 0.15),
),
);
}
/// 金色渐变卡片容器
BoxDecoration _goldGradientDecoration() {
return BoxDecoration(
borderRadius: BorderRadius.circular(AppRadius.xl),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
_goldAccent.withValues(alpha: 0.15),
_surfaceCard,
],
),
border: Border.all(
color: _goldAccent.withValues(alpha: 0.3),
width: 1,
),
);
}
/// 状态胶囊标签
Widget _statusBadge(String text, Color textColor, Color bgColor) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
text,
style: _inter(fontSize: 11, fontWeight: FontWeight.w600, color: textColor),
),
);
}
/// 全宽按钮
Widget _fullWidthButton({
required String text,
required Color backgroundColor,
required Color foregroundColor,
required VoidCallback? onPressed,
Color? disabledBackgroundColor,
}) {
return SizedBox(
width: double.infinity,
height: 44,
child: ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.lg),
),
disabledBackgroundColor:
disabledBackgroundColor ?? backgroundColor.withValues(alpha: 0.3),
disabledForegroundColor: foregroundColor.withValues(alpha: 0.7),
),
child: Text(
text,
style: _inter(fontSize: 14, fontWeight: FontWeight.w700, color: foregroundColor),
),
),
);
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDark
backgroundColor: _isDark
? AppColorScheme.darkBackground
: const Color(0xFFF8FAFC),
appBar: AppBar(
backgroundColor: isDark
backgroundColor: _isDark
? AppColorScheme.darkBackground
: Colors.white,
elevation: 0,
@@ -114,7 +193,7 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
titleSpacing: 0,
title: Text(
'福利中心',
style: GoogleFonts.inter(
style: _inter(
fontSize: 17,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
@@ -128,32 +207,25 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
body: _isLoading
? Center(
child: CircularProgressIndicator(
color: _goldAccent(context),
color: _goldAccent,
strokeWidth: 2.5,
),
)
: RefreshIndicator(
onRefresh: _loadData,
color: _goldAccent(context),
color: _goldAccent,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 推广码卡片(金色渐变边框)
_buildReferralCodeCard(context),
const SizedBox(height: 16),
// 新人福利卡片
_buildNewUserBonusCard(context),
const SizedBox(height: 16),
// 推广奖励列表
_buildReferralRewardsSection(context),
const SizedBox(height: 16),
// 奖励规则
_buildRulesCard(context),
],
),
@@ -169,37 +241,22 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
Widget _buildReferralCodeCard(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final referralCode = _welfareData?['referralCode'] as String? ?? '';
final gold = _goldAccent(context);
return Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppRadius.xl),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
_goldAccentWithOpacity(context, 0.15),
_surfaceCard(context),
],
),
border: Border.all(
color: _goldAccentWithOpacity(context, 0.3),
width: 1,
),
),
decoration: _goldGradientDecoration(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header Row: gift icon + 标题
Row(
children: [
Icon(LucideIcons.gift, color: gold, size: 24),
Icon(LucideIcons.gift, color: _goldAccent, size: 24),
const SizedBox(width: 10),
Text(
'我的邀请码',
style: GoogleFonts.inter(
style: _inter(
fontSize: 16,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
@@ -208,20 +265,15 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
],
),
const SizedBox(height: 16),
// 邀请码
Text(
referralCode.isEmpty ? '暂无邀请码' : referralCode,
style: GoogleFonts.inter(
style: _inter(
fontSize: 24,
fontWeight: FontWeight.w800,
letterSpacing: 2,
color: gold,
),
color: _goldAccent,
).copyWith(letterSpacing: 2),
),
const SizedBox(height: 16),
// 复制邀请码按钮
SizedBox(
width: double.infinity,
height: 40,
@@ -233,20 +285,17 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
ToastUtils.show('邀请码已复制');
},
style: ElevatedButton.styleFrom(
backgroundColor: gold,
foregroundColor: _textInverse(context),
backgroundColor: _goldAccent,
foregroundColor: _textInverse,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.lg),
),
disabledBackgroundColor: gold.withOpacity(0.4),
disabledBackgroundColor: _goldAccent.withValues(alpha: 0.4),
),
child: Text(
'复制邀请码',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
),
style: _inter(fontSize: 14, fontWeight: FontWeight.w600, color: _textInverse),
),
),
),
@@ -265,10 +314,8 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
final eligible = newUserBonus?['eligible'] as bool? ?? false;
final claimed = newUserBonus?['claimed'] as bool? ?? false;
final deposited = newUserBonus?['deposited'] as bool? ?? false;
final green = _profitGreen(context);
final greenBg = _profitGreenBg(context);
// 状态标签
// 状态判定
String badgeText;
bool showAvailableBadge;
String buttonText;
@@ -298,13 +345,7 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: _surfaceCard(context),
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
decoration: _cardDecoration(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -314,78 +355,40 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
children: [
Text(
'新人福利',
style: GoogleFonts.inter(
style: _inter(
fontSize: 16,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
if (showAvailableBadge)
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: greenBg,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
badgeText,
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w600,
color: green,
),
),
),
_statusBadge(badgeText, _profitGreen, _profitGreenBg),
],
),
const SizedBox(height: 12),
// 金额
Text(
'+100 USDT',
style: GoogleFonts.inter(
style: _inter(
fontSize: 28,
fontWeight: FontWeight.w800,
color: claimed ? colorScheme.onSurfaceVariant : green,
color: claimed ? colorScheme.onSurfaceVariant : _profitGreen,
),
),
const SizedBox(height: 8),
// 描述
Text(
description,
style: GoogleFonts.inter(
style: _inter(
fontSize: 13,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
// 领取按钮
SizedBox(
width: double.infinity,
height: 44,
child: ElevatedButton(
onPressed: canClaim ? () => _claimNewUserBonus() : null,
style: ElevatedButton.styleFrom(
backgroundColor: green,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.lg),
),
disabledBackgroundColor: green.withOpacity(0.3),
disabledForegroundColor: Colors.white70,
),
child: Text(
buttonText,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
),
_fullWidthButton(
text: buttonText,
backgroundColor: _profitGreen,
foregroundColor: Colors.white,
onPressed: canClaim ? () => _claimNewUserBonus() : null,
),
],
),
@@ -405,40 +408,29 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section Header
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'推广奖励',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
'每邀请一位好友充值达标奖励100 USDT',
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.normal,
color: _textMuted(context),
),
),
],
Text(
'推广奖励',
style: _inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
'每邀请一位好友充值达标奖励100 USDT',
style: _inter(
fontSize: 11,
fontWeight: FontWeight.normal,
color: _textMuted,
),
),
const SizedBox(height: 12),
// 推广列表卡片
Container(
width: double.infinity,
decoration: BoxDecoration(
color: _surfaceCard(context),
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
decoration: _cardDecoration(),
child: referralRewards.isEmpty
? _buildEmptyReferralList(context)
: _buildReferralListItems(context, referralRewards),
@@ -457,13 +449,14 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
Icon(
LucideIcons.users,
size: 36,
color: _textMuted(context).withOpacity(0.4),
color: _textMuted.withValues(alpha: 0.4),
),
const SizedBox(height: 8),
Text(
'暂无推广用户',
style: GoogleFonts.inter(
style: _inter(
fontSize: 13,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
@@ -475,9 +468,6 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
Widget _buildReferralListItems(BuildContext context, List<dynamic> referralRewards) {
final colorScheme = Theme.of(context).colorScheme;
final gold = _goldAccent(context);
final green = _profitGreen(context);
final greenBg = _profitGreenBg(context);
return Column(
children: List.generate(referralRewards.length, (index) {
@@ -488,92 +478,17 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
final milestones = data['milestones'] as List<dynamic>? ?? [];
final isLast = index == referralRewards.length - 1;
// 判断状态
bool hasClaimable = claimableCount > 0;
bool hasAnyMilestone = milestones.isNotEmpty;
bool allClaimed = milestones.isNotEmpty &&
milestones.every((m) => (m as Map<String, dynamic>)['claimed'] == true);
// 进度计算
double progress = 0;
if (milestones.isNotEmpty) {
int earnedCount = milestones.where((m) {
final milestone = m as Map<String, dynamic>;
return milestone['earned'] as bool? ?? false;
}).length;
progress = earnedCount / milestones.length;
} else {
// 无里程碑数据时,根据充值金额估算
final deposit = double.tryParse(totalDeposit) ?? 0;
progress = (deposit / 1000).clamp(0.0, 1.0);
}
// 状态颜色和文字
Color progressColor;
Color statusTextColor;
String statusText;
Widget? actionWidget;
if (hasClaimable) {
// 可领取 (achieved, green)
progressColor = green;
statusTextColor = green;
statusText = '';
actionWidget = GestureDetector(
onTap: () => _claimReferralBonus(data['userId'] as int, milestones.isNotEmpty ? (milestones.firstWhere(
(m) => (m as Map<String, dynamic>)['claimable'] == true,
orElse: () => milestones.first,
) as Map<String, dynamic>)['milestone'] as int? ?? 1 : 1),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
decoration: BoxDecoration(
color: greenBg,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
'领取',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
color: green,
),
),
),
);
} else if (progress > 0) {
// 进行中 (amber / gold)
progressColor = gold;
statusTextColor = const Color(0xFFD97706);
statusText = '';
actionWidget = Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFFEF3C7),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
'进行中',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
color: statusTextColor,
),
),
);
} else {
// 待达标 (gray)
progressColor = _bgTertiary(context);
statusTextColor = _textMuted(context);
statusText = '';
actionWidget = Text(
'待达标',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
color: _textMuted(context),
),
);
}
final progress = _computeProgress(milestones, totalDeposit);
// 操作按钮
final actionWidget = _buildReferralAction(
data: data,
claimableCount: claimableCount,
milestones: milestones,
progress: progress,
);
// 进度条颜色
final progressColor = _referralProgressColor(claimableCount, progress);
return Column(
children: [
@@ -581,48 +496,21 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Top Row: avatar + name + deposit + action
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
// Avatar
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: _bgTertiary(context),
shape: BoxShape.circle,
),
child: Center(
child: Text(
username.isNotEmpty ? username[0].toUpperCase() : '?',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: colorScheme.onSurfaceVariant,
),
),
),
),
_buildAvatar(username),
const SizedBox(width: 10),
Text(
username,
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
),
style: _inter(fontSize: 13, fontWeight: FontWeight.w500, color: colorScheme.onSurface),
),
const SizedBox(width: 10),
Text(
'充值: \u00A5$totalDeposit',
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
style: _inter(fontSize: 11, fontWeight: FontWeight.normal, color: colorScheme.onSurfaceVariant),
),
],
),
@@ -630,15 +518,13 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
],
),
const SizedBox(height: 10),
// Progress bar
ClipRRect(
borderRadius: BorderRadius.circular(3),
child: SizedBox(
height: 6,
child: LinearProgressIndicator(
value: progress,
backgroundColor: _bgTertiary(context),
backgroundColor: _bgTertiary,
valueColor: AlwaysStoppedAnimation<Color>(progressColor),
minHeight: 6,
),
@@ -651,7 +537,7 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
Divider(
height: 1,
thickness: 1,
color: colorScheme.outlineVariant.withOpacity(0.15),
color: colorScheme.outlineVariant.withValues(alpha: 0.15),
),
],
);
@@ -659,6 +545,77 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
);
}
Widget _buildAvatar(String username) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: _bgTertiary,
shape: BoxShape.circle,
),
child: Center(
child: Text(
username.isNotEmpty ? username[0].toUpperCase() : '?',
style: _inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: colorScheme.onSurfaceVariant,
),
),
),
);
}
/// 计算推荐奖励进度
double _computeProgress(List<dynamic> milestones, String totalDeposit) {
if (milestones.isNotEmpty) {
int earnedCount = milestones.where((m) {
final milestone = m as Map<String, dynamic>;
return milestone['earned'] as bool? ?? false;
}).length;
return earnedCount / milestones.length;
}
final deposit = double.tryParse(totalDeposit) ?? 0;
return (deposit / 1000).clamp(0.0, 1.0);
}
/// 根据状态获取进度条颜色
Color _referralProgressColor(int claimableCount, double progress) {
if (claimableCount > 0) return _profitGreen;
if (progress > 0) return _goldAccent;
return _bgTertiary;
}
/// 构建推荐奖励的操作按钮
Widget? _buildReferralAction({
required Map<String, dynamic> data,
required int claimableCount,
required List<dynamic> milestones,
required double progress,
}) {
if (claimableCount > 0) {
final int milestoneValue = milestones.isNotEmpty
? (milestones.firstWhere(
(m) => (m as Map<String, dynamic>)['claimable'] == true,
orElse: () => milestones.first,
) as Map<String, dynamic>)['milestone'] as int? ?? 1
: 1;
return GestureDetector(
onTap: () => _claimReferralBonus(data['userId'] as int, milestoneValue),
child: _statusBadge('领取', _profitGreen, _profitGreenBg),
);
}
if (progress > 0) {
return _statusBadge('进行中', const Color(0xFFD97706), const Color(0xFFFEF3C7));
}
return Text(
'待达标',
style: _inter(fontSize: 12, fontWeight: FontWeight.w500, color: _textMuted),
);
}
// ============================================
// 奖励规则卡片
// ============================================
@@ -670,7 +627,7 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
width: double.infinity,
padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
decoration: BoxDecoration(
color: _bgTertiary(context),
color: _bgTertiary,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Column(
@@ -678,32 +635,33 @@ class _WelfareCenterPageState extends State<WelfareCenterPage> {
children: [
Text(
'奖励规则',
style: GoogleFonts.inter(
style: _inter(
fontSize: 13,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 8),
_buildRuleItem('新用户注册完成实名认证奖励 100 USDT', context),
_buildRuleItem('邀请好友充值每达 1000 USDT双方各获得 100 USDT', context),
_buildRuleItem('奖励直接发放至资金账户', context),
_buildRuleItem('新用户注册完成实名认证奖励 100 USDT'),
_buildRuleItem('邀请好友充值每达 1000 USDT双方各获得 100 USDT'),
_buildRuleItem('奖励直接发放至资金账户'),
],
),
);
}
Widget _buildRuleItem(String text, BuildContext context) {
Widget _buildRuleItem(String text) {
final ruleTextColor = _isDark
? AppColorScheme.darkOnSurfaceVariant
: const Color(0xFF475569);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Text(
'\u2022 $text',
style: GoogleFonts.inter(
style: _inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: Theme.of(context).brightness == Brightness.dark
? AppColorScheme.darkOnSurfaceVariant
: const Color(0xFF475569),
color: ruleTextColor,
),
),
);

View File

@@ -7,7 +7,6 @@ import 'package:bot_toast/bot_toast.dart';
import 'package:provider/provider.dart';
import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_theme.dart' show AppRadius;
import '../../../core/utils/toast_utils.dart';
import '../../../core/event/app_event_bus.dart';
import '../../../providers/asset_provider.dart';
@@ -52,36 +51,48 @@ class _FundOrdersPageState extends State<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 isDark = Theme.of(context).brightness == Brightness.dark;
final bgColor = isDark
? AppColorScheme.darkBackground
: AppColorScheme.lightBackground;
final c = _colors;
return Scaffold(
backgroundColor: bgColor,
backgroundColor: c.background,
appBar: AppBar(
leading: IconButton(
icon: const Icon(LucideIcons.arrowLeft, size: 20),
onPressed: () => Navigator.of(context).pop(),
),
title: Text(
'充提记录',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
backgroundColor: bgColor,
title: Text('充提记录', style: _inter(fontSize: 16, fontWeight: FontWeight.w600, color: c.primaryText)),
backgroundColor: c.background,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
),
body: Column(
children: [
_buildFilterTabs(context, isDark),
Expanded(child: _buildOrderList(context, isDark)),
_buildFilterTabs(),
Expanded(child: _buildOrderList()),
],
),
);
@@ -90,13 +101,8 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
// ---------------------------------------------------------------------------
// Filter Tabs - pill-style segmented control
// ---------------------------------------------------------------------------
Widget _buildFilterTabs(BuildContext context, bool isDark) {
final bgColor = isDark
? AppColorScheme.darkSurfaceContainerHigh
: AppColorScheme.lightSurfaceHigh;
final activeBgColor = isDark
? AppColorScheme.darkOnSurface
: Colors.white;
Widget _buildFilterTabs() {
final c = _colors;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
@@ -104,33 +110,23 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
height: 40,
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: bgColor,
color: c.tabBg,
borderRadius: AppRadius.radiusMd,
),
child: Row(
children: [
_buildPillTab('全部', 0, activeBgColor, isDark),
_buildPillTab('充值', 1, activeBgColor, isDark),
_buildPillTab('提现', 2, activeBgColor, isDark),
_buildPillTab('全部', 0),
_buildPillTab('充值', 1),
_buildPillTab('提现', 2),
],
),
),
);
}
Widget _buildPillTab(
String label,
int index,
Color activeBgColor,
bool isDark,
) {
Widget _buildPillTab(String label, int index) {
final c = _colors;
final isActive = _activeTab == index;
final activeTextColor = isDark
? AppColorScheme.darkBackground
: AppColorScheme.lightOnSurface;
final inactiveTextColor = isDark
? AppColorScheme.darkOnSurfaceVariant
: AppColorScheme.lightOnSurfaceVariant;
return Expanded(
child: GestureDetector(
@@ -142,16 +138,16 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
},
child: Container(
decoration: BoxDecoration(
color: isActive ? activeBgColor : Colors.transparent,
color: isActive ? c.activeTabBg : Colors.transparent,
borderRadius: AppRadius.radiusSm,
),
child: Center(
child: Text(
label,
style: GoogleFonts.inter(
style: _inter(
fontSize: 14,
fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
color: isActive ? activeTextColor : inactiveTextColor,
color: isActive ? c.activeTabText : c.inactiveTabText,
),
),
),
@@ -163,7 +159,9 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
// ---------------------------------------------------------------------------
// Order List
// ---------------------------------------------------------------------------
Widget _buildOrderList(BuildContext context, bool isDark) {
Widget _buildOrderList() {
final c = _colors;
return Consumer<AssetProvider>(
builder: (context, provider, _) {
final orders = provider.fundOrders;
@@ -178,23 +176,9 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
LucideIcons.inbox,
size: 64,
color: isDark
? AppColorScheme.darkOnSurfaceMuted
: AppColorScheme.lightOnSurfaceMuted,
),
Icon(LucideIcons.inbox, size: 64, color: c.mutedText),
const SizedBox(height: 16),
Text(
'暂无订单记录',
style: GoogleFonts.inter(
fontSize: 14,
color: isDark
? AppColorScheme.darkOnSurfaceVariant
: AppColorScheme.lightOnSurfaceVariant,
),
),
Text('暂无订单记录', style: _inter(fontSize: 14, fontWeight: FontWeight.normal, color: c.secondaryText)),
],
),
);
@@ -207,7 +191,7 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
itemCount: orders.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
return _buildOrderCard(orders[index], isDark);
return _buildOrderCard(orders[index]);
},
),
);
@@ -218,52 +202,35 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
// ---------------------------------------------------------------------------
// Order Card
// ---------------------------------------------------------------------------
Widget _buildOrderCard(OrderFund order, bool isDark) {
final cardBg = isDark
? AppColorScheme.darkSurfaceContainer
: AppColorScheme.lightSurfaceLowest;
final borderColor = isDark
? AppColorScheme.darkOutlineVariant.withValues(alpha: 0.15)
: AppColorScheme.lightOutlineVariant.withValues(alpha: 0.5);
final primaryText = isDark
? AppColorScheme.darkOnSurface
: AppColorScheme.lightOnSurface;
final mutedText = isDark
? AppColorScheme.darkOnSurfaceMuted
: AppColorScheme.lightOnSurfaceMuted;
Widget _buildOrderCard(OrderFund order) {
final c = _colors;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: cardBg,
color: c.cardBg,
borderRadius: AppRadius.radiusLg,
border: Border.all(color: borderColor, width: 1),
border: Border.all(color: c.borderColor, width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header: type badge + status badge
_buildCardHeader(order, isDark),
_buildCardHeader(order),
const SizedBox(height: 12),
// Amount
_buildAmountRow(order, primaryText),
_buildAmountRow(order),
const SizedBox(height: 12),
// Detail rows
_buildDetailRows(order, primaryText, mutedText),
// Rejection reason
_buildDetailRows(order),
if (order.rejectReason != null) ...[
const SizedBox(height: 8),
_buildRejectionReason(order),
],
// Payable amount (withdrawal with fee)
if (order.receivableAmount != null && !order.isDeposit) ...[
const SizedBox(height: 8),
_buildPayableRow(order, isDark, primaryText),
_buildPayableRow(order),
],
// Action buttons
if (order.canCancel || order.canConfirmPay) ...[
const SizedBox(height: 12),
_buildActions(order, isDark),
_buildActions(order),
],
],
),
@@ -273,30 +240,28 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
// ---------------------------------------------------------------------------
// Card Header - type badge + status badge
// ---------------------------------------------------------------------------
Widget _buildCardHeader(OrderFund order, bool isDark) {
final upColor = AppColorScheme.getUpColor(isDark);
final downColor = AppColorScheme.getDownColor(isDark);
final upBg = AppColorScheme.getUpBackgroundColor(isDark, opacity: 0.12);
final downBg = AppColorScheme.getDownBackgroundColor(isDark, opacity: 0.12);
Widget _buildCardHeader(OrderFund order) {
final upColor = AppColorScheme.getUpColor(_isDark);
final downColor = AppColorScheme.getDownColor(_isDark);
final upBg = AppColorScheme.getUpBackgroundColor(_isDark, opacity: 0.12);
final downBg = AppColorScheme.getDownBackgroundColor(_isDark, opacity: 0.12);
final typeColor = order.isDeposit ? upColor : downColor;
final typeBg = order.isDeposit ? upBg : downBg;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Type badge (充值 / 提现)
_buildBadge(order.typeText, typeColor, typeBg),
// Status badge
_buildStatusBadge(order, isDark),
_buildStatusBadge(order),
],
);
}
Widget _buildStatusBadge(OrderFund order, bool isDark) {
final upColor = AppColorScheme.getUpColor(isDark);
final downColor = AppColorScheme.getDownColor(isDark);
final upBg = AppColorScheme.getUpBackgroundColor(isDark, opacity: 0.12);
final downBg = AppColorScheme.getDownBackgroundColor(isDark, opacity: 0.12);
Widget _buildStatusBadge(OrderFund order) {
final upColor = AppColorScheme.getUpColor(_isDark);
final downColor = AppColorScheme.getDownColor(_isDark);
final upBg = AppColorScheme.getUpBackgroundColor(_isDark, opacity: 0.12);
final downBg = AppColorScheme.getDownBackgroundColor(_isDark, opacity: 0.12);
const amberColor = Color(0xFFD97706);
const amberBg = Color(0xFFFEF3C7);
@@ -345,86 +310,63 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
color: bgColor,
borderRadius: BorderRadius.circular(4),
),
child: Text(
text,
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w600,
color: textColor,
),
),
child: Text(text, style: _inter(fontSize: 11, fontWeight: FontWeight.w600, color: textColor)),
);
}
// ---------------------------------------------------------------------------
// Amount Row
// ---------------------------------------------------------------------------
Widget _buildAmountRow(OrderFund order, Color primaryText) {
Widget _buildAmountRow(OrderFund order) {
final c = _colors;
return Text(
'${order.isDeposit ? '+' : '-'}${order.amount} USDT',
style: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w700,
color: primaryText,
),
style: _inter(fontSize: 18, fontWeight: FontWeight.w700, color: c.primaryText),
);
}
// ---------------------------------------------------------------------------
// Detail Rows
// ---------------------------------------------------------------------------
Widget _buildDetailRows(
OrderFund order,
Color primaryText,
Color mutedText,
) {
Widget _buildDetailRows(OrderFund order) {
final c = _colors;
return Column(
children: [
// Order number
_buildDetailRow('订单号', order.orderNo, primaryText, mutedText),
_buildDetailRow('订单号', order.orderNo, c),
const SizedBox(height: 6),
// Network / wallet address
if (order.walletAddress != null) ...[
_buildDetailRow(
'网络',
order.remark.isNotEmpty ? order.remark : '-',
primaryText,
mutedText,
c,
),
const SizedBox(height: 6),
_buildDetailRow(
'地址',
_truncateAddress(order.walletAddress!),
primaryText,
mutedText,
c,
trailing: GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: order.walletAddress!));
ToastUtils.show('地址已复制');
},
child: Icon(
LucideIcons.copy,
size: 14,
color: mutedText,
),
child: Icon(LucideIcons.copy, size: 14, color: c.mutedText),
),
),
const SizedBox(height: 6),
] else if (order.remark.isNotEmpty) ...[
_buildDetailRow('网络', order.remark, primaryText, mutedText),
_buildDetailRow('网络', order.remark, c),
const SizedBox(height: 6),
],
// Fee (withdrawal)
if (order.fee != null && !order.isDeposit) ...[
_buildDetailRow('手续费', '${order.fee}%', primaryText, mutedText),
_buildDetailRow('手续费', '${order.fee}%', c),
const SizedBox(height: 6),
],
// Time
_buildDetailRow(
'时间',
_formatTime(order.createTime),
primaryText,
mutedText,
c,
),
],
);
@@ -433,46 +375,26 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
Widget _buildDetailRow(
String label,
String value,
Color primaryText,
Color mutedText, {
_OrderColors c, {
Widget? trailing,
}) {
final valueStyle = _inter(fontSize: 12, fontWeight: FontWeight.normal, color: c.primaryText);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: mutedText,
),
),
Text(label, style: _inter(fontSize: 12, fontWeight: FontWeight.normal, color: c.mutedText)),
if (trailing != null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
value,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: primaryText,
),
),
Text(value, style: valueStyle),
const SizedBox(width: 4),
trailing,
],
)
else
Text(
value,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: primaryText,
),
),
Text(value, style: valueStyle),
],
);
}
@@ -481,57 +403,29 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
// Rejection Reason
// ---------------------------------------------------------------------------
Widget _buildRejectionReason(OrderFund order) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Text(
'拒绝原因: ${order.rejectReason}',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: AppColorScheme.getDownColor(isDark),
),
style: _inter(fontSize: 12, fontWeight: FontWeight.normal, color: AppColorScheme.getDownColor(_isDark)),
);
}
// ---------------------------------------------------------------------------
// Payable Amount Row (withdrawal)
// ---------------------------------------------------------------------------
Widget _buildPayableRow(
OrderFund order,
bool isDark,
Color primaryText,
) {
final bgTertiary = isDark
? AppColorScheme.darkSurfaceContainerHigh
: AppColorScheme.lightSurfaceHigh;
final secondaryText = isDark
? AppColorScheme.darkOnSurfaceVariant
: AppColorScheme.lightOnSurfaceVariant;
Widget _buildPayableRow(OrderFund order) {
final c = _colors;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: bgTertiary,
color: c.bgTertiary,
borderRadius: AppRadius.radiusSm,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'应付金额',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: secondaryText,
),
),
Text(
'${order.receivableAmount} USDT',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w600,
color: primaryText,
),
),
Text('应付金额', style: _inter(fontSize: 13, fontWeight: FontWeight.w500, color: c.secondaryText)),
Text('${order.receivableAmount} USDT', style: _inter(fontSize: 13, fontWeight: FontWeight.w600, color: c.primaryText)),
],
),
);
@@ -540,9 +434,9 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
// ---------------------------------------------------------------------------
// Action Buttons
// ---------------------------------------------------------------------------
Widget _buildActions(OrderFund order, bool isDark) {
final upColor = AppColorScheme.getUpColor(isDark);
final downColor = AppColorScheme.getDownColor(isDark);
Widget _buildActions(OrderFund order) {
final upColor = AppColorScheme.getUpColor(_isDark);
final downColor = AppColorScheme.getDownColor(_isDark);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
@@ -556,14 +450,7 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
borderRadius: AppRadius.radiusSm,
border: Border.all(color: downColor, width: 1),
),
child: Text(
'取消订单',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: downColor,
),
),
child: Text('取消订单', style: _inter(fontSize: 13, fontWeight: FontWeight.w500, color: downColor)),
),
),
if (order.canCancel && order.canConfirmPay)
@@ -577,14 +464,7 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
color: upColor,
borderRadius: AppRadius.radiusSm,
),
child: Text(
'已打款',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
child: Text('已打款', style: _inter(fontSize: 13, fontWeight: FontWeight.w500, color: Colors.white)),
),
),
],
@@ -663,3 +543,37 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
);
}
}
/// 充提订单页面的主题感知颜色集合
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,
)),
),
),
),
);
}
}

View File

@@ -1,16 +1,19 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../data/models/coin.dart';
import '../../../providers/market_provider.dart';
import '../../../providers/asset_provider.dart';
import '../../../data/services/trade_service.dart';
import '../../components/glass_panel.dart';
import '../../components/neon_glow.dart';
import 'components/coin_selector.dart';
import 'components/price_card.dart';
import 'components/placeholder_card.dart';
import 'components/trade_form_card.dart';
import 'components/trade_button.dart';
import 'components/confirm_dialog.dart';
/// 交易页面
///
@@ -124,7 +127,7 @@ class _TradePageState extends State<TradePage>
child: Column(
children: [
// 币种选择器卡片
_CoinSelector(
CoinSelector(
selectedCoin: _selectedCoin,
coins: market.allCoins
.where((c) =>
@@ -143,16 +146,16 @@ class _TradePageState extends State<TradePage>
// 价格卡片
if (_selectedCoin != null)
_PriceCard(coin: _selectedCoin!)
PriceCard(coin: _selectedCoin!)
else
_PlaceholderCard(
PlaceholderCard(
message: '请先选择交易币种',
colorScheme: colorScheme,
),
const SizedBox(height: AppSpacing.md),
// 交易表单卡片(内含买入/卖出切换 + 表单)
_TradeFormCard(
TradeFormCard(
tradeType: _tradeType,
selectedCoin: _selectedCoin,
amountController: _amountController,
@@ -173,7 +176,7 @@ class _TradePageState extends State<TradePage>
SizedBox(
width: double.infinity,
height: 48,
child: _TradeButton(
child: TradeButton(
isBuy: _tradeType == 0,
coinCode: _selectedCoin?.code,
enabled: _canTrade() && !_isSubmitting,
@@ -217,7 +220,7 @@ class _TradePageState extends State<TradePage>
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => _ConfirmDialog(
builder: (ctx) => ConfirmDialog(
isBuy: isBuy,
coinCode: coinCode,
price: price,
@@ -288,899 +291,3 @@ class _TradePageState extends State<TradePage>
);
}
}
/// 确认对话框
class _ConfirmDialog extends StatelessWidget {
final bool isBuy;
final String coinCode;
final String price;
final String quantity;
final String amount;
const _ConfirmDialog({
required this.isBuy,
required this.coinCode,
required this.price,
required this.quantity,
required this.amount,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final actionColor = isBuy
? AppColorScheme.getUpColor(isDark)
: AppColorScheme.getDownColor(isDark);
return Dialog(
backgroundColor: Colors.transparent,
child: GlassPanel(
borderRadius: BorderRadius.circular(AppRadius.lg),
padding: EdgeInsets.all(AppSpacing.lg),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
'确认${isBuy ? '买入' : '卖出'}',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
),
SizedBox(height: AppSpacing.lg),
_dialogRow('交易对', '$coinCode/USDT', colorScheme),
SizedBox(height: AppSpacing.sm),
_dialogRow('委托价格', '$price USDT', colorScheme),
SizedBox(height: AppSpacing.sm),
_dialogRow('交易金额', '$amount USDT', colorScheme,
valueColor: actionColor),
SizedBox(height: AppSpacing.sm),
_dialogRow('交易数量', '$quantity $coinCode', colorScheme),
SizedBox(height: AppSpacing.lg),
Row(
children: [
Expanded(
child: NeonButton(
text: '取消',
type: NeonButtonType.outline,
onPressed: () => Navigator.of(context).pop(false),
height: 44,
showGlow: false,
),
),
SizedBox(width: AppSpacing.sm),
Expanded(
child: NeonButton(
text: '确认${isBuy ? '买入' : '卖出'}',
type: isBuy ? NeonButtonType.tertiary : NeonButtonType.error,
onPressed: () => Navigator.of(context).pop(true),
height: 44,
showGlow: true,
),
),
],
),
],
),
),
);
}
Widget _dialogRow(String label, String value, ColorScheme colorScheme,
{Color? valueColor}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
)),
Text(value,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: valueColor ?? colorScheme.onSurface,
)),
],
);
}
}
// ============================================
// 币种选择器 - 设计稿 Coin Selector Card
// card背景 + 圆角lg + border + padding:16
// 横向布局coinInfo(竖向 pair+name) + chevronDown
// ============================================
class _CoinSelector extends StatelessWidget {
final Coin? selectedCoin;
final List<Coin> coins;
final ValueChanged<Coin> onCoinSelected;
const _CoinSelector({
required this.selectedCoin,
required this.coins,
required this.onCoinSelected,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return GestureDetector(
onTap: () => _showCoinPicker(context),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 币种信息:交易对 + 名称
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
selectedCoin != null
? '${selectedCoin!.code}/USDT'
: '选择币种',
style: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 2),
Text(
selectedCoin?.name ?? '点击选择交易对',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
],
),
// 下拉箭头
Icon(LucideIcons.chevronDown,
size: 16, color: colorScheme.onSurfaceVariant),
],
),
),
);
}
void _showCoinPicker(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (ctx) => Container(
height: MediaQuery.of(ctx).size.height * 0.65,
decoration: BoxDecoration(
color: isDark
? colorScheme.surface
: colorScheme.surfaceContainerLowest,
borderRadius:
BorderRadius.vertical(top: Radius.circular(AppRadius.xxl)),
),
child: Column(
children: [
// 拖动指示器
Container(
margin: EdgeInsets.only(top: AppSpacing.sm),
width: 40,
height: 4,
decoration: BoxDecoration(
color: colorScheme.onSurfaceVariant.withOpacity(0.3),
borderRadius: BorderRadius.circular(2),
),
),
// 标题栏
Padding(
padding: EdgeInsets.all(AppSpacing.lg),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('选择币种',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
)),
GestureDetector(
onTap: () => Navigator.of(ctx).pop(),
child: Icon(LucideIcons.x,
color: colorScheme.onSurfaceVariant),
),
],
),
),
Divider(height: 1, color: colorScheme.outlineVariant.withOpacity(0.2)),
// 币种列表
Expanded(
child: ListView.builder(
padding: EdgeInsets.symmetric(vertical: AppSpacing.sm),
itemCount: coins.length,
itemBuilder: (listCtx, index) =>
_buildCoinItem(coins[index], context, listCtx),
),
),
],
),
),
);
}
Widget _buildCoinItem(
Coin coin, BuildContext context, BuildContext sheetContext) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final isSelected = selectedCoin?.code == coin.code;
final changeColor = coin.isUp
? AppColorScheme.getUpColor(isDark)
: AppColorScheme.getDownColor(isDark);
return GestureDetector(
onTap: () {
Navigator.of(sheetContext).pop();
onCoinSelected(coin);
},
child: Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.lg, vertical: AppSpacing.md),
color:
isSelected ? colorScheme.primary.withOpacity(0.1) : Colors.transparent,
child: Row(
children: [
_CoinAvatar(icon: coin.displayIcon),
SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 第一行:币种代码 + USDT + 价格 + 涨跌幅
Row(
children: [
Text(coin.code,
style: GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
)),
SizedBox(width: AppSpacing.xs),
Text('/USDT',
style: GoogleFonts.inter(
fontSize: 11,
color: colorScheme.onSurfaceVariant,
)),
const Spacer(),
Text('\$${coin.formattedPrice}',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
)),
SizedBox(width: AppSpacing.sm),
// 涨跌幅徽章
Container(
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: changeColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(coin.formattedChange,
style: GoogleFonts.inter(
fontSize: 11,
color: changeColor,
fontWeight: FontWeight.w600,
)),
),
if (isSelected) ...[
SizedBox(width: AppSpacing.sm),
Icon(LucideIcons.check,
size: 16, color: colorScheme.primary),
],
],
),
SizedBox(height: 3),
// 第二行:币种名称
Text(coin.name,
style: GoogleFonts.inter(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
)),
],
),
),
],
),
),
);
}
}
/// 币种头像
class _CoinAvatar extends StatelessWidget {
final String? icon;
const _CoinAvatar({this.icon});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(color: colorScheme.primary.withOpacity(0.2)),
),
child: Center(
child: Text(icon ?? '?',
style: TextStyle(
fontSize: 20,
color: colorScheme.primary,
fontWeight: FontWeight.bold,
)),
),
);
}
}
// ============================================
// 价格卡片 - 设计稿 Price Card
// card背景 + 圆角lg + border + padding:20 + gap:8
// 竖向布局:
// priceRow: 大号价格(32px bold) + 涨跌幅徽章(圆角sm涨绿背景)
// subtitle: "24h 变化"
// ============================================
class _PriceCard extends StatelessWidget {
final Coin coin;
const _PriceCard({required this.coin});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final isUp = coin.isUp;
final changeColor =
isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.getDownColor(isDark);
final changeBgColor = isUp
? AppColorScheme.getUpBackgroundColor(isDark)
: AppColorScheme.getDownBackgroundColor(isDark);
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 价格行:大号价格 + 涨跌幅徽章
Row(
children: [
Text(
coin.formattedPrice,
style: GoogleFonts.inter(
fontSize: 32,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
fontFeatures: [FontFeature.tabularFigures()],
),
),
const SizedBox(width: AppSpacing.sm),
// 涨跌幅徽章 - 圆角sm涨绿背景
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm, vertical: AppSpacing.xs),
decoration: BoxDecoration(
color: changeBgColor,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
coin.formattedChange,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
color: changeColor,
fontFeatures: [FontFeature.tabularFigures()],
),
),
),
],
),
const SizedBox(height: AppSpacing.sm),
// 副标题
Text(
'24h 变化',
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
],
),
);
}
}
/// 占位卡片
class _PlaceholderCard extends StatelessWidget {
final String message;
final ColorScheme colorScheme;
const _PlaceholderCard({required this.message, required this.colorScheme});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(AppSpacing.xl),
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Center(
child: Text(message,
style: GoogleFonts.inter(
color: colorScheme.onSurfaceVariant,
fontSize: 14,
)),
),
);
}
}
// ============================================
// 交易表单卡片 - 设计稿 Trade Form Card
// card背景 + 圆角lg + border + padding:20 + gap:16
// 竖向布局:
// Buy/Sell Toggle圆角mdclip横向两等宽按钮
// 金额label行"交易金额" + "USDT"
// 输入框bg-tertiary圆角md高48
// 可用余额文字
// 快捷比例按钮行25% 50% 75% 100%gap:8
// 计算数量行
// ============================================
class _TradeFormCard extends StatelessWidget {
final int tradeType;
final Coin? selectedCoin;
final TextEditingController amountController;
final String availableUsdt;
final String availableCoinQty;
final String calculatedQuantity;
final String maxAmount;
final ValueChanged<int> onTradeTypeChanged;
final VoidCallback onAmountChanged;
final ValueChanged<double> onFillPercent;
const _TradeFormCard({
required this.tradeType,
required this.selectedCoin,
required this.amountController,
required this.availableUsdt,
required this.availableCoinQty,
required this.calculatedQuantity,
required this.maxAmount,
required this.onTradeTypeChanged,
required this.onAmountChanged,
required this.onFillPercent,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final isBuy = tradeType == 0;
final actionColor = isBuy
? AppColorScheme.getUpColor(isDark)
: AppColorScheme.getDownColor(isDark);
// 设计稿中 card 背景色
final cardBgColor = isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerLowest;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: cardBgColor,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ---- 买入/卖出切换 ----
// 设计稿ClipRRect + 圆角md两等宽按钮
ClipRRect(
borderRadius: BorderRadius.circular(AppRadius.md),
child: Row(
children: [
// 买入按钮
Expanded(
child: GestureDetector(
onTap: () => onTradeTypeChanged(0),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
height: 40,
decoration: BoxDecoration(
color: isBuy
? AppColorScheme.buyButtonFill
: cardBgColor,
border: isBuy
? null
: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15)),
),
child: Center(
child: Text(
'买入',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: isBuy
? Colors.white
: colorScheme.onSurfaceVariant,
),
),
),
),
),
),
// 卖出按钮
Expanded(
child: GestureDetector(
onTap: () => onTradeTypeChanged(1),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
height: 40,
decoration: BoxDecoration(
color: !isBuy
? AppColorScheme.sellButtonFill
: cardBgColor,
border: !isBuy
? null
: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15)),
),
child: Center(
child: Text(
'卖出',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: !isBuy
? Colors.white
: colorScheme.onSurfaceVariant,
),
),
),
),
),
),
],
),
),
const SizedBox(height: AppSpacing.md + AppSpacing.sm),
// ---- 交易金额 label 行 ----
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('交易金额',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
)),
Text('USDT',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
)),
],
),
const SizedBox(height: AppSpacing.sm),
// ---- 金额输入框 ----
_AmountInput(
amountController: amountController,
maxAmount: maxAmount,
isBuy: isBuy,
actionColor: actionColor,
onChanged: onAmountChanged,
),
const SizedBox(height: AppSpacing.sm),
// ---- 可用余额 ----
Text(
isBuy
? '可用: $availableUsdt USDT'
: '可用: $availableCoinQty ${selectedCoin?.code ?? ""}',
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: AppSpacing.md),
// ---- 快捷比例按钮 25% 50% 75% 100% ----
// 设计稿gap:8圆角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,
)),
),
),
),
);
}
}
// ============================================
// CTA 交易按钮 - 设计稿 Buy Button
// profit-green底 / sell-red底圆角lg高48白字16px bold
// ============================================
class _TradeButton extends StatelessWidget {
final bool isBuy;
final String? coinCode;
final bool enabled;
final bool isLoading;
final VoidCallback onPressed;
const _TradeButton({
required this.isBuy,
required this.coinCode,
required this.enabled,
required this.isLoading,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final fillColor =
isBuy ? AppColorScheme.buyButtonFill : AppColorScheme.sellButtonFill;
return GestureDetector(
onTap: enabled ? onPressed : null,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: 48,
decoration: BoxDecoration(
color: enabled ? fillColor : colorScheme.onSurface.withOpacity(0.08),
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Center(
child: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(
'${isBuy ? '买入' : '卖出'} ${coinCode ?? ""}',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w700,
color: enabled
? Colors.white
: colorScheme.onSurface.withOpacity(0.3),
),
),
),
),
);
}
}
// ============================================
// 金额输入框(含超额提示)
// 设计稿bg-tertiary圆角md高48
// ============================================
class _AmountInput extends StatefulWidget {
final TextEditingController amountController;
final String maxAmount;
final bool isBuy;
final Color actionColor;
final VoidCallback onChanged;
const _AmountInput({
required this.amountController,
required this.maxAmount,
required this.isBuy,
required this.actionColor,
required this.onChanged,
});
@override
State<_AmountInput> createState() => _AmountInputState();
}
class _AmountInputState extends State<_AmountInput> {
bool _isExceeded = false;
void _checkLimit() {
final input = double.tryParse(widget.amountController.text) ?? 0;
final max = double.tryParse(widget.maxAmount) ?? 0;
final exceeded = widget.isBuy && input > max && max > 0 && input > 0;
if (exceeded != _isExceeded) {
setState(() => _isExceeded = exceeded);
}
widget.onChanged();
}
@override
void initState() {
super.initState();
widget.amountController.addListener(_checkLimit);
}
@override
void dispose() {
widget.amountController.removeListener(_checkLimit);
super.dispose();
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final warningColor = AppColorScheme.warning;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 48,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest.withOpacity(0.3),
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: TextField(
controller: widget.amountController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
onChanged: (_) => _checkLimit(),
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.normal,
color: colorScheme.onSurface,
fontFeatures: [FontFeature.tabularFigures()],
),
decoration: InputDecoration(
hintText: '请输入金额',
hintStyle: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant.withOpacity(0.5),
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
),
),
),
),
if (_isExceeded)
Padding(
padding: EdgeInsets.only(top: AppSpacing.xs),
child: Row(
children: [
Icon(Icons.error_outline, size: 13, color: warningColor),
SizedBox(width: 4),
Text(
'超出可用USDT余额',
style: GoogleFonts.inter(
fontSize: 11,
color: warningColor,
),
),
],
),
),
],
);
}
}