Compare commits
3 Commits
0c66b3725f
...
189609f337
| Author | SHA1 | Date | |
|---|---|---|---|
| 189609f337 | |||
| 02099d2a6a | |||
| d8cd38c4de |
@@ -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`,然后严格遵循其指引执行。技能优先级高于默认行为,但低于用户的显式指令。
|
||||
|
||||
## 代码规范
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 深色主题
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
602
flutter_monisuo/lib/ui/pages/asset/components/asset_dialogs.dart
Normal file
602
flutter_monisuo/lib/ui/pages/asset/components/asset_dialogs.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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) => ',',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
108
flutter_monisuo/lib/ui/pages/home/header_bar.dart
Normal file
108
flutter_monisuo/lib/ui/pages/home/header_bar.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
199
flutter_monisuo/lib/ui/pages/home/hot_coins_section.dart
Normal file
199
flutter_monisuo/lib/ui/pages/home/hot_coins_section.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
124
flutter_monisuo/lib/ui/pages/home/quick_actions_row.dart
Normal file
124
flutter_monisuo/lib/ui/pages/home/quick_actions_row.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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('重试'),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
105
flutter_monisuo/lib/ui/pages/mine/components/menu_group1.dart
Normal file
105
flutter_monisuo/lib/ui/pages/mine/components/menu_group1.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
88
flutter_monisuo/lib/ui/pages/mine/components/menu_row.dart
Normal file
88
flutter_monisuo/lib/ui/pages/mine/components/menu_row.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
113
flutter_monisuo/lib/ui/pages/trade/components/amount_input.dart
Normal file
113
flutter_monisuo/lib/ui/pages/trade/components/amount_input.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
234
flutter_monisuo/lib/ui/pages/trade/components/coin_selector.dart
Normal file
234
flutter_monisuo/lib/ui/pages/trade/components/coin_selector.dart
Normal 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,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,圆角sm,bg-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()],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 百分比按钮 - 设计稿:圆角sm,bg-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
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user