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.
This commit is contained in:
2026-04-05 22:24:04 +08:00
parent e2624b845a
commit d8cd38c4de
17 changed files with 3980 additions and 2922 deletions

View File

@@ -1,155 +0,0 @@
import 'package:flutter/material.dart';
/// 应用颜色常量 - 统一的颜色系统
///
/// 设计原则:
/// 1. 语义化命名 - 颜色按用途命名,而非外观
/// 2. 对比度保证 - 文字与背景对比度 >= 4.5:1 (WCAG AA)
/// 3. 一致性 - 同一语义用途使用同一颜色
class AppColors {
AppColors._();
// ============================================
// 品牌色 (Brand Colors) - 专业蓝
// ============================================
/// 主品牌色 - 专业蓝,代表信任与稳定
static const Color primary = Color(0xFF2563EB);
/// 主品牌色浅色变体
static const Color primaryLight = Color(0xFF3B82F6);
/// 主品牌色深色变体
static const Color primaryDark = Color(0xFF1D4ED8);
/// 主品牌色渐变
static const LinearGradient primaryGradient = LinearGradient(
colors: [primary, primaryDark],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
// ============================================
// 语义色 (Semantic Colors)
// ============================================
/// 成功/上涨 - 绿色系
static const Color success = Color(0xFF00C853);
static const Color up = success;
/// 警告 - 橙色系
static const Color warning = Color(0xFFFF9800);
/// 错误/下跌 - 红色系
static const Color error = Color(0xFFFF5252);
static const Color down = error;
/// 信息 - 蓝色系
static const Color info = Color(0xFF2196F3);
/// 交易类型色
static const Color deposit = success;
static const Color withdraw = warning;
static const Color trade = info;
// ============================================
// 深色主题背景色 (Dark Theme Backgrounds)
// ============================================
/// 主背景色 - 最深的背景
static const Color background = Color(0xFF0F0F1A);
/// 卡片背景色
static const Color cardBackground = Color(0xFF1A1A2E);
/// Scaffold 背景色
static const Color scaffoldBackground = background;
/// 表面色 - 用于弹出层、对话框
static const Color surface = Color(0xFF16213E);
/// 悬停状态背景
static const Color hoverBackground = Color(0xFF252542);
// ============================================
// 文字颜色 (Text Colors)
// 对比度均 >= 4.5:1 (基于深色背景)
// ============================================
/// 主要文字 - 白色,对比度 21:1
static const Color textPrimary = Color(0xFFFFFFFF);
/// 次要文字 - 浅灰色,对比度 ~10:1
static const Color textSecondary = Color(0xFFB0B0B0);
/// 提示文字 - 中灰色,对比度 ~5:1
static const Color textHint = Color(0xFF808080);
/// 禁用文字 - 深灰色,对比度 ~3:1
static const Color textDisabled = Color(0xFF4D4D4D);
/// 链接文字
static const Color textLink = primary;
// ============================================
// 边框和分割线 (Borders & Dividers)
// ============================================
/// 默认边框色
static const Color border = Color(0xFF2A2A45);
/// 分割线颜色
static const Color divider = border;
/// 焦点边框色
static const Color focusBorder = primary;
/// 输入框边框色
static const Color inputBorder = Color(0xFF3A3A55);
// ============================================
// 输入框颜色 (Input Colors)
// ============================================
/// 输入框背景
static const Color inputBackground = cardBackground;
/// 输入框焦点边框
static const Color inputFocusBorder = primary;
// ============================================
// 按钮渐变 (Button Gradients)
// ============================================
/// 买入按钮渐变
static const LinearGradient buyGradient = LinearGradient(
colors: [Color(0xFF00C853), Color(0xFF00A844)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// 卖出按钮渐变
static const LinearGradient sellGradient = LinearGradient(
colors: [Color(0xFFFF5252), Color(0xFFD32F2F)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
// ============================================
// 渐变色 (Gradient Colors)
// ============================================
/// 资产卡片渐变色
static const List<Color> gradientColors = [primary, primaryDark];
// ============================================
// 工具方法 (Utility Methods)
// ============================================
/// 获取涨跌颜色
static Color getChangeColor(bool isUp) => isUp ? up : down;
/// 获取涨跌背景色(带透明度)
static Color getChangeBackgroundColor(bool isUp) =>
isUp ? up.withValues(alpha: 0.15) : down.withValues(alpha: 0.15);
}

View File

@@ -20,23 +20,23 @@ class AppColorScheme {
AppColorScheme._();
// ============================================
// 深色主题 - "黑金传奇" (Material Design 3)
// 背景 #0A0E14 | 主色 #1E3A8A | 强调 #D4AF37
// 深色主题 - "Slate Dark" (Material Design 3)
// 背景 #0B1120 | 主色 #1E3A8A | 强调 #D4AF37
// ============================================
/// 背景基色 - 深邃
static const Color darkBackground = Color(0xFF0A0E14);
/// 背景基色 - Slate 深蓝
static const Color darkBackground = Color(0xFF0B1120);
/// Surface 层次 (从低到高) - Material Design 3 规范
static const Color darkSurfaceDim = Color(0xFF0A0E14);
static const Color darkSurfaceDim = Color(0xFF0B1120);
static const Color darkSurfaceLowest = Color(0xFF000000);
static const Color darkSurfaceLow = Color(0xFF0F1219);
static const Color darkSurface = Color(0xFF0A0E14);
static const Color darkSurfaceContainer = Color(0xFF151921);
static const Color darkSurfaceContainerHigh = Color(0xFF1B1F28);
static const Color darkSurfaceContainerHighest = Color(0xFF21252F);
static const Color darkSurfaceBright = Color(0xFF272B35);
static const Color darkSurfaceVariant = Color(0xFF21252F);
static const Color darkSurfaceLow = Color(0xFF0F172A);
static const Color darkSurface = Color(0xFF0B1120);
static const Color darkSurfaceContainer = Color(0xFF0F172A);
static const Color darkSurfaceContainerHigh = Color(0xFF1E293B);
static const Color darkSurfaceContainerHighest = Color(0xFF253349);
static const Color darkSurfaceBright = Color(0xFF334155);
static const Color darkSurfaceVariant = Color(0xFF1E293B);
/// 兼容旧名称
static const Color darkSurfaceContainerLowest = darkSurfaceLowest;
@@ -45,8 +45,8 @@ class AppColorScheme {
static const Color darkSurfaceHighest = darkSurfaceContainerHighest;
/// Ghost Border
static const Color darkOutline = Color(0xFF73757d);
static const Color darkOutlineVariant = Color(0xFF45484f);
static const Color darkOutline = Color(0xFF64748B);
static const Color darkOutlineVariant = Color(0xFF334155);
/// Primary - 专业蓝 #1E3A8A (主要交互)
static const Color darkPrimary = Color(0xFF1E3A8A);
@@ -89,38 +89,38 @@ class AppColorScheme {
static const Color darkOnErrorContainer = Color(0xFFffa8a3);
/// 文本色
static const Color darkOnSurface = Color(0xFFecedf6);
static const Color darkOnSurfaceVariant = Color(0xFFa9abb3);
static const Color darkOnSurfaceMuted = Color(0xFF6b6d75);
static const Color darkOnBackground = Color(0xFFecedf6);
static const Color darkInverseSurface = Color(0xFFf9f9ff);
static const Color darkInverseOnSurface = Color(0xFF52555c);
static const Color darkOnSurface = Color(0xFFF8FAFC);
static const Color darkOnSurfaceVariant = Color(0xFF94A3B8);
static const Color darkOnSurfaceMuted = Color(0xFF64748B);
static const Color darkOnBackground = Color(0xFFF8FAFC);
static const Color darkInverseSurface = Color(0xFFF8FAFC);
static const Color darkInverseOnSurface = Color(0xFF475569);
static const Color darkInversePrimary = Color(0xFF1E40AF);
static const Color darkSurfaceTint = Color(0xFFD4AF37);
// ============================================
// 浅色主题 - "白金殿堂"
// 背景 #FAFAFA | 主色 #1E40AF | 强调 #FFD700
// 浅色主题 - "Slate Light" (Material Design 3)
// 背景 #F8FAFC | 主色 #1E40AF | 强调 #D4AF37
// ============================================
/// 背景基色 - 纯净白
static const Color lightBackground = Color(0xFFFAFAFA);
/// 背景基色 - Slate 50
static const Color lightBackground = Color(0xFFF8FAFC);
/// Surface 层次 (从低到高)
static const Color lightSurfaceLowest = Color(0xFFffffff);
static const Color lightSurfaceLow = Color(0xFFF5F5F5);
static const Color lightSurface = Color(0xFFFAFAFA);
static const Color lightSurfaceHigh = Color(0xFFF0F0F0);
static const Color lightSurfaceHighest = Color(0xFFE8E8E8);
static const Color lightSurfaceLowest = Color(0xFFFFFFFF);
static const Color lightSurfaceLow = Color(0xFFF1F5F9);
static const Color lightSurface = Color(0xFFF8FAFC);
static const Color lightSurfaceHigh = Color(0xFFE2E8F0);
static const Color lightSurfaceHighest = Color(0xFFCBD5E1);
/// Ghost Border
static const Color lightOutlineVariant = Color(0xFFD0D0D0);
/// Ghost Border - Slate 300
static const Color lightOutlineVariant = Color(0xFFCBD5E1);
/// Primary - 专业蓝 #1E40AF (主要交互)
static const Color lightPrimary = Color(0xFF1E40AF);
static const Color lightPrimaryContainer = Color(0xFF3B82F6);
/// Secondary - 亮金 #FFD700 (白金强调色)
/// Secondary - 亮金 #D4AF37 (白金强调色)
static const Color lightSecondary = Color(0xFFD4AF37);
static const Color lightSecondaryContainer = Color(0xFFFFE44D);
@@ -128,10 +128,10 @@ class AppColorScheme {
static const Color lightTertiary = Color(0xFF00875A);
static const Color lightTertiaryContainer = Color(0xFFd4f5e9);
/// 文本色
static const Color lightOnSurface = Color(0xFF1A1A1A);
static const Color lightOnSurfaceVariant = Color(0xFF5a5d60);
static const Color lightOnSurfaceMuted = Color(0xFF8a8d90);
/// 文本色 - Slate
static const Color lightOnSurface = Color(0xFF0F172A);
static const Color lightOnSurfaceVariant = Color(0xFF475569);
static const Color lightOnSurfaceMuted = Color(0xFF94A3B8);
// ============================================
// Glass Panel 毛玻璃效果颜色
@@ -369,19 +369,19 @@ class AppColorScheme {
// ============================================
/// 浅色主题 Error 色
static const Color lightError = Color(0xFFd7383b);
static const Color lightError = Color(0xFFDC2626);
static const Color lightOnError = Color(0xFFFFFFFF);
/// 浅色主题 Outline 色
static const Color lightOutline = Color(0xFF73757d);
/// 浅色主题 Outline 色 - Slate 500
static const Color lightOutline = Color(0xFF64748B);
/// 浅色主题 Surface 色 (扩展)
static const Color lightSurfaceBright = Color(0xFFffffff);
static const Color lightSurfaceDim = Color(0xFFF0F0F0);
static const Color lightSurfaceVariant = Color(0xFFF0F0F0);
static const Color lightSurfaceContainer = Color(0xFFF5F5F5);
static const Color lightSurfaceContainerHigh = Color(0xFFF0F0F0);
static const Color lightSurfaceContainerHighest = Color(0xFFE8E8E8);
/// 浅色主题 Surface 色 (扩展) - Slate 系列
static const Color lightSurfaceBright = Color(0xFFFFFFFF);
static const Color lightSurfaceDim = Color(0xFFE2E8F0);
static const Color lightSurfaceVariant = Color(0xFFE2E8F0);
static const Color lightSurfaceContainer = Color(0xFFF1F5F9);
static const Color lightSurfaceContainerHigh = Color(0xFFE2E8F0);
static const Color lightSurfaceContainerHighest = Color(0xFFCBD5E1);
static ColorScheme get lightMaterial => ColorScheme.light(
primary: lightPrimary,
@@ -419,62 +419,44 @@ class AppColorScheme {
);
// ============================================
// 兼容性常量 (已废弃,保留向后兼容)
// 兼容性别名(替代 theme/app_colors.dart
// 映射旧名到新系统,避免 breaking change
// ============================================
@Deprecated('Use darkPrimary instead')
static const Color primaryDark = darkPrimary;
// 背景色
static const Color background = darkBackground;
static const Color cardBackground = darkSurfaceContainer;
static const Color inputBackground = darkSurfaceContainerHigh;
static const Color scaffoldBackground = darkBackground;
static const Color modalBackground = darkSurfaceContainerHigh;
static const Color hoverBackground = darkSurfaceBright;
@Deprecated('Use lightPrimary instead')
static const Color primaryLight = lightPrimary;
// 文字色
static const Color textPrimary = darkOnSurface;
static const Color textSecondary = darkOnSurfaceVariant;
static const Color textHint = darkOnSurfaceMuted;
static const Color textDisabled = darkInverseSurface;
static const Color textLink = darkPrimary;
@Deprecated('Use darkBackground instead')
static const Color _darkBackground = darkBackground;
// 边框色
static const Color border = darkOutlineVariant;
static const Color divider = darkSurfaceContainer;
static const Color inputBorder = darkOnSurfaceMuted;
static const Color inputFocusBorder = darkPrimary;
static const Color focusBorder = darkPrimary;
@Deprecated('Use darkSurfaceContainer instead')
static const Color _darkCardBackground = darkSurfaceContainer;
// 交易类型色
static const Color deposit = up;
static const Color withdraw = warning;
static const Color trade = info;
@Deprecated('Use darkSurfaceContainerHigh instead')
static const Color _darkSecondary = darkSurfaceContainerHigh;
@Deprecated('Use darkSurfaceContainerHigh instead')
static const Color _darkMuted = darkSurfaceContainerHigh;
@Deprecated('Use darkOutlineVariant instead')
static const Color _darkBorder = darkOutlineVariant;
@Deprecated('Use darkOnSurface instead')
static const Color _darkTextPrimary = darkOnSurface;
@Deprecated('Use darkOnSurfaceVariant instead')
static const Color _darkTextSecondary = darkOnSurfaceVariant;
@Deprecated('Use darkOnSurfaceMuted instead')
static const Color _darkTextHint = darkOnSurfaceMuted;
@Deprecated('Use lightBackground instead')
static const Color _lightBackground = lightBackground;
@Deprecated('Use lightSurfaceLowest instead')
static const Color _lightCardBackground = lightSurfaceLowest;
@Deprecated('Use lightSurfaceHigh instead')
static const Color _lightSecondary = lightSurfaceHigh;
@Deprecated('Use lightSurfaceHigh instead')
static const Color _lightMuted = lightSurfaceHigh;
@Deprecated('Use lightOutlineVariant instead')
static const Color _lightBorder = lightOutlineVariant;
@Deprecated('Use lightOnSurface instead')
static const Color _lightTextPrimary = lightOnSurface;
@Deprecated('Use lightOnSurfaceVariant instead')
static const Color _lightTextSecondary = lightOnSurfaceVariant;
@Deprecated('Use lightOnSurfaceMuted instead')
static const Color _lightTextHint = lightOnSurfaceMuted;
// 旧渐变
static const List<Color> gradientColors = [darkPrimary, darkPrimaryContainer];
static const LinearGradient primaryGradient = LinearGradient(
colors: [darkPrimary, darkPrimaryContainer],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
}
/// 创建 Shadcn 深色主题

View File

@@ -1,145 +0,0 @@
import 'package:flutter/material.dart';
/// 数字货币应用颜色系统
///
/// 设计原则:
/// 1. 所有文字与背景对比度 >= 4.5:1 (WCAG AA)
/// 2. 涨跌色使用国际通用标准 (绿涨红跌)
/// 3. 背景色层次分明,易于区分
/// 4. 杜绝文字和背景颜色一样无法区分的情况
class AppColors {
AppColors._();
// ============================================
// 品牌色 (Brand Colors) - 专业蓝
// ============================================
/// 主色 - 专业蓝,代表信任与稳定
static const Color primary = Color(0xFF2563EB);
static const Color primaryLight = Color(0xFF3B82F6);
static const Color primaryDark = Color(0xFF1D4ED8);
/// 主色渐变 - 用于卡片、按钮等
static const LinearGradient primaryGradient = LinearGradient(
colors: [primary, primaryDark],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
// ============================================
// 交易色 (Trading Colors)
// ============================================
/// 涨/买入 - 标准绿色 (国际通用)
static const Color up = Color(0xFF00C853);
/// 跌/卖出 - 标准红色 (国际通用)
static const Color down = Color(0xFFFF5252);
/// 买入按钮渐变
static const LinearGradient buyGradient = LinearGradient(
colors: [Color(0xFF00C853), Color(0xFF00A844)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// 卖出按钮渐变
static const LinearGradient sellGradient = LinearGradient(
colors: [Color(0xFFFF5252), Color(0xFFD32F2F)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
// ============================================
// 功能色 (Semantic Colors)
// ============================================
/// 成功
static const Color success = Color(0xFF00C853);
/// 警告
static const Color warning = Color(0xFFFF9800);
/// 错误
static const Color error = Color(0xFFFF5252);
/// 信息
static const Color info = Color(0xFF2196F3);
/// 充值
static const Color deposit = Color(0xFF00C853);
/// 提现
static const Color withdraw = Color(0xFFFF9800);
/// 划转/交易
static const Color trade = Color(0xFF2196F3);
// ============================================
// 背景色 (Dark Theme Backgrounds)
// ============================================
/// 页面背景 - 最深
static const Color background = Color(0xFF0F0F1A);
/// 卡片背景 - 中等深度
static const Color cardBackground = Color(0xFF1A1A2E);
/// 输入框背景 - 稍浅
static const Color inputBackground = Color(0xFF16213E);
/// Scaffold 背景 (兼容旧代码)
static const Color scaffoldBackground = Color(0xFF0F0F1A);
/// 模态框背景
static const Color modalBackground = Color(0xFF1E1E32);
// ============================================
// 文字颜色 (Text Colors)
// ============================================
/// 主要文字 - 白色,对比度 21:1
static const Color textPrimary = Color(0xFFFFFFFF);
/// 次要文字 - 浅灰蓝,对比度约 8:1
static const Color textSecondary = Color(0xFFB0B0C0);
/// 提示文字 - 中灰,对比度约 4.7:1
static const Color textHint = Color(0xFF6B6B80);
/// 禁用文字 - 暗灰
static const Color textDisabled = Color(0xFF4A4A5A);
/// 链接文字 - 品牌蓝
static const Color textLink = Color(0xFF2563EB);
// ============================================
// 边框与分割线 (Borders & Dividers)
// ============================================
/// 边框 - 低透明度白色
static const Color border = Color(0x14FFFFFF); // 8% white
/// 分割线 - 更低透明度
static const Color divider = Color(0x0FFFFFFF); // 6% white
/// 输入框边框
static const Color inputBorder = Color(0x1AFFFFFF); // 10% white
/// 输入框聚焦边框 - 品牌蓝
static const Color inputFocusBorder = Color(0xFF2563EB);
// ============================================
// 便捷方法
// ============================================
/// 根据涨跌获取颜色
static Color getChangeColor(bool isUp) => isUp ? up : down;
/// 获取带透明度的涨跌背景色
static Color getChangeBackgroundColor(bool isUp) =>
isUp ? up.withValues(alpha: 0.15) : down.withValues(alpha: 0.15);
/// 渐变色 (兼容旧代码) - 品牌蓝
static const List<Color> gradientColors = [Color(0xFF2563EB), Color(0xFF1D4ED8)];
}

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'app_colors.dart';
import 'app_color_scheme.dart';
/// 文字样式系统
///
@@ -16,7 +16,7 @@ class AppTextStyles {
static const TextStyle h1 = TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
color: AppColorScheme.textPrimary,
height: 1.3,
);
@@ -24,7 +24,7 @@ class AppTextStyles {
static const TextStyle h2 = TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
color: AppColorScheme.textPrimary,
height: 1.3,
);
@@ -32,7 +32,7 @@ class AppTextStyles {
static const TextStyle h3 = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
color: AppColorScheme.textPrimary,
height: 1.4,
);
@@ -40,7 +40,7 @@ class AppTextStyles {
static const TextStyle h4 = TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
color: AppColorScheme.textPrimary,
height: 1.4,
);
@@ -52,7 +52,7 @@ class AppTextStyles {
static const TextStyle body1 = TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: AppColors.textPrimary,
color: AppColorScheme.textPrimary,
height: 1.5,
);
@@ -60,7 +60,7 @@ class AppTextStyles {
static const TextStyle body2 = TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
color: AppColors.textPrimary,
color: AppColorScheme.textPrimary,
height: 1.5,
);
@@ -72,7 +72,7 @@ class AppTextStyles {
static const TextStyle caption = TextStyle(
fontSize: 12,
fontWeight: FontWeight.normal,
color: AppColors.textSecondary,
color: AppColorScheme.textSecondary,
height: 1.4,
);
@@ -80,7 +80,7 @@ class AppTextStyles {
static const TextStyle small = TextStyle(
fontSize: 11,
fontWeight: FontWeight.normal,
color: AppColors.textSecondary,
color: AppColorScheme.textSecondary,
height: 1.3,
);
@@ -88,7 +88,7 @@ class AppTextStyles {
static const TextStyle hint = TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
color: AppColors.textHint,
color: AppColorScheme.textHint,
height: 1.4,
);
@@ -100,7 +100,7 @@ class AppTextStyles {
static const TextStyle amount = TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
color: AppColorScheme.textPrimary,
height: 1.2,
fontFeatures: [FontFeature.tabularFigures()],
);
@@ -109,7 +109,7 @@ class AppTextStyles {
static const TextStyle price = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
color: AppColorScheme.textPrimary,
height: 1.3,
fontFeatures: [FontFeature.tabularFigures()],
);
@@ -126,7 +126,7 @@ class AppTextStyles {
static const TextStyle button = TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
color: AppColorScheme.textPrimary,
height: 1.2,
);
@@ -134,7 +134,7 @@ class AppTextStyles {
static const TextStyle link = TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColors.textLink,
color: AppColorScheme.textLink,
decoration: TextDecoration.underline,
height: 1.4,
);

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,13 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:bot_toast/bot_toast.dart';
import 'package:provider/provider.dart';
import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_theme.dart' show AppRadius;
import '../../../core/utils/toast_utils.dart';
import '../../../core/event/app_event_bus.dart';
import '../../../providers/asset_provider.dart';
@@ -21,10 +25,6 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
int _activeTab = 0; // 0=全部, 1=充值, 2=提现
StreamSubscription<AppEvent>? _eventSub;
// 颜色常量
static const upColor = Color(0xFF00C853);
static const downColor = Color(0xFFFF5252);
@override
void initState() {
super.initState();
@@ -54,44 +54,83 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final isDark = Theme.of(context).brightness == Brightness.dark;
final bgColor = isDark
? AppColorScheme.darkBackground
: AppColorScheme.lightBackground;
return Scaffold(
backgroundColor: theme.colorScheme.background,
backgroundColor: bgColor,
appBar: AppBar(
title: const Text('充提记录'),
backgroundColor: theme.colorScheme.background,
leading: IconButton(
icon: const Icon(LucideIcons.arrowLeft, size: 20),
onPressed: () => Navigator.of(context).pop(),
),
title: Text(
'充提记录',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
backgroundColor: bgColor,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
),
body: Column(
children: [
_buildTabs(),
Expanded(child: _buildOrderList()),
_buildFilterTabs(context, isDark),
Expanded(child: _buildOrderList(context, isDark)),
],
),
);
}
Widget _buildTabs() {
final theme = ShadTheme.of(context);
// ---------------------------------------------------------------------------
// 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;
return Container(
padding: const EdgeInsets.all(16),
child: Row(
children: [
_buildTab('全部', 0),
const SizedBox(width: 12),
_buildTab('充值', 1),
const SizedBox(width: 12),
_buildTab('提现', 2),
],
return Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Container(
height: 40,
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: bgColor,
borderRadius: AppRadius.radiusMd,
),
child: Row(
children: [
_buildPillTab('全部', 0, activeBgColor, isDark),
_buildPillTab('充值', 1, activeBgColor, isDark),
_buildPillTab('提现', 2, activeBgColor, isDark),
],
),
),
);
}
Widget _buildTab(String label, int index) {
final theme = ShadTheme.of(context);
Widget _buildPillTab(
String label,
int index,
Color activeBgColor,
bool isDark,
) {
final isActive = _activeTab == index;
final activeTextColor = isDark
? AppColorScheme.darkBackground
: AppColorScheme.lightOnSurface;
final inactiveTextColor = isDark
? AppColorScheme.darkOnSurfaceVariant
: AppColorScheme.lightOnSurfaceVariant;
return Expanded(
child: GestureDetector(
@@ -102,20 +141,17 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
}
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isActive ? theme.colorScheme.primary : theme.colorScheme.card,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isActive ? theme.colorScheme.primary : theme.colorScheme.border,
),
color: isActive ? activeBgColor : Colors.transparent,
borderRadius: AppRadius.radiusSm,
),
child: Center(
child: Text(
label,
style: TextStyle(
color: isActive ? Colors.white : theme.colorScheme.mutedForeground,
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
color: isActive ? activeTextColor : inactiveTextColor,
),
),
),
@@ -124,7 +160,10 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
);
}
Widget _buildOrderList() {
// ---------------------------------------------------------------------------
// Order List
// ---------------------------------------------------------------------------
Widget _buildOrderList(BuildContext context, bool isDark) {
return Consumer<AssetProvider>(
builder: (context, provider, _) {
final orders = provider.fundOrders;
@@ -142,12 +181,19 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
Icon(
LucideIcons.inbox,
size: 64,
color: Colors.grey[400],
color: isDark
? AppColorScheme.darkOnSurfaceMuted
: AppColorScheme.lightOnSurfaceMuted,
),
const SizedBox(height: 16),
Text(
'暂无订单记录',
style: TextStyle(color: Colors.grey[600]),
style: GoogleFonts.inter(
fontSize: 14,
color: isDark
? AppColorScheme.darkOnSurfaceVariant
: AppColorScheme.lightOnSurfaceVariant,
),
),
],
),
@@ -157,11 +203,11 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
return RefreshIndicator(
onRefresh: () async => _loadData(),
child: ListView.separated(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
itemCount: orders.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
return _buildOrderCard(orders[index]);
return _buildOrderCard(orders[index], isDark);
},
),
);
@@ -169,190 +215,392 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
);
}
Widget _buildOrderCard(OrderFund order) {
final theme = ShadTheme.of(context);
final isDeposit = order.isDeposit;
// ---------------------------------------------------------------------------
// 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;
return ShadCard(
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: cardBg,
borderRadius: AppRadius.radiusLg,
border: Border.all(color: borderColor, width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isDeposit
? upColor.withValues(alpha: 0.1)
: downColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
order.typeText,
style: TextStyle(
color: isDeposit ? upColor : downColor,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 8),
_buildStatusBadge(order),
],
),
Text(
order.orderNo,
style: const TextStyle(fontSize: 11, color: Colors.grey),
),
],
),
// Header: type badge + status badge
_buildCardHeader(order, isDark),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${isDeposit ? '+' : '-'}${order.amount} USDT',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: isDeposit ? upColor : downColor,
),
),
if (order.canCancel || order.canConfirmPay)
Row(
children: [
if (order.canConfirmPay)
ShadButton.outline(
size: ShadButtonSize.sm,
onPressed: () => _confirmPay(order),
child: const Text('已打款'),
),
if (order.canCancel) ...[
const SizedBox(width: 8),
ShadButton.destructive(
size: ShadButtonSize.sm,
onPressed: () => _cancelOrder(order),
child: const Text('取消'),
),
],
],
),
],
),
// Amount
_buildAmountRow(order, primaryText),
const SizedBox(height: 12),
// 显示地址信息
if (order.walletAddress != null) ...[
const Divider(),
// Detail rows
_buildDetailRows(order, primaryText, mutedText),
// Rejection reason
if (order.rejectReason != null) ...[
const SizedBox(height: 8),
Row(
children: [
Text(
'${isDeposit ? '充值地址' : '提现地址'}: ',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
Expanded(
child: Text(
order.walletAddress!,
style: const TextStyle(fontSize: 11, fontFamily: 'monospace'),
overflow: TextOverflow.ellipsis,
),
),
GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: order.walletAddress!));
ToastUtils.show('地址已复制');
},
child: Icon(LucideIcons.copy, size: 14, color: Colors.grey[600]),
),
],
),
_buildRejectionReason(order),
],
if (order.withdrawContact != null) ...[
const SizedBox(height: 4),
Text(
'联系方式: ${order.withdrawContact}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
// Payable amount (withdrawal with fee)
if (order.receivableAmount != null && !order.isDeposit) ...[
const SizedBox(height: 8),
_buildPayableRow(order, isDark, primaryText),
],
const SizedBox(height: 8),
// Action buttons
if (order.canCancel || order.canConfirmPay) ...[
const SizedBox(height: 12),
_buildActions(order, isDark),
],
],
),
);
}
// ---------------------------------------------------------------------------
// 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);
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),
],
);
}
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);
const amberColor = Color(0xFFD97706);
const amberBg = Color(0xFFFEF3C7);
Color bgColor;
Color textColor;
if (order.isDeposit) {
switch (order.status) {
case 1: // 待付款
case 2: // 待确认
bgColor = amberBg;
textColor = amberColor;
break;
case 3: // 已完成
bgColor = upBg;
textColor = upColor;
break;
default: // 已驳回/已取消
bgColor = downBg;
textColor = downColor;
}
} else {
switch (order.status) {
case 1: // 待审批
case 5: // 待财务审核
bgColor = amberBg;
textColor = amberColor;
break;
case 2: // 已完成
bgColor = upBg;
textColor = upColor;
break;
default: // 已驳回/已取消
bgColor = downBg;
textColor = downColor;
}
}
return _buildBadge(order.statusText, textColor, bgColor);
}
Widget _buildBadge(String text, Color textColor, Color bgColor) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(4),
),
child: Text(
text,
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w600,
color: textColor,
),
),
);
}
// ---------------------------------------------------------------------------
// Amount Row
// ---------------------------------------------------------------------------
Widget _buildAmountRow(OrderFund order, Color primaryText) {
return Text(
'${order.isDeposit ? '+' : '-'}${order.amount} USDT',
style: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w700,
color: primaryText,
),
);
}
// ---------------------------------------------------------------------------
// Detail Rows
// ---------------------------------------------------------------------------
Widget _buildDetailRows(
OrderFund order,
Color primaryText,
Color mutedText,
) {
return Column(
children: [
// Order number
_buildDetailRow('订单号', order.orderNo, primaryText, mutedText),
const SizedBox(height: 6),
// Network / wallet address
if (order.walletAddress != null) ...[
_buildDetailRow(
'网络',
order.remark.isNotEmpty ? order.remark : '-',
primaryText,
mutedText,
),
const SizedBox(height: 6),
_buildDetailRow(
'地址',
_truncateAddress(order.walletAddress!),
primaryText,
mutedText,
trailing: GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: order.walletAddress!));
ToastUtils.show('地址已复制');
},
child: Icon(
LucideIcons.copy,
size: 14,
color: mutedText,
),
),
),
const SizedBox(height: 6),
] else if (order.remark.isNotEmpty) ...[
_buildDetailRow('网络', order.remark, primaryText, mutedText),
const SizedBox(height: 6),
],
// Fee (withdrawal)
if (order.fee != null && !order.isDeposit) ...[
_buildDetailRow('手续费', '${order.fee}%', primaryText, mutedText),
const SizedBox(height: 6),
],
// Time
_buildDetailRow(
'时间',
_formatTime(order.createTime),
primaryText,
mutedText,
),
],
);
}
Widget _buildDetailRow(
String label,
String value,
Color primaryText,
Color mutedText, {
Widget? trailing,
}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: mutedText,
),
),
if (trailing != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'创建: ${_formatTime(order.createTime)}',
style: const TextStyle(fontSize: 11, color: Colors.grey),
),
if (order.rejectReason != null)
Expanded(
child: Text(
'驳回: ${order.rejectReason}',
style: const TextStyle(fontSize: 11, color: downColor),
textAlign: TextAlign.right,
overflow: TextOverflow.ellipsis,
),
value,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: primaryText,
),
),
const SizedBox(width: 4),
trailing,
],
)
else
Text(
value,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: primaryText,
),
),
],
);
}
// ---------------------------------------------------------------------------
// 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),
),
);
}
// ---------------------------------------------------------------------------
// 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;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: 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,
),
),
],
),
);
}
Widget _buildStatusBadge(OrderFund order) {
Color bgColor;
Color textColor;
// ---------------------------------------------------------------------------
// Action Buttons
// ---------------------------------------------------------------------------
Widget _buildActions(OrderFund order, bool isDark) {
final upColor = AppColorScheme.getUpColor(isDark);
final downColor = AppColorScheme.getDownColor(isDark);
// 根据类型和状态设置颜色
if (order.type == 1) {
// 充值状态
switch (order.status) {
case 1: // 待付款
case 2: // 待确认
bgColor = Colors.orange.withValues(alpha: 0.1);
textColor = Colors.orange;
break;
case 3: // 已完成
bgColor = upColor.withValues(alpha: 0.1);
textColor = upColor;
break;
default: // 已驳回/已取消
bgColor = downColor.withValues(alpha: 0.1);
textColor = downColor;
}
} else {
// 提现状态
switch (order.status) {
case 1: // 待审批
bgColor = Colors.orange.withValues(alpha: 0.1);
textColor = Colors.orange;
break;
case 2: // 已完成
bgColor = upColor.withValues(alpha: 0.1);
textColor = upColor;
break;
default: // 已驳回/已取消
bgColor = downColor.withValues(alpha: 0.1);
textColor = downColor;
}
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(4),
),
child: Text(
order.statusText,
style: TextStyle(fontSize: 11, color: textColor),
),
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (order.canCancel)
GestureDetector(
onTap: () => _cancelOrder(order),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
borderRadius: AppRadius.radiusSm,
border: Border.all(color: downColor, width: 1),
),
child: Text(
'取消订单',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: downColor,
),
),
),
),
if (order.canCancel && order.canConfirmPay)
const SizedBox(width: 12),
if (order.canConfirmPay)
GestureDetector(
onTap: () => _confirmPay(order),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: upColor,
borderRadius: AppRadius.radiusSm,
),
child: Text(
'已打款',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
),
],
);
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
String _truncateAddress(String address) {
if (address.length > 12) {
return '${address.substring(0, 4)}...${address.substring(address.length - 4)}';
}
return address;
}
String _formatTime(DateTime? time) {
if (time == null) return '-';
return '${time.year}-${time.month.toString().padLeft(2, '0')}-${time.day.toString().padLeft(2, '0')} '
@@ -360,60 +608,58 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
}
void _confirmPay(OrderFund order) async {
final confirmed = await showShadDialog<bool>(
final confirmed = await showShadConfirmDialog(
context: context,
builder: (context) => ShadDialog.alert(
title: const Text('确认已打款'),
description: const Text('确认您已完成向指定地址的转账?'),
actions: [
ShadButton.outline(
child: const Text('取消'),
onPressed: () => Navigator.pop(context, false),
),
ShadButton(
child: const Text('确认'),
onPressed: () => Navigator.pop(context, true),
),
],
),
title: '确认已打款',
description: '确认您已完成向指定地址的转账?',
);
if (confirmed == true && mounted) {
final response = await context.read<AssetProvider>().confirmPay(order.orderNo);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(response.success ? '确认成功,请等待审核' : response.message ?? '确认失败')),
);
BotToast.showText(text: response.success ? '确认成功,请等待审核' : response.message ?? '确认失败');
}
}
}
void _cancelOrder(OrderFund order) async {
final confirmed = await showShadDialog<bool>(
final confirmed = await showShadConfirmDialog(
context: context,
builder: (context) => ShadDialog.alert(
title: const Text('取消订单'),
description: Text('确定要取消订单 ${order.orderNo} 吗?'),
actions: [
ShadButton.outline(
child: const Text('返回'),
onPressed: () => Navigator.pop(context, false),
),
ShadButton.destructive(
child: const Text('确定取消'),
onPressed: () => Navigator.pop(context, true),
),
],
),
title: '取消订单',
description: '确定要取消订单 ${order.orderNo} 吗?',
destructive: true,
);
if (confirmed == true && mounted) {
final response = await context.read<AssetProvider>().cancelOrder(order.orderNo);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(response.success ? '订单已取消' : response.message ?? '取消失败')),
);
BotToast.showText(text: response.success ? '订单已取消' : response.message ?? '取消失败');
}
}
}
Future<bool?> showShadConfirmDialog({
required BuildContext context,
required String title,
required String description,
bool destructive = false,
}) {
return showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(description),
actions: [
TextButton(
child: const Text('取消'),
onPressed: () => Navigator.pop(context, false),
),
TextButton(
child: Text(destructive ? '确定取消' : '确认'),
onPressed: () => Navigator.pop(context, true),
),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

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