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,135 +176,49 @@ 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,
duration: const Duration(milliseconds: 300), textPrimary: textPrimary,
switchInCurve: Curves.easeInOut, textSecondary: textSecondary,
switchOutCurve: Curves.easeInOut, textMuted: textMuted,
transitionBuilder: (widget, animation) { textInverse: textInverse,
return SlideTransition( accentPrimary: accentPrimary,
position: Tween<Offset>(
begin: const Offset(0, -1),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: widget,
),
);
},
child: _direction == 1
? _buildAccountCard(
key: const ValueKey('from-card'),
label: '',
accountName: _fromLabel,
balance: _fromBalance,
isDark: isDark,
colorScheme: colorScheme,
)
: _buildAccountCard(
key: const ValueKey('to-card-top'),
label: '',
accountName: _toLabel,
balance: _toBalance,
isDark: isDark,
colorScheme: colorScheme,
),
);
},
), ),
// 方向切换按钮(固定在中间) const SizedBox(height: 24),
GestureDetector(
onTap: _toggleDirection, // --- Amount Section ---
child: Container( _buildAmountSection(
margin: EdgeInsets.symmetric(vertical: AppSpacing.sm), isDark: isDark,
padding: EdgeInsets.all(AppSpacing.sm + AppSpacing.xs), bgTertiary: bgTertiary,
decoration: BoxDecoration( textPrimary: textPrimary,
color: colorScheme.primary, textSecondary: textSecondary,
shape: BoxShape.circle, textMuted: textMuted,
boxShadow: [ goldAccent: goldAccent,
BoxShadow(
color: colorScheme.primary.withOpacity(0.3),
blurRadius: 8,
),
],
),
child: Icon(
Icons.swap_vert,
color: colorScheme.onPrimary,
size: 22,
),
),
), ),
// 第二个卡片位置 - 带动画 const SizedBox(height: 24),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300), // --- Tips Card ---
switchInCurve: Curves.easeInOut, _buildTipsCard(
switchOutCurve: Curves.easeInOut, profitGreen: profitGreen,
transitionBuilder: (widget, animation) { profitGreenBg: profitGreenBg,
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: widget,
),
);
},
child: _direction == 1
? _buildAccountCard(
key: const ValueKey('to-card'),
label: '',
accountName: _toLabel,
balance: _toBalance,
isDark: isDark,
colorScheme: colorScheme,
)
: _buildAccountCard(
key: const ValueKey('from-card-bottom'),
label: '',
accountName: _fromLabel,
balance: _fromBalance,
isDark: isDark,
colorScheme: colorScheme,
),
), ),
SizedBox(height: AppSpacing.lg), const SizedBox(height: 24),
// 金额输入卡片 // --- Confirm Button ---
_buildAmountSection(colorScheme, isDark), _buildConfirmButton(
accentPrimary: accentPrimary,
SizedBox(height: AppSpacing.lg), textInverse: textInverse,
// 确认按钮
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),
], ],
), ),
); );
@@ -299,275 +227,345 @@ class _TransferPageState extends State<TransferPage> {
); );
} }
/// 账户卡片 /// Transfer direction card with source, swap, destination
Widget _buildAccountCard({ Widget _buildTransferDirectionCard({
required ColorScheme colorScheme,
required bool isDark,
required Color surfaceCard,
required Color borderDefault,
required Color textPrimary,
required Color textSecondary,
required Color textMuted,
required Color textInverse,
required Color accentPrimary,
}) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: surfaceCard,
borderRadius: BorderRadius.circular(AppRadius.xl),
border: Border.all(color: borderDefault.withOpacity(0.6)),
),
child: Column(
children: [
// Source account
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
transitionBuilder: (widget, animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, -1),
end: Offset.zero,
).animate(animation),
child: FadeTransition(opacity: animation, child: widget),
);
},
child: _buildAccountRow(
key: ValueKey('src-$_direction'),
label: '',
accountName: _fromLabel,
balance: _fromBalance,
isDark: isDark,
textMuted: textMuted,
textPrimary: textPrimary,
textSecondary: textSecondary,
),
),
// Swap button
GestureDetector(
onTap: _toggleDirection,
child: Container(
width: 36,
height: 36,
margin: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: accentPrimary,
shape: BoxShape.circle,
),
child: Center(
child: Icon(
LucideIcons.arrowUpDown,
size: 18,
color: textInverse,
),
),
),
),
// Destination account
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
transitionBuilder: (widget, animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(animation),
child: FadeTransition(opacity: animation, child: widget),
);
},
child: _buildAccountRow(
key: ValueKey('dst-$_direction'),
label: '',
accountName: _toLabel,
balance: _toBalance,
isDark: isDark,
textMuted: textMuted,
textPrimary: textPrimary,
textSecondary: textSecondary,
),
),
],
),
);
}
/// Single account row inside the direction card
Widget _buildAccountRow({
Key? key, 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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm, vertical: AppSpacing.xs),
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: colorScheme.primary,
),
),
),
SizedBox(width: AppSpacing.sm),
Text(
accountName,
style: GoogleFonts.spaceGrotesk(
fontSize: 16,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
],
),
SizedBox(height: AppSpacing.sm),
Row(
children: [
Text(
'可用余额',
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
SizedBox(width: AppSpacing.xs),
Text(
'$balance USDT',
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
],
),
],
),
);
}
/// 金额输入区域
Widget _buildAmountSection(ColorScheme colorScheme, bool isDark) {
return Container(
width: double.infinity,
padding: EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: isDark ? colorScheme.surfaceContainer : colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Label row
Text( Text(
'划转金额', label,
style: TextStyle( style: GoogleFonts.inter(
fontSize: 13, fontSize: 11,
color: colorScheme.onSurfaceVariant, fontWeight: FontWeight.normal,
color: textMuted,
), ),
), ),
SizedBox(height: AppSpacing.sm), const SizedBox(height: 8),
// 金额输入行 // Account name + balance row
Row( Row(
crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded( // Account name with icon
child: TextField( Row(
controller: _amountController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,8}')),
],
style: GoogleFonts.spaceGrotesk(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
decoration: InputDecoration(
hintText: '0.00',
hintStyle: GoogleFonts.spaceGrotesk(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.onSurfaceVariant.withOpacity(0.3),
),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
),
),
),
Padding(
padding: EdgeInsets.only(bottom: 4),
child: Text(
'USDT',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurfaceVariant,
),
),
),
],
),
SizedBox(height: AppSpacing.sm),
// 快捷按钮行
Row(
children: [
Text(
'可用: ${_availableBalance}',
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
Spacer(),
_buildQuickButton('25%', 0.25, colorScheme),
SizedBox(width: AppSpacing.xs),
_buildQuickButton('50%', 0.50, colorScheme),
SizedBox(width: AppSpacing.xs),
_buildQuickButton('75%', 0.75, colorScheme),
SizedBox(width: AppSpacing.xs),
_buildQuickButton('全部', 1.0, colorScheme),
],
),
if (_direction == 2) ...[
SizedBox(height: AppSpacing.sm),
Container(
padding: EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: AppColorScheme.warning.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Row(
children: [ children: [
Icon( Icon(
LucideIcons.triangleAlert, label == '' ? LucideIcons.wallet : LucideIcons.repeat,
size: 14, size: 18,
color: AppColorScheme.warning, color: textSecondary,
), ),
SizedBox(width: AppSpacing.xs), const SizedBox(width: 10),
Expanded( Text(
child: Text( accountName,
'仅支持 USDT 资产划转到资金账户', style: GoogleFonts.inter(
style: TextStyle( fontSize: 14,
fontSize: 11, fontWeight: FontWeight.w600,
color: AppColorScheme.warning, color: textPrimary,
),
), ),
), ),
], ],
), ),
// Balance
Text(
'\u00A5 ${_formatBalance(balance)}',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: textPrimary,
),
),
],
),
],
),
);
}
/// Format balance for display
String _formatBalance(String balance) {
final val = double.tryParse(balance);
if (val == null) return '0.00';
return val.toStringAsFixed(2);
}
/// Amount input section
Widget _buildAmountSection({
required bool isDark,
required Color bgTertiary,
required Color textPrimary,
required Color textSecondary,
required Color textMuted,
required Color goldAccent,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label row: "划转金额" + "全部划转"
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'划转金额',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: textSecondary,
),
),
GestureDetector(
onTap: () => _setQuickAmount(1.0),
child: Text(
'全部划转',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
color: goldAccent,
),
),
), ),
], ],
],
),
);
}
/// 快捷百分比按钮
Widget _buildQuickButton(String label, double percent, ColorScheme colorScheme) {
return GestureDetector(
onTap: () => _setQuickAmount(percent),
child: Container(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm, vertical: AppSpacing.xs),
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
), ),
child: Text( const SizedBox(height: 12),
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: colorScheme.primary,
),
),
),
);
}
/// 划转说明 // Amount input field
Widget _buildTips(ColorScheme colorScheme) { GestureDetector(
return Container( onTap: () => _focusNode.requestFocus(),
padding: EdgeInsets.all(AppSpacing.md), child: Container(
decoration: BoxDecoration( width: double.infinity,
color: colorScheme.surfaceContainerHighest, height: 56,
borderRadius: BorderRadius.circular(AppRadius.md), padding: const EdgeInsets.symmetric(horizontal: 16),
),
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( decoration: BoxDecoration(
color: colorScheme.onSurfaceVariant, color: bgTertiary,
shape: BoxShape.circle, borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Input
Expanded(
child: TextField(
controller: _amountController,
focusNode: _focusNode,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,8}')),
],
style: GoogleFonts.inter(
fontSize: 28,
fontWeight: FontWeight.w700,
color: textPrimary,
),
decoration: InputDecoration(
hintText: '0.00',
hintStyle: GoogleFonts.inter(
fontSize: 28,
fontWeight: FontWeight.w700,
color: textMuted,
),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
),
),
),
// Suffix
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
'USDT',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.normal,
color: textMuted,
),
),
),
],
), ),
), ),
),
const SizedBox(height: 12),
// Percent buttons
Row(
children: [
_buildPercentButton('25%', 0.25, isDark, bgTertiary, textSecondary),
const SizedBox(width: 8),
_buildPercentButton('50%', 0.50, isDark, bgTertiary, textSecondary),
const SizedBox(width: 8),
_buildPercentButton('75%', 0.75, isDark, bgTertiary, textSecondary),
const SizedBox(width: 8),
_buildPercentButton('100%', 1.0, isDark, bgTertiary, textSecondary),
],
),
],
);
}
/// Percent quick button
Widget _buildPercentButton(String label, double percent, bool isDark, Color bgTertiary, Color textSecondary) {
return Expanded(
child: GestureDetector(
onTap: () => _setQuickAmount(percent),
child: Container(
height: 36,
decoration: BoxDecoration(
color: bgTertiary,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Center(
child: Text(
label,
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: textSecondary,
),
),
),
),
),
);
}
/// Tips card with green background
Widget _buildTipsCard({
required Color profitGreen,
required Color profitGreenBg,
}) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: profitGreenBg,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Row(
children: [
Icon(
LucideIcons.info,
size: 16,
color: profitGreen,
),
const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
text, '划转即时到账,无需手续费',
style: TextStyle( style: GoogleFonts.inter(
fontSize: 11, fontSize: 12,
color: colorScheme.onSurfaceVariant, fontWeight: FontWeight.normal,
color: profitGreen,
), ),
), ),
), ),
@@ -575,4 +573,43 @@ class _TransferPageState extends State<TransferPage> {
), ),
); );
} }
/// Confirm button
Widget _buildConfirmButton({
required Color accentPrimary,
required Color textInverse,
}) {
return SizedBox(
width: double.infinity,
height: 52,
child: GestureDetector(
onTap: _isLoading ? null : _doTransfer,
child: Container(
decoration: BoxDecoration(
color: accentPrimary,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Center(
child: _isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(textInverse),
),
)
: Text(
'确认划转',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w700,
color: textInverse,
),
),
),
),
),
);
}
} }

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart'; import 'package: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,38 +17,52 @@ 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(
child: ShadForm( padding: const EdgeInsets.symmetric(
key: formKey, horizontal: AppSpacing.xl,
child: Column( vertical: AppSpacing.xxl,
mainAxisAlignment: MainAxisAlignment.center, ),
crossAxisAlignment: CrossAxisAlignment.stretch, child: ShadForm(
children: [ key: formKey,
_buildHeader(theme), child: Column(
SizedBox(height: AppSpacing.xxl), children: [
_buildUsernameField(), // 顶部品牌区域
SizedBox(height: AppSpacing.md), _buildBrandSection(isDark),
_buildPasswordField(), const SizedBox(height: AppSpacing.xxl),
SizedBox(height: AppSpacing.lg), // 表单区域
_buildLoginButton(), _buildFormSection(isDark),
SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.xl),
_buildRegisterLink(theme), // 底部注册链接
], _buildRegisterRow(isDark),
), ],
), ),
), ),
), ),
@@ -55,87 +70,258 @@ class _LoginPageState extends State<LoginPage> {
); );
} }
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)],
),
),
alignment: Alignment.center,
child: Text(
'M',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w800,
color: isDark
? AppColorScheme.darkOnSurface
: Colors.white,
),
),
), ),
SizedBox(height: AppSpacing.lg), const SizedBox(height: AppSpacing.md),
// 品牌名 "MONISUO"
Text( 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( // 表单区域 - 用户名 + 密码 + 登录按钮
id: 'username', // ============================================
label: const Text('用户名'),
placeholder: const Text('请输入用户名'), Widget _buildFormSection(bool isDark) {
leading: const Icon(LucideIcons.user), return Column(
validator: _validateUsername, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildUsernameField(isDark),
const SizedBox(height: AppSpacing.md),
_buildPasswordField(isDark),
const SizedBox(height: AppSpacing.sm),
_buildLoginButton(isDark),
],
); );
} }
Widget _buildPasswordField() { Widget _buildUsernameField(bool isDark) {
return ShadInputFormField( final borderColor = isDark
id: 'password', ? AppColorScheme.darkOutlineVariant
label: const Text('密码'), : AppColorScheme.lightOutlineVariant;
placeholder: const Text('请输入密码'), final cardColor = isDark
obscureText: true, ? AppColorScheme.darkSurfaceContainer
leading: const Icon(LucideIcons.lock), : AppColorScheme.lightSurfaceLowest;
validator: _validatePassword, final iconColor = isDark
? AppColorScheme.darkOnSurfaceMuted
: AppColorScheme.lightOnSurfaceMuted;
return SizedBox(
height: _inputHeight,
child: ShadInputFormField(
id: 'username',
placeholder: const Text('请输入用户名'),
leading: Padding(
padding: const EdgeInsets.only(right: AppSpacing.sm),
child: Icon(LucideIcons.user, size: 18, color: iconColor),
),
validator: _validateUsername,
controller: _usernameController,
decoration: ShadDecoration(
border: ShadBorder.all(
color: borderColor,
radius: BorderRadius.circular(_designRadiusLg),
),
),
style: TextStyle(
fontSize: 14,
color: isDark
? AppColorScheme.darkOnSurface
: AppColorScheme.lightOnSurface,
),
),
); );
} }
Widget _buildLoginButton() { Widget _buildPasswordField(bool isDark) {
final borderColor = isDark
? AppColorScheme.darkOutlineVariant
: AppColorScheme.lightOutlineVariant;
final iconColor = isDark
? AppColorScheme.darkOnSurfaceMuted
: AppColorScheme.lightOnSurfaceMuted;
return SizedBox(
height: _inputHeight,
child: ShadInputFormField(
id: 'password',
placeholder: const Text('请输入密码'),
obscureText: _obscurePassword,
leading: Padding(
padding: const EdgeInsets.only(right: AppSpacing.sm),
child: Icon(LucideIcons.lock, size: 18, color: iconColor),
),
trailing: GestureDetector(
onTap: () => setState(() => _obscurePassword = !_obscurePassword),
child: Icon(
_obscurePassword ? LucideIcons.eyeOff : LucideIcons.eye,
size: 18,
color: iconColor,
),
),
validator: _validatePassword,
controller: _passwordController,
decoration: ShadDecoration(
border: ShadBorder.all(
color: borderColor,
radius: BorderRadius.circular(_designRadiusLg),
),
),
style: TextStyle(
fontSize: 14,
color: isDark
? AppColorScheme.darkOnSurface
: AppColorScheme.lightOnSurface,
),
),
);
}
Widget _buildLoginButton(bool isDark) {
// 设计稿: accent-primary = light:#1F2937 / dark:#D4AF37
final buttonColor = isDark
? AppColorScheme.darkSecondary
: const Color(0xFF1F2937);
final textColor = isDark
? AppColorScheme.darkBackground
: Colors.white;
return Consumer<AuthProvider>( return Consumer<AuthProvider>(
builder: (context, auth, _) { builder: (context, auth, _) {
return ShadButton( return SizedBox(
onPressed: auth.isLoading ? null : () => _handleLogin(auth), height: _buttonHeight,
child: auth.isLoading child: ShadButton(
? const SizedBox.square( onPressed: auth.isLoading ? null : () => _handleLogin(auth),
dimension: _loadingIndicatorSize, backgroundColor: buttonColor,
child: CircularProgressIndicator( foregroundColor: textColor,
strokeWidth: 2, decoration: ShadDecoration(
color: Colors.white, border: ShadBorder.all(
radius: BorderRadius.circular(_designRadiusLg),
),
),
child: auth.isLoading
? SizedBox.square(
dimension: _loadingIndicatorSize,
child: CircularProgressIndicator(
strokeWidth: 2,
color: textColor,
),
)
: Text(
'登录',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: textColor,
),
), ),
) ),
: const Text('登录'),
); );
}, },
); );
} }
Widget _buildRegisterLink(ShadThemeData theme) { // ============================================
return Row( // 底部注册链接
mainAxisAlignment: MainAxisAlignment.center, // ============================================
children: [
Text( Widget _buildRegisterRow(bool isDark) {
'还没有账号?', // gold-accent: light=#F59E0B / dark=#D4AF37
style: theme.textTheme.muted, final goldColor = isDark
), ? AppColorScheme.darkSecondary
ShadButton.link( : const Color(0xFFF59E0B);
onPressed: _navigateToRegister, final secondaryTextColor = isDark
child: const Text('立即注册'), ? AppColorScheme.darkOnSurfaceVariant
), : AppColorScheme.lightOnSurfaceVariant;
],
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.xl),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'还没有账户?',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.normal,
color: secondaryTextColor,
),
),
const SizedBox(width: AppSpacing.xs),
GestureDetector(
onTap: _navigateToRegister,
child: Text(
'立即注册',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: goldColor,
),
),
),
],
),
); );
} }
// ============================================
// Validators // 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(
shrinkWrap: true, decoration: BoxDecoration(
physics: const NeverScrollableScrollPhysics(), color: colorScheme.surfaceContainer,
itemCount: coins.length, borderRadius: BorderRadius.circular(AppRadius.lg),
separatorBuilder: (_, __) => SizedBox(height: AppSpacing.sm), border: Border.all(
itemBuilder: (context, index) => _CoinListItem(coin: coins[index]), color: colorScheme.outlineVariant.withOpacity(0.15),
width: 1,
),
),
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: coins.length,
separatorBuilder: (_, __) => Divider(
height: 1,
thickness: 1,
color: colorScheme.outlineVariant.withOpacity(0.5 * 0.15),
indent: 16,
endIndent: 16,
),
itemBuilder: (context, index) => _CoinRow(coin: coins[index]),
),
); );
} }
@@ -135,13 +187,13 @@ class _MarketPageState extends State<MarketPage>
mainAxisAlignment: MainAxisAlignment.center, 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),
child: Column( height: 130,
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
// 图标 + 币种代码 mainAxisAlignment: MainAxisAlignment.spaceBetween,
Row( children: [
children: [ // 第一行:币种名称 + 涨跌徽章
Container( Row(
width: 44, mainAxisAlignment: MainAxisAlignment.spaceBetween,
height: 44, children: [
decoration: BoxDecoration( Text(
color: colorScheme.primary.withOpacity(0.1), '${coin.code}/USDT',
borderRadius: BorderRadius.circular(AppRadius.xl), style: GoogleFonts.inter(
), fontSize: 14,
child: Center( fontWeight: FontWeight.w600,
child: Text( color: colorScheme.onSurface,
coin.displayIcon,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
),
),
SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
coin.code,
style: GoogleFonts.spaceGrotesk(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
),
],
),
SizedBox(height: AppSpacing.md),
// 当前价格
Text(
'\$${coin.formattedPrice}',
style: GoogleFonts.spaceGrotesk(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
SizedBox(height: AppSpacing.xs),
// 24h 涨跌幅
Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: changeBgColor,
borderRadius: BorderRadius.circular(AppRadius.sm),
border: Border.all(color: changeColor.withOpacity(0.2)),
),
child: Text(
coin.formattedChange,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: changeColor,
), ),
), ),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: changeBgColor,
borderRadius: BorderRadius.circular(4),
),
child: Text(
coin.formattedChange,
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w600,
color: changeColor,
),
),
),
],
),
// 第二行:价格
Text(
'\$${_formatFeaturedPrice(coin)}',
style: GoogleFonts.inter(
fontSize: 24,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
), ),
], ),
), // 第三行:币种全名
Text(
coin.name,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
// 第四行:迷你柱状图
Expanded(
child: _MiniBarChart(isUp: isUp, isDark: isDark, seed: coin.code.hashCode),
),
],
),
); );
} }
/// 精选卡片使用简短价格格式(带逗号)
String _formatFeaturedPrice(Coin coin) {
if (coin.price >= 1000) {
return _addCommas(coin.price.toStringAsFixed(2));
}
return coin.price.toStringAsFixed(2);
}
String _addCommas(String text) {
final parts = text.split('.');
final intPart = parts[0];
final decPart = parts.length > 1 ? '.${parts[1]}' : '';
final buffer = StringBuffer();
int count = 0;
for (int i = intPart.length - 1; i >= 0; i--) {
if (count > 0 && count % 3 == 0) {
buffer.write(',');
}
buffer.write(intPart[i]);
count++;
}
return '${buffer.toString().split('').reversed.join()}$decPart';
}
} }
/// 下半区列表项 /// 迷你柱状图(模拟价格走势)
class _CoinListItem extends StatelessWidget { class _MiniBarChart extends StatelessWidget {
final bool isUp;
final bool isDark;
final int seed;
const _MiniBarChart({required this.isUp, required this.isDark, required this.seed});
@override
Widget build(BuildContext context) {
final barColor = isUp
? AppColorScheme.getUpColor(isDark)
: AppColorScheme.getDownColor(isDark);
// 生成随机但确定的高度序列
final heights = _generateHeights();
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: heights.map((h) {
return Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 1.5),
child: Container(
height: h,
decoration: BoxDecoration(
color: barColor,
borderRadius: BorderRadius.circular(2),
),
),
),
);
}).toList(),
);
}
List<double> _generateHeights() {
final random = Random(seed);
final base = 8.0;
final range = 16.0;
return List.generate(6, (_) => base + random.nextDouble() * range);
}
}
/// 币种列表行
class _CoinRow extends StatelessWidget {
final Coin coin; 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,
mainAxisSize: MainAxisSize.min,
children: [ children: [
Row( Text(
children: [ '${coin.code}/USDT',
Text( style: GoogleFonts.inter(
coin.code, fontSize: 14,
style: GoogleFonts.spaceGrotesk( fontWeight: FontWeight.w700,
fontSize: 14, color: colorScheme.onSurface,
fontWeight: FontWeight.w600, ),
color: colorScheme.onSurface,
),
),
SizedBox(width: AppSpacing.xs),
Text(
'/USDT',
style: TextStyle(
fontSize: 11,
color: colorScheme.onSurfaceVariant,
),
),
],
), ),
const SizedBox(height: 2),
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: Row( child: Container(
children: [ height: 40,
_buildTab('全部', 0), padding: const EdgeInsets.all(3),
const SizedBox(width: 12), decoration: BoxDecoration(
_buildTab('充值', 1), color: bgColor,
const SizedBox(width: 12), borderRadius: AppRadius.radiusMd,
_buildTab('提现', 2), ),
], child: Row(
children: [
_buildPillTab('全部', 0, activeBgColor, isDark),
_buildPillTab('充值', 1, activeBgColor, isDark),
_buildPillTab('提现', 2, activeBgColor, isDark),
],
),
), ),
); );
} }
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) ...[ // Payable amount (withdrawal with fee)
const SizedBox(height: 4), if (order.receivableAmount != null && !order.isDeposit) ...[
Text( const SizedBox(height: 8),
'联系方式: ${order.withdrawContact}', _buildPayableRow(order, isDark, primaryText),
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
], ],
const SizedBox(height: 8), // Action buttons
if (order.canCancel || order.canConfirmPay) ...[
const SizedBox(height: 12),
_buildActions(order, isDark),
],
],
),
);
}
// ---------------------------------------------------------------------------
// Card Header - type badge + status badge
// ---------------------------------------------------------------------------
Widget _buildCardHeader(OrderFund order, bool isDark) {
final upColor = AppColorScheme.getUpColor(isDark);
final downColor = AppColorScheme.getDownColor(isDark);
final upBg = AppColorScheme.getUpBackgroundColor(isDark, opacity: 0.12);
final downBg = AppColorScheme.getDownBackgroundColor(isDark, opacity: 0.12);
final typeColor = order.isDeposit ? upColor : downColor;
final typeBg = order.isDeposit ? upBg : downBg;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Type badge (充值 / 提现)
_buildBadge(order.typeText, typeColor, typeBg),
// Status badge
_buildStatusBadge(order, isDark),
],
);
}
Widget _buildStatusBadge(OrderFund order, bool isDark) {
final upColor = AppColorScheme.getUpColor(isDark);
final downColor = AppColorScheme.getDownColor(isDark);
final upBg = AppColorScheme.getUpBackgroundColor(isDark, opacity: 0.12);
final downBg = AppColorScheme.getDownBackgroundColor(isDark, opacity: 0.12);
const amberColor = Color(0xFFD97706);
const amberBg = Color(0xFFFEF3C7);
Color bgColor;
Color textColor;
if (order.isDeposit) {
switch (order.status) {
case 1: // 待付款
case 2: // 待确认
bgColor = amberBg;
textColor = amberColor;
break;
case 3: // 已完成
bgColor = upBg;
textColor = upColor;
break;
default: // 已驳回/已取消
bgColor = downBg;
textColor = downColor;
}
} else {
switch (order.status) {
case 1: // 待审批
case 5: // 待财务审核
bgColor = amberBg;
textColor = amberColor;
break;
case 2: // 已完成
bgColor = upBg;
textColor = upColor;
break;
default: // 已驳回/已取消
bgColor = downBg;
textColor = downColor;
}
}
return _buildBadge(order.statusText, textColor, bgColor);
}
Widget _buildBadge(String text, Color textColor, Color bgColor) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(4),
),
child: Text(
text,
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w600,
color: textColor,
),
),
);
}
// ---------------------------------------------------------------------------
// Amount Row
// ---------------------------------------------------------------------------
Widget _buildAmountRow(OrderFund order, Color primaryText) {
return Text(
'${order.isDeposit ? '+' : '-'}${order.amount} USDT',
style: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w700,
color: primaryText,
),
);
}
// ---------------------------------------------------------------------------
// Detail Rows
// ---------------------------------------------------------------------------
Widget _buildDetailRows(
OrderFund order,
Color primaryText,
Color mutedText,
) {
return Column(
children: [
// Order number
_buildDetailRow('订单号', order.orderNo, primaryText, mutedText),
const SizedBox(height: 6),
// Network / wallet address
if (order.walletAddress != null) ...[
_buildDetailRow(
'网络',
order.remark.isNotEmpty ? order.remark : '-',
primaryText,
mutedText,
),
const SizedBox(height: 6),
_buildDetailRow(
'地址',
_truncateAddress(order.walletAddress!),
primaryText,
mutedText,
trailing: GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: order.walletAddress!));
ToastUtils.show('地址已复制');
},
child: Icon(
LucideIcons.copy,
size: 14,
color: mutedText,
),
),
),
const SizedBox(height: 6),
] else if (order.remark.isNotEmpty) ...[
_buildDetailRow('网络', order.remark, primaryText, mutedText),
const SizedBox(height: 6),
],
// Fee (withdrawal)
if (order.fee != null && !order.isDeposit) ...[
_buildDetailRow('手续费', '${order.fee}%', primaryText, mutedText),
const SizedBox(height: 6),
],
// Time
_buildDetailRow(
'时间',
_formatTime(order.createTime),
primaryText,
mutedText,
),
],
);
}
Widget _buildDetailRow(
String label,
String value,
Color primaryText,
Color mutedText, {
Widget? trailing,
}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: mutedText,
),
),
if (trailing != null)
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
'创建: ${_formatTime(order.createTime)}', value,
style: const TextStyle(fontSize: 11, color: Colors.grey), style: GoogleFonts.inter(
), fontSize: 12,
if (order.rejectReason != null) fontWeight: FontWeight.normal,
Expanded( color: primaryText,
child: Text(
'驳回: ${order.rejectReason}',
style: const TextStyle(fontSize: 11, color: downColor),
textAlign: TextAlign.right,
overflow: TextOverflow.ellipsis,
),
), ),
),
const SizedBox(width: 4),
trailing,
], ],
)
else
Text(
value,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: primaryText,
),
),
],
);
}
// ---------------------------------------------------------------------------
// Rejection Reason
// ---------------------------------------------------------------------------
Widget _buildRejectionReason(OrderFund order) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Text(
'拒绝原因: ${order.rejectReason}',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: AppColorScheme.getDownColor(isDark),
),
);
}
// ---------------------------------------------------------------------------
// Payable Amount Row (withdrawal)
// ---------------------------------------------------------------------------
Widget _buildPayableRow(
OrderFund order,
bool isDark,
Color primaryText,
) {
final bgTertiary = isDark
? AppColorScheme.darkSurfaceContainerHigh
: AppColorScheme.lightSurfaceHigh;
final secondaryText = isDark
? AppColorScheme.darkOnSurfaceVariant
: AppColorScheme.lightOnSurfaceVariant;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: bgTertiary,
borderRadius: AppRadius.radiusSm,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'应付金额',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: secondaryText,
),
),
Text(
'${order.receivableAmount} USDT',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w600,
color: primaryText,
),
), ),
], ],
), ),
); );
} }
Widget _buildStatusBadge(OrderFund order) { // ---------------------------------------------------------------------------
Color bgColor; // Action Buttons
Color textColor; // ---------------------------------------------------------------------------
Widget _buildActions(OrderFund order, bool isDark) {
final upColor = AppColorScheme.getUpColor(isDark);
final downColor = AppColorScheme.getDownColor(isDark);
// 根据类型和状态设置颜色 return Row(
if (order.type == 1) { mainAxisAlignment: MainAxisAlignment.end,
// 充值状态 children: [
switch (order.status) { if (order.canCancel)
case 1: // 待付款 GestureDetector(
case 2: // 待确认 onTap: () => _cancelOrder(order),
bgColor = Colors.orange.withValues(alpha: 0.1); child: Container(
textColor = Colors.orange; padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
break; decoration: BoxDecoration(
case 3: // 已完成 borderRadius: AppRadius.radiusSm,
bgColor = upColor.withValues(alpha: 0.1); border: Border.all(color: downColor, width: 1),
textColor = upColor; ),
break; child: Text(
default: // 已驳回/已取消 '取消订单',
bgColor = downColor.withValues(alpha: 0.1); style: GoogleFonts.inter(
textColor = downColor; fontSize: 13,
} fontWeight: FontWeight.w500,
} else { color: downColor,
// 提现状态 ),
switch (order.status) { ),
case 1: // 待审批 ),
bgColor = Colors.orange.withValues(alpha: 0.1); ),
textColor = Colors.orange; if (order.canCancel && order.canConfirmPay)
break; const SizedBox(width: 12),
case 2: // 已完成 if (order.canConfirmPay)
bgColor = upColor.withValues(alpha: 0.1); GestureDetector(
textColor = upColor; onTap: () => _confirmPay(order),
break; child: Container(
default: // 已驳回/已取消 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
bgColor = downColor.withValues(alpha: 0.1); decoration: BoxDecoration(
textColor = downColor; color: upColor,
} borderRadius: AppRadius.radiusSm,
} ),
child: Text(
return Container( '已打款',
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), style: GoogleFonts.inter(
decoration: BoxDecoration( fontSize: 13,
color: bgColor, fontWeight: FontWeight.w500,
borderRadius: BorderRadius.circular(4), color: Colors.white,
), ),
child: Text( ),
order.statusText, ),
style: TextStyle(fontSize: 11, color: textColor), ),
), ],
); );
} }
// ---------------------------------------------------------------------------
// 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),
),
],
),
);
}
} }

File diff suppressed because it is too large Load Diff

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';