Files
monisuo/flutter_monisuo/lib/ui/pages/asset/asset_page.dart
sion123 d8cd38c4de feat(theme): update color scheme with new Slate theme and improved surface hierarchy
Updated the app's color scheme to implement a new "Slate" theme with refined dark and light variants. Changed background colors from #0A0E14 to #0B1120 for dark mode and updated surface layer colors to follow Material Design 3 specifications. Modified text colors and outline variants for better contrast and accessibility. Updated font sizes in transaction details screen from 11px to 12px for improved readability.
2026-04-05 22:24:04 +08:00

1259 lines
42 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:bot_toast/bot_toast.dart';
import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../core/utils/toast_utils.dart';
import '../../../core/event/app_event_bus.dart';
import '../../../data/models/account_models.dart';
import '../../../providers/asset_provider.dart';
import '../../../providers/auth_provider.dart';
import '../../shared/ui_constants.dart';
import '../../components/glass_panel.dart';
import '../../components/neon_glow.dart';
import '../orders/fund_orders_page.dart';
import 'transfer_page.dart';
/// 资产页面 - Matching .pen design spec (CMcqs)
class AssetPage extends StatefulWidget {
const AssetPage({super.key});
@override
State<AssetPage> createState() => _AssetPageState();
}
class _AssetPageState extends State<AssetPage> with AutomaticKeepAliveClientMixin {
int _activeTab = 0;
StreamSubscription<AppEvent>? _eventSub;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadData();
_listenEvents();
});
}
@override
void dispose() {
_eventSub?.cancel();
super.dispose();
}
void _listenEvents() {
final eventBus = context.read<AppEventBus>();
_eventSub = eventBus.on(AppEventType.assetChanged, (_) {
if (mounted) {
context.read<AssetProvider>().refreshAll(force: true);
}
});
}
void _loadData() {
context.read<AssetProvider>().refreshAll(force: true);
}
@override
Widget build(BuildContext context) {
super.build(context);
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colorScheme.background,
body: Consumer<AssetProvider>(
builder: (context, provider, _) {
return RefreshIndicator(
onRefresh: () => provider.refreshAll(force: true),
color: colorScheme.primary,
backgroundColor: colorScheme.surfaceContainerHighest,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.fromLTRB(AppSpacing.md, AppSpacing.md + 8, AppSpacing.md, 32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Page title: "资产" 22px bold — matching .pen titleFrame padding [16,0,8,0]
Padding(
padding: const EdgeInsets.only(top: 16, bottom: 8),
child: Text(
'资产',
style: GoogleFonts.inter(
fontSize: 22,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
),
const SizedBox(height: AppSpacing.sm),
// Account tab switcher — pill-style matching .pen UE6xC
_AccountTabSwitcher(
selectedIndex: _activeTab,
onChanged: (index) => setState(() => _activeTab = index),
),
const SizedBox(height: AppSpacing.md),
// Balance card — matching .pen 59637 (cornerRadius lg, stroke, padding 20, gap 12)
_BalanceCard(
provider: provider,
activeTab: _activeTab,
),
const SizedBox(height: AppSpacing.md),
// Action buttons row — matching .pen pIpHe (gap 12)
_ActionButtonsRow(
onDeposit: () => _showDepositDialog(context),
onWithdraw: () => _showWithdrawDialog(context, provider.fundAccount?.balance),
onTransfer: () => _navigateToTransfer(context),
),
const SizedBox(height: AppSpacing.md),
// Records link row — matching .pen fLHtq (cornerRadius lg, padding [14,16], stroke)
_RecordsLinkRow(
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const FundOrdersPage()),
),
),
const SizedBox(height: AppSpacing.md),
// Holdings section — matching .pen th9BG + 6X6tC
_HoldingsSection(holdings: _activeTab == 1 ? provider.holdings : []),
],
),
),
);
},
),
);
}
}
// ============================================
// Account Tab Switcher — .pen node UE6xC
// height: 40, padding: 3, cornerRadius: md, fill: $bg-tertiary
// activeTab: fill $bg-primary, cornerRadius sm, shadow blur 3, color #0000000D, offset y 1
// activeTabText: 14px, fontWeight 600, fill $text-primary
// inactiveTabText: 14px, fontWeight 500, fill $text-secondary
// ============================================
class _AccountTabSwitcher extends StatelessWidget {
final int selectedIndex;
final ValueChanged<int> onChanged;
const _AccountTabSwitcher({
required this.selectedIndex,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
height: 40,
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: isDark ? colorScheme.surfaceContainerHighest : colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: Row(
children: [
_buildTab(
context: context,
label: '资金账户',
isSelected: selectedIndex == 0,
onTap: () => onChanged(0),
isDark: isDark,
),
_buildTab(
context: context,
label: '交易账户',
isSelected: selectedIndex == 1,
onTap: () => onChanged(1),
isDark: isDark,
),
],
),
);
}
Widget _buildTab({
required BuildContext context,
required String label,
required bool isSelected,
required VoidCallback onTap,
required bool isDark,
}) {
final colorScheme = Theme.of(context).colorScheme;
return Expanded(
child: GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: isSelected
? colorScheme.surface
: Colors.transparent,
borderRadius: BorderRadius.circular(AppRadius.sm),
boxShadow: isSelected
? [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 3,
offset: const Offset(0, 1),
),
]
: null,
),
alignment: Alignment.center,
child: Text(
label,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected ? colorScheme.onSurface : colorScheme.onSurfaceVariant,
),
),
),
),
);
}
}
// ============================================
// Balance Card — .pen node 59637
// cornerRadius: lg, fill: $surface-card, padding: 20, stroke: $border-default 1px, gap: 12
// balLabel: "USDT 余额" 12px normal $text-secondary
// balAmount: "25,680.50" 28px w700 $text-primary
// balSubRow: "≈ $25,680.50 USD" 12px normal $text-muted
// ============================================
class _BalanceCard extends StatelessWidget {
final AssetProvider provider;
final int activeTab;
const _BalanceCard({
required this.provider,
required this.activeTab,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final displayBalance = activeTab == 0
? (provider.fundAccount?.balance ?? provider.overview?.fundBalance ?? '0.00')
: _calculateTradeTotal();
return GlassPanel(
padding: const EdgeInsets.all(20),
borderRadius: BorderRadius.circular(AppRadius.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'USDT 余额',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
Text(
_formatBalance(displayBalance),
style: GoogleFonts.inter(
fontSize: 28,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 12),
Text(
'\u2248 \$${_formatBalance(displayBalance)} USD',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: isDark ? AppColorScheme.darkOnSurfaceMuted : colorScheme.onSurfaceVariant,
),
),
],
),
);
}
String _calculateTradeTotal() {
double total = 0;
for (var h in provider.holdings) {
total += double.tryParse(h.currentValue?.toString() ?? '0') ?? 0;
}
return total.toStringAsFixed(2);
}
String _formatBalance(String balance) {
final d = double.tryParse(balance) ?? 0;
return d.toStringAsFixed(2).replaceAllMapped(
RegExp(r'\B(?=(\d{3})+(?!\d))'),
(Match m) => ',',
);
}
}
// ============================================
// Action Buttons Row — .pen node pIpHe
// gap: 12, three buttons evenly distributed
// Each button: circle 48x48 fill $bg-tertiary, cornerRadius 24
// icon: 20px $accent-primary (lucide: arrow-up-right / arrow-down-left / repeat)
// label: 12px w500 $text-secondary
// ============================================
class _ActionButtonsRow extends StatelessWidget {
final VoidCallback onDeposit;
final VoidCallback onWithdraw;
final VoidCallback onTransfer;
const _ActionButtonsRow({
required this.onDeposit,
required this.onWithdraw,
required this.onTransfer,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final accentColor = isDark ? colorScheme.secondary : colorScheme.primary;
final bgColor = isDark ? colorScheme.surfaceContainerHighest : colorScheme.surfaceContainerHigh;
return Row(
children: [
_ActionButton(
icon: LucideIcons.arrowUpRight,
label: '充值',
accentColor: accentColor,
bgColor: bgColor,
onTap: onDeposit,
),
const SizedBox(width: 12),
_ActionButton(
icon: LucideIcons.arrowDownLeft,
label: '提现',
accentColor: accentColor,
bgColor: bgColor,
onTap: onWithdraw,
),
const SizedBox(width: 12),
_ActionButton(
icon: LucideIcons.repeat,
label: '划转',
accentColor: accentColor,
bgColor: bgColor,
onTap: onTransfer,
),
],
);
}
}
/// Single action button — matching .pen btn1/btn2/btn3
class _ActionButton extends StatelessWidget {
final IconData icon;
final String label;
final Color accentColor;
final Color bgColor;
final VoidCallback onTap;
const _ActionButton({
required this.icon,
required this.label,
required this.accentColor,
required this.bgColor,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Column(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: bgColor,
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Icon(
icon,
size: 20,
color: accentColor,
),
),
const SizedBox(height: 6),
Text(
label,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
);
}
}
// ============================================
// Records Link Row — .pen node fLHtq
// cornerRadius: lg, fill: $surface-card, padding: [14, 16], stroke: $border-default 1px
// recordsText: "充提记录" 14px w500 $text-primary
// recordsChevron: lucide chevron-right 16px $text-muted
// ============================================
class _RecordsLinkRow extends StatelessWidget {
final VoidCallback onTap;
const _RecordsLinkRow({required this.onTap});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final mutedColor = isDark ? AppColorScheme.darkOnSurfaceMuted : colorScheme.onSurfaceVariant;
return GestureDetector(
onTap: onTap,
child: GlassPanel(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: 14),
borderRadius: BorderRadius.circular(AppRadius.lg),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'充提记录',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
),
),
Icon(
LucideIcons.chevronRight,
size: 16,
color: mutedColor,
),
],
),
),
);
}
}
// ============================================
// Holdings Section — .pen nodes th9BG (header) + 6X6tC (card)
// Holdings Header: "交易账户持仓" 16px w600 $text-primary | "查看全部 >" 12px normal $text-secondary
// Holdings Card: cornerRadius lg, fill $surface-card, stroke $border-default 1px
// Each row: padding [14, 16], justifyContent space_between
// Left: avatar (36x36, cornerRadius 18, fill $accent-light) + coin info (gap 2)
// AvatarText: coinCode first letter, 14px w700 $accent-primary
// CoinName: 14px w600 $text-primary
// CoinAmt: 12px normal $text-secondary
// Right: value + pnl (gap 2, alignItems end)
// Value: 13px w500 $text-primary
// Pnl: 12px w500, $profit-green / $loss-red
// Divider: $border-default height 1 opacity 0.5
// ============================================
class _HoldingsSection extends StatelessWidget {
final List holdings;
const _HoldingsSection({required this.holdings});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Column(
children: [
// Header row: "交易账户持仓" + "查看全部 >"
Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'交易账户持仓',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
Text(
'查看全部 >',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
// Holdings card — uses real provider.holdings data
if (holdings.isEmpty)
Padding(
padding: const EdgeInsets.all(AppSpacing.xl),
child: Text(
'暂无持仓',
style: GoogleFonts.inter(
fontSize: 13,
color: colorScheme.onSurfaceVariant,
),
),
)
else
GlassPanel(
padding: EdgeInsets.zero,
borderRadius: BorderRadius.circular(AppRadius.lg),
child: Column(
children: List.generate(holdings.length, (index) {
final h = holdings[index] as AccountTrade;
final isProfit = h.profitRate >= 0;
return Column(
children: [
_HoldingRow(
coinCode: h.coinCode,
quantity: double.tryParse(h.quantity)?.toStringAsFixed(4) ?? h.quantity,
value: '${double.tryParse(h.currentValue)?.toStringAsFixed(2) ?? h.currentValue} USDT',
profitRate: '${isProfit ? '+' : ''}${h.profitRate.toStringAsFixed(2)}%',
isProfit: isProfit,
),
if (index < holdings.length - 1) const _HoldingDivider(),
],
);
}),
),
),
],
);
}
}
/// Divider between holding rows — .pen node BCCbR / yejhE
/// fill: $border-default, height: 1, opacity: 0.5
class _HoldingDivider extends StatelessWidget {
const _HoldingDivider();
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
height: 1,
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
);
}
}
/// Holding row — matching .pen nodes dAt4j / eK6vq / jiSUK
/// padding [14, 16], space_between layout
/// Left: avatar circle (36x36, radius 18, fill $accent-light) + coin info (gap 2)
/// Right: value + pnl (gap 2, align end)
class _HoldingRow extends StatelessWidget {
final String coinCode;
final String quantity;
final String value;
final String profitRate;
final bool isProfit;
const _HoldingRow({
required this.coinCode,
required this.quantity,
required this.value,
required this.profitRate,
required this.isProfit,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final accentColor = isDark ? colorScheme.secondary : colorScheme.primary;
final accentBgColor = accentColor.withValues(alpha: 0.1);
final profitColor = isProfit ? AppColorScheme.getUpColor(isDark) : AppColorScheme.getDownColor(isDark);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: 14),
child: Row(
children: [
// Avatar circle with first letter — .pen SJNDJ/EjSIN/3GQ5M
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: accentBgColor,
borderRadius: BorderRadius.circular(18),
),
alignment: Alignment.center,
child: Text(
coinCode.substring(0, 1),
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w700,
color: accentColor,
),
),
),
const SizedBox(width: 10),
// Coin name + quantity — .pen fivxJ/Kxv3d/5CsoQ
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
coinCode,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 2),
Text(
quantity,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
// Value + profit rate — .pen vYJsU/2nLAg/IlWck
Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Text(
value,
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 2),
Text(
profitRate,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
color: profitColor,
),
),
],
),
],
),
);
}
}
// ============================================
// Dialogs — kept from original with style updates
// ============================================
void _showDepositDialog(BuildContext context) {
final amountController = TextEditingController();
final formKey = GlobalKey<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,
),
),
],
),
],
),
),
),
);
}
class _InfoRow extends StatelessWidget {
final String label;
final String value;
final bool isBold;
const _InfoRow({required this.label, required this.value, this.isBold = false});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: GoogleFonts.inter(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
Text(
value,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
color: colorScheme.onSurface,
),
),
],
);
}
}
class _WalletAddressCard extends StatelessWidget {
final String address;
final String network;
const _WalletAddressCard({required this.address, required this.network});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(
color: colorScheme.outlineVariant.withValues(alpha: 0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
address,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
),
GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: address));
ToastUtils.show('地址已复制到剪贴板');
},
child: Container(
padding: const EdgeInsets.all(AppSpacing.xs),
decoration: BoxDecoration(
color: colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Icon(
LucideIcons.copy,
size: 16,
color: colorScheme.primary,
),
),
),
],
),
const SizedBox(height: AppSpacing.sm),
Text(
'网络: $network',
style: GoogleFonts.inter(
fontSize: 11,
color: colorScheme.onSurfaceVariant,
),
),
],
),
);
}
}
void _showWithdrawDialog(BuildContext context, String? balance) {
final amountController = TextEditingController();
final addressController = TextEditingController();
final contactController = TextEditingController();
final formKey = GlobalKey<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 _navigateToTransfer(BuildContext context) async {
final result = await Navigator.push<bool>(
context,
MaterialPageRoute(builder: (_) => const TransferPage()),
);
if (result == true && context.mounted) {
context.read<AssetProvider>().refreshAll(force: true);
}
}
void _showResultDialog(BuildContext context, String title, String? message) {
final colorScheme = Theme.of(context).colorScheme;
showShadDialog(
context: context,
builder: (ctx) => Dialog(
backgroundColor: Colors.transparent,
child: GlassPanel(
borderRadius: BorderRadius.circular(AppRadius.lg),
padding: 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,
),
),
],
),
),
),
);
}