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._(); AppColorScheme._();
// ============================================ // ============================================
// 深色主题 - "黑金传奇" (Material Design 3) // 深色主题 - "Slate Dark" (Material Design 3)
// 背景 #0A0E14 | 主色 #1E3A8A | 强调 #D4AF37 // 背景 #0B1120 | 主色 #1E3A8A | 强调 #D4AF37
// ============================================ // ============================================
/// 背景基色 - 深邃 /// 背景基色 - Slate 深蓝
static const Color darkBackground = Color(0xFF0A0E14); static const Color darkBackground = Color(0xFF0B1120);
/// Surface 层次 (从低到高) - Material Design 3 规范 /// 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 darkSurfaceLowest = Color(0xFF000000);
static const Color darkSurfaceLow = Color(0xFF0F1219); static const Color darkSurfaceLow = Color(0xFF0F172A);
static const Color darkSurface = Color(0xFF0A0E14); static const Color darkSurface = Color(0xFF0B1120);
static const Color darkSurfaceContainer = Color(0xFF151921); static const Color darkSurfaceContainer = Color(0xFF0F172A);
static const Color darkSurfaceContainerHigh = Color(0xFF1B1F28); static const Color darkSurfaceContainerHigh = Color(0xFF1E293B);
static const Color darkSurfaceContainerHighest = Color(0xFF21252F); static const Color darkSurfaceContainerHighest = Color(0xFF253349);
static const Color darkSurfaceBright = Color(0xFF272B35); static const Color darkSurfaceBright = Color(0xFF334155);
static const Color darkSurfaceVariant = Color(0xFF21252F); static const Color darkSurfaceVariant = Color(0xFF1E293B);
/// 兼容旧名称 /// 兼容旧名称
static const Color darkSurfaceContainerLowest = darkSurfaceLowest; static const Color darkSurfaceContainerLowest = darkSurfaceLowest;
@@ -45,8 +45,8 @@ class AppColorScheme {
static const Color darkSurfaceHighest = darkSurfaceContainerHighest; static const Color darkSurfaceHighest = darkSurfaceContainerHighest;
/// Ghost Border /// Ghost Border
static const Color darkOutline = Color(0xFF73757d); static const Color darkOutline = Color(0xFF64748B);
static const Color darkOutlineVariant = Color(0xFF45484f); static const Color darkOutlineVariant = Color(0xFF334155);
/// Primary - 专业蓝 #1E3A8A (主要交互) /// Primary - 专业蓝 #1E3A8A (主要交互)
static const Color darkPrimary = Color(0xFF1E3A8A); static const Color darkPrimary = Color(0xFF1E3A8A);
@@ -89,38 +89,38 @@ class AppColorScheme {
static const Color darkOnErrorContainer = Color(0xFFffa8a3); static const Color darkOnErrorContainer = Color(0xFFffa8a3);
/// 文本色 /// 文本色
static const Color darkOnSurface = Color(0xFFecedf6); static const Color darkOnSurface = Color(0xFFF8FAFC);
static const Color darkOnSurfaceVariant = Color(0xFFa9abb3); static const Color darkOnSurfaceVariant = Color(0xFF94A3B8);
static const Color darkOnSurfaceMuted = Color(0xFF6b6d75); static const Color darkOnSurfaceMuted = Color(0xFF64748B);
static const Color darkOnBackground = Color(0xFFecedf6); static const Color darkOnBackground = Color(0xFFF8FAFC);
static const Color darkInverseSurface = Color(0xFFf9f9ff); static const Color darkInverseSurface = Color(0xFFF8FAFC);
static const Color darkInverseOnSurface = Color(0xFF52555c); static const Color darkInverseOnSurface = Color(0xFF475569);
static const Color darkInversePrimary = Color(0xFF1E40AF); static const Color darkInversePrimary = Color(0xFF1E40AF);
static const Color darkSurfaceTint = Color(0xFFD4AF37); static const Color darkSurfaceTint = Color(0xFFD4AF37);
// ============================================ // ============================================
// 浅色主题 - "白金殿堂" // 浅色主题 - "Slate Light" (Material Design 3)
// 背景 #FAFAFA | 主色 #1E40AF | 强调 #FFD700 // 背景 #F8FAFC | 主色 #1E40AF | 强调 #D4AF37
// ============================================ // ============================================
/// 背景基色 - 纯净白 /// 背景基色 - Slate 50
static const Color lightBackground = Color(0xFFFAFAFA); static const Color lightBackground = Color(0xFFF8FAFC);
/// Surface 层次 (从低到高) /// Surface 层次 (从低到高)
static const Color lightSurfaceLowest = Color(0xFFffffff); static const Color lightSurfaceLowest = Color(0xFFFFFFFF);
static const Color lightSurfaceLow = Color(0xFFF5F5F5); static const Color lightSurfaceLow = Color(0xFFF1F5F9);
static const Color lightSurface = Color(0xFFFAFAFA); static const Color lightSurface = Color(0xFFF8FAFC);
static const Color lightSurfaceHigh = Color(0xFFF0F0F0); static const Color lightSurfaceHigh = Color(0xFFE2E8F0);
static const Color lightSurfaceHighest = Color(0xFFE8E8E8); static const Color lightSurfaceHighest = Color(0xFFCBD5E1);
/// Ghost Border /// Ghost Border - Slate 300
static const Color lightOutlineVariant = Color(0xFFD0D0D0); static const Color lightOutlineVariant = Color(0xFFCBD5E1);
/// Primary - 专业蓝 #1E40AF (主要交互) /// Primary - 专业蓝 #1E40AF (主要交互)
static const Color lightPrimary = Color(0xFF1E40AF); static const Color lightPrimary = Color(0xFF1E40AF);
static const Color lightPrimaryContainer = Color(0xFF3B82F6); static const Color lightPrimaryContainer = Color(0xFF3B82F6);
/// Secondary - 亮金 #FFD700 (白金强调色) /// Secondary - 亮金 #D4AF37 (白金强调色)
static const Color lightSecondary = Color(0xFFD4AF37); static const Color lightSecondary = Color(0xFFD4AF37);
static const Color lightSecondaryContainer = Color(0xFFFFE44D); static const Color lightSecondaryContainer = Color(0xFFFFE44D);
@@ -128,10 +128,10 @@ class AppColorScheme {
static const Color lightTertiary = Color(0xFF00875A); static const Color lightTertiary = Color(0xFF00875A);
static const Color lightTertiaryContainer = Color(0xFFd4f5e9); static const Color lightTertiaryContainer = Color(0xFFd4f5e9);
/// 文本色 /// 文本色 - Slate
static const Color lightOnSurface = Color(0xFF1A1A1A); static const Color lightOnSurface = Color(0xFF0F172A);
static const Color lightOnSurfaceVariant = Color(0xFF5a5d60); static const Color lightOnSurfaceVariant = Color(0xFF475569);
static const Color lightOnSurfaceMuted = Color(0xFF8a8d90); static const Color lightOnSurfaceMuted = Color(0xFF94A3B8);
// ============================================ // ============================================
// Glass Panel 毛玻璃效果颜色 // Glass Panel 毛玻璃效果颜色
@@ -369,19 +369,19 @@ class AppColorScheme {
// ============================================ // ============================================
/// 浅色主题 Error 色 /// 浅色主题 Error 色
static const Color lightError = Color(0xFFd7383b); static const Color lightError = Color(0xFFDC2626);
static const Color lightOnError = Color(0xFFFFFFFF); static const Color lightOnError = Color(0xFFFFFFFF);
/// 浅色主题 Outline 色 /// 浅色主题 Outline 色 - Slate 500
static const Color lightOutline = Color(0xFF73757d); static const Color lightOutline = Color(0xFF64748B);
/// 浅色主题 Surface 色 (扩展) /// 浅色主题 Surface 色 (扩展) - Slate 系列
static const Color lightSurfaceBright = Color(0xFFffffff); static const Color lightSurfaceBright = Color(0xFFFFFFFF);
static const Color lightSurfaceDim = Color(0xFFF0F0F0); static const Color lightSurfaceDim = Color(0xFFE2E8F0);
static const Color lightSurfaceVariant = Color(0xFFF0F0F0); static const Color lightSurfaceVariant = Color(0xFFE2E8F0);
static const Color lightSurfaceContainer = Color(0xFFF5F5F5); static const Color lightSurfaceContainer = Color(0xFFF1F5F9);
static const Color lightSurfaceContainerHigh = Color(0xFFF0F0F0); static const Color lightSurfaceContainerHigh = Color(0xFFE2E8F0);
static const Color lightSurfaceContainerHighest = Color(0xFFE8E8E8); static const Color lightSurfaceContainerHighest = Color(0xFFCBD5E1);
static ColorScheme get lightMaterial => ColorScheme.light( static ColorScheme get lightMaterial => ColorScheme.light(
primary: lightPrimary, 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; static const List<Color> gradientColors = [darkPrimary, darkPrimaryContainer];
static const LinearGradient primaryGradient = LinearGradient(
@Deprecated('Use darkSurfaceContainerHigh instead') colors: [darkPrimary, darkPrimaryContainer],
static const Color _darkMuted = darkSurfaceContainerHigh; begin: Alignment.topLeft,
end: Alignment.bottomRight,
@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;
} }
/// 创建 Shadcn 深色主题 /// 创建 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 'package:flutter/material.dart';
import 'app_colors.dart'; import 'app_color_scheme.dart';
/// 文字样式系统 /// 文字样式系统
/// ///
@@ -16,7 +16,7 @@ class AppTextStyles {
static const TextStyle h1 = TextStyle( static const TextStyle h1 = TextStyle(
fontSize: 22, fontSize: 22,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: AppColors.textPrimary, color: AppColorScheme.textPrimary,
height: 1.3, height: 1.3,
); );
@@ -24,7 +24,7 @@ class AppTextStyles {
static const TextStyle h2 = TextStyle( static const TextStyle h2 = TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: AppColors.textPrimary, color: AppColorScheme.textPrimary,
height: 1.3, height: 1.3,
); );
@@ -32,7 +32,7 @@ class AppTextStyles {
static const TextStyle h3 = TextStyle( static const TextStyle h3 = TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.textPrimary, color: AppColorScheme.textPrimary,
height: 1.4, height: 1.4,
); );
@@ -40,7 +40,7 @@ class AppTextStyles {
static const TextStyle h4 = TextStyle( static const TextStyle h4 = TextStyle(
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.textPrimary, color: AppColorScheme.textPrimary,
height: 1.4, height: 1.4,
); );
@@ -52,7 +52,7 @@ class AppTextStyles {
static const TextStyle body1 = TextStyle( static const TextStyle body1 = TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
color: AppColors.textPrimary, color: AppColorScheme.textPrimary,
height: 1.5, height: 1.5,
); );
@@ -60,7 +60,7 @@ class AppTextStyles {
static const TextStyle body2 = TextStyle( static const TextStyle body2 = TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
color: AppColors.textPrimary, color: AppColorScheme.textPrimary,
height: 1.5, height: 1.5,
); );
@@ -72,7 +72,7 @@ class AppTextStyles {
static const TextStyle caption = TextStyle( static const TextStyle caption = TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
color: AppColors.textSecondary, color: AppColorScheme.textSecondary,
height: 1.4, height: 1.4,
); );
@@ -80,7 +80,7 @@ class AppTextStyles {
static const TextStyle small = TextStyle( static const TextStyle small = TextStyle(
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
color: AppColors.textSecondary, color: AppColorScheme.textSecondary,
height: 1.3, height: 1.3,
); );
@@ -88,7 +88,7 @@ class AppTextStyles {
static const TextStyle hint = TextStyle( static const TextStyle hint = TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
color: AppColors.textHint, color: AppColorScheme.textHint,
height: 1.4, height: 1.4,
); );
@@ -100,7 +100,7 @@ class AppTextStyles {
static const TextStyle amount = TextStyle( static const TextStyle amount = TextStyle(
fontSize: 22, fontSize: 22,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: AppColors.textPrimary, color: AppColorScheme.textPrimary,
height: 1.2, height: 1.2,
fontFeatures: [FontFeature.tabularFigures()], fontFeatures: [FontFeature.tabularFigures()],
); );
@@ -109,7 +109,7 @@ class AppTextStyles {
static const TextStyle price = TextStyle( static const TextStyle price = TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.textPrimary, color: AppColorScheme.textPrimary,
height: 1.3, height: 1.3,
fontFeatures: [FontFeature.tabularFigures()], fontFeatures: [FontFeature.tabularFigures()],
); );
@@ -126,7 +126,7 @@ class AppTextStyles {
static const TextStyle button = TextStyle( static const TextStyle button = TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.textPrimary, color: AppColorScheme.textPrimary,
height: 1.2, height: 1.2,
); );
@@ -134,7 +134,7 @@ class AppTextStyles {
static const TextStyle link = TextStyle( static const TextStyle link = TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: AppColors.textLink, color: AppColorScheme.textLink,
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
height: 1.4, 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/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.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_color_scheme.dart';
import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_spacing.dart';
import '../../../providers/asset_provider.dart'; import '../../../providers/asset_provider.dart';
import '../../../data/models/account_models.dart'; import '../../../data/models/account_models.dart';
import '../../shared/ui_constants.dart';
import '../../components/neon_glow.dart';
/// 划转页面 - 币安风格 /// 划转页面
class TransferPage extends StatefulWidget { class TransferPage extends StatefulWidget {
const TransferPage({super.key}); const TransferPage({super.key});
@@ -20,6 +18,7 @@ class TransferPage extends StatefulWidget {
class _TransferPageState extends State<TransferPage> { class _TransferPageState extends State<TransferPage> {
final _amountController = TextEditingController(); final _amountController = TextEditingController();
final _focusNode = FocusNode();
int _direction = 1; // 1: 资金→交易, 2: 交易→资金 int _direction = 1; // 1: 资金→交易, 2: 交易→资金
bool _isLoading = false; bool _isLoading = false;
@@ -34,6 +33,7 @@ class _TransferPageState extends State<TransferPage> {
@override @override
void dispose() { void dispose() {
_amountController.dispose(); _amountController.dispose();
_focusNode.dispose();
super.dispose(); super.dispose();
} }
@@ -124,7 +124,6 @@ class _TransferPageState extends State<TransferPage> {
void _setQuickAmount(double percent) { void _setQuickAmount(double percent) {
final available = double.tryParse(_availableBalance) ?? 0; final available = double.tryParse(_availableBalance) ?? 0;
final amount = available * percent; final amount = available * percent;
// 保留8位小数去除末尾0
_amountController.text = amount.toStringAsFixed(8).replaceAll(RegExp(r'\.?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 colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark; 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( return Scaffold(
backgroundColor: colorScheme.background, backgroundColor: bgSecondary,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.transparent, backgroundColor: isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF),
elevation: 0, elevation: 0,
scrolledUnderElevation: 0,
leading: IconButton( leading: IconButton(
icon: Icon(Icons.arrow_back, color: colorScheme.onSurface), icon: Icon(LucideIcons.arrowLeft, color: textPrimary, size: 20),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
), ),
title: Text( title: Text(
'资金划转', '账户划转',
style: GoogleFonts.spaceGrotesk( style: GoogleFonts.inter(
fontSize: 14, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w600,
color: colorScheme.onSurface, color: textPrimary,
), ),
), ),
centerTitle: true, centerTitle: true,
@@ -162,15 +176,81 @@ class _TransferPageState extends State<TransferPage> {
body: Consumer<AssetProvider>( body: Consumer<AssetProvider>(
builder: (context, provider, _) { builder: (context, provider, _) {
return SingleChildScrollView( return SingleChildScrollView(
padding: AppSpacing.pagePadding, padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
child: Column( child: Column(
children: [ children: [
// 第一个卡片位置 - 带动画 // --- Transfer Direction Card ---
TweenAnimationBuilder<double>( _buildTransferDirectionCard(
tween: Tween(begin: 0, end: 1), colorScheme: colorScheme,
duration: const Duration(milliseconds: 300), isDark: isDark,
builder: (context, value, child) { surfaceCard: surfaceCard,
return AnimatedSwitcher( borderDefault: borderDefault,
textPrimary: textPrimary,
textSecondary: textSecondary,
textMuted: textMuted,
textInverse: textInverse,
accentPrimary: accentPrimary,
),
const SizedBox(height: 24),
// --- Amount Section ---
_buildAmountSection(
isDark: isDark,
bgTertiary: bgTertiary,
textPrimary: textPrimary,
textSecondary: textSecondary,
textMuted: textMuted,
goldAccent: goldAccent,
),
const SizedBox(height: 24),
// --- Tips Card ---
_buildTipsCard(
profitGreen: profitGreen,
profitGreenBg: profitGreenBg,
),
const SizedBox(height: 24),
// --- Confirm Button ---
_buildConfirmButton(
accentPrimary: accentPrimary,
textInverse: textInverse,
),
],
),
);
},
),
);
}
/// 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), duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeInOut, switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut, switchOutCurve: Curves.easeInOut,
@@ -180,58 +260,43 @@ class _TransferPageState extends State<TransferPage> {
begin: const Offset(0, -1), begin: const Offset(0, -1),
end: Offset.zero, end: Offset.zero,
).animate(animation), ).animate(animation),
child: FadeTransition( child: FadeTransition(opacity: animation, child: widget),
opacity: animation,
child: widget,
),
); );
}, },
child: _direction == 1 child: _buildAccountRow(
? _buildAccountCard( key: ValueKey('src-$_direction'),
key: const ValueKey('from-card'),
label: '', label: '',
accountName: _fromLabel, accountName: _fromLabel,
balance: _fromBalance, balance: _fromBalance,
isDark: isDark, isDark: isDark,
colorScheme: colorScheme, textMuted: textMuted,
) textPrimary: textPrimary,
: _buildAccountCard( textSecondary: textSecondary,
key: const ValueKey('to-card-top'),
label: '',
accountName: _toLabel,
balance: _toBalance,
isDark: isDark,
colorScheme: colorScheme,
), ),
);
},
), ),
// 方向切换按钮(固定在中间) // Swap button
GestureDetector( GestureDetector(
onTap: _toggleDirection, onTap: _toggleDirection,
child: Container( child: Container(
margin: EdgeInsets.symmetric(vertical: AppSpacing.sm), width: 36,
padding: EdgeInsets.all(AppSpacing.sm + AppSpacing.xs), height: 36,
margin: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.primary, color: accentPrimary,
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: colorScheme.primary.withOpacity(0.3),
blurRadius: 8,
),
],
), ),
child: Center(
child: Icon( child: Icon(
Icons.swap_vert, LucideIcons.arrowUpDown,
color: colorScheme.onPrimary, size: 18,
size: 22, color: textInverse,
),
), ),
), ),
), ),
// 第二个卡片位置 - 带动画 // Destination account
AnimatedSwitcher( AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeInOut, switchInCurve: Curves.easeInOut,
@@ -242,131 +307,82 @@ class _TransferPageState extends State<TransferPage> {
begin: const Offset(0, 1), begin: const Offset(0, 1),
end: Offset.zero, end: Offset.zero,
).animate(animation), ).animate(animation),
child: FadeTransition( child: FadeTransition(opacity: animation, child: widget),
opacity: animation,
child: widget,
),
); );
}, },
child: _direction == 1 child: _buildAccountRow(
? _buildAccountCard( key: ValueKey('dst-$_direction'),
key: const ValueKey('to-card'),
label: '', label: '',
accountName: _toLabel, accountName: _toLabel,
balance: _toBalance, balance: _toBalance,
isDark: isDark, isDark: isDark,
colorScheme: colorScheme, textMuted: textMuted,
) textPrimary: textPrimary,
: _buildAccountCard( textSecondary: textSecondary,
key: const ValueKey('from-card-bottom'),
label: '',
accountName: _fromLabel,
balance: _fromBalance,
isDark: isDark,
colorScheme: colorScheme,
), ),
), ),
SizedBox(height: AppSpacing.lg),
// 金额输入卡片
_buildAmountSection(colorScheme, isDark),
SizedBox(height: AppSpacing.lg),
// 确认按钮
SizedBox(
width: double.infinity,
child: NeonButton(
text: _isLoading ? '处理中...' : '确认划转',
icon: _isLoading ? null : LucideIcons.arrowRightLeft,
type: NeonButtonType.primary,
onPressed: _isLoading ? null : _doTransfer,
height: 52,
showGlow: true,
),
),
SizedBox(height: AppSpacing.md),
// 划转说明
_buildTips(colorScheme),
], ],
), ),
); );
},
),
);
} }
/// 账户卡片 /// Single account row inside the direction card
Widget _buildAccountCard({ Widget _buildAccountRow({
Key? key, Key? key,
required String label, required String label,
required String accountName, required String accountName,
required String balance, required String balance,
required bool isDark, required bool isDark,
required ColorScheme colorScheme, required Color textMuted,
required Color textPrimary,
required Color textSecondary,
}) { }) {
return Container( return Container(
key: key, key: key,
width: double.infinity, 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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Label row
Text(
label,
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.normal,
color: textMuted,
),
),
const SizedBox(height: 8),
// Account name + balance row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Account name with icon
Row( Row(
children: [ children: [
Container( Icon(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm, vertical: AppSpacing.xs), label == '' ? LucideIcons.wallet : LucideIcons.repeat,
decoration: BoxDecoration( size: 18,
color: colorScheme.primary.withOpacity(0.1), color: textSecondary,
borderRadius: BorderRadius.circular(AppRadius.sm),
), ),
child: Text( const SizedBox(width: 10),
label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: colorScheme.primary,
),
),
),
SizedBox(width: AppSpacing.sm),
Text( Text(
accountName, accountName,
style: GoogleFonts.spaceGrotesk( style: GoogleFonts.inter(
fontSize: 16, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: colorScheme.onSurface, color: textPrimary,
), ),
), ),
], ],
), ),
SizedBox(height: AppSpacing.sm), // Balance
Row(
children: [
Text( Text(
'可用余额', '\u00A5 ${_formatBalance(balance)}',
style: TextStyle( style: GoogleFonts.inter(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
SizedBox(width: AppSpacing.xs),
Text(
'$balance USDT',
style: GoogleFonts.spaceGrotesk(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w600,
color: colorScheme.onSurface, color: textPrimary,
), ),
), ),
], ],
@@ -376,51 +392,86 @@ class _TransferPageState extends State<TransferPage> {
); );
} }
/// 金额输入区域 /// Format balance for display
Widget _buildAmountSection(ColorScheme colorScheme, bool isDark) { String _formatBalance(String balance) {
return Container( final val = double.tryParse(balance);
width: double.infinity, if (val == null) return '0.00';
padding: EdgeInsets.all(AppSpacing.md), return val.toStringAsFixed(2);
decoration: BoxDecoration( }
color: isDark ? colorScheme.surfaceContainer : colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.lg), /// Amount input section
border: Border.all( Widget _buildAmountSection({
color: colorScheme.outlineVariant.withOpacity(0.15), required bool isDark,
), required Color bgTertiary,
), required Color textPrimary,
child: Column( required Color textSecondary,
required Color textMuted,
required Color goldAccent,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label row: "划转金额" + "全部划转"
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'划转金额', '划转金额',
style: TextStyle( style: GoogleFonts.inter(
fontSize: 13, fontSize: 14,
color: colorScheme.onSurfaceVariant, fontWeight: FontWeight.w500,
color: textSecondary,
), ),
), ),
SizedBox(height: AppSpacing.sm), GestureDetector(
// 金额输入行 onTap: () => _setQuickAmount(1.0),
Row( child: Text(
crossAxisAlignment: CrossAxisAlignment.end, '全部划转',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
color: goldAccent,
),
),
),
],
),
const SizedBox(height: 12),
// Amount input field
GestureDetector(
onTap: () => _focusNode.requestFocus(),
child: Container(
width: double.infinity,
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: bgTertiary,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
// Input
Expanded( Expanded(
child: TextField( child: TextField(
controller: _amountController, controller: _amountController,
focusNode: _focusNode,
keyboardType: const TextInputType.numberWithOptions(decimal: true), keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,8}')), FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,8}')),
], ],
style: GoogleFonts.spaceGrotesk( style: GoogleFonts.inter(
fontSize: 18, fontSize: 28,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w700,
color: colorScheme.onSurface, color: textPrimary,
), ),
decoration: InputDecoration( decoration: InputDecoration(
hintText: '0.00', hintText: '0.00',
hintStyle: GoogleFonts.spaceGrotesk( hintStyle: GoogleFonts.inter(
fontSize: 18, fontSize: 28,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w700,
color: colorScheme.onSurfaceVariant.withOpacity(0.3), color: textMuted,
), ),
border: InputBorder.none, border: InputBorder.none,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
@@ -428,150 +479,136 @@ class _TransferPageState extends State<TransferPage> {
), ),
), ),
), ),
// Suffix
Padding( Padding(
padding: EdgeInsets.only(bottom: 4), padding: const EdgeInsets.only(left: 8),
child: Text( child: Text(
'USDT', 'USDT',
style: TextStyle( style: GoogleFonts.inter(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant, color: textMuted,
), ),
), ),
), ),
], ],
), ),
SizedBox(height: AppSpacing.sm), ),
// 快捷按钮行 ),
const SizedBox(height: 12),
// Percent buttons
Row( Row(
children: [ children: [
Text( _buildPercentButton('25%', 0.25, isDark, bgTertiary, textSecondary),
'可用: ${_availableBalance}', const SizedBox(width: 8),
style: TextStyle( _buildPercentButton('50%', 0.50, isDark, bgTertiary, textSecondary),
fontSize: 12, const SizedBox(width: 8),
color: colorScheme.onSurfaceVariant, _buildPercentButton('75%', 0.75, isDark, bgTertiary, textSecondary),
), const SizedBox(width: 8),
), _buildPercentButton('100%', 1.0, isDark, bgTertiary, textSecondary),
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),
/// 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( decoration: BoxDecoration(
color: AppColorScheme.warning.withOpacity(0.1), color: bgTertiary,
borderRadius: BorderRadius.circular(AppRadius.sm), 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( child: Row(
children: [ children: [
Icon( Icon(
LucideIcons.triangleAlert, LucideIcons.info,
size: 14, size: 16,
color: AppColorScheme.warning, color: profitGreen,
), ),
SizedBox(width: AppSpacing.xs), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
'仅支持 USDT 资产划转到资金账户', '划转即时到账,无需手续费',
style: TextStyle( style: GoogleFonts.inter(
fontSize: 11, fontSize: 12,
color: AppColorScheme.warning, fontWeight: FontWeight.normal,
color: profitGreen,
), ),
), ),
), ),
], ],
), ),
),
],
],
),
); );
} }
/// 快捷百分比按钮 /// Confirm button
Widget _buildQuickButton(String label, double percent, ColorScheme colorScheme) { Widget _buildConfirmButton({
return GestureDetector( required Color accentPrimary,
onTap: () => _setQuickAmount(percent), required Color textInverse,
}) {
return SizedBox(
width: double.infinity,
height: 52,
child: GestureDetector(
onTap: _isLoading ? null : _doTransfer,
child: Container( child: Container(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm, vertical: AppSpacing.xs),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1), color: accentPrimary,
borderRadius: BorderRadius.circular(AppRadius.sm), borderRadius: BorderRadius.circular(AppRadius.lg),
), ),
child: Text( child: Center(
label, child: _isLoading
style: TextStyle( ? SizedBox(
fontSize: 12, width: 20,
fontWeight: FontWeight.w600, height: 20,
color: colorScheme.primary, child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(textInverse),
),
)
: Text(
'确认划转',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w700,
color: textInverse,
), ),
), ),
), ),
);
}
/// 划转说明
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),
decoration: BoxDecoration(
color: colorScheme.onSurfaceVariant,
shape: BoxShape.circle,
),
),
Expanded(
child: Text(
text,
style: TextStyle(
fontSize: 11,
color: colorScheme.onSurfaceVariant,
),
),
),
],
), ),
); );
} }

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_spacing.dart';
import '../../../providers/auth_provider.dart'; import '../../../providers/auth_provider.dart';
import '../main/main_page.dart'; import '../main/main_page.dart';
@@ -16,126 +17,311 @@ class LoginPage extends StatefulWidget {
class _LoginPageState extends State<LoginPage> { class _LoginPageState extends State<LoginPage> {
final formKey = GlobalKey<ShadFormState>(); 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 _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = ShadTheme.of(context); final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold( return Scaffold(
body: Center( backgroundColor: isDark
child: ConstrainedBox( ? AppColorScheme.darkBackground
constraints: const BoxConstraints(maxWidth: _maxFormWidth), : AppColorScheme.lightSurface,
child: Padding( body: SafeArea(
padding: EdgeInsets.all(AppSpacing.lg), child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.xl,
vertical: AppSpacing.xxl,
),
child: ShadForm( child: ShadForm(
key: formKey, key: formKey,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_buildHeader(theme), // 顶部品牌区域
SizedBox(height: AppSpacing.xxl), _buildBrandSection(isDark),
_buildUsernameField(), const SizedBox(height: AppSpacing.xxl),
SizedBox(height: AppSpacing.md), // 表单区域
_buildPasswordField(), _buildFormSection(isDark),
SizedBox(height: AppSpacing.lg), const SizedBox(height: AppSpacing.xl),
_buildLoginButton(), // 底部注册链接
SizedBox(height: AppSpacing.md), _buildRegisterRow(isDark),
_buildRegisterLink(theme),
], ],
), ),
), ),
), ),
), ),
),
); );
} }
Widget _buildHeader(ShadThemeData theme) { // ============================================
// 品牌区域 - Logo + 品牌名 + 标语
// ============================================
Widget _buildBrandSection(bool isDark) {
return Column( return Column(
children: [ children: [
Icon( // Logo 圆形:渐变 #1F2937 → #374151内含 "M"
LucideIcons.trendingUp, Container(
size: _logoSize, width: _logoCircleSize,
color: theme.colorScheme.primary, height: _logoCircleSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF1F2937), Color(0xFF374151)],
), ),
SizedBox(height: AppSpacing.lg), ),
alignment: Alignment.center,
child: Text(
'M',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w800,
color: isDark
? AppColorScheme.darkOnSurface
: Colors.white,
),
),
),
const SizedBox(height: AppSpacing.md),
// 品牌名 "MONISUO"
Text( Text(
'模拟所', 'MONISUO',
style: theme.textTheme.h1, style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w800,
letterSpacing: 3,
color: isDark
? AppColorScheme.darkOnSurface
: AppColorScheme.lightOnSurface,
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
SizedBox(height: AppSpacing.sm), const SizedBox(height: AppSpacing.md),
// 标语
Text( Text(
'虚拟货币模拟交易平台', '虚拟货币模拟交易平台',
style: theme.textTheme.muted, style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
color: isDark
? AppColorScheme.darkOnSurfaceVariant
: AppColorScheme.lightOnSurfaceVariant,
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
], ],
); );
} }
Widget _buildUsernameField() { // ============================================
return ShadInputFormField( // 表单区域 - 用户名 + 密码 + 登录按钮
// ============================================
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 _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', id: 'username',
label: const Text('用户名'),
placeholder: const Text('请输入用户名'), placeholder: const Text('请输入用户名'),
leading: const Icon(LucideIcons.user), leading: Padding(
padding: const EdgeInsets.only(right: AppSpacing.sm),
child: Icon(LucideIcons.user, size: 18, color: iconColor),
),
validator: _validateUsername, 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 _buildPasswordField() { Widget _buildPasswordField(bool isDark) {
return ShadInputFormField( final borderColor = isDark
? AppColorScheme.darkOutlineVariant
: AppColorScheme.lightOutlineVariant;
final iconColor = isDark
? AppColorScheme.darkOnSurfaceMuted
: AppColorScheme.lightOnSurfaceMuted;
return SizedBox(
height: _inputHeight,
child: ShadInputFormField(
id: 'password', id: 'password',
label: const Text('密码'),
placeholder: const Text('请输入密码'), placeholder: const Text('请输入密码'),
obscureText: true, obscureText: _obscurePassword,
leading: const Icon(LucideIcons.lock), 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, 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() { 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>( return Consumer<AuthProvider>(
builder: (context, auth, _) { builder: (context, auth, _) {
return ShadButton( return SizedBox(
height: _buttonHeight,
child: ShadButton(
onPressed: auth.isLoading ? null : () => _handleLogin(auth), onPressed: auth.isLoading ? null : () => _handleLogin(auth),
backgroundColor: buttonColor,
foregroundColor: textColor,
decoration: ShadDecoration(
border: ShadBorder.all(
radius: BorderRadius.circular(_designRadiusLg),
),
),
child: auth.isLoading child: auth.isLoading
? const SizedBox.square( ? SizedBox.square(
dimension: _loadingIndicatorSize, dimension: _loadingIndicatorSize,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
color: Colors.white, color: textColor,
), ),
) )
: const Text('登录'), : Text(
'登录',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: textColor,
),
),
),
); );
}, },
); );
} }
Widget _buildRegisterLink(ShadThemeData theme) { // ============================================
return Row( // 底部注册链接
// ============================================
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, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
'还没有账', '还没有账',
style: theme.textTheme.muted, 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,
),
), ),
ShadButton.link(
onPressed: _navigateToRegister,
child: const Text('立即注册'),
), ),
], ],
),
); );
} }
// ============================================
// Validators // Validators
// ============================================
String? _validateUsername(String? value) { String? _validateUsername(String? value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return '请输入用户名'; return '请输入用户名';
@@ -156,7 +342,10 @@ class _LoginPageState extends State<LoginPage> {
return null; return null;
} }
// ============================================
// Actions // Actions
// ============================================
Future<void> _handleLogin(AuthProvider auth) async { Future<void> _handleLogin(AuthProvider auth) async {
if (!formKey.currentState!.saveAndValidate()) return; if (!formKey.currentState!.saveAndValidate()) return;
@@ -176,7 +365,6 @@ class _LoginPageState extends State<LoginPage> {
} }
void _navigateToMainPage() { void _navigateToMainPage() {
// 使用 Navigator 跳转到主页面,替换当前页面
Navigator.of(context).pushAndRemoveUntil( Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const MainPage()), MaterialPageRoute(builder: (context) => const MainPage()),
(route) => false, (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 '../../components/neon_glow.dart';
import '../main/main_page.dart'; import '../main/main_page.dart';
import '../mine/welfare_center_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 { class HomePage extends StatefulWidget {
@@ -102,8 +105,8 @@ class _HomePageState extends State<HomePage>
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 问候 // Header
_GreetingSection(), HeaderBar(),
SizedBox(height: AppSpacing.md), SizedBox(height: AppSpacing.md),
// 资产卡片(含总盈利 + 可折叠盈亏日历) // 资产卡片(含总盈利 + 可折叠盈亏日历)
_AssetCard( _AssetCard(
@@ -111,6 +114,15 @@ class _HomePageState extends State<HomePage>
onDeposit: _showDeposit, onDeposit: _showDeposit,
), ),
SizedBox(height: AppSpacing.md), SizedBox(height: AppSpacing.md),
// 快捷操作栏
QuickActionsRow(
onDeposit: _showDeposit,
onWithdraw: () => _navigateToAssetPage(),
onTransfer: () => _navigateToAssetPage(),
onProfit: () {},
onBills: () => _navigateToAssetPage(),
),
SizedBox(height: AppSpacing.md),
// 福利中心入口卡片 // 福利中心入口卡片
_WelfareCard( _WelfareCard(
totalClaimable: _totalClaimable, totalClaimable: _totalClaimable,
@@ -120,6 +132,9 @@ class _HomePageState extends State<HomePage>
), ),
), ),
SizedBox(height: AppSpacing.lg), SizedBox(height: AppSpacing.lg),
// 热门币种
HotCoinsSection(),
SizedBox(height: AppSpacing.lg),
// 持仓 // 持仓
_HoldingsSection(holdings: provider.holdings), _HoldingsSection(holdings: provider.holdings),
], ],
@@ -416,40 +431,7 @@ class _HomePageState extends State<HomePage>
} }
} }
/// 问候区域 /// Header 栏:品牌名 + 搜索/通知/头像
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,
),
),
],
);
},
);
}
}
/// 资产卡片(含总盈利 + 可折叠盈亏日历) /// 资产卡片(含总盈利 + 可折叠盈亏日历)
class _AssetCard extends StatefulWidget { class _AssetCard extends StatefulWidget {
final AssetOverview? overview; 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:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import '../../../core/theme/app_color_scheme.dart'; import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_spacing.dart' show AppRadius;
import '../../../data/models/coin.dart'; import '../../../data/models/coin.dart';
import '../../../providers/market_provider.dart'; import '../../../providers/market_provider.dart';
import '../../components/glass_panel.dart'; import '../../components/glass_panel.dart';
@@ -53,24 +55,30 @@ class _MarketPageState extends State<MarketPage>
backgroundColor: colorScheme.surfaceContainerHighest, backgroundColor: colorScheme.surfaceContainerHighest,
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
padding: AppSpacing.pagePadding, padding: const EdgeInsets.only(top: 16, left: 16, right: 16, bottom: 32),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 上半区BTC + ETH 突出展示 // 页面标题 "行情"
_buildFeaturedSection(provider), Padding(
SizedBox(height: AppSpacing.lg), padding: const EdgeInsets.only(top: 0, bottom: 8),
// 下半区标题 child: Text(
Text( '行情',
'代币列表', style: GoogleFonts.inter(
style: GoogleFonts.spaceGrotesk( fontSize: 22,
fontSize: 16, fontWeight: FontWeight.w700,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface, 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), _buildCoinList(provider),
], ],
), ),
@@ -81,7 +89,7 @@ class _MarketPageState extends State<MarketPage>
); );
} }
/// 上半区BTC + ETH 大卡片 /// 精选区域BTC + ETH 大卡片
Widget _buildFeaturedSection(MarketProvider provider) { Widget _buildFeaturedSection(MarketProvider provider) {
final featured = provider.featuredCoins; final featured = provider.featuredCoins;
if (featured.isEmpty) return const SizedBox.shrink(); if (featured.isEmpty) return const SizedBox.shrink();
@@ -95,7 +103,7 @@ class _MarketPageState extends State<MarketPage>
Expanded(child: _FeaturedCard(coin: btc)) Expanded(child: _FeaturedCard(coin: btc))
else else
const Expanded(child: SizedBox.shrink()), const Expanded(child: SizedBox.shrink()),
SizedBox(width: AppSpacing.md), const SizedBox(width: 12),
if (eth != null) if (eth != null)
Expanded(child: _FeaturedCard(coin: eth)) Expanded(child: _FeaturedCard(coin: eth))
else 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) { Widget _buildCoinList(MarketProvider provider) {
final coins = provider.otherCoins; final coins = provider.otherCoins;
final colorScheme = Theme.of(context).colorScheme;
if (coins.isEmpty) { if (coins.isEmpty) {
return _EmptyState( return _EmptyState(
@@ -116,12 +152,28 @@ class _MarketPageState extends State<MarketPage>
); );
} }
return ListView.separated( 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, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemCount: coins.length, itemCount: coins.length,
separatorBuilder: (_, __) => SizedBox(height: AppSpacing.sm), separatorBuilder: (_, __) => Divider(
itemBuilder: (context, index) => _CoinListItem(coin: coins[index]), 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, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(LucideIcons.circleAlert, size: 48, color: colorScheme.error), Icon(LucideIcons.circleAlert, size: 48, color: colorScheme.error),
SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
Text( Text(
provider.error ?? '加载失败', provider.error ?? '加载失败',
style: TextStyle(color: colorScheme.error), style: TextStyle(color: colorScheme.error),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
ShadButton( ShadButton(
onPressed: () => provider.refresh(), onPressed: () => provider.refresh(),
child: const Text('重试'), child: const Text('重试'),
@@ -153,7 +205,7 @@ class _MarketPageState extends State<MarketPage>
} }
} }
/// 上半区大卡片BTC / ETH /// 精选卡片BTC / ETH (130px 高度,含迷你柱状图)
class _FeaturedCard extends StatelessWidget { class _FeaturedCard extends StatelessWidget {
final Coin coin; final Coin coin;
@@ -168,89 +220,146 @@ class _FeaturedCard extends StatelessWidget {
isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.down; isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.down;
final changeBgColor = isUp final changeBgColor = isUp
? AppColorScheme.getUpBackgroundColor(isDark) ? AppColorScheme.getUpBackgroundColor(isDark)
: colorScheme.error.withOpacity(0.1); : AppColorScheme.getDownBackgroundColor(isDark);
return GlassPanel( return GlassPanel(
padding: EdgeInsets.all(AppSpacing.lg), padding: const EdgeInsets.all(16),
height: 130,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
// 图标 + 币种代码 // 第一行:币种名称 + 涨跌徽章
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ 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( Text(
'\$${coin.formattedPrice}', '${coin.code}/USDT',
style: GoogleFonts.spaceGrotesk( style: GoogleFonts.inter(
fontSize: 18, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w600,
color: colorScheme.onSurface, color: colorScheme.onSurface,
), ),
), ),
SizedBox(height: AppSpacing.xs),
// 24h 涨跌幅
Container( Container(
padding: EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: changeBgColor, color: changeBgColor,
borderRadius: BorderRadius.circular(AppRadius.sm), borderRadius: BorderRadius.circular(4),
border: Border.all(color: changeColor.withOpacity(0.2)),
), ),
child: Text( child: Text(
coin.formattedChange, coin.formattedChange,
style: TextStyle( style: GoogleFonts.inter(
fontSize: 13, fontSize: 11,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w600,
color: changeColor, 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; final Coin coin;
const _CoinListItem({required this.coin}); const _CoinRow({required this.coin});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -258,102 +367,72 @@ class _CoinListItem extends StatelessWidget {
final isDark = Theme.of(context).brightness == Brightness.dark; final isDark = Theme.of(context).brightness == Brightness.dark;
final isUp = coin.isUp; final isUp = coin.isUp;
final changeColor = final changeColor =
isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.down; isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.getDownColor(isDark);
final changeBgColor = isUp final changeBgColor = isUp
? AppColorScheme.getUpBackgroundColor(isDark) ? AppColorScheme.getUpBackgroundColor(isDark)
: colorScheme.error.withOpacity(0.1); : AppColorScheme.getDownBackgroundColor(isDark);
return GestureDetector( return GestureDetector(
onTap: () => _navigateToTrade(context), onTap: () => _navigateToTrade(context),
child: GlassPanel( behavior: HitTestBehavior.opaque,
padding: EdgeInsets.symmetric( child: Padding(
horizontal: AppSpacing.md, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
vertical: AppSpacing.sm + AppSpacing.xs,
),
child: Row( child: Row(
children: [ children: [
// 币种图标 // 头像:圆形字母头像
Container( _CoinAvatar(letter: coin.displayIcon, code: coin.code),
width: 40, const SizedBox(width: 10),
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),
// 币种信息
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ mainAxisSize: MainAxisSize.min,
Row(
children: [ children: [
Text( Text(
coin.code, '${coin.code}/USDT',
style: GoogleFonts.spaceGrotesk( style: GoogleFonts.inter(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w700,
color: colorScheme.onSurface, color: colorScheme.onSurface,
), ),
), ),
SizedBox(width: AppSpacing.xs), const SizedBox(height: 2),
Text(
'/USDT',
style: TextStyle(
fontSize: 11,
color: colorScheme.onSurfaceVariant,
),
),
],
),
Text( Text(
coin.name, coin.name,
style: TextStyle( style: GoogleFonts.inter(
fontSize: 12, fontSize: 11,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant, color: colorScheme.onSurfaceVariant,
), ),
), ),
], ],
), ),
), ),
// 价格和涨跌幅 // 右侧:价格 + 涨跌标签
Column( Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
'\$${coin.formattedPrice}', '\$${coin.formattedPrice}',
style: GoogleFonts.spaceGrotesk( style: GoogleFonts.inter(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: colorScheme.onSurface, color: colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 2),
Container( Container(
padding: EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
horizontal: AppSpacing.sm,
vertical: 2,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: changeBgColor, color: changeBgColor,
borderRadius: BorderRadius.circular(AppRadius.sm), borderRadius: BorderRadius.circular(4),
border: Border.all(color: changeColor.withOpacity(0.2)),
), ),
child: Text( child: Text(
coin.formattedChange, coin.formattedChange,
style: TextStyle( style: GoogleFonts.inter(
fontSize: 12, fontSize: 11,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w500,
color: changeColor, 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 { class _EmptyState extends StatelessWidget {
final IconData icon; final IconData icon;
@@ -386,17 +514,17 @@ class _EmptyState extends StatelessWidget {
return Center( return Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(AppSpacing.xl), padding: const EdgeInsets.all(AppSpacing.xl),
child: Column( child: Column(
children: [ children: [
Icon(icon, size: 48, color: colorScheme.onSurfaceVariant), Icon(icon, size: 48, color: colorScheme.onSurfaceVariant),
SizedBox(height: AppSpacing.sm + AppSpacing.xs), const SizedBox(height: 12),
Text( Text(
message, message,
style: TextStyle(color: colorScheme.onSurfaceVariant), style: TextStyle(color: colorScheme.onSurfaceVariant),
), ),
if (onRetry != null) ...[ if (onRetry != null) ...[
SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
ShadButton( ShadButton(
onPressed: onRetry, onPressed: onRetry,
child: const Text('重试'), 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 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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:bot_toast/bot_toast.dart';
import 'package:provider/provider.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/utils/toast_utils.dart';
import '../../../core/event/app_event_bus.dart'; import '../../../core/event/app_event_bus.dart';
import '../../../providers/asset_provider.dart'; import '../../../providers/asset_provider.dart';
@@ -21,10 +25,6 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
int _activeTab = 0; // 0=全部, 1=充值, 2=提现 int _activeTab = 0; // 0=全部, 1=充值, 2=提现
StreamSubscription<AppEvent>? _eventSub; StreamSubscription<AppEvent>? _eventSub;
// 颜色常量
static const upColor = Color(0xFF00C853);
static const downColor = Color(0xFFFF5252);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -54,44 +54,83 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
@override @override
Widget build(BuildContext context) { 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( return Scaffold(
backgroundColor: theme.colorScheme.background, backgroundColor: bgColor,
appBar: AppBar( appBar: AppBar(
title: const Text('充提记录'), leading: IconButton(
backgroundColor: theme.colorScheme.background, 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, elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
), ),
body: Column( body: Column(
children: [ children: [
_buildTabs(), _buildFilterTabs(context, isDark),
Expanded(child: _buildOrderList()), 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( return Padding(
padding: const EdgeInsets.all(16), 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( child: Row(
children: [ children: [
_buildTab('全部', 0), _buildPillTab('全部', 0, activeBgColor, isDark),
const SizedBox(width: 12), _buildPillTab('充值', 1, activeBgColor, isDark),
_buildTab('充值', 1), _buildPillTab('提现', 2, activeBgColor, isDark),
const SizedBox(width: 12),
_buildTab('提现', 2),
], ],
), ),
),
); );
} }
Widget _buildTab(String label, int index) { Widget _buildPillTab(
final theme = ShadTheme.of(context); String label,
int index,
Color activeBgColor,
bool isDark,
) {
final isActive = _activeTab == index; final isActive = _activeTab == index;
final activeTextColor = isDark
? AppColorScheme.darkBackground
: AppColorScheme.lightOnSurface;
final inactiveTextColor = isDark
? AppColorScheme.darkOnSurfaceVariant
: AppColorScheme.lightOnSurfaceVariant;
return Expanded( return Expanded(
child: GestureDetector( child: GestureDetector(
@@ -102,20 +141,17 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
} }
}, },
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isActive ? theme.colorScheme.primary : theme.colorScheme.card, color: isActive ? activeBgColor : Colors.transparent,
borderRadius: BorderRadius.circular(8), borderRadius: AppRadius.radiusSm,
border: Border.all(
color: isActive ? theme.colorScheme.primary : theme.colorScheme.border,
),
), ),
child: Center( child: Center(
child: Text( child: Text(
label, label,
style: TextStyle( style: GoogleFonts.inter(
color: isActive ? Colors.white : theme.colorScheme.mutedForeground, fontSize: 14,
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal, 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>( return Consumer<AssetProvider>(
builder: (context, provider, _) { builder: (context, provider, _) {
final orders = provider.fundOrders; final orders = provider.fundOrders;
@@ -142,12 +181,19 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
Icon( Icon(
LucideIcons.inbox, LucideIcons.inbox,
size: 64, size: 64,
color: Colors.grey[400], color: isDark
? AppColorScheme.darkOnSurfaceMuted
: AppColorScheme.lightOnSurfaceMuted,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( 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( return RefreshIndicator(
onRefresh: () async => _loadData(), onRefresh: () async => _loadData(),
child: ListView.separated( child: ListView.separated(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
itemCount: orders.length, itemCount: orders.length,
separatorBuilder: (_, __) => const SizedBox(height: 12), separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) { 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); // Order Card
final isDeposit = order.isDeposit; // ---------------------------------------------------------------------------
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), padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: cardBg,
borderRadius: AppRadius.radiusLg,
border: Border.all(color: borderColor, width: 1),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( // Header: type badge + status badge
mainAxisAlignment: MainAxisAlignment.spaceBetween, _buildCardHeader(order, isDark),
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),
),
],
),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( // Amount
mainAxisAlignment: MainAxisAlignment.spaceBetween, _buildAmountRow(order, primaryText),
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('取消'),
),
],
],
),
],
),
const SizedBox(height: 12), const SizedBox(height: 12),
// 显示地址信息 // Detail rows
if (order.walletAddress != null) ...[ _buildDetailRows(order, primaryText, mutedText),
const Divider(), // Rejection reason
if (order.rejectReason != null) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
Row( _buildRejectionReason(order),
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]),
),
],
),
],
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), const SizedBox(height: 8),
Row( _buildPayableRow(order, isDark, primaryText),
mainAxisAlignment: MainAxisAlignment.spaceBetween, ],
children: [ // Action buttons
Text( if (order.canCancel || order.canConfirmPay) ...[
'创建: ${_formatTime(order.createTime)}', const SizedBox(height: 12),
style: const TextStyle(fontSize: 11, color: Colors.grey), _buildActions(order, isDark),
),
if (order.rejectReason != null)
Expanded(
child: Text(
'驳回: ${order.rejectReason}',
style: const TextStyle(fontSize: 11, color: downColor),
textAlign: TextAlign.right,
overflow: TextOverflow.ellipsis,
),
),
], ],
),
], ],
), ),
); );
} }
Widget _buildStatusBadge(OrderFund order) { // ---------------------------------------------------------------------------
// 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 bgColor;
Color textColor; Color textColor;
// 根据类型和状态设置颜色 if (order.isDeposit) {
if (order.type == 1) {
// 充值状态
switch (order.status) { switch (order.status) {
case 1: // 待付款 case 1: // 待付款
case 2: // 待确认 case 2: // 待确认
bgColor = Colors.orange.withValues(alpha: 0.1); bgColor = amberBg;
textColor = Colors.orange; textColor = amberColor;
break; break;
case 3: // 已完成 case 3: // 已完成
bgColor = upColor.withValues(alpha: 0.1); bgColor = upBg;
textColor = upColor; textColor = upColor;
break; break;
default: // 已驳回/已取消 default: // 已驳回/已取消
bgColor = downColor.withValues(alpha: 0.1); bgColor = downBg;
textColor = downColor; textColor = downColor;
} }
} else { } else {
// 提现状态
switch (order.status) { switch (order.status) {
case 1: // 待审批 case 1: // 待审批
bgColor = Colors.orange.withValues(alpha: 0.1); case 5: // 待财务审核
textColor = Colors.orange; bgColor = amberBg;
textColor = amberColor;
break; break;
case 2: // 已完成 case 2: // 已完成
bgColor = upColor.withValues(alpha: 0.1); bgColor = upBg;
textColor = upColor; textColor = upColor;
break; break;
default: // 已驳回/已取消 default: // 已驳回/已取消
bgColor = downColor.withValues(alpha: 0.1); bgColor = downBg;
textColor = downColor; textColor = downColor;
} }
} }
return _buildBadge(order.statusText, textColor, bgColor);
}
Widget _buildBadge(String text, Color textColor, Color bgColor) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: bgColor, color: bgColor,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: Text( child: Text(
order.statusText, text,
style: TextStyle(fontSize: 11, color: textColor), 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(
mainAxisSize: MainAxisSize.min,
children: [
Text(
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,
),
),
],
),
);
}
// ---------------------------------------------------------------------------
// Action Buttons
// ---------------------------------------------------------------------------
Widget _buildActions(OrderFund order, bool isDark) {
final upColor = AppColorScheme.getUpColor(isDark);
final downColor = AppColorScheme.getDownColor(isDark);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (order.canCancel)
GestureDetector(
onTap: () => _cancelOrder(order),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
borderRadius: AppRadius.radiusSm,
border: Border.all(color: downColor, width: 1),
),
child: Text(
'取消订单',
style: 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) { String _formatTime(DateTime? time) {
if (time == null) return '-'; if (time == null) return '-';
return '${time.year}-${time.month.toString().padLeft(2, '0')}-${time.day.toString().padLeft(2, '0')} ' 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 { void _confirmPay(OrderFund order) async {
final confirmed = await showShadDialog<bool>( final confirmed = await showShadConfirmDialog(
context: context, context: context,
builder: (context) => ShadDialog.alert( title: '确认已打款',
title: const Text('确认已打款'), description: '确认您已完成向指定地址的转账?',
description: const Text('确认您已完成向指定地址的转账?'),
actions: [
ShadButton.outline(
child: const Text('取消'),
onPressed: () => Navigator.pop(context, false),
),
ShadButton(
child: const Text('确认'),
onPressed: () => Navigator.pop(context, true),
),
],
),
); );
if (confirmed == true && mounted) { if (confirmed == true && mounted) {
final response = await context.read<AssetProvider>().confirmPay(order.orderNo); final response = await context.read<AssetProvider>().confirmPay(order.orderNo);
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( BotToast.showText(text: response.success ? '确认成功,请等待审核' : response.message ?? '确认失败');
SnackBar(content: Text(response.success ? '确认成功,请等待审核' : response.message ?? '确认失败')),
);
} }
} }
} }
void _cancelOrder(OrderFund order) async { void _cancelOrder(OrderFund order) async {
final confirmed = await showShadDialog<bool>( final confirmed = await showShadConfirmDialog(
context: context, context: context,
builder: (context) => ShadDialog.alert( title: '取消订单',
title: const Text('取消订单'), description: '确定要取消订单 ${order.orderNo} 吗?',
description: Text('确定要取消订单 ${order.orderNo} 吗?'), destructive: true,
actions: [
ShadButton.outline(
child: const Text('返回'),
onPressed: () => Navigator.pop(context, false),
),
ShadButton.destructive(
child: const Text('确定取消'),
onPressed: () => Navigator.pop(context, true),
),
],
),
); );
if (confirmed == true && mounted) { if (confirmed == true && mounted) {
final response = await context.read<AssetProvider>().cancelOrder(order.orderNo); final response = await context.read<AssetProvider>().cancelOrder(order.orderNo);
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( BotToast.showText(text: response.success ? '订单已取消' : response.message ?? '取消失败');
SnackBar(content: 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),
),
],
),
); );
} }
}
}
} }

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.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_color_scheme.dart';
import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_spacing.dart';
import '../../../data/models/coin.dart'; import '../../../data/models/coin.dart';
@@ -12,6 +13,13 @@ import '../../components/glass_panel.dart';
import '../../components/neon_glow.dart'; import '../../components/neon_glow.dart';
/// 交易页面 /// 交易页面
///
/// 设计稿 Trade 页面,布局结构:
/// - 币种选择器卡片Coin Selector Card
/// - 价格卡片Price Card大号价格 + 涨跌幅徽章 + 副标题
/// - 买入/卖出切换Buy/Sell Toggle
/// - 交易表单卡片Trade Form Card金额输入 + 快捷比例 + 计算数量
/// - CTA 买入/卖出按钮Buy/Sell Button
class TradePage extends StatefulWidget { class TradePage extends StatefulWidget {
final String? initialCoinCode; final String? initialCoinCode;
@@ -108,11 +116,14 @@ class _TradePageState extends State<TradePage>
backgroundColor: colorScheme.background, backgroundColor: colorScheme.background,
body: Consumer2<MarketProvider, AssetProvider>( body: Consumer2<MarketProvider, AssetProvider>(
builder: (context, market, asset, _) { builder: (context, market, asset, _) {
return SingleChildScrollView( return SafeArea(
padding: AppSpacing.pagePadding, child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(
AppSpacing.md, 0, AppSpacing.md, AppSpacing.xl + AppSpacing.sm,
),
child: Column( child: Column(
children: [ children: [
// 币种选择器 // 币种选择器卡片
_CoinSelector( _CoinSelector(
selectedCoin: _selectedCoin, selectedCoin: _selectedCoin,
coins: market.allCoins coins: market.allCoins
@@ -128,17 +139,19 @@ class _TradePageState extends State<TradePage>
}); });
}, },
), ),
SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
// 价格卡片 // 价格卡片
if (_selectedCoin != null) if (_selectedCoin != null)
_PriceCard(coin: _selectedCoin!) _PriceCard(coin: _selectedCoin!)
else else
const _PlaceholderCard(message: '请先选择交易币种'), _PlaceholderCard(
message: '请先选择交易币种',
colorScheme: colorScheme,
),
const SizedBox(height: AppSpacing.md),
SizedBox(height: AppSpacing.md), // 交易表单卡片(内含买入/卖出切换 + 表单)
// 交易表单
_TradeFormCard( _TradeFormCard(
tradeType: _tradeType, tradeType: _tradeType,
selectedCoin: _selectedCoin, selectedCoin: _selectedCoin,
@@ -154,40 +167,22 @@ class _TradePageState extends State<TradePage>
onAmountChanged: () => setState(() {}), onAmountChanged: () => setState(() {}),
onFillPercent: (pct) => _fillPercent(pct), onFillPercent: (pct) => _fillPercent(pct),
), ),
const SizedBox(height: AppSpacing.md),
SizedBox(height: AppSpacing.lg), // CTA 买入/卖出按钮
SizedBox(
// 买入 + 卖出双按钮 width: double.infinity,
Row( height: 48,
children: [
Expanded(
child: _TradeButton( child: _TradeButton(
isBuy: true, isBuy: _tradeType == 0,
coinCode: _selectedCoin?.code, coinCode: _selectedCoin?.code,
enabled: _canTrade() && !_isSubmitting, enabled: _canTrade() && !_isSubmitting,
isLoading: _isSubmitting && _tradeType == 0, isLoading: _isSubmitting,
onPressed: () { onPressed: _executeTrade,
_tradeType = 0;
_executeTrade();
},
),
),
SizedBox(width: AppSpacing.md),
Expanded(
child: _TradeButton(
isBuy: false,
coinCode: _selectedCoin?.code,
enabled: _canTrade() && !_isSubmitting,
isLoading: _isSubmitting && _tradeType == 1,
onPressed: () {
_tradeType = 1;
_executeTrade();
},
), ),
), ),
], ],
), ),
],
), ),
); );
}, },
@@ -330,7 +325,7 @@ class _ConfirmDialog extends StatelessWidget {
Center( Center(
child: Text( child: Text(
'确认${isBuy ? '买入' : '卖出'}', '确认${isBuy ? '买入' : '卖出'}',
style: GoogleFonts.spaceGrotesk( style: GoogleFonts.inter(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: colorScheme.onSurface, color: colorScheme.onSurface,
@@ -382,10 +377,13 @@ class _ConfirmDialog extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(label, Text(label,
style: TextStyle( style: GoogleFonts.inter(
fontSize: 14, color: colorScheme.onSurfaceVariant)), fontSize: 14,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
)),
Text(value, Text(value,
style: GoogleFonts.spaceGrotesk( style: GoogleFonts.inter(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: valueColor ?? colorScheme.onSurface, color: valueColor ?? colorScheme.onSurface,
@@ -395,7 +393,12 @@ class _ConfirmDialog extends StatelessWidget {
} }
} }
/// 币种选择器 // ============================================
// 币种选择器 - 设计稿 Coin Selector Card
// card背景 + 圆角lg + border + padding:16
// 横向布局coinInfo(竖向 pair+name) + chevronDown
// ============================================
class _CoinSelector extends StatelessWidget { class _CoinSelector extends StatelessWidget {
final Coin? selectedCoin; final Coin? selectedCoin;
final List<Coin> coins; final List<Coin> coins;
@@ -410,41 +413,54 @@ class _CoinSelector extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return GestureDetector( return GestureDetector(
onTap: () => _showCoinPicker(context), onTap: () => _showCoinPicker(context),
child: GlassPanel( child: Container(
padding: EdgeInsets.all(AppSpacing.md), width: double.infinity,
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
_CoinAvatar(icon: selectedCoin?.displayIcon), // 币种信息:交易对 + 名称
SizedBox(width: AppSpacing.sm + AppSpacing.xs), Column(
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
selectedCoin != null selectedCoin != null
? '${selectedCoin!.code}/USDT' ? '${selectedCoin!.code}/USDT'
: '选择币种', : '选择币种',
style: GoogleFonts.spaceGrotesk( style: GoogleFonts.inter(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w700,
color: colorScheme.onSurface, color: colorScheme.onSurface,
), ),
), ),
SizedBox(height: AppSpacing.xs), const SizedBox(height: 2),
Text( Text(
selectedCoin?.name ?? '点击选择交易对', selectedCoin?.name ?? '点击选择交易对',
style: TextStyle( style: GoogleFonts.inter(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant, color: colorScheme.onSurfaceVariant,
), ),
), ),
], ],
), ),
), // 下拉箭头
Icon(LucideIcons.chevronDown, color: colorScheme.onSurfaceVariant), Icon(LucideIcons.chevronDown,
size: 16, color: colorScheme.onSurfaceVariant),
], ],
), ),
), ),
@@ -470,6 +486,7 @@ class _CoinSelector extends StatelessWidget {
), ),
child: Column( child: Column(
children: [ children: [
// 拖动指示器
Container( Container(
margin: EdgeInsets.only(top: AppSpacing.sm), margin: EdgeInsets.only(top: AppSpacing.sm),
width: 40, width: 40,
@@ -479,13 +496,14 @@ class _CoinSelector extends StatelessWidget {
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
), ),
), ),
// 标题栏
Padding( Padding(
padding: EdgeInsets.all(AppSpacing.lg), padding: EdgeInsets.all(AppSpacing.lg),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text('选择币种', Text('选择币种',
style: GoogleFonts.spaceGrotesk( style: GoogleFonts.inter(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: colorScheme.onSurface, color: colorScheme.onSurface,
@@ -499,6 +517,7 @@ class _CoinSelector extends StatelessWidget {
), ),
), ),
Divider(height: 1, color: colorScheme.outlineVariant.withOpacity(0.2)), Divider(height: 1, color: colorScheme.outlineVariant.withOpacity(0.2)),
// 币种列表
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
padding: EdgeInsets.symmetric(vertical: AppSpacing.sm), padding: EdgeInsets.symmetric(vertical: AppSpacing.sm),
@@ -528,8 +547,8 @@ class _CoinSelector extends StatelessWidget {
onCoinSelected(coin); onCoinSelected(coin);
}, },
child: Container( child: Container(
padding: padding: EdgeInsets.symmetric(
EdgeInsets.symmetric(horizontal: AppSpacing.lg, vertical: AppSpacing.md), horizontal: AppSpacing.lg, vertical: AppSpacing.md),
color: color:
isSelected ? colorScheme.primary.withOpacity(0.1) : Colors.transparent, isSelected ? colorScheme.primary.withOpacity(0.1) : Colors.transparent,
child: Row( child: Row(
@@ -540,38 +559,38 @@ class _CoinSelector extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 第一行:币种代码 + 价格 + 涨跌幅 // 第一行:币种代码 + USDT + 价格 + 涨跌幅
Row( Row(
children: [ children: [
Text(coin.code, Text(coin.code,
style: GoogleFonts.spaceGrotesk( style: GoogleFonts.inter(
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: colorScheme.onSurface, color: colorScheme.onSurface,
)), )),
SizedBox(width: AppSpacing.xs), SizedBox(width: AppSpacing.xs),
Text('/USDT', Text('/USDT',
style: TextStyle( style: GoogleFonts.inter(
fontSize: 11, fontSize: 11,
color: colorScheme.onSurfaceVariant, color: colorScheme.onSurfaceVariant,
)), )),
const Spacer(), const Spacer(),
Text('\$${coin.formattedPrice}', Text('\$${coin.formattedPrice}',
style: GoogleFonts.spaceGrotesk( style: GoogleFonts.inter(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: colorScheme.onSurface, color: colorScheme.onSurface,
)), )),
SizedBox(width: AppSpacing.sm), SizedBox(width: AppSpacing.sm),
// 涨跌幅徽章
Container( Container(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
horizontal: 6, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: changeColor.withOpacity(0.1), color: changeColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm), borderRadius: BorderRadius.circular(AppRadius.sm),
), ),
child: Text(coin.formattedChange, child: Text(coin.formattedChange,
style: TextStyle( style: GoogleFonts.inter(
fontSize: 11, fontSize: 11,
color: changeColor, color: changeColor,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -587,7 +606,7 @@ class _CoinSelector extends StatelessWidget {
SizedBox(height: 3), SizedBox(height: 3),
// 第二行:币种名称 // 第二行:币种名称
Text(coin.name, Text(coin.name,
style: TextStyle( style: GoogleFonts.inter(
fontSize: 12, fontSize: 12,
color: colorScheme.onSurfaceVariant, color: colorScheme.onSurfaceVariant,
)), )),
@@ -629,7 +648,14 @@ class _CoinAvatar extends StatelessWidget {
} }
} }
/// 价格卡片 - 重新设计 // ============================================
// 价格卡片 - 设计稿 Price Card
// card背景 + 圆角lg + border + padding:20 + gap:8
// 竖向布局:
// priceRow: 大号价格(32px bold) + 涨跌幅徽章(圆角sm涨绿背景)
// subtitle: "24h 变化"
// ============================================
class _PriceCard extends StatelessWidget { class _PriceCard extends StatelessWidget {
final Coin coin; final Coin coin;
const _PriceCard({required this.coin}); const _PriceCard({required this.coin});
@@ -640,80 +666,68 @@ class _PriceCard extends StatelessWidget {
final isDark = Theme.of(context).brightness == Brightness.dark; final isDark = Theme.of(context).brightness == Brightness.dark;
final isUp = coin.isUp; final isUp = coin.isUp;
final changeColor = final changeColor =
isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.down; isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.getDownColor(isDark);
final changeBgColor = isUp final changeBgColor = isUp
? AppColorScheme.getUpBackgroundColor(isDark) ? AppColorScheme.getUpBackgroundColor(isDark)
: colorScheme.error.withOpacity(0.1); : AppColorScheme.getDownBackgroundColor(isDark);
return GlassPanel( return Container(
padding: EdgeInsets.symmetric( width: double.infinity,
horizontal: AppSpacing.lg, vertical: AppSpacing.md + AppSpacing.sm), padding: const EdgeInsets.all(20),
child: Row( decoration: BoxDecoration(
children: [ color: isDark
// 左侧:币种标签 + 价格 ? colorScheme.surfaceContainer
Expanded( : colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 价格行:大号价格 + 涨跌幅徽章
Row( Row(
children: [ children: [
Text(
coin.formattedPrice,
style: GoogleFonts.inter(
fontSize: 32,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
fontFeatures: [FontFeature.tabularFigures()],
),
),
const SizedBox(width: AppSpacing.sm),
// 涨跌幅徽章 - 圆角sm涨绿背景
Container( Container(
padding: EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm, vertical: 2), horizontal: AppSpacing.sm, vertical: AppSpacing.xs),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.08), color: changeBgColor,
borderRadius: BorderRadius.circular(AppRadius.sm), borderRadius: BorderRadius.circular(AppRadius.sm),
), ),
child: Text( child: Text(
'${coin.code}/USDT',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: colorScheme.primary,
),
),
),
],
),
SizedBox(height: AppSpacing.sm),
Text(
'\$${coin.formattedPrice}',
style: GoogleFonts.spaceGrotesk(
fontSize: 20,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
],
),
),
// 右侧:涨跌幅
Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md, vertical: AppSpacing.sm + 2),
decoration: BoxDecoration(
color: changeBgColor,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(color: changeColor.withOpacity(0.15)),
),
child: Column(
children: [
Text(
'24h',
style: TextStyle(
fontSize: 10,
color: changeColor.withOpacity(0.7),
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 2),
Text(
coin.formattedChange, coin.formattedChange,
style: TextStyle( style: GoogleFonts.inter(
fontSize: 16, color: changeColor, fontWeight: FontWeight.w700), fontSize: 12,
fontWeight: FontWeight.w600,
color: changeColor,
fontFeatures: [FontFeature.tabularFigures()],
),
),
), ),
], ],
), ),
const SizedBox(height: AppSpacing.sm),
// 副标题
Text(
'24h 变化',
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
), ),
], ],
), ),
@@ -724,16 +738,27 @@ class _PriceCard extends StatelessWidget {
/// 占位卡片 /// 占位卡片
class _PlaceholderCard extends StatelessWidget { class _PlaceholderCard extends StatelessWidget {
final String message; final String message;
const _PlaceholderCard({required this.message}); final ColorScheme colorScheme;
const _PlaceholderCard({required this.message, required this.colorScheme});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme; final isDark = Theme.of(context).brightness == Brightness.dark;
return GlassPanel( return Container(
padding: EdgeInsets.all(AppSpacing.xl), width: double.infinity,
padding: const EdgeInsets.all(AppSpacing.xl),
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Center( child: Center(
child: Text(message, child: Text(message,
style: TextStyle( style: GoogleFonts.inter(
color: colorScheme.onSurfaceVariant, color: colorScheme.onSurfaceVariant,
fontSize: 14, fontSize: 14,
)), )),
@@ -742,7 +767,18 @@ class _PlaceholderCard extends StatelessWidget {
} }
} }
/// 交易表单卡片 - 重新设计 // ============================================
// 交易表单卡片 - 设计稿 Trade Form Card
// card背景 + 圆角lg + border + padding:20 + gap:16
// 竖向布局:
// Buy/Sell Toggle圆角mdclip横向两等宽按钮
// 金额label行"交易金额" + "USDT"
// 输入框bg-tertiary圆角md高48
// 可用余额文字
// 快捷比例按钮行25% 50% 75% 100%gap:8
// 计算数量行
// ============================================
class _TradeFormCard extends StatelessWidget { class _TradeFormCard extends StatelessWidget {
final int tradeType; final int tradeType;
final Coin? selectedCoin; final Coin? selectedCoin;
@@ -777,66 +813,120 @@ class _TradeFormCard extends StatelessWidget {
? AppColorScheme.getUpColor(isDark) ? AppColorScheme.getUpColor(isDark)
: AppColorScheme.getDownColor(isDark); : AppColorScheme.getDownColor(isDark);
return GlassPanel( // 设计稿中 card 背景色
padding: EdgeInsets.all(AppSpacing.lg), final cardBgColor = isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerLowest;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: cardBgColor,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 买入/卖出切换 - 重新设计 // ---- 买入/卖出切换 ----
Container( // 设计稿ClipRRect + 圆角md两等宽按钮
padding: EdgeInsets.all(4), ClipRRect(
decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppRadius.md),
color: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.xl),
),
child: Row( child: Row(
children: [ children: [
// 买入按钮
Expanded( Expanded(
child: _buildTypeButton( child: GestureDetector(
context: context,
label: '买入',
isActive: isBuy,
color: AppColorScheme.buyButtonFill,
icon: LucideIcons.trendingUp,
onTap: () => onTradeTypeChanged(0), onTap: () => onTradeTypeChanged(0),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
height: 40,
decoration: BoxDecoration(
color: isBuy
? AppColorScheme.buyButtonFill
: cardBgColor,
border: isBuy
? null
: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15)),
),
child: Center(
child: Text(
'买入',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: isBuy
? Colors.white
: colorScheme.onSurfaceVariant,
), ),
), ),
SizedBox(width: 4), ),
),
),
),
// 卖出按钮
Expanded( Expanded(
child: _buildTypeButton( child: GestureDetector(
context: context,
label: '卖出',
isActive: !isBuy,
color: AppColorScheme.sellButtonFill,
icon: LucideIcons.trendingDown,
onTap: () => onTradeTypeChanged(1), onTap: () => onTradeTypeChanged(1),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
height: 40,
decoration: BoxDecoration(
color: !isBuy
? AppColorScheme.sellButtonFill
: cardBgColor,
border: !isBuy
? null
: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15)),
),
child: Center(
child: Text(
'卖出',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: !isBuy
? Colors.white
: colorScheme.onSurfaceVariant,
),
),
),
),
), ),
), ),
], ],
), ),
), ),
SizedBox(height: AppSpacing.lg), const SizedBox(height: AppSpacing.md + AppSpacing.sm),
// 交易金额输入 // ---- 交易金额 label 行 ----
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text('交易金额', Text('交易金额',
style: TextStyle( style: GoogleFonts.inter(
fontSize: 13, fontSize: 12,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
)),
Text('USDT',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: colorScheme.onSurface, color: colorScheme.onSurface,
)), )),
Text('USDT',
style: GoogleFonts.spaceGrotesk(
fontSize: 11,
fontWeight: FontWeight.w700,
color: actionColor.withOpacity(0.7),
letterSpacing: 0.5,
)),
], ],
), ),
SizedBox(height: AppSpacing.sm), const SizedBox(height: AppSpacing.sm),
// ---- 金额输入框 ----
_AmountInput( _AmountInput(
amountController: amountController, amountController: amountController,
maxAmount: maxAmount, maxAmount: maxAmount,
@@ -844,153 +934,79 @@ class _TradeFormCard extends StatelessWidget {
actionColor: actionColor, actionColor: actionColor,
onChanged: onAmountChanged, onChanged: onAmountChanged,
), ),
SizedBox(height: AppSpacing.sm), const SizedBox(height: AppSpacing.sm),
// 快捷比例按钮 - 药丸样式 // ---- 可用余额 ----
Row(
children: [
_buildPctButton('25%', 0.25, colorScheme, actionColor),
SizedBox(width: 6),
_buildPctButton('50%', 0.5, colorScheme, actionColor),
SizedBox(width: 6),
_buildPctButton('75%', 0.75, colorScheme, actionColor),
SizedBox(width: 6),
_buildPctButton('全部', 1.0, colorScheme, actionColor),
],
),
SizedBox(height: AppSpacing.lg),
// 预计数量 + 可用余额 - 卡片样式
Container(
padding: EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.1)),
),
child: Column(
children: [
// 预计数量
Row(
children: [
Icon(LucideIcons.calculator,
size: 14, color: colorScheme.onSurfaceVariant),
SizedBox(width: AppSpacing.sm),
Text('预计数量',
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
)),
const Spacer(),
Text(
'$calculatedQuantity ${selectedCoin?.code ?? ''}',
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
],
),
Padding(
padding: EdgeInsets.symmetric(vertical: AppSpacing.sm),
child: Divider(
height: 1,
color: colorScheme.outlineVariant.withOpacity(0.08)),
),
// 可用余额
Row(
children: [
Icon(LucideIcons.wallet,
size: 14, color: colorScheme.onSurfaceVariant),
SizedBox(width: AppSpacing.sm),
Text(isBuy ? '可用 USDT' : '可用 ${selectedCoin?.code ?? ""}',
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
)),
const Spacer(),
Text( Text(
isBuy isBuy
? '$availableUsdt USDT' ? '可用: $availableUsdt USDT'
: '$availableCoinQty ${selectedCoin?.code ?? ""}', : '可用: $availableCoinQty ${selectedCoin?.code ?? ""}',
style: GoogleFonts.spaceGrotesk( style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: AppSpacing.md),
// ---- 快捷比例按钮 25% 50% 75% 100% ----
// 设计稿gap:8圆角smbg-tertiary高32
Row(
children: [
_buildPctButton('25%', 0.25, colorScheme),
const SizedBox(width: AppSpacing.sm),
_buildPctButton('50%', 0.5, colorScheme),
const SizedBox(width: AppSpacing.sm),
_buildPctButton('75%', 0.75, colorScheme),
const SizedBox(width: AppSpacing.sm),
_buildPctButton('100%', 1.0, colorScheme),
],
),
const SizedBox(height: AppSpacing.md + AppSpacing.sm),
// ---- 计算数量行 ----
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('交易数量',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
)),
Text(
'$calculatedQuantity ${selectedCoin?.code ?? ''}',
style: GoogleFonts.inter(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: colorScheme.onSurface, color: colorScheme.onSurface,
fontFeatures: [FontFeature.tabularFigures()],
), ),
), ),
], ],
), ),
], ],
), ),
),
],
),
); );
} }
/// 买入/卖出切换按钮 - 实心填充 + 图标 /// 百分比按钮 - 设计稿圆角smbg-tertiary高32
Widget _buildTypeButton({ Widget _buildPctButton(String label, double pct, ColorScheme colorScheme) {
required BuildContext context,
required String label,
required bool isActive,
required Color color,
required IconData icon,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
padding: EdgeInsets.symmetric(vertical: AppSpacing.sm + 4),
decoration: BoxDecoration(
color: isActive ? color : Colors.transparent,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon,
size: 16,
color: isActive ? Colors.white : color.withOpacity(0.5)),
SizedBox(width: AppSpacing.xs),
Text(
label,
style: TextStyle(
color: isActive ? Colors.white : color.withOpacity(0.5),
fontWeight: FontWeight.w700,
fontSize: 15,
letterSpacing: 0.5,
),
),
],
),
),
);
}
/// 百分比按钮 - 药丸样式
Widget _buildPctButton(
String label, double pct, ColorScheme colorScheme, Color actionColor) {
return Expanded( return Expanded(
child: GestureDetector( child: GestureDetector(
onTap: () => onFillPercent(pct), onTap: () => onFillPercent(pct),
child: Container( child: Container(
padding: EdgeInsets.symmetric(vertical: AppSpacing.sm - 2), height: 32,
decoration: BoxDecoration( decoration: BoxDecoration(
color: actionColor.withOpacity(0.06), color: colorScheme.surfaceContainerHighest.withOpacity(0.5),
borderRadius: BorderRadius.circular(AppRadius.full), borderRadius: BorderRadius.circular(AppRadius.sm),
border: Border.all(color: actionColor.withOpacity(0.12)),
), ),
child: Center( child: Center(
child: Text(label, child: Text(label,
style: TextStyle( style: GoogleFonts.inter(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w500,
color: actionColor.withOpacity(0.8), color: colorScheme.onSurfaceVariant,
)), )),
), ),
), ),
@@ -999,7 +1015,11 @@ class _TradeFormCard extends StatelessWidget {
} }
} }
/// 交易按钮 - 使用 NeonButton 风格 // ============================================
// CTA 交易按钮 - 设计稿 Buy Button
// profit-green底 / sell-red底圆角lg高48白字16px bold
// ============================================
class _TradeButton extends StatelessWidget { class _TradeButton extends StatelessWidget {
final bool isBuy; final bool isBuy;
final String? coinCode; final String? coinCode;
@@ -1025,7 +1045,7 @@ class _TradeButton extends StatelessWidget {
onTap: enabled ? onPressed : null, onTap: enabled ? onPressed : null,
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
height: 52, height: 48,
decoration: BoxDecoration( decoration: BoxDecoration(
color: enabled ? fillColor : colorScheme.onSurface.withOpacity(0.08), color: enabled ? fillColor : colorScheme.onSurface.withOpacity(0.08),
borderRadius: BorderRadius.circular(AppRadius.lg), borderRadius: BorderRadius.circular(AppRadius.lg),
@@ -1040,37 +1060,27 @@ class _TradeButton extends StatelessWidget {
color: Colors.white, color: Colors.white,
), ),
) )
: Row( : Text(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isBuy ? LucideIcons.trendingUp : LucideIcons.trendingDown,
size: 16,
color: enabled
? Colors.white
: colorScheme.onSurface.withOpacity(0.3),
),
SizedBox(width: AppSpacing.xs),
Text(
'${isBuy ? '买入' : '卖出'} ${coinCode ?? ""}', '${isBuy ? '买入' : '卖出'} ${coinCode ?? ""}',
style: GoogleFonts.spaceGrotesk( style: GoogleFonts.inter(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: enabled color: enabled
? Colors.white ? Colors.white
: colorScheme.onSurface.withOpacity(0.3), : colorScheme.onSurface.withOpacity(0.3),
letterSpacing: 0.5,
), ),
), ),
],
),
), ),
), ),
); );
} }
} }
/// 金额输入框(含超额提示) // ============================================
// 金额输入框(含超额提示)
// 设计稿bg-tertiary圆角md高48
// ============================================
class _AmountInput extends StatefulWidget { class _AmountInput extends StatefulWidget {
final TextEditingController amountController; final TextEditingController amountController;
final String maxAmount; final String maxAmount;
@@ -1124,56 +1134,32 @@ class _AmountInputState extends State<_AmountInput> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
height: 48,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.surfaceContainerLowest, color: colorScheme.surfaceContainerHighest.withOpacity(0.3),
borderRadius: BorderRadius.circular(AppRadius.xl), borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(
color: _isExceeded
? warningColor.withOpacity(0.5)
: widget.actionColor.withOpacity(0.15),
),
), ),
child: TextField( child: TextField(
controller: widget.amountController, controller: widget.amountController,
keyboardType: const TextInputType.numberWithOptions(decimal: true), keyboardType: const TextInputType.numberWithOptions(decimal: true),
onChanged: (_) => _checkLimit(), onChanged: (_) => _checkLimit(),
style: GoogleFonts.spaceGrotesk( style: GoogleFonts.inter(
fontSize: 18, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.normal,
color: colorScheme.onSurface, color: colorScheme.onSurface,
fontFeatures: [FontFeature.tabularFigures()],
), ),
decoration: InputDecoration( decoration: InputDecoration(
hintText: '0.00', hintText: '请输入金额',
hintStyle: TextStyle( hintStyle: GoogleFonts.inter(
color: colorScheme.outlineVariant.withOpacity(0.4)), fontSize: 14,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant.withOpacity(0.5),
),
border: InputBorder.none, border: InputBorder.none,
contentPadding: EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md, horizontal: AppSpacing.md,
vertical: AppSpacing.md,
), ),
suffixIcon: Padding(
padding: EdgeInsets.only(right: AppSpacing.sm),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.sm, vertical: AppSpacing.xs),
decoration: BoxDecoration(
color: widget.actionColor.withOpacity(0.08),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text('USDT',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: widget.actionColor.withOpacity(0.7),
)),
),
],
),
),
suffixIconConstraints: const BoxConstraints(minWidth: 60),
), ),
), ),
), ),
@@ -1186,7 +1172,10 @@ class _AmountInputState extends State<_AmountInput> {
SizedBox(width: 4), SizedBox(width: 4),
Text( Text(
'超出可用USDT余额', '超出可用USDT余额',
style: TextStyle(fontSize: 11, color: warningColor), style: GoogleFonts.inter(
fontSize: 11,
color: warningColor,
),
), ),
], ],
), ),

View File

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