Compare commits

...

3 Commits

Author SHA1 Message Date
189609f337 fix(order): correct payment label text in fund order card
Updated the text label from '应收款项' (receivables) to '应付款' (payables) in the fund order card component to accurately reflect the payment direction. Also corrected a typo in the Chinese text for 'fund orders list'.
2026-04-05 22:42:21 +08:00
02099d2a6a docs: relocate skills system documentation and refactor asset page components
Move skills system documentation from bottom to top of CLAUDE.md for better organization. Refactor Flutter asset page by extracting UI components into separate files and updating import structure for improved modularity.
2026-04-05 22:38:56 +08:00
d8cd38c4de feat(theme): update color scheme with new Slate theme and improved surface hierarchy
Updated the app's color scheme to implement a new "Slate" theme with refined dark and light variants. Changed background colors from #0A0E14 to #0B1120 for dark mode and updated surface layer colors to follow Material Design 3 specifications. Modified text colors and outline variants for better contrast and accessibility. Updated font sizes in transaction details screen from 11px to 12px for improved readability.
2026-04-05 22:24:04 +08:00
41 changed files with 5182 additions and 4279 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../../core/theme/app_spacing.dart';
/// 账户标签切换器 — .pen node UE6xC
/// height: 40, padding: 3, cornerRadius: md, fill: $bg-tertiary
/// activeTab: fill $bg-primary, cornerRadius sm, shadow blur 3, color #0000000D, offset y 1
/// activeTabText: 14px, fontWeight 600, fill $text-primary
/// inactiveTabText: 14px, fontWeight 500, fill $text-secondary
class AccountTabSwitcher extends StatelessWidget {
final int selectedIndex;
final ValueChanged<int> onChanged;
const AccountTabSwitcher({
super.key,
required this.selectedIndex,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
height: 40,
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: isDark ? colorScheme.surfaceContainerHighest : colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: Row(
children: [
_buildTab(
context: context,
label: '资金账户',
isSelected: selectedIndex == 0,
onTap: () => onChanged(0),
isDark: isDark,
),
_buildTab(
context: context,
label: '交易账户',
isSelected: selectedIndex == 1,
onTap: () => onChanged(1),
isDark: isDark,
),
],
),
);
}
Widget _buildTab({
required BuildContext context,
required String label,
required bool isSelected,
required VoidCallback onTap,
required bool isDark,
}) {
final colorScheme = Theme.of(context).colorScheme;
return Expanded(
child: GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: isSelected
? colorScheme.surface
: Colors.transparent,
borderRadius: BorderRadius.circular(AppRadius.sm),
boxShadow: isSelected
? [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 3,
offset: const Offset(0, 1),
),
]
: null,
),
alignment: Alignment.center,
child: Text(
label,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected ? colorScheme.onSurface : colorScheme.onSurfaceVariant,
),
),
),
),
);
}
}

View File

@@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
/// 操作按钮行 — .pen node pIpHe
/// gap: 12, three buttons evenly distributed
/// Each button: circle 48x48 fill $bg-tertiary, cornerRadius 24
/// icon: 20px $accent-primary (lucide: arrow-up-right / arrow-down-left / repeat)
/// label: 12px w500 $text-secondary
class ActionButtonsRow extends StatelessWidget {
final VoidCallback onDeposit;
final VoidCallback onWithdraw;
final VoidCallback onTransfer;
const ActionButtonsRow({
super.key,
required this.onDeposit,
required this.onWithdraw,
required this.onTransfer,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final accentColor = isDark ? colorScheme.secondary : colorScheme.primary;
final bgColor = isDark ? colorScheme.surfaceContainerHighest : colorScheme.surfaceContainerHigh;
return Row(
children: [
ActionButton(
icon: LucideIcons.arrowUpRight,
label: '充值',
accentColor: accentColor,
bgColor: bgColor,
onTap: onDeposit,
),
const SizedBox(width: 12),
ActionButton(
icon: LucideIcons.arrowDownLeft,
label: '提现',
accentColor: accentColor,
bgColor: bgColor,
onTap: onWithdraw,
),
const SizedBox(width: 12),
ActionButton(
icon: LucideIcons.repeat,
label: '划转',
accentColor: accentColor,
bgColor: bgColor,
onTap: onTransfer,
),
],
);
}
}
/// 单个操作按钮 — matching .pen btn1/btn2/btn3
class ActionButton extends StatelessWidget {
final IconData icon;
final String label;
final Color accentColor;
final Color bgColor;
final VoidCallback onTap;
const ActionButton({
super.key,
required this.icon,
required this.label,
required this.accentColor,
required this.bgColor,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Column(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: bgColor,
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Icon(
icon,
size: 20,
color: accentColor,
),
),
const SizedBox(height: 6),
Text(
label,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,602 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:provider/provider.dart';
import '../../../../core/theme/app_color_scheme.dart';
import '../../../../core/theme/app_spacing.dart';
import '../../../../core/utils/toast_utils.dart';
import '../../../../providers/asset_provider.dart';
import '../../../components/glass_panel.dart';
import '../../../components/neon_glow.dart';
import '../../../shared/ui_constants.dart';
// ============================================
// Dialog helpers — shared sub-widgets
// ============================================
/// 信息行 — 用于对话框中显示 label/value 键值对
class InfoRow extends StatelessWidget {
final String label;
final String value;
final bool isBold;
const InfoRow({
super.key,
required this.label,
required this.value,
this.isBold = false,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: GoogleFonts.inter(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
Text(
value,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
color: colorScheme.onSurface,
),
),
],
);
}
}
/// 钱包地址卡片 — 用于充值结果对话框中展示钱包地址
class WalletAddressCard extends StatelessWidget {
final String address;
final String network;
const WalletAddressCard({
super.key,
required this.address,
required this.network,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(
color: colorScheme.outlineVariant.withValues(alpha: 0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
address,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
),
GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: address));
ToastUtils.show('地址已复制到剪贴板');
},
child: Container(
padding: const EdgeInsets.all(AppSpacing.xs),
decoration: BoxDecoration(
color: colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Icon(
LucideIcons.copy,
size: 16,
color: colorScheme.primary,
),
),
),
],
),
const SizedBox(height: AppSpacing.sm),
Text(
'网络: $network',
style: GoogleFonts.inter(
fontSize: 11,
color: colorScheme.onSurfaceVariant,
),
),
],
),
);
}
}
// ============================================
// Dialog functions — kept from original with style updates
// ============================================
/// 充值对话框
void showDepositDialog(BuildContext context) {
final amountController = TextEditingController();
final formKey = GlobalKey<ShadFormState>();
final colorScheme = Theme.of(context).colorScheme;
showShadDialog(
context: context,
builder: (ctx) => Dialog(
backgroundColor: Colors.transparent,
child: GlassPanel(
borderRadius: BorderRadius.circular(AppRadius.lg),
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'充值',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
const SizedBox(height: AppSpacing.xs),
Text(
'Asset: USDT',
style: GoogleFonts.inter(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
],
),
Container(
padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: Icon(
LucideIcons.wallet,
color: colorScheme.secondary,
),
),
],
),
const SizedBox(height: AppSpacing.lg),
ShadForm(
key: formKey,
child: ShadInputFormField(
id: 'amount',
controller: amountController,
label: const Text('充值金额'),
placeholder: const Text('最低 1000 USDT'),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
validator: (v) {
if (v == null || v.isEmpty) return '请输入金额';
final n = double.tryParse(v);
if (n == null || n <= 0) return '请输入有效金额';
if (n < 1000) return '单笔最低充值1000 USDT';
return null;
},
),
),
const SizedBox(height: AppSpacing.lg),
Row(
children: [
Expanded(
child: NeonButton(
text: '取消',
type: NeonButtonType.outline,
onPressed: () => Navigator.of(ctx).pop(),
height: 48,
showGlow: false,
),
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: NeonButton(
text: '下一步',
type: NeonButtonType.primary,
onPressed: () async {
if (formKey.currentState!.saveAndValidate()) {
Navigator.of(ctx).pop();
final response = await context.read<AssetProvider>().deposit(
amount: amountController.text,
);
if (context.mounted) {
if (response.success && response.data != null) {
showDepositResultDialog(context, response.data!);
} else {
showResultDialog(context, '申请失败', response.message);
}
}
}
},
height: 48,
showGlow: true,
),
),
],
),
],
),
),
),
);
}
/// 充值结果对话框 — 展示钱包地址和确认打款
void showDepositResultDialog(BuildContext context, Map<String, dynamic> data) {
final orderNo = data['orderNo'] as String? ?? '';
final amount = data['amount']?.toString() ?? '0.00';
final walletAddress = data['walletAddress'] as String? ?? '';
final walletNetwork = data['walletNetwork'] as String? ?? 'TRC20';
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
showShadDialog(
context: context,
builder: (ctx) => Dialog(
backgroundColor: Colors.transparent,
child: GlassPanel(
borderRadius: BorderRadius.circular(AppRadius.lg),
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
NeonIcon(
icon: Icons.check_circle,
color: AppColorScheme.getUpColor(isDark),
size: 24,
),
const SizedBox(width: AppSpacing.sm),
Text(
'充值申请成功',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
],
),
const SizedBox(height: AppSpacing.lg),
InfoRow(label: '订单号', value: orderNo),
const SizedBox(height: AppSpacing.sm),
InfoRow(label: '充值金额', value: '$amount USDT', isBold: true),
const SizedBox(height: AppSpacing.lg),
Text(
'请向以下地址转账:',
style: GoogleFonts.inter(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: AppSpacing.sm),
WalletAddressCard(address: walletAddress, network: walletNetwork),
const SizedBox(height: AppSpacing.md),
Container(
padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: AppColorScheme.warning.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(
color: AppColorScheme.warning.withValues(alpha: 0.2),
),
),
child: Row(
children: [
Icon(Icons.info_outline, size: 16, color: AppColorScheme.warning),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
'转账完成后请点击"已打款"按钮确认',
style: GoogleFonts.inter(fontSize: 12, color: AppColorScheme.warning),
),
),
],
),
),
const SizedBox(height: AppSpacing.lg),
Row(
children: [
Expanded(
child: NeonButton(
text: '稍后确认',
type: NeonButtonType.outline,
onPressed: () => Navigator.of(ctx).pop(),
height: 44,
showGlow: false,
),
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: NeonButton(
text: '已打款',
type: NeonButtonType.primary,
onPressed: () async {
Navigator.of(ctx).pop();
final response = await context.read<AssetProvider>().confirmPay(orderNo);
if (context.mounted) {
showResultDialog(
context,
response.success ? '确认成功' : '确认失败',
response.success ? '请等待管理员审核' : response.message,
);
}
},
height: 44,
showGlow: true,
),
),
],
),
],
),
),
),
);
}
/// 提现对话框
void showWithdrawDialog(BuildContext context, String? balance) {
final amountController = TextEditingController();
final addressController = TextEditingController();
final contactController = TextEditingController();
final formKey = GlobalKey<ShadFormState>();
final colorScheme = Theme.of(context).colorScheme;
showShadDialog(
context: context,
builder: (ctx) => Dialog(
backgroundColor: Colors.transparent,
child: GlassPanel(
borderRadius: BorderRadius.circular(AppRadius.lg),
padding: const EdgeInsets.all(AppSpacing.lg),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: Icon(
LucideIcons.wallet,
color: colorScheme.primary,
),
),
const SizedBox(width: AppSpacing.sm),
Text(
'提现',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
],
),
const SizedBox(height: AppSpacing.xs),
Text(
'安全地将您的资产转移到外部钱包地址',
style: GoogleFonts.inter(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
if (balance != null) ...[
const SizedBox(height: AppSpacing.md),
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
decoration: BoxDecoration(
color: AppColorScheme.up.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(AppRadius.full),
border: Border.all(
color: AppColorScheme.up.withValues(alpha: 0.2),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'可用余额: ',
style: GoogleFonts.inter(
fontSize: 10,
color: colorScheme.onSurfaceVariant,
),
),
Text(
'$balance USDT',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.bold,
color: AppColorScheme.up,
),
),
],
),
),
],
const SizedBox(height: AppSpacing.lg),
ShadForm(
key: formKey,
child: Column(
children: [
ShadInputFormField(
id: 'amount',
controller: amountController,
label: const Text('提现金额'),
placeholder: const Text('请输入提现金额(USDT)'),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
validator: Validators.amount,
),
const SizedBox(height: AppSpacing.md),
ShadInputFormField(
id: 'address',
controller: addressController,
label: const Text('目标地址'),
placeholder: const Text('请输入提现地址'),
validator: (v) => Validators.required(v, '提现地址'),
),
const SizedBox(height: AppSpacing.md),
ShadInputFormField(
id: 'contact',
controller: contactController,
label: const Text('联系方式(可选)'),
placeholder: const Text('联系方式'),
),
],
),
),
const SizedBox(height: AppSpacing.lg),
Row(
children: [
Expanded(
child: NeonButton(
text: '取消',
type: NeonButtonType.outline,
onPressed: () => Navigator.of(ctx).pop(),
height: 44,
showGlow: false,
),
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: NeonButton(
text: '提交',
type: NeonButtonType.primary,
onPressed: () async {
if (formKey.currentState!.saveAndValidate()) {
Navigator.of(ctx).pop();
final response = await context.read<AssetProvider>().withdraw(
amount: amountController.text,
withdrawAddress: addressController.text,
withdrawContact: contactController.text.isNotEmpty
? contactController.text
: null,
);
if (context.mounted) {
showResultDialog(
context,
response.success ? '申请成功' : '申请失败',
response.success ? '请等待管理员审批' : response.message,
);
}
}
},
height: 44,
showGlow: true,
),
),
],
),
const SizedBox(height: AppSpacing.md),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.verified_user,
size: 12,
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
),
const SizedBox(width: AppSpacing.xs),
Text(
'End-to-End Encrypted Transaction',
style: GoogleFonts.inter(
fontSize: 10,
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
),
),
],
),
],
),
),
),
),
);
}
/// 通用结果对话框 — 展示操作成功/失败信息
void showResultDialog(BuildContext context, String title, String? message) {
final colorScheme = Theme.of(context).colorScheme;
showShadDialog(
context: context,
builder: (ctx) => Dialog(
backgroundColor: Colors.transparent,
child: GlassPanel(
borderRadius: BorderRadius.circular(AppRadius.lg),
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
if (message != null) ...[
const SizedBox(height: AppSpacing.sm),
Text(
message,
style: GoogleFonts.inter(color: colorScheme.onSurfaceVariant),
textAlign: TextAlign.center,
),
],
const SizedBox(height: AppSpacing.lg),
SizedBox(
width: double.infinity,
child: NeonButton(
text: '确定',
type: NeonButtonType.primary,
onPressed: () => Navigator.of(ctx).pop(),
height: 44,
showGlow: false,
),
),
],
),
),
),
);
}

View File

@@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../../core/theme/app_color_scheme.dart';
import '../../../../core/theme/app_spacing.dart';
import '../../../../providers/asset_provider.dart';
import '../../../components/glass_panel.dart';
/// 余额卡片 — .pen node 59637
/// cornerRadius: lg, fill: $surface-card, padding: 20, stroke: $border-default 1px, gap: 12
/// balLabel: "USDT 余额" 12px normal $text-secondary
/// balAmount: "25,680.50" 28px w700 $text-primary
/// balSubRow: "≈ $25,680.50 USD" 12px normal $text-muted
class BalanceCard extends StatelessWidget {
final AssetProvider provider;
final int activeTab;
const BalanceCard({
super.key,
required this.provider,
required this.activeTab,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final displayBalance = activeTab == 0
? (provider.fundAccount?.balance ?? provider.overview?.fundBalance ?? '0.00')
: _calculateTradeTotal();
return GlassPanel(
padding: const EdgeInsets.all(20),
borderRadius: BorderRadius.circular(AppRadius.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'USDT 余额',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
Text(
_formatBalance(displayBalance),
style: GoogleFonts.inter(
fontSize: 28,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 12),
Text(
'\u2248 \$${_formatBalance(displayBalance)} USD',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: isDark ? AppColorScheme.darkOnSurfaceMuted : colorScheme.onSurfaceVariant,
),
),
],
),
);
}
String _calculateTradeTotal() {
double total = 0;
for (var h in provider.holdings) {
total += double.tryParse(h.currentValue?.toString() ?? '0') ?? 0;
}
return total.toStringAsFixed(2);
}
String _formatBalance(String balance) {
final d = double.tryParse(balance) ?? 0;
return d.toStringAsFixed(2).replaceAllMapped(
RegExp(r'\B(?=(\d{3})+(?!\d))'),
(Match m) => ',',
);
}
}

View File

@@ -0,0 +1,208 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../../core/theme/app_color_scheme.dart';
import '../../../../core/theme/app_spacing.dart';
import '../../../../data/models/account_models.dart';
import '../../../components/glass_panel.dart';
/// 持仓区域 — .pen nodes th9BG (header) + 6X6tC (card)
/// Holdings Header: "交易账户持仓" 16px w600 $text-primary | "查看全部 >" 12px normal $text-secondary
/// Holdings Card: cornerRadius lg, fill $surface-card, stroke $border-default 1px
class HoldingsSection extends StatelessWidget {
final List holdings;
const HoldingsSection({super.key, required this.holdings});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Column(
children: [
// Header row: "交易账户持仓" + "查看全部 >"
Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'交易账户持仓',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
Text(
'查看全部 >',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
// Holdings card — uses real provider.holdings data
if (holdings.isEmpty)
Padding(
padding: const EdgeInsets.all(AppSpacing.xl),
child: Text(
'暂无持仓',
style: GoogleFonts.inter(
fontSize: 13,
color: colorScheme.onSurfaceVariant,
),
),
)
else
GlassPanel(
padding: EdgeInsets.zero,
borderRadius: BorderRadius.circular(AppRadius.lg),
child: Column(
children: List.generate(holdings.length, (index) {
final h = holdings[index] as AccountTrade;
final isProfit = h.profitRate >= 0;
return Column(
children: [
HoldingRow(
coinCode: h.coinCode,
quantity: double.tryParse(h.quantity)?.toStringAsFixed(4) ?? h.quantity,
value: '${double.tryParse(h.currentValue)?.toStringAsFixed(2) ?? h.currentValue} USDT',
profitRate: '${isProfit ? '+' : ''}${h.profitRate.toStringAsFixed(2)}%',
isProfit: isProfit,
),
if (index < holdings.length - 1) const HoldingDivider(),
],
);
}),
),
),
],
);
}
}
/// 持仓行分隔线 — .pen node BCCbR / yejhE
/// fill: $border-default, height: 1, opacity: 0.5
class HoldingDivider extends StatelessWidget {
const HoldingDivider({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
height: 1,
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
);
}
}
/// 持仓行 — matching .pen nodes dAt4j / eK6vq / jiSUK
/// padding [14, 16], space_between layout
/// Left: avatar circle (36x36, radius 18, fill $accent-light) + coin info (gap 2)
/// Right: value + pnl (gap 2, align end)
class HoldingRow extends StatelessWidget {
final String coinCode;
final String quantity;
final String value;
final String profitRate;
final bool isProfit;
const HoldingRow({
super.key,
required this.coinCode,
required this.quantity,
required this.value,
required this.profitRate,
required this.isProfit,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final accentColor = isDark ? colorScheme.secondary : colorScheme.primary;
final accentBgColor = accentColor.withValues(alpha: 0.1);
final profitColor = isProfit ? AppColorScheme.getUpColor(isDark) : AppColorScheme.getDownColor(isDark);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: 14),
child: Row(
children: [
// Avatar circle with first letter — .pen SJNDJ/EjSIN/3GQ5M
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: accentBgColor,
borderRadius: BorderRadius.circular(18),
),
alignment: Alignment.center,
child: Text(
coinCode.substring(0, 1),
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w700,
color: accentColor,
),
),
),
const SizedBox(width: 10),
// Coin name + quantity — .pen fivxJ/Kxv3d/5CsoQ
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
coinCode,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 2),
Text(
quantity,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
// Value + profit rate — .pen vYJsU/2nLAg/IlWck
Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Text(
value,
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 2),
Text(
profitRate,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
color: profitColor,
),
),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../../../core/theme/app_color_scheme.dart';
import '../../../../core/theme/app_spacing.dart';
import '../../../components/glass_panel.dart';
/// 充提记录链接行 — .pen node fLHtq
/// cornerRadius: lg, fill: $surface-card, padding: [14, 16], stroke: $border-default 1px
/// recordsText: "充提记录" 14px w500 $text-primary
/// recordsChevron: lucide chevron-right 16px $text-muted
class RecordsLinkRow extends StatelessWidget {
final VoidCallback onTap;
const RecordsLinkRow({super.key, required this.onTap});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final mutedColor = isDark ? AppColorScheme.darkOnSurfaceMuted : colorScheme.onSurfaceVariant;
return GestureDetector(
onTap: onTap,
child: GlassPanel(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: 14),
borderRadius: BorderRadius.circular(AppRadius.lg),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'充提记录',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
),
),
Icon(
LucideIcons.chevronRight,
size: 16,
color: mutedColor,
),
],
),
),
);
}
}

View File

@@ -1,16 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../core/theme/app_color_scheme.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../providers/asset_provider.dart';
import '../../../data/models/account_models.dart';
import '../../shared/ui_constants.dart';
import '../../components/neon_glow.dart';
/// 划转页面 - 币安风格
/// 划转页面
class TransferPage extends StatefulWidget {
const TransferPage({super.key});
@@ -20,6 +17,7 @@ class TransferPage extends StatefulWidget {
class _TransferPageState extends State<TransferPage> {
final _amountController = TextEditingController();
final _focusNode = FocusNode();
int _direction = 1; // 1: 资金→交易, 2: 交易→资金
bool _isLoading = false;
@@ -34,9 +32,14 @@ class _TransferPageState extends State<TransferPage> {
@override
void dispose() {
_amountController.dispose();
_focusNode.dispose();
super.dispose();
}
// ============================================
// 数据访问
// ============================================
/// 获取资金账户余额
String get _fundBalance {
final provider = context.read<AssetProvider>();
@@ -64,9 +67,7 @@ class _TransferPageState extends State<TransferPage> {
}
/// 获取当前可用余额(根据方向)
String get _availableBalance {
return _direction == 1 ? _fundBalance : _tradeUsdtBalance;
}
String get _availableBalance => _direction == 1 ? _fundBalance : _tradeUsdtBalance;
/// 从账户名
String get _fromLabel => _direction == 1 ? '资金账户' : '交易账户';
@@ -74,6 +75,27 @@ class _TransferPageState extends State<TransferPage> {
String get _fromBalance => _direction == 1 ? _fundBalance : _tradeUsdtBalance;
String get _toBalance => _direction == 1 ? _tradeUsdtBalance : _fundBalance;
// ============================================
// 主题辅助
// ============================================
bool get _isDark => Theme.of(context).brightness == Brightness.dark;
/// 一次性获取所有主题感知颜色
_TransferColors get _colors => _TransferColors(_isDark);
TextStyle _inter({
required double fontSize,
required FontWeight fontWeight,
required Color color,
}) {
return GoogleFonts.inter(fontSize: fontSize, fontWeight: fontWeight, color: color);
}
// ============================================
// 业务逻辑
// ============================================
/// 执行划转
Future<void> _doTransfer() async {
final amount = _amountController.text;
@@ -124,7 +146,6 @@ class _TransferPageState extends State<TransferPage> {
void _setQuickAmount(double percent) {
final available = double.tryParse(_availableBalance) ?? 0;
final amount = available * percent;
// 保留8位小数去除末尾0
_amountController.text = amount.toStringAsFixed(8).replaceAll(RegExp(r'\.?0+$'), '');
}
@@ -135,162 +156,43 @@ class _TransferPageState extends State<TransferPage> {
});
}
// ============================================
// 构建 UI
// ============================================
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final c = _colors;
return Scaffold(
backgroundColor: colorScheme.background,
backgroundColor: c.bgSecondary,
appBar: AppBar(
backgroundColor: Colors.transparent,
backgroundColor: c.surfaceCard,
elevation: 0,
scrolledUnderElevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back, color: colorScheme.onSurface),
icon: Icon(LucideIcons.arrowLeft, color: c.textPrimary, size: 20),
onPressed: () => Navigator.of(context).pop(),
),
title: Text(
'资金划转',
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
'账户划转',
style: _inter(fontSize: 16, fontWeight: FontWeight.w600, color: c.textPrimary),
),
centerTitle: true,
),
body: Consumer<AssetProvider>(
builder: (context, provider, _) {
return SingleChildScrollView(
padding: AppSpacing.pagePadding,
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
child: Column(
children: [
// 第一个卡片位置 - 带动画
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: const Duration(milliseconds: 300),
builder: (context, value, child) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
transitionBuilder: (widget, animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, -1),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: widget,
),
);
},
child: _direction == 1
? _buildAccountCard(
key: const ValueKey('from-card'),
label: '',
accountName: _fromLabel,
balance: _fromBalance,
isDark: isDark,
colorScheme: colorScheme,
)
: _buildAccountCard(
key: const ValueKey('to-card-top'),
label: '',
accountName: _toLabel,
balance: _toBalance,
isDark: isDark,
colorScheme: colorScheme,
),
);
},
),
// 方向切换按钮(固定在中间)
GestureDetector(
onTap: _toggleDirection,
child: Container(
margin: EdgeInsets.symmetric(vertical: AppSpacing.sm),
padding: EdgeInsets.all(AppSpacing.sm + AppSpacing.xs),
decoration: BoxDecoration(
color: colorScheme.primary,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: colorScheme.primary.withOpacity(0.3),
blurRadius: 8,
),
],
),
child: Icon(
Icons.swap_vert,
color: colorScheme.onPrimary,
size: 22,
),
),
),
// 第二个卡片位置 - 带动画
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
transitionBuilder: (widget, animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(animation),
child: FadeTransition(
opacity: animation,
child: widget,
),
);
},
child: _direction == 1
? _buildAccountCard(
key: const ValueKey('to-card'),
label: '',
accountName: _toLabel,
balance: _toBalance,
isDark: isDark,
colorScheme: colorScheme,
)
: _buildAccountCard(
key: const ValueKey('from-card-bottom'),
label: '',
accountName: _fromLabel,
balance: _fromBalance,
isDark: isDark,
colorScheme: colorScheme,
),
),
SizedBox(height: AppSpacing.lg),
// 金额输入卡片
_buildAmountSection(colorScheme, isDark),
SizedBox(height: AppSpacing.lg),
// 确认按钮
SizedBox(
width: double.infinity,
child: NeonButton(
text: _isLoading ? '处理中...' : '确认划转',
icon: _isLoading ? null : LucideIcons.arrowRightLeft,
type: NeonButtonType.primary,
onPressed: _isLoading ? null : _doTransfer,
height: 52,
showGlow: true,
),
),
SizedBox(height: AppSpacing.md),
// 划转说明
_buildTips(colorScheme),
_buildTransferDirectionCard(c),
const SizedBox(height: 24),
_buildAmountSection(c),
const SizedBox(height: 24),
_buildTipsCard(c),
const SizedBox(height: 24),
_buildConfirmButton(c),
],
),
);
@@ -299,280 +201,317 @@ class _TransferPageState extends State<TransferPage> {
);
}
/// 账户卡片
Widget _buildAccountCard({
Key? key,
// ============================================
// Transfer direction card
// ============================================
Widget _buildTransferDirectionCard(_TransferColors c) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: c.surfaceCard,
borderRadius: BorderRadius.circular(AppRadius.xl),
border: Border.all(color: c.borderDefault.withValues(alpha: 0.6)),
),
child: Column(
children: [
// Source account
_animatedSwitcher(
key: 'src-$_direction',
beginOffset: const Offset(0, -1),
child: _buildAccountRow(
label: '',
accountName: _fromLabel,
balance: _fromBalance,
c: c,
),
),
// Swap button
GestureDetector(
onTap: _toggleDirection,
child: Container(
width: 36,
height: 36,
margin: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: c.accentPrimary,
shape: BoxShape.circle,
),
child: Center(
child: Icon(LucideIcons.arrowUpDown, size: 18, color: c.textInverse),
),
),
),
// Destination account
_animatedSwitcher(
key: 'dst-$_direction',
beginOffset: const Offset(0, 1),
child: _buildAccountRow(
label: '',
accountName: _toLabel,
balance: _toBalance,
c: c,
),
),
],
),
);
}
/// 统一的 AnimatedSwitcher 构造
Widget _animatedSwitcher({
required String key,
required Offset beginOffset,
required Widget child,
}) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
transitionBuilder: (widget, animation) {
return SlideTransition(
position: Tween<Offset>(begin: beginOffset, end: Offset.zero).animate(animation),
child: FadeTransition(opacity: animation, child: widget),
);
},
child: KeyedSubtree(key: ValueKey(key), child: child),
);
}
/// Single account row inside the direction card
Widget _buildAccountRow({
required String label,
required String accountName,
required String balance,
required bool isDark,
required ColorScheme colorScheme,
required _TransferColors c,
}) {
return Container(
key: key,
return SizedBox(
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: [
Text(label, style: _inter(fontSize: 11, fontWeight: FontWeight.normal, color: c.textMuted)),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm, vertical: AppSpacing.xs),
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: colorScheme.primary,
),
),
),
SizedBox(width: AppSpacing.sm),
Text(
accountName,
style: GoogleFonts.spaceGrotesk(
fontSize: 16,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
],
),
SizedBox(height: AppSpacing.sm),
Row(
children: [
Text(
'可用余额',
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
SizedBox(width: AppSpacing.xs),
Text(
'$balance USDT',
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
],
),
],
),
);
}
/// 金额输入区域
Widget _buildAmountSection(ColorScheme colorScheme, bool isDark) {
return Container(
width: double.infinity,
padding: EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: isDark ? colorScheme.surfaceContainer : colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'划转金额',
style: TextStyle(
fontSize: 13,
color: colorScheme.onSurfaceVariant,
),
),
SizedBox(height: AppSpacing.sm),
// 金额输入行
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: TextField(
controller: _amountController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,8}')),
],
style: GoogleFonts.spaceGrotesk(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
decoration: InputDecoration(
hintText: '0.00',
hintStyle: GoogleFonts.spaceGrotesk(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.onSurfaceVariant.withOpacity(0.3),
),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
),
),
),
Padding(
padding: EdgeInsets.only(bottom: 4),
child: Text(
'USDT',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurfaceVariant,
),
),
),
],
),
SizedBox(height: AppSpacing.sm),
// 快捷按钮行
Row(
children: [
Text(
'可用: ${_availableBalance}',
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
Spacer(),
_buildQuickButton('25%', 0.25, colorScheme),
SizedBox(width: AppSpacing.xs),
_buildQuickButton('50%', 0.50, colorScheme),
SizedBox(width: AppSpacing.xs),
_buildQuickButton('75%', 0.75, colorScheme),
SizedBox(width: AppSpacing.xs),
_buildQuickButton('全部', 1.0, colorScheme),
],
),
if (_direction == 2) ...[
SizedBox(height: AppSpacing.sm),
Container(
padding: EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: AppColorScheme.warning.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Row(
Row(
children: [
Icon(
LucideIcons.triangleAlert,
size: 14,
color: AppColorScheme.warning,
),
SizedBox(width: AppSpacing.xs),
Expanded(
child: Text(
'仅支持 USDT 资产划转到资金账户',
style: TextStyle(
fontSize: 11,
color: AppColorScheme.warning,
),
),
label == '' ? LucideIcons.wallet : LucideIcons.repeat,
size: 18,
color: c.textSecondary,
),
const SizedBox(width: 10),
Text(accountName, style: _inter(fontSize: 14, fontWeight: FontWeight.w600, color: c.textPrimary)),
],
),
Text(
'\u00A5 ${_formatBalance(balance)}',
style: _inter(fontSize: 14, fontWeight: FontWeight.w600, color: c.textPrimary),
),
],
),
],
),
);
}
// ============================================
// Amount input section
// ============================================
Widget _buildAmountSection(_TransferColors c) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label row: "划转金额" + "全部划转"
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('划转金额', style: _inter(fontSize: 14, fontWeight: FontWeight.w500, color: c.textSecondary)),
GestureDetector(
onTap: () => _setQuickAmount(1.0),
child: Text('全部划转', style: _inter(fontSize: 12, fontWeight: FontWeight.w600, color: c.goldAccent)),
),
],
],
),
);
}
/// 快捷百分比按钮
Widget _buildQuickButton(String label, double percent, ColorScheme colorScheme) {
return GestureDetector(
onTap: () => _setQuickAmount(percent),
child: Container(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm, vertical: AppSpacing.xs),
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: colorScheme.primary,
),
),
),
);
}
const SizedBox(height: 12),
/// 划转说明
Widget _buildTips(ColorScheme colorScheme) {
return Container(
padding: EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'划转说明',
style: GoogleFonts.spaceGrotesk(
fontSize: 12,
fontWeight: FontWeight.w600,
color: colorScheme.onSurfaceVariant,
),
),
SizedBox(height: AppSpacing.sm),
_buildTipItem('资金账户用于充提,交易账户用于买卖币种', colorScheme),
_buildTipItem('划转操作即时到账,不可撤销', colorScheme),
_buildTipItem('交易账户只有 USDT 可直接划转到资金账户', colorScheme),
_buildTipItem('其他币种需先卖出换成 USDT 后才能划转', colorScheme),
],
),
);
}
Widget _buildTipItem(String text, ColorScheme colorScheme) {
return Padding(
padding: EdgeInsets.only(bottom: AppSpacing.xs),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 4,
height: 4,
margin: EdgeInsets.only(top: 6, right: AppSpacing.sm),
// Amount input field
GestureDetector(
onTap: () => _focusNode.requestFocus(),
child: Container(
width: double.infinity,
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: colorScheme.onSurfaceVariant,
shape: BoxShape.circle,
color: c.bgTertiary,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: TextField(
controller: _amountController,
focusNode: _focusNode,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,8}')),
],
style: _inter(fontSize: 28, fontWeight: FontWeight.w700, color: c.textPrimary),
decoration: InputDecoration(
hintText: '0.00',
hintStyle: _inter(fontSize: 28, fontWeight: FontWeight.w700, color: c.textMuted),
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
),
),
),
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text('USDT', style: _inter(fontSize: 14, fontWeight: FontWeight.normal, color: c.textMuted)),
),
],
),
),
),
const SizedBox(height: 12),
// Percent buttons
Row(
children: [0.25, 0.50, 0.75, 1.0].asMap().entries.map((entry) {
final index = entry.key;
final percent = entry.value;
final label = '${(percent * 100).toInt()}%';
return Padding(
padding: EdgeInsets.only(left: index > 0 ? 8 : 0),
child: _buildPercentButton(label, percent, c),
);
}).toList(),
),
],
);
}
Widget _buildPercentButton(String label, double percent, _TransferColors c) {
return Expanded(
child: GestureDetector(
onTap: () => _setQuickAmount(percent),
child: Container(
height: 36,
decoration: BoxDecoration(
color: c.bgTertiary,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Center(
child: Text(label, style: _inter(fontSize: 13, fontWeight: FontWeight.w500, color: c.textSecondary)),
),
),
),
);
}
// ============================================
// Tips card & Confirm button
// ============================================
Widget _buildTipsCard(_TransferColors c) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: c.profitGreenBg,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Row(
children: [
Icon(LucideIcons.info, size: 16, color: c.profitGreen),
const SizedBox(width: 8),
Expanded(
child: Text(
text,
style: TextStyle(
fontSize: 11,
color: colorScheme.onSurfaceVariant,
),
'划转即时到账,无需手续费',
style: _inter(fontSize: 12, fontWeight: FontWeight.normal, color: c.profitGreen),
),
),
],
),
);
}
Widget _buildConfirmButton(_TransferColors c) {
return SizedBox(
width: double.infinity,
height: 52,
child: GestureDetector(
onTap: _isLoading ? null : _doTransfer,
child: Container(
decoration: BoxDecoration(
color: c.accentPrimary,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Center(
child: _isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(c.textInverse),
),
)
: Text(
'确认划转',
style: _inter(fontSize: 16, fontWeight: FontWeight.w700, color: c.textInverse),
),
),
),
),
);
}
// ============================================
// Helpers
// ============================================
String _formatBalance(String balance) {
final val = double.tryParse(balance);
if (val == null) return '0.00';
return val.toStringAsFixed(2);
}
}
/// 主题感知颜色集合,避免在 build() 中重复定义大量局部变量
class _TransferColors {
final Color bgSecondary;
final Color surfaceCard;
final Color bgTertiary;
final Color borderDefault;
final Color textPrimary;
final Color textSecondary;
final Color textMuted;
final Color textInverse;
final Color accentPrimary;
final Color goldAccent;
final Color profitGreen;
final Color profitGreenBg;
_TransferColors(bool isDark)
: bgSecondary = isDark ? const Color(0xFF0B1120) : const Color(0xFFF8FAFC),
surfaceCard = isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF),
bgTertiary = isDark ? const Color(0xFF1E293B) : const Color(0xFFF1F5F9),
borderDefault = isDark ? const Color(0xFF334155) : const Color(0xFFE2E8F0),
textPrimary = isDark ? const Color(0xFFF8FAFC) : const Color(0xFF0F172A),
textSecondary = isDark ? const Color(0xFF94A3B8) : const Color(0xFF475569),
textMuted = isDark ? const Color(0xFF64748B) : const Color(0xFF94A3B8),
textInverse = isDark ? const Color(0xFF0F172A) : const Color(0xFFFFFFFF),
accentPrimary = isDark ? const Color(0xFFD4AF37) : const Color(0xFF1F2937),
goldAccent = isDark ? const Color(0xFFD4AF37) : const Color(0xFFF59E0B),
profitGreen = isDark ? const Color(0xFF4ADE80) : const Color(0xFF16A34A),
profitGreenBg = isDark ? const Color(0xFF052E16) : const Color(0xFFF0FDF4);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/app_spacing.dart';
/// 信息行组件(用于关于对话框)
class InfoRow extends StatelessWidget {
final IconData icon;
final String text;
const InfoRow({super.key, required this.icon, required this.text});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Row(
children: [
Icon(icon, size: 14, color: colorScheme.onSurfaceVariant),
SizedBox(width: AppSpacing.sm),
Text(
text,
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
],
);
}
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
/// 圆形头像组件
///
/// 显示用户首字母或默认比特币符号。通过 [radius] 控制大小,
/// [fontSize] 控制文字大小,[text] 可传入用户头像文字。
class AvatarCircle extends StatelessWidget {
final double radius;
final double fontSize;
final String? text;
const AvatarCircle({
super.key,
required this.radius,
required this.fontSize,
this.text,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return CircleAvatar(
radius: radius,
backgroundColor: colorScheme.primary.withOpacity(0.15),
child: Text(
text ?? '',
style: TextStyle(
fontSize: fontSize,
color: colorScheme.primary,
fontWeight: FontWeight.w700,
),
),
);
}
}

View File

@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../../core/theme/app_color_scheme.dart';
import '../../../../core/theme/app_spacing.dart';
/// 退出登录按钮
class LogoutButton extends StatelessWidget {
final VoidCallback onLogout;
const LogoutButton({super.key, required this.onLogout});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onLogout,
child: Container(
width: double.infinity,
height: 48,
decoration: BoxDecoration(
color: AppColorScheme.down.withOpacity(0.05),
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: AppColorScheme.down.withOpacity(0.15),
),
),
child: Center(
child: Text(
'退出登录',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColorScheme.down,
),
),
),
),
);
}
}

View File

@@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../../../core/theme/app_color_scheme.dart';
import '../../../../core/theme/app_spacing.dart';
import '../kyc_page.dart';
import '../welfare_center_page.dart';
import 'menu_group_container.dart';
import 'menu_row.dart';
import 'menu_trailing_widgets.dart';
/// 菜单分组1 - 福利中心 / 实名认证 / 安全设置 / 消息通知
class MenuGroup1 extends StatelessWidget {
final int kycStatus;
final void Function(String) onShowComingSoon;
const MenuGroup1({
super.key,
required this.kycStatus,
required this.onShowComingSoon,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return MenuGroupContainer(
child: Column(
children: [
// 福利中心
MenuRow(
icon: LucideIcons.gift,
iconColor: AppColorScheme.darkSecondary, // gold
title: '福利中心',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const WelfareCenterPage()),
);
},
),
const MenuDivider(),
// 实名认证
MenuRow(
icon: LucideIcons.shieldCheck,
iconColor: AppColorScheme.getUpColor(isDark),
title: '实名认证',
trailing: KycBadge(kycStatus: kycStatus),
onTap: () {
if (kycStatus == 2) {
showKycStatusDialog(context);
} else {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const KycPage()),
);
}
},
),
const MenuDivider(),
// 安全设置
MenuRow(
icon: LucideIcons.lock,
iconColor: colorScheme.onSurfaceVariant,
title: '安全设置',
onTap: () => onShowComingSoon('安全设置'),
),
const MenuDivider(),
// 消息通知
MenuRow(
icon: LucideIcons.bell,
iconColor: colorScheme.onSurfaceVariant,
title: '消息通知',
trailing: const RedDotIndicator(),
onTap: () => onShowComingSoon('消息通知'),
),
],
),
);
}
}
/// 显示 KYC 认证状态对话框
void showKycStatusDialog(BuildContext context) {
showShadDialog(
context: context,
builder: (ctx) => ShadDialog.alert(
title: Row(
children: [
Icon(Icons.check_circle, color: AppColorScheme.up, size: 20),
SizedBox(width: AppSpacing.sm),
const Text('实名认证'),
],
),
description: const Text('您的实名认证已通过'),
actions: [
ShadButton(
child: const Text('确定'),
onPressed: () => Navigator.of(ctx).pop(),
),
],
),
);
}

View File

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'menu_group_container.dart';
import 'menu_row.dart';
import 'menu_trailing_widgets.dart';
/// 菜单分组2 - 深色模式 / 系统设置 / 关于我们
class MenuGroup2 extends StatelessWidget {
final VoidCallback onShowAbout;
const MenuGroup2({super.key, required this.onShowAbout});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return MenuGroupContainer(
child: Column(
children: [
// 深色模式
const DarkModeRow(),
const MenuDivider(),
// 系统设置
MenuRow(
icon: LucideIcons.settings,
iconColor: colorScheme.onSurfaceVariant,
title: '系统设置',
onTap: () {
// TODO: 系统设置
},
),
const MenuDivider(),
// 关于我们
MenuRow(
icon: LucideIcons.info,
iconColor: colorScheme.onSurfaceVariant,
title: '关于我们',
onTap: onShowAbout,
),
],
),
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/app_spacing.dart';
/// 菜单分组容器 - 统一的圆角卡片样式
///
/// 所有菜单分组共享相同的容器样式:背景色、圆角、边框。
/// 通过 [child] 传入菜单项 Column。
class MenuGroupContainer extends StatelessWidget {
final Widget child;
const MenuGroupContainer({super.key, required this.child});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
width: double.infinity,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: child,
);
}
}

View File

@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
/// 单行菜单项icon-in-box + title + trailing (chevron / badge / toggle)
///
/// 通用菜单行组件,[icon] 和 [iconColor] 控制左侧图标,
/// [title] 为菜单文字,[trailing] 为右侧自定义内容(默认显示 chevron
/// [onTap] 为点击回调。
class MenuRow extends StatelessWidget {
final IconData icon;
final Color iconColor;
final String title;
final Widget? trailing;
final VoidCallback? onTap;
const MenuRow({
super.key,
required this.icon,
required this.iconColor,
required this.title,
this.trailing,
this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
// Icon in 36x36 rounded container
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.dark
? Theme.of(context).colorScheme.surfaceContainerHigh
: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Icon(icon, size: 18, color: iconColor),
),
),
const SizedBox(width: 10),
// Title
Expanded(
child: Text(
title,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
// Trailing
if (trailing != null)
trailing!
else
Icon(
LucideIcons.chevronRight,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
),
),
);
}
}
/// 菜单组内分割线
class MenuDivider extends StatelessWidget {
const MenuDivider({super.key});
@override
Widget build(BuildContext context) {
return Container(
height: 1,
color: Theme.of(context).colorScheme.outlineVariant.withOpacity(0.15),
margin: const EdgeInsets.only(left: 62),
);
}
}

View File

@@ -0,0 +1,196 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:provider/provider.dart';
import '../../../../core/theme/app_color_scheme.dart';
import '../../../../core/theme/app_spacing.dart';
import '../../../../providers/theme_provider.dart';
/// KYC 状态徽章 (e.g. "已认证" green badge + chevron)
///
/// 根据 [kycStatus] 显示不同状态:
/// - 2: 已认证(绿色)
/// - 1: 审核中(橙色)
/// - 其他: 仅显示 chevron
class KycBadge extends StatelessWidget {
final int kycStatus;
const KycBadge({super.key, required this.kycStatus});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final green = AppColorScheme.getUpColor(isDark);
if (kycStatus == 2) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: green.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
'已认证',
style: GoogleFonts.inter(
fontSize: 10,
fontWeight: FontWeight.w500,
color: green,
),
),
),
const SizedBox(width: 8),
Icon(
LucideIcons.chevronRight,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
);
}
if (kycStatus == 1) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: AppColorScheme.warning.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
'审核中',
style: GoogleFonts.inter(
fontSize: 10,
fontWeight: FontWeight.w500,
color: AppColorScheme.warning,
),
),
),
const SizedBox(width: 8),
Icon(
LucideIcons.chevronRight,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
);
}
return Icon(
LucideIcons.chevronRight,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
);
}
}
/// 红点指示器 - 消息通知 + chevron
class RedDotIndicator extends StatelessWidget {
const RedDotIndicator({super.key});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: AppColorScheme.down,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Icon(
LucideIcons.chevronRight,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
);
}
}
/// 深色模式切换行
class DarkModeRow extends StatelessWidget {
const DarkModeRow({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final themeProvider = context.watch<ThemeProvider>();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
// Icon in 36x36 rounded container
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainerHigh
: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Icon(
LucideIcons.moon,
size: 18,
color: colorScheme.onSurfaceVariant,
),
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
'深色模式',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
),
),
),
// Toggle switch - matching .pen design (44x24 rounded pill)
GestureDetector(
onTap: () => themeProvider.toggleTheme(),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 44,
height: 24,
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainerHigh
: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: AnimatedAlign(
duration: const Duration(milliseconds: 200),
alignment:
themeProvider.isDarkMode
? Alignment.centerRight
: Alignment.centerLeft,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: colorScheme.onSurface,
shape: BoxShape.circle,
),
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../../../core/theme/app_spacing.dart';
import 'avatar_circle.dart';
/// 用户资料卡片 - 头像 + 用户名 + 徽章 + chevron
class ProfileCard extends StatelessWidget {
final dynamic user;
const ProfileCard({super.key, required this.user});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Row(
children: [
// Avatar
AvatarCircle(
radius: 24,
fontSize: 18,
text: user?.avatarText,
),
const SizedBox(width: 12),
// Name + badge column
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user?.username ?? '未登录',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
'普通用户',
style: GoogleFonts.inter(
fontSize: 10,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
// Chevron
Icon(
LucideIcons.chevronRight,
size: 16,
color: colorScheme.onSurfaceVariant,
),
],
),
);
}
}

View File

@@ -5,31 +5,15 @@ import 'package:google_fonts/google_fonts.dart';
import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../providers/auth_provider.dart';
import 'kyc_page.dart';
import '../../../providers/theme_provider.dart';
import '../auth/login_page.dart';
import '../../components/glass_panel.dart';
import '../../components/neon_glow.dart';
import 'welfare_center_page.dart';
import 'components/about_dialog_helpers.dart';
import 'components/avatar_circle.dart';
import 'components/logout_button.dart';
import 'components/menu_group1.dart';
import 'components/menu_group2.dart';
import 'components/profile_card.dart';
/// 菜单项数据模型
class _MenuItem {
final IconData icon;
final String title;
final String? subtitle;
final Color? iconColor;
final VoidCallback onTap;
const _MenuItem({
required this.icon,
required this.title,
this.subtitle,
this.iconColor,
required this.onTap,
});
}
/// 我的页面 - Material Design 3 风格
/// 我的页面 - 匹配 .pen 设计稿
class MinePage extends StatefulWidget {
const MinePage({super.key});
@@ -37,7 +21,8 @@ class MinePage extends StatefulWidget {
State<MinePage> createState() => _MinePageState();
}
class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin {
class _MinePageState extends State<MinePage>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@@ -51,26 +36,31 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
body: Consumer<AuthProvider>(
builder: (context, auth, _) {
return SingleChildScrollView(
padding: AppSpacing.pagePadding,
padding: EdgeInsets.fromLTRB(
AppSpacing.md,
AppSpacing.md,
AppSpacing.md,
AppSpacing.xl + AppSpacing.md,
),
child: Column(
children: [
_UserCard(user: auth.user),
SizedBox(height: AppSpacing.md),
_MenuList(
onShowComingSoon: _showComingSoon,
onShowAbout: _showAboutDialog,
ProfileCard(user: auth.user),
SizedBox(height: AppSpacing.sm),
MenuGroup1(
kycStatus: auth.user?.kycStatus ?? 0,
onShowComingSoon: _showComingSoon,
),
SizedBox(height: AppSpacing.xl),
_LogoutButton(onLogout: () => _handleLogout(auth)),
SizedBox(height: AppSpacing.sm),
MenuGroup2(onShowAbout: _showAboutDialog),
SizedBox(height: AppSpacing.lg),
// 版本信息
LogoutButton(onLogout: () => _handleLogout(auth)),
SizedBox(height: AppSpacing.md),
Text(
'System Build v1.0.0-Neo',
style: TextStyle(
fontSize: 10,
color: colorScheme.onSurfaceVariant.withOpacity(0.4),
letterSpacing: 0.3,
'System Build v1.0.0',
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant.withOpacity(0.5),
),
),
],
@@ -110,7 +100,7 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
builder: (context) => ShadDialog(
title: Row(
children: [
_AppLogo(radius: 20, fontSize: 16),
AvatarCircle(radius: 20, fontSize: 16),
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
const Text('模拟所'),
],
@@ -124,9 +114,11 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
SizedBox(height: AppSpacing.md),
_InfoRow(icon: Icons.code, text: '版本: 1.0.0'),
InfoRow(icon: Icons.code, text: '版本: 1.0.0'),
SizedBox(height: AppSpacing.sm),
_InfoRow(icon: Icons.favorite, text: 'Built with Flutter & Material Design 3'),
InfoRow(
icon: Icons.favorite,
text: 'Built with Flutter & Material Design 3'),
],
),
actions: [
@@ -168,460 +160,3 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
);
}
}
/// 用户卡片组件 - Material Design 3 风格
class _UserCard extends StatelessWidget {
final dynamic user;
const _UserCard({required this.user});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return GlassPanel(
padding: EdgeInsets.all(AppSpacing.lg + AppSpacing.sm),
child: Row(
children: [
// 头像 - 带霓虹边框
Stack(
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: colorScheme.primary.withOpacity(isDark ? 0.15 : 0.08),
blurRadius: 20,
),
],
),
child: _AppLogo(radius: 36, fontSize: 20, text: user?.avatarText),
),
// 验证徽章
Positioned(
bottom: 0,
right: 0,
child: Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppColorScheme.up,
shape: BoxShape.circle,
border: Border.all(
color: colorScheme.background,
width: 2,
),
),
child: Icon(
Icons.verified,
size: 14,
color: colorScheme.onTertiary,
),
),
),
],
),
SizedBox(width: AppSpacing.md + AppSpacing.xs),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user?.username ?? '未登录',
style: GoogleFonts.spaceGrotesk(
fontSize: 16,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
SizedBox(height: AppSpacing.sm),
// 用户等级标签
Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.full),
border: Border.all(
color: colorScheme.primary.withOpacity(0.2),
),
),
child: Text(
'普通用户',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
color: colorScheme.primary,
),
),
),
],
),
),
Icon(
LucideIcons.chevronRight,
color: colorScheme.onSurfaceVariant,
),
],
),
);
}
}
/// 应用 Logo 组件
class _AppLogo extends StatelessWidget {
final double radius;
final double fontSize;
final String? text;
const _AppLogo({required this.radius, required this.fontSize, this.text});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return CircleAvatar(
radius: radius,
backgroundColor: colorScheme.primary.withOpacity(0.2),
child: Text(
text ?? '',
style: TextStyle(
fontSize: fontSize,
color: colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
);
}
}
/// 信息行组件
class _InfoRow extends StatelessWidget {
final IconData icon;
final String text;
const _InfoRow({required this.icon, required this.text});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Row(
children: [
Icon(icon, size: 14, color: colorScheme.onSurfaceVariant),
SizedBox(width: AppSpacing.sm),
Text(
text,
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
],
);
}
}
/// 菜单列表组件 - Glass Panel 风格
class _MenuList extends StatelessWidget {
final void Function(String) onShowComingSoon;
final VoidCallback onShowAbout;
final int kycStatus;
const _MenuList({
required this.onShowComingSoon,
required this.onShowAbout,
required this.kycStatus,
});
@override
Widget build(BuildContext context) {
final themeProvider = context.watch<ThemeProvider>();
final colorScheme = Theme.of(context).colorScheme;
return GlassPanel(
padding: EdgeInsets.zero,
borderRadius: BorderRadius.circular(AppRadius.xxl),
child: Column(
children: [
// 主题切换开关
_ThemeToggleTile(isDarkMode: themeProvider.isDarkMode),
_buildDivider(),
// 菜单项
..._buildMenuItems(context, colorScheme),
],
),
);
}
Widget _buildDivider() {
return Container(
margin: EdgeInsets.only(left: 56),
height: 1,
color: AppColorScheme.glassPanelBorder,
);
}
List<Widget> _buildMenuItems(BuildContext context, ColorScheme colorScheme) {
final items = [
_MenuItem(
icon: LucideIcons.gift,
title: '福利中心',
subtitle: '首充奖励 + 推广奖励',
iconColor: colorScheme.primary,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const WelfareCenterPage()),
);
},
),
_MenuItem(
icon: LucideIcons.userCheck,
title: '实名认证',
subtitle: kycStatus == 2
? '已认证'
: kycStatus == 1
? '审核中'
: '完成实名认证,解锁更多功能',
iconColor: kycStatus == 2 ? AppColorScheme.up : colorScheme.primary,
onTap: () {
if (kycStatus == 2) {
_showKycStatusDialog(context);
} else {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const KycPage()),
);
}
},
),
_MenuItem(
icon: LucideIcons.shield,
title: '安全设置',
subtitle: '密码、二次验证等安全设置',
iconColor: colorScheme.secondary,
onTap: () => onShowComingSoon('安全设置'),
),
_MenuItem(
icon: LucideIcons.bell,
title: '消息通知',
subtitle: '管理消息推送设置',
iconColor: AppColorScheme.up,
onTap: () => onShowComingSoon('消息通知'),
),
_MenuItem(
icon: LucideIcons.settings,
title: '系统设置',
subtitle: '主题、语言等偏好设置',
iconColor: colorScheme.primary,
onTap: () => onShowComingSoon('系统设置'),
),
_MenuItem(
icon: LucideIcons.info,
title: '关于我们',
subtitle: '版本信息与用户协议',
iconColor: colorScheme.onSurfaceVariant,
onTap: onShowAbout,
),
];
return [
for (var i = 0; i < items.length; i++) ...[
_MenuItemTile(item: items[i]),
if (i < items.length - 1) _buildDivider(),
],
];
}
}
void _showKycStatusDialog(BuildContext context) {
showShadDialog(
context: context,
builder: (ctx) => ShadDialog.alert(
title: Row(
children: [
Icon(Icons.check_circle, color: AppColorScheme.up, size: 20),
SizedBox(width: AppSpacing.sm),
const Text('实名认证'),
],
),
description: const Text('您的实名认证已通过'),
actions: [
ShadButton(
child: const Text('确定'),
onPressed: () => Navigator.of(ctx).pop(),
),
],
),
);
}
/// 主题切换组件
class _ThemeToggleTile extends StatelessWidget {
final bool isDarkMode;
const _ThemeToggleTile({required this.isDarkMode});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final themeProvider = context.read<ThemeProvider>();
return InkWell(
onTap: () => themeProvider.toggleTheme(),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm + AppSpacing.xs,
),
child: Row(
children: [
_MenuIcon(
icon: isDarkMode ? LucideIcons.moon : LucideIcons.sun,
color: colorScheme.primary,
),
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'深色模式',
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
),
),
SizedBox(height: AppSpacing.xs / 2),
Text(
isDarkMode ? '当前:深色主题' : '当前:浅色主题',
style: TextStyle(
fontSize: 11,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
Switch(
value: isDarkMode,
onChanged: (_) => themeProvider.toggleTheme(),
activeTrackColor: colorScheme.primary.withOpacity(0.5),
activeColor: colorScheme.primary,
),
],
),
),
);
}
}
/// 菜单项组件
class _MenuItemTile extends StatelessWidget {
final _MenuItem item;
const _MenuItemTile({required this.item});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return InkWell(
onTap: item.onTap,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm + AppSpacing.xs,
),
child: Row(
children: [
_MenuIcon(icon: item.icon, color: item.iconColor),
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
),
),
if (item.subtitle != null) ...[
SizedBox(height: AppSpacing.xs / 2),
Text(
item.subtitle!,
style: TextStyle(
fontSize: 11,
color: colorScheme.onSurfaceVariant,
),
),
],
],
),
),
Icon(
LucideIcons.chevronRight,
size: 18,
color: colorScheme.onSurfaceVariant,
),
],
),
),
);
}
}
/// 菜单图标组件 - Material Design 3 风格
class _MenuIcon extends StatelessWidget {
final IconData icon;
final Color? color;
const _MenuIcon({required this.icon, this.color});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final iconColor = color ?? colorScheme.primary;
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.md + AppSpacing.xs),
border: Border.all(
color: iconColor.withOpacity(0.2),
),
),
child: Icon(icon, size: 20, color: iconColor),
);
}
}
/// 退出登录按钮 - 带霓虹光效
class _LogoutButton extends StatelessWidget {
final VoidCallback onLogout;
const _LogoutButton({required this.onLogout});
@override
Widget build(BuildContext context) {
return NeonButton(
text: 'Logout Terminal',
type: NeonButtonType.error,
icon: Icons.logout,
onPressed: onLogout,
width: double.infinity,
showGlow: true,
);
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../../core/theme/app_color_scheme.dart';
import '../../../../core/theme/app_spacing.dart';
/// 金额输入框组件(含超额提示)
///
/// 设计稿bg-tertiary圆角md高48。
/// 输入金额超过可用 USDT 余额时显示警告提示。
class AmountInput extends StatefulWidget {
final TextEditingController amountController;
final String maxAmount;
final bool isBuy;
final Color actionColor;
final VoidCallback onChanged;
const AmountInput({
super.key,
required this.amountController,
required this.maxAmount,
required this.isBuy,
required this.actionColor,
required this.onChanged,
});
@override
State<AmountInput> createState() => _AmountInputState();
}
class _AmountInputState extends State<AmountInput> {
bool _isExceeded = false;
void _checkLimit() {
final input = double.tryParse(widget.amountController.text) ?? 0;
final max = double.tryParse(widget.maxAmount) ?? 0;
final exceeded = widget.isBuy && input > max && max > 0 && input > 0;
if (exceeded != _isExceeded) {
setState(() => _isExceeded = exceeded);
}
widget.onChanged();
}
@override
void initState() {
super.initState();
widget.amountController.addListener(_checkLimit);
}
@override
void dispose() {
widget.amountController.removeListener(_checkLimit);
super.dispose();
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final warningColor = AppColorScheme.warning;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 48,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest.withOpacity(0.3),
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: TextField(
controller: widget.amountController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
onChanged: (_) => _checkLimit(),
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.normal,
color: colorScheme.onSurface,
fontFeatures: [FontFeature.tabularFigures()],
),
decoration: InputDecoration(
hintText: '请输入金额',
hintStyle: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant.withOpacity(0.5),
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
),
),
),
),
if (_isExceeded)
Padding(
padding: EdgeInsets.only(top: AppSpacing.xs),
child: Row(
children: [
Icon(Icons.error_outline, size: 13, color: warningColor),
SizedBox(width: 4),
Text(
'超出可用USDT余额',
style: GoogleFonts.inter(
fontSize: 11,
color: warningColor,
),
),
],
),
),
],
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/app_spacing.dart';
/// 币种头像组件
///
/// 显示币种图标或首字母的圆形头像,带主题色边框和背景。
class CoinAvatar extends StatelessWidget {
final String? icon;
const CoinAvatar({super.key, this.icon});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(color: colorScheme.primary.withOpacity(0.2)),
),
child: Center(
child: Text(icon ?? '?',
style: TextStyle(
fontSize: 20,
color: colorScheme.primary,
fontWeight: FontWeight.bold,
)),
),
);
}
}

View File

@@ -0,0 +1,234 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../../../core/theme/app_color_scheme.dart';
import '../../../../core/theme/app_spacing.dart';
import '../../../../data/models/coin.dart';
import 'coin_avatar.dart';
/// 币种选择器组件
///
/// 显示当前选中的币种交易对,点击弹出底部弹窗选择币种。
/// 卡片背景 + 圆角lg + border + padding:16
/// 横向布局coinInfo(竖向 pair+name) + chevronDown
class CoinSelector extends StatelessWidget {
final Coin? selectedCoin;
final List<Coin> coins;
final ValueChanged<Coin> onCoinSelected;
const CoinSelector({
super.key,
required this.selectedCoin,
required this.coins,
required this.onCoinSelected,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return GestureDetector(
onTap: () => _showCoinPicker(context),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 币种信息:交易对 + 名称
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
selectedCoin != null
? '${selectedCoin!.code}/USDT'
: '选择币种',
style: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 2),
Text(
selectedCoin?.name ?? '点击选择交易对',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
],
),
// 下拉箭头
Icon(LucideIcons.chevronDown,
size: 16, color: colorScheme.onSurfaceVariant),
],
),
),
);
}
void _showCoinPicker(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (ctx) => Container(
height: MediaQuery.of(ctx).size.height * 0.65,
decoration: BoxDecoration(
color: isDark
? colorScheme.surface
: colorScheme.surfaceContainerLowest,
borderRadius:
BorderRadius.vertical(top: Radius.circular(AppRadius.xxl)),
),
child: Column(
children: [
// 拖动指示器
Container(
margin: EdgeInsets.only(top: AppSpacing.sm),
width: 40,
height: 4,
decoration: BoxDecoration(
color: colorScheme.onSurfaceVariant.withOpacity(0.3),
borderRadius: BorderRadius.circular(2),
),
),
// 标题栏
Padding(
padding: EdgeInsets.all(AppSpacing.lg),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('选择币种',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
)),
GestureDetector(
onTap: () => Navigator.of(ctx).pop(),
child: Icon(LucideIcons.x,
color: colorScheme.onSurfaceVariant),
),
],
),
),
Divider(height: 1, color: colorScheme.outlineVariant.withOpacity(0.2)),
// 币种列表
Expanded(
child: ListView.builder(
padding: EdgeInsets.symmetric(vertical: AppSpacing.sm),
itemCount: coins.length,
itemBuilder: (listCtx, index) =>
_buildCoinItem(coins[index], context, listCtx),
),
),
],
),
),
);
}
Widget _buildCoinItem(
Coin coin, BuildContext context, BuildContext sheetContext) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final isSelected = selectedCoin?.code == coin.code;
final changeColor = coin.isUp
? AppColorScheme.getUpColor(isDark)
: AppColorScheme.getDownColor(isDark);
return GestureDetector(
onTap: () {
Navigator.of(sheetContext).pop();
onCoinSelected(coin);
},
child: Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.lg, vertical: AppSpacing.md),
color:
isSelected ? colorScheme.primary.withOpacity(0.1) : Colors.transparent,
child: Row(
children: [
CoinAvatar(icon: coin.displayIcon),
SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 第一行:币种代码 + USDT + 价格 + 涨跌幅
Row(
children: [
Text(coin.code,
style: GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
)),
SizedBox(width: AppSpacing.xs),
Text('/USDT',
style: GoogleFonts.inter(
fontSize: 11,
color: colorScheme.onSurfaceVariant,
)),
const Spacer(),
Text('\$${coin.formattedPrice}',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
)),
SizedBox(width: AppSpacing.sm),
// 涨跌幅徽章
Container(
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: changeColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(coin.formattedChange,
style: GoogleFonts.inter(
fontSize: 11,
color: changeColor,
fontWeight: FontWeight.w600,
)),
),
if (isSelected) ...[
SizedBox(width: AppSpacing.sm),
Icon(LucideIcons.check,
size: 16, color: colorScheme.primary),
],
],
),
SizedBox(height: 3),
// 第二行:币种名称
Text(coin.name,
style: GoogleFonts.inter(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
)),
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../../core/theme/app_color_scheme.dart';
import '../../../../core/theme/app_spacing.dart';
import '../../../components/glass_panel.dart';
import '../../../components/neon_glow.dart';
/// 交易确认对话框
///
/// 显示交易详情(交易对、委托价格、交易金额、交易数量),
/// 用户确认后执行交易。
class ConfirmDialog extends StatelessWidget {
final bool isBuy;
final String coinCode;
final String price;
final String quantity;
final String amount;
const ConfirmDialog({
super.key,
required this.isBuy,
required this.coinCode,
required this.price,
required this.quantity,
required this.amount,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final actionColor = isBuy
? AppColorScheme.getUpColor(isDark)
: AppColorScheme.getDownColor(isDark);
return Dialog(
backgroundColor: Colors.transparent,
child: GlassPanel(
borderRadius: BorderRadius.circular(AppRadius.lg),
padding: EdgeInsets.all(AppSpacing.lg),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
'确认${isBuy ? '买入' : '卖出'}',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
),
SizedBox(height: AppSpacing.lg),
_dialogRow('交易对', '$coinCode/USDT', colorScheme),
SizedBox(height: AppSpacing.sm),
_dialogRow('委托价格', '$price USDT', colorScheme),
SizedBox(height: AppSpacing.sm),
_dialogRow('交易金额', '$amount USDT', colorScheme,
valueColor: actionColor),
SizedBox(height: AppSpacing.sm),
_dialogRow('交易数量', '$quantity $coinCode', colorScheme),
SizedBox(height: AppSpacing.lg),
Row(
children: [
Expanded(
child: NeonButton(
text: '取消',
type: NeonButtonType.outline,
onPressed: () => Navigator.of(context).pop(false),
height: 44,
showGlow: false,
),
),
SizedBox(width: AppSpacing.sm),
Expanded(
child: NeonButton(
text: '确认${isBuy ? '买入' : '卖出'}',
type: isBuy ? NeonButtonType.tertiary : NeonButtonType.error,
onPressed: () => Navigator.of(context).pop(true),
height: 44,
showGlow: true,
),
),
],
),
],
),
),
);
}
Widget _dialogRow(String label, String value, ColorScheme colorScheme,
{Color? valueColor}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
)),
Text(value,
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: valueColor ?? colorScheme.onSurface,
)),
],
);
}
}

View File

@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../../core/theme/app_spacing.dart';
/// 占位卡片组件
///
/// 当未选择币种时显示的占位提示卡片。
class PlaceholderCard extends StatelessWidget {
final String message;
final ColorScheme colorScheme;
const PlaceholderCard({
super.key,
required this.message,
required this.colorScheme,
});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(AppSpacing.xl),
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Center(
child: Text(message,
style: GoogleFonts.inter(
color: colorScheme.onSurfaceVariant,
fontSize: 14,
)),
),
);
}
}

View File

@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../../core/theme/app_color_scheme.dart';
import '../../../../core/theme/app_spacing.dart';
import '../../../../data/models/coin.dart';
/// 价格卡片组件
///
/// 显示当前币种价格和 24h 涨跌幅。
/// 布局:大号价格(32px bold) + 涨跌幅徽章(圆角sm涨绿背景) + "24h 变化" 副标题。
class PriceCard extends StatelessWidget {
final Coin coin;
const PriceCard({super.key, required this.coin});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final isUp = coin.isUp;
final changeColor =
isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.getDownColor(isDark);
final changeBgColor = isUp
? AppColorScheme.getUpBackgroundColor(isDark)
: AppColorScheme.getDownBackgroundColor(isDark);
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 价格行:大号价格 + 涨跌幅徽章
Row(
children: [
Text(
coin.formattedPrice,
style: GoogleFonts.inter(
fontSize: 32,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
fontFeatures: [FontFeature.tabularFigures()],
),
),
const SizedBox(width: AppSpacing.sm),
// 涨跌幅徽章 - 圆角sm涨绿背景
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm, vertical: AppSpacing.xs),
decoration: BoxDecoration(
color: changeBgColor,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
coin.formattedChange,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
color: changeColor,
fontFeatures: [FontFeature.tabularFigures()],
),
),
),
],
),
const SizedBox(height: AppSpacing.sm),
// 副标题
Text(
'24h 变化',
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../../core/theme/app_color_scheme.dart';
import '../../../../core/theme/app_spacing.dart';
/// 交易按钮组件
///
/// CTA 买入/卖出按钮。profit-green底 / sell-red底圆角lg高48白字16px bold。
class TradeButton extends StatelessWidget {
final bool isBuy;
final String? coinCode;
final bool enabled;
final bool isLoading;
final VoidCallback onPressed;
const TradeButton({
super.key,
required this.isBuy,
required this.coinCode,
required this.enabled,
required this.isLoading,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final fillColor =
isBuy ? AppColorScheme.buyButtonFill : AppColorScheme.sellButtonFill;
return GestureDetector(
onTap: enabled ? onPressed : null,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: 48,
decoration: BoxDecoration(
color: enabled ? fillColor : colorScheme.onSurface.withOpacity(0.08),
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Center(
child: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(
'${isBuy ? '买入' : '卖出'} ${coinCode ?? ""}',
style: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w700,
color: enabled
? Colors.white
: colorScheme.onSurface.withOpacity(0.3),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,247 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../../core/theme/app_color_scheme.dart';
import '../../../../core/theme/app_spacing.dart';
import '../../../../data/models/coin.dart';
import 'amount_input.dart';
/// 交易表单卡片组件
///
/// 包含买入/卖出切换、金额输入、可用余额、快捷比例按钮、计算数量行。
/// card背景 + 圆角lg + border + padding:20 + gap:16
class TradeFormCard extends StatelessWidget {
final int tradeType;
final Coin? selectedCoin;
final TextEditingController amountController;
final String availableUsdt;
final String availableCoinQty;
final String calculatedQuantity;
final String maxAmount;
final ValueChanged<int> onTradeTypeChanged;
final VoidCallback onAmountChanged;
final ValueChanged<double> onFillPercent;
const TradeFormCard({
super.key,
required this.tradeType,
required this.selectedCoin,
required this.amountController,
required this.availableUsdt,
required this.availableCoinQty,
required this.calculatedQuantity,
required this.maxAmount,
required this.onTradeTypeChanged,
required this.onAmountChanged,
required this.onFillPercent,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final isBuy = tradeType == 0;
final actionColor = isBuy
? AppColorScheme.getUpColor(isDark)
: AppColorScheme.getDownColor(isDark);
// 设计稿中 card 背景色
final cardBgColor = isDark
? colorScheme.surfaceContainer
: colorScheme.surfaceContainerLowest;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: cardBgColor,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ---- 买入/卖出切换 ----
// 设计稿ClipRRect + 圆角md两等宽按钮
ClipRRect(
borderRadius: BorderRadius.circular(AppRadius.md),
child: Row(
children: [
// 买入按钮
Expanded(
child: GestureDetector(
onTap: () => onTradeTypeChanged(0),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
height: 40,
decoration: BoxDecoration(
color: isBuy
? AppColorScheme.buyButtonFill
: cardBgColor,
border: isBuy
? null
: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15)),
),
child: Center(
child: Text(
'买入',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: isBuy
? Colors.white
: colorScheme.onSurfaceVariant,
),
),
),
),
),
),
// 卖出按钮
Expanded(
child: GestureDetector(
onTap: () => onTradeTypeChanged(1),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
height: 40,
decoration: BoxDecoration(
color: !isBuy
? AppColorScheme.sellButtonFill
: cardBgColor,
border: !isBuy
? null
: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.15)),
),
child: Center(
child: Text(
'卖出',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: !isBuy
? Colors.white
: colorScheme.onSurfaceVariant,
),
),
),
),
),
),
],
),
),
const SizedBox(height: AppSpacing.md + AppSpacing.sm),
// ---- 交易金额 label 行 ----
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('交易金额',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
)),
Text('USDT',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
)),
],
),
const SizedBox(height: AppSpacing.sm),
// ---- 金额输入框 ----
AmountInput(
amountController: amountController,
maxAmount: maxAmount,
isBuy: isBuy,
actionColor: actionColor,
onChanged: onAmountChanged,
),
const SizedBox(height: AppSpacing.sm),
// ---- 可用余额 ----
Text(
isBuy
? '可用: $availableUsdt USDT'
: '可用: $availableCoinQty ${selectedCoin?.code ?? ""}',
style: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: AppSpacing.md),
// ---- 快捷比例按钮 25% 50% 75% 100% ----
// 设计稿gap:8圆角smbg-tertiary高32
Row(
children: [
_buildPctButton('25%', 0.25, colorScheme),
const SizedBox(width: AppSpacing.sm),
_buildPctButton('50%', 0.5, colorScheme),
const SizedBox(width: AppSpacing.sm),
_buildPctButton('75%', 0.75, colorScheme),
const SizedBox(width: AppSpacing.sm),
_buildPctButton('100%', 1.0, colorScheme),
],
),
const SizedBox(height: AppSpacing.md + AppSpacing.sm),
// ---- 计算数量行 ----
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('交易数量',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.normal,
color: colorScheme.onSurfaceVariant,
)),
Text(
'$calculatedQuantity ${selectedCoin?.code ?? ''}',
style: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
fontFeatures: [FontFeature.tabularFigures()],
),
),
],
),
],
),
);
}
/// 百分比按钮 - 设计稿圆角smbg-tertiary高32
Widget _buildPctButton(String label, double pct, ColorScheme colorScheme) {
return Expanded(
child: GestureDetector(
onTap: () => onFillPercent(pct),
child: Container(
height: 32,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest.withOpacity(0.5),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Center(
child: Text(label,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
color: colorScheme.onSurfaceVariant,
)),
),
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

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