feat(ui): 应用新设计系统到 Flutter 项目

- 更新颜色系统为 Material Design 3
  * Primary: #72dcff (青色)
  * Secondary: #dd8bfb (紫色)
  * Tertiary: #afffd1 (绿色)

- 创建新的 UI 组件
  * GlassPanel: 毛玻璃效果面板
  * NeonGlow: 霓虹光效组件
  * GradientButton: 渐变按钮组件

- 更新所有页面样式
  * 交易页面 (trade_page.dart)
  * 行情页面 (market_page.dart)
  * 资产页面 (asset_page.dart)
  * 我的页面 (mine_page.dart)
  * 订单页面 (orders_page.dart)

- 支持深色和浅色主题
- 所有 UI 文字使用中文
- 保持现有 API 接口不变

变更统计:
- 9 个文件修改
- 1,893 行新增
- 691 行删除
- 3 个新组件
This commit is contained in:
2026-03-24 02:16:19 +08:00
parent dc61d845a5
commit df0e8beba9
11 changed files with 2625 additions and 705 deletions

View File

@@ -1,52 +1,96 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:shadcn_ui/shadcn_ui.dart';
/// "The Kinetic Vault" & "The Ethereal Terminal" 双主题颜色系统 /// Material Design 3 颜色系统 - "The Kinetic Vault" & "The Ethereal Terminal"
/// ///
/// 深色主题: "The Kinetic Vault" - 高端加密编辑风格 /// 深色主题: "The Kinetic Vault" - 赛博朋克风格
/// 浅色主题: "The Ethereal Terminal" - 高端金融科技风格 /// 浅色主题: "The Ethereal Terminal" - 高端金融科技风格
/// ///
/// 设计原则: /// 设计原则:
/// - 无边框规则: 禁止 1px solid 边框,使用层次色变化 /// - Material Design 3 配色方案
/// - 层次化设计: 通过 surface-container 层次而非阴影 /// - Glass Panel 毛玻璃效果
/// - Neon Glow 霓虹光效
/// - 渐变 CTA: primary → primary_container (135度) /// - 渐变 CTA: primary → primary_container (135度)
class AppColorScheme { class AppColorScheme {
AppColorScheme._(); AppColorScheme._();
// ============================================ // ============================================
// 深色主题 - "The Kinetic Vault" // 深色主题 - "The Kinetic Vault" (Material Design 3)
// ============================================ // ============================================
/// 背景基色 - 最深 /// 背景基色 - 最深
static const Color darkBackground = Color(0xFF0b0e14); static const Color darkBackground = Color(0xFF0b0e14);
/// Surface 层次 (从低到高) /// Surface 层次 (从低到高) - Material Design 3 规范
static const Color darkSurfaceLowest = Color(0xFF0d1017); static const Color darkSurfaceDim = Color(0xFF0b0e14);
static const Color darkSurfaceLowest = Color(0xFF000000);
static const Color darkSurfaceLow = Color(0xFF10131a); static const Color darkSurfaceLow = Color(0xFF10131a);
static const Color darkSurface = Color(0xFF151921); static const Color darkSurface = Color(0xFF0b0e14);
static const Color darkSurfaceHigh = Color(0xFF1a1f2a); static const Color darkSurfaceContainer = Color(0xFF161a21);
static const Color darkSurfaceHighest = Color(0xFF22262f); static const Color darkSurfaceContainerHigh = Color(0xFF1c2028);
static const Color darkSurfaceContainerHighest = Color(0xFF22262f);
static const Color darkSurfaceBright = Color(0xFF282c36); static const Color darkSurfaceBright = Color(0xFF282c36);
static const Color darkSurfaceVariant = Color(0xFF22262f);
/// Ghost Border - 后备边框 /// 兼容旧名称
static const Color darkSurfaceContainerLowest = darkSurfaceLowest;
static const Color darkSurfaceContainerLow = darkSurfaceLow;
static const Color darkSurfaceHigh = darkSurfaceContainerHigh;
static const Color darkSurfaceHighest = darkSurfaceContainerHighest;
/// Ghost Border
static const Color darkOutline = Color(0xFF73757d);
static const Color darkOutlineVariant = Color(0xFF45484f); static const Color darkOutlineVariant = Color(0xFF45484f);
/// Primary - Neon Blue (主要交互) /// Primary - Neon Cyan (青色 - 主要交互)
static const Color darkPrimary = Color(0xFF72dcff); static const Color darkPrimary = Color(0xFF72dcff);
static const Color darkPrimaryContainer = Color(0xFF4ac8f0); static const Color darkPrimaryDim = Color(0xFF00c3ed);
static const Color darkPrimaryContainer = Color(0xFF00d2ff);
static const Color darkPrimaryFixed = Color(0xFF00d2ff);
static const Color darkPrimaryFixedDim = Color(0xFF00c3ed);
static const Color darkOnPrimary = Color(0xFF004c5e);
static const Color darkOnPrimaryContainer = Color(0xFF004253);
static const Color darkOnPrimaryFixed = Color(0xFF002c38);
static const Color darkOnPrimaryFixedVariant = Color(0xFF004c5e);
/// Secondary - Electric Purple (次要强调) /// Secondary - Electric Purple (紫色 - 次要强调)
static const Color darkSecondary = Color(0xFFdd8bfb); static const Color darkSecondary = Color(0xFFdd8bfb);
static const Color darkSecondaryContainer = Color(0xFF2d1f3d); static const Color darkSecondaryDim = Color(0xFFce7eec);
static const Color darkSecondaryContainer = Color(0xFF6e208c);
static const Color darkSecondaryFixed = Color(0xFFf2c1ff);
static const Color darkSecondaryFixedDim = Color(0xFFebadff);
static const Color darkOnSecondary = Color(0xFF4c0068);
static const Color darkOnSecondaryContainer = Color(0xFFf1bfff);
static const Color darkOnSecondaryFixed = Color(0xFF580078);
static const Color darkOnSecondaryFixedVariant = Color(0xFF792c97);
/// Tertiary - Emerald Green (仅用于成功/盈利/买入) /// Tertiary - Neon Green (绿色 - 仅用于成功/盈利/买入)
static const Color darkTertiary = Color(0xFFafffd1); static const Color darkTertiary = Color(0xFFafffd1);
static const Color darkTertiaryContainer = Color(0xFF1a3d2d); static const Color darkTertiaryDim = Color(0xFF00efa0);
static const Color darkTertiaryContainer = Color(0xFF00ffab);
static const Color darkTertiaryFixed = Color(0xFF00ffab);
static const Color darkTertiaryFixedDim = Color(0xFF00efa0);
static const Color darkOnTertiary = Color(0xFF006642);
static const Color darkOnTertiaryContainer = Color(0xFF005c3b);
static const Color darkOnTertiaryFixed = Color(0xFF00472d);
static const Color darkOnTertiaryFixedVariant = Color(0xFF006742);
/// Error - Neon Red (红色 - 错误/卖出)
static const Color darkError = Color(0xFFff716c);
static const Color darkErrorDim = Color(0xFFd7383b);
static const Color darkErrorContainer = Color(0xFF9f0519);
static const Color darkOnError = Color(0xFF490006);
static const Color darkOnErrorContainer = Color(0xFFffa8a3);
/// 文本色 /// 文本色
static const Color darkOnSurface = Color(0xFFecedf6); static const Color darkOnSurface = Color(0xFFecedf6);
static const Color darkOnSurfaceVariant = Color(0xFFa9abb3); static const Color darkOnSurfaceVariant = Color(0xFFa9abb3);
static const Color darkOnSurfaceMuted = Color(0xFF6b6d75); 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 darkInversePrimary = Color(0xFF00687f);
static const Color darkSurfaceTint = Color(0xFF72dcff);
// ============================================ // ============================================
// 浅色主题 - "The Ethereal Terminal" // 浅色主题 - "The Ethereal Terminal"
@@ -56,11 +100,11 @@ class AppColorScheme {
static const Color lightBackground = Color(0xFFf5f7f9); static const Color lightBackground = Color(0xFFf5f7f9);
/// Surface 层次 (从低到高) /// Surface 层次 (从低到高)
static const Color lightSurfaceLowest = Color(0xFFffffff); // Elevated (pop) static const Color lightSurfaceLowest = Color(0xFFffffff);
static const Color lightSurfaceLow = Color(0xFFeef1f3); // Softly recessed static const Color lightSurfaceLow = Color(0xFFeef1f3);
static const Color lightSurface = Color(0xFFf5f7f9); // Base canvas static const Color lightSurface = Color(0xFFf5f7f9);
static const Color lightSurfaceHigh = Color(0xFFe8ebef); // Elevated static const Color lightSurfaceHigh = Color(0xFFe8ebef);
static const Color lightSurfaceHighest = Color(0xFFd9dde0); // Navigation anchor static const Color lightSurfaceHighest = Color(0xFFd9dde0);
/// Ghost Border /// Ghost Border
static const Color lightOutlineVariant = Color(0xFFc4c8cc); static const Color lightOutlineVariant = Color(0xFFc4c8cc);
@@ -82,6 +126,29 @@ class AppColorScheme {
static const Color lightOnSurfaceVariant = Color(0xFF5a5d60); static const Color lightOnSurfaceVariant = Color(0xFF5a5d60);
static const Color lightOnSurfaceMuted = Color(0xFF8a8d90); static const Color lightOnSurfaceMuted = Color(0xFF8a8d90);
// ============================================
// Glass Panel 毛玻璃效果颜色
// ============================================
/// Glass Panel 背景色 - rgba(34, 38, 47, 0.4)
static const Color glassPanelBackground = Color(0x6622262f);
/// Glass Panel 边框色 - rgba(69, 72, 79, 0.15)
static const Color glassPanelBorder = Color(0x2645484f);
// ============================================
// Neon Glow 霓虹光效颜色
// ============================================
/// Primary Glow - rgba(114, 220, 255, 0.15)
static const Color neonGlowPrimary = Color(0x2672dcff);
/// Secondary Glow - rgba(221, 139, 251, 0.15)
static const Color neonGlowSecondary = Color(0x26dd8bfb);
/// Tertiary Glow - rgba(175, 255, 209, 0.2)
static const Color neonGlowTertiary = Color(0x33afffd1);
// ============================================ // ============================================
// 语义色 - 明暗通用 // 语义色 - 明暗通用
// ============================================ // ============================================
@@ -90,10 +157,13 @@ class AppColorScheme {
static const Color up = darkTertiary; static const Color up = darkTertiary;
static const Color success = darkTertiary; static const Color success = darkTertiary;
/// 跌/卖出/错误 /// 跌/卖出/错误 - 使用 Material Design 3 error
static const Color down = Color(0xFFFF5252); static const Color down = Color(0xFFff716c);
static const Color error = down; static const Color error = down;
/// 静默/禁用/次要
static const Color muted = darkOnSurfaceVariant;
/// 警告 /// 警告
static const Color warning = Color(0xFFFF9800); static const Color warning = Color(0xFFFF9800);
@@ -123,14 +193,14 @@ class AppColorScheme {
/// 买入按钮渐变 /// 买入按钮渐变
static const LinearGradient buyGradient = LinearGradient( static const LinearGradient buyGradient = LinearGradient(
colors: [darkTertiary, Color(0xFF7de8b8)], colors: [darkTertiary, darkTertiaryContainer],
begin: Alignment(-0.7, -0.7), begin: Alignment(-0.7, -0.7),
end: Alignment(0.7, 0.7), end: Alignment(0.7, 0.7),
); );
/// 卖出按钮渐变 /// 卖出按钮渐变
static const LinearGradient sellGradient = LinearGradient( static const LinearGradient sellGradient = LinearGradient(
colors: [down, Color(0xFFe84545)], colors: [darkError, darkErrorDim],
begin: Alignment(-0.7, -0.7), begin: Alignment(-0.7, -0.7),
end: Alignment(0.7, 0.7), end: Alignment(0.7, 0.7),
); );
@@ -149,15 +219,15 @@ class AppColorScheme {
static ShadColorScheme get darkShad => ShadColorScheme( static ShadColorScheme get darkShad => ShadColorScheme(
background: darkBackground, background: darkBackground,
foreground: darkOnSurface, foreground: darkOnSurface,
card: darkSurface, card: darkSurfaceContainer,
cardForeground: darkOnSurface, cardForeground: darkOnSurface,
popover: darkSurfaceHigh, popover: darkSurfaceContainerHigh,
popoverForeground: darkOnSurface, popoverForeground: darkOnSurface,
primary: darkPrimary, primary: darkPrimary,
primaryForeground: darkBackground, primaryForeground: darkOnPrimary,
secondary: darkSecondary, secondary: darkSecondary,
secondaryForeground: darkOnSurface, secondaryForeground: darkOnSurface,
muted: darkSurfaceHigh, muted: darkSurfaceContainerHigh,
mutedForeground: darkOnSurfaceVariant, mutedForeground: darkOnSurfaceVariant,
accent: darkPrimary.withValues(alpha: 0.15), accent: darkPrimary.withValues(alpha: 0.15),
accentForeground: darkPrimary, accentForeground: darkPrimary,
@@ -181,7 +251,7 @@ class AppColorScheme {
popover: lightSurfaceLowest, popover: lightSurfaceLowest,
popoverForeground: lightOnSurface, popoverForeground: lightOnSurface,
primary: lightPrimary, primary: lightPrimary,
primaryForeground: Color(0xFFFFFFFF), primaryForeground: const Color(0xFFFFFFFF),
secondary: lightSecondary, secondary: lightSecondary,
secondaryForeground: lightOnSurface, secondaryForeground: lightOnSurface,
muted: lightSurfaceHigh, muted: lightSurfaceHigh,
@@ -189,7 +259,7 @@ class AppColorScheme {
accent: lightPrimary.withValues(alpha: 0.1), accent: lightPrimary.withValues(alpha: 0.1),
accentForeground: lightPrimary, accentForeground: lightPrimary,
destructive: error, destructive: error,
destructiveForeground: Color(0xFFFFFFFF), destructiveForeground: const Color(0xFFFFFFFF),
border: lightOutlineVariant.withValues(alpha: 0.5), border: lightOutlineVariant.withValues(alpha: 0.5),
input: lightOutlineVariant.withValues(alpha: 0.3), input: lightOutlineVariant.withValues(alpha: 0.3),
ring: lightPrimary, ring: lightPrimary,
@@ -197,26 +267,42 @@ class AppColorScheme {
); );
// ============================================ // ============================================
// Material ColorScheme - 深色主题 // Material ColorScheme - 深色主题 (Material Design 3)
// ============================================ // ============================================
static ColorScheme get darkMaterial => ColorScheme.dark( static ColorScheme get darkMaterial => ColorScheme.dark(
primary: darkPrimary, primary: darkPrimary,
onPrimary: darkBackground, onPrimary: darkOnPrimary,
primaryContainer: darkPrimaryContainer,
onPrimaryContainer: darkOnPrimaryContainer,
secondary: darkSecondary, secondary: darkSecondary,
onSecondary: darkOnSurface, onSecondary: darkOnSecondary,
secondaryContainer: darkSecondaryContainer,
onSecondaryContainer: darkOnSecondaryContainer,
tertiary: darkTertiary, tertiary: darkTertiary,
onTertiary: darkBackground, onTertiary: darkOnTertiary,
error: error, tertiaryContainer: darkTertiaryContainer,
onError: darkOnSurface, onTertiaryContainer: darkOnTertiaryContainer,
error: darkError,
onError: darkOnError,
errorContainer: darkErrorContainer,
onErrorContainer: darkOnErrorContainer,
surface: darkSurface, surface: darkSurface,
onSurface: darkOnSurface, onSurface: darkOnSurface,
surfaceDim: darkSurfaceDim,
surfaceBright: darkSurfaceBright,
surfaceContainerLowest: darkSurfaceLowest, surfaceContainerLowest: darkSurfaceLowest,
surfaceContainerLow: darkSurfaceLow, surfaceContainerLow: darkSurfaceLow,
surfaceContainer: darkSurface, surfaceContainer: darkSurfaceContainer,
surfaceContainerHigh: darkSurfaceHigh, surfaceContainerHigh: darkSurfaceContainerHigh,
surfaceContainerHighest: darkSurfaceHighest, surfaceContainerHighest: darkSurfaceContainerHighest,
onSurfaceVariant: darkOnSurfaceVariant,
outline: darkOutline,
outlineVariant: darkOutlineVariant, outlineVariant: darkOutlineVariant,
inverseSurface: darkInverseSurface,
onInverseSurface: darkInverseOnSurface,
inversePrimary: darkInversePrimary,
surfaceTint: darkSurfaceTint,
); );
// ============================================ // ============================================
@@ -225,13 +311,13 @@ class AppColorScheme {
static ColorScheme get lightMaterial => ColorScheme.light( static ColorScheme get lightMaterial => ColorScheme.light(
primary: lightPrimary, primary: lightPrimary,
onPrimary: Color(0xFFFFFFFF), onPrimary: const Color(0xFFFFFFFF),
secondary: lightSecondary, secondary: lightSecondary,
onSecondary: Color(0xFFFFFFFF), onSecondary: const Color(0xFFFFFFFF),
tertiary: lightTertiary, tertiary: lightTertiary,
onTertiary: Color(0xFFFFFFFF), onTertiary: const Color(0xFFFFFFFF),
error: error, error: error,
onError: Color(0xFFFFFFFF), onError: const Color(0xFFFFFFFF),
surface: lightSurface, surface: lightSurface,
onSurface: lightOnSurface, onSurface: lightOnSurface,
surfaceContainerLowest: lightSurfaceLowest, surfaceContainerLowest: lightSurfaceLowest,
@@ -255,14 +341,14 @@ class AppColorScheme {
@Deprecated('Use darkBackground instead') @Deprecated('Use darkBackground instead')
static const Color _darkBackground = darkBackground; static const Color _darkBackground = darkBackground;
@Deprecated('Use darkSurface instead') @Deprecated('Use darkSurfaceContainer instead')
static const Color _darkCardBackground = darkSurface; static const Color _darkCardBackground = darkSurfaceContainer;
@Deprecated('Use darkSurfaceHigh instead') @Deprecated('Use darkSurfaceContainerHigh instead')
static const Color _darkSecondary = darkSurfaceHigh; static const Color _darkSecondary = darkSurfaceContainerHigh;
@Deprecated('Use darkSurfaceHigh instead') @Deprecated('Use darkSurfaceContainerHigh instead')
static const Color _darkMuted = darkSurfaceHigh; static const Color _darkMuted = darkSurfaceContainerHigh;
@Deprecated('Use darkOutlineVariant instead') @Deprecated('Use darkOutlineVariant instead')
static const Color _darkBorder = darkOutlineVariant; static const Color _darkBorder = darkOutlineVariant;

View File

@@ -4,3 +4,5 @@ library components;
export 'coin_card.dart'; export 'coin_card.dart';
export 'trade_button.dart'; export 'trade_button.dart';
export 'asset_card.dart'; export 'asset_card.dart';
export 'glass_panel.dart';
export 'neon_glow.dart';

View File

@@ -0,0 +1,315 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import '../../core/theme/app_color_scheme.dart';
import '../../core/theme/app_spacing.dart';
/// GlassPanel - 毛玻璃效果面板
///
/// Material Design 3 风格的毛玻璃效果组件
/// 用于卡片、弹窗、底部抽屉等需要毛玻璃效果的容器
///
/// 示例:
/// ```dart
/// GlassPanel(
/// child: Text('内容'),
/// )
/// ```
class GlassPanel extends StatelessWidget {
/// 子组件
final Widget child;
/// 模糊程度,默认 20.0
final double blur;
/// 背景色,默认使用 GlassPanel 背景色
final Color? backgroundColor;
/// 边框色,默认使用 GlassPanel 边框色
final Color? borderColor;
/// 圆角,默认特大圆角
final BorderRadius? borderRadius;
/// 内边距
final EdgeInsetsGeometry? padding;
/// 外边距
final EdgeInsetsGeometry? margin;
/// 宽度
final double? width;
/// 高度
final double? height;
/// 是否显示边框
final bool showBorder;
const GlassPanel({
super.key,
required this.child,
this.blur = 20.0,
this.backgroundColor,
this.borderColor,
this.borderRadius,
this.padding,
this.margin,
this.width,
this.height,
this.showBorder = true,
});
@override
Widget build(BuildContext context) {
final bgColor = backgroundColor ?? AppColorScheme.glassPanelBackground;
final brColor = borderColor ?? AppColorScheme.glassPanelBorder;
final br = borderRadius ?? BorderRadius.circular(AppRadius.xl);
Widget content = ClipRRect(
borderRadius: br,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
child: Container(
width: width,
height: height,
padding: padding ?? EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: bgColor,
borderRadius: br,
border: showBorder
? Border.all(
color: brColor,
width: 1,
)
: null,
),
child: child,
),
),
);
if (margin != null) {
content = Padding(
padding: margin!,
child: content,
);
}
return content;
}
}
/// GlassCard - 带毛玻璃效果的卡片
///
/// 用于列表项、信息展示等场景
/// 预设了常用配置,简化使用
class GlassCard extends StatelessWidget {
/// 子组件
final Widget child;
/// 点击回调
final VoidCallback? onTap;
/// 长按回调
final VoidCallback? onLongPress;
/// 内边距
final EdgeInsetsGeometry? padding;
/// 外边距
final EdgeInsetsGeometry? margin;
/// 圆角
final BorderRadius? borderRadius;
/// 是否显示霓虹光效
final bool showNeonGlow;
/// 霓虹光效颜色
final Color? neonGlowColor;
const GlassCard({
super.key,
required this.child,
this.onTap,
this.onLongPress,
this.padding,
this.margin,
this.borderRadius,
this.showNeonGlow = false,
this.neonGlowColor,
});
@override
Widget build(BuildContext context) {
final br = borderRadius ?? BorderRadius.circular(AppRadius.xl);
final glowColor = neonGlowColor ?? AppColorScheme.neonGlowPrimary;
Widget card = GlassPanel(
padding: padding ?? EdgeInsets.all(AppSpacing.md),
margin: margin,
borderRadius: br,
child: child,
);
if (showNeonGlow) {
card = Container(
decoration: BoxDecoration(
borderRadius: br,
boxShadow: [
BoxShadow(
color: glowColor,
blurRadius: 15,
spreadRadius: 0,
),
],
),
child: card,
);
}
if (onTap != null || onLongPress != null) {
return GestureDetector(
onTap: onTap,
onLongPress: onLongPress,
child: card,
);
}
return card;
}
}
/// GlassBottomSheet - 毛玻璃底部抽屉
///
/// 用于弹出的底部面板
class GlassBottomSheet extends StatelessWidget {
/// 子组件
final Widget child;
/// 标题
final String? title;
/// 是否显示关闭按钮
final bool showCloseButton;
/// 内边距
final EdgeInsetsGeometry? padding;
const GlassBottomSheet({
super.key,
required this.child,
this.title,
this.showCloseButton = true,
this.padding,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: AppColorScheme.glassPanelBackground,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppRadius.xxl),
),
border: Border.all(
color: AppColorScheme.glassPanelBorder,
),
),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppRadius.xxl),
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 顶部拖动条
Container(
margin: const EdgeInsets.only(top: 12, bottom: 8),
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColorScheme.darkOutlineVariant.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(2),
),
),
// 标题栏
if (title != null || showCloseButton)
Padding(
padding: EdgeInsets.fromLTRB(
AppSpacing.lg,
AppSpacing.sm,
AppSpacing.sm,
AppSpacing.md,
),
child: Row(
children: [
if (title != null)
Expanded(
child: Text(
title!,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColorScheme.darkOnSurface,
),
),
),
if (showCloseButton)
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
padding: EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: AppColorScheme.darkOutlineVariant
.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
size: 18,
color: AppColorScheme.darkOnSurfaceVariant,
),
),
),
],
),
),
// 内容
Padding(
padding: padding ??
EdgeInsets.fromLTRB(
AppSpacing.lg,
0,
AppSpacing.lg,
AppSpacing.xl,
),
child: child,
),
],
),
),
),
);
}
/// 显示底部抽屉
static Future<T?> show<T>({
required BuildContext context,
required Widget Function(BuildContext) builder,
bool isScrollControlled = false,
bool isDismissible = true,
bool enableDrag = true,
}) {
return showModalBottomSheet<T>(
context: context,
isScrollControlled: isScrollControlled,
isDismissible: isDismissible,
enableDrag: enableDrag,
backgroundColor: Colors.transparent,
builder: builder,
);
}
}

View File

@@ -0,0 +1,403 @@
import 'package:flutter/material.dart';
import '../../core/theme/app_color_scheme.dart';
import '../../core/theme/app_spacing.dart';
/// NeonGlow - 霓虹光效组件
///
/// Material Design 3 风格的霓虹光效
/// 用于按钮、卡片、图标等需要突出显示的元素
///
/// 光效类型:
/// - Primary: 青色光效 (#72dcff)
/// - Secondary: 紫色光效 (#dd8bfb)
/// - Tertiary: 绿色光效 (#afffd1)
/// - Error: 红色光效 (#ff716c)
class NeonGlow extends StatelessWidget {
/// 子组件
final Widget child;
/// 光效颜色
final Color glowColor;
/// 模糊半径,默认 15.0
final double blurRadius;
/// 扩散半径,默认 0.0
final double spreadRadius;
/// 圆角
final BorderRadius? borderRadius;
const NeonGlow({
super.key,
required this.child,
required this.glowColor,
this.blurRadius = 15.0,
this.spreadRadius = 0.0,
this.borderRadius,
});
/// Primary 霓虹光效 (青色)
factory NeonGlow.primary({
Key? key,
required Widget child,
double blurRadius = 15.0,
BorderRadius? borderRadius,
}) {
return NeonGlow(
key: key,
glowColor: AppColorScheme.neonGlowPrimary,
blurRadius: blurRadius,
borderRadius: borderRadius,
child: child,
);
}
/// Secondary 霓虹光效 (紫色)
factory NeonGlow.secondary({
Key? key,
required Widget child,
double blurRadius = 15.0,
BorderRadius? borderRadius,
}) {
return NeonGlow(
key: key,
glowColor: AppColorScheme.neonGlowSecondary,
blurRadius: blurRadius,
borderRadius: borderRadius,
child: child,
);
}
/// Tertiary 霓虹光效 (绿色)
factory NeonGlow.tertiary({
Key? key,
required Widget child,
double blurRadius = 20.0,
BorderRadius? borderRadius,
}) {
return NeonGlow(
key: key,
glowColor: AppColorScheme.neonGlowTertiary,
blurRadius: blurRadius,
borderRadius: borderRadius,
child: child,
);
}
/// Error 霓虹光效 (红色)
factory NeonGlow.error({
Key? key,
required Widget child,
double blurRadius = 15.0,
BorderRadius? borderRadius,
}) {
return NeonGlow(
key: key,
glowColor: AppColorScheme.darkError.withValues(alpha: 0.3),
blurRadius: blurRadius,
borderRadius: borderRadius,
child: child,
);
}
@override
Widget build(BuildContext context) {
final br = borderRadius ?? BorderRadius.circular(AppRadius.xl);
return Container(
decoration: BoxDecoration(
borderRadius: br,
boxShadow: [
BoxShadow(
color: glowColor,
blurRadius: blurRadius,
spreadRadius: spreadRadius,
),
],
),
child: child,
);
}
}
/// NeonButton - 带霓虹光效的按钮
///
/// 预设了常用配置,简化使用
class NeonButton extends StatefulWidget {
/// 按钮文本
final String text;
/// 点击回调
final VoidCallback? onPressed;
/// 按钮类型
final NeonButtonType type;
/// 是否显示光效
final bool showGlow;
/// 图标
final IconData? icon;
/// 是否加载中
final bool isLoading;
/// 按钮宽度
final double? width;
/// 按钮高度,默认 48
final double height;
const NeonButton({
super.key,
required this.text,
this.onPressed,
this.type = NeonButtonType.primary,
this.showGlow = true,
this.icon,
this.isLoading = false,
this.width,
this.height = 48,
});
@override
State<NeonButton> createState() => _NeonButtonState();
}
class _NeonButtonState extends State<NeonButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
bool _isPressed = false;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _onTapDown(TapDownDetails details) {
setState(() => _isPressed = true);
_controller.forward();
}
void _onTapUp(TapUpDetails details) {
setState(() => _isPressed = false);
_controller.reverse();
}
void _onTapCancel() {
setState(() => _isPressed = false);
_controller.reverse();
}
Color get _backgroundColor {
switch (widget.type) {
case NeonButtonType.primary:
return AppColorScheme.darkPrimary;
case NeonButtonType.secondary:
return AppColorScheme.darkSecondary;
case NeonButtonType.tertiary:
return AppColorScheme.darkTertiary;
case NeonButtonType.error:
return AppColorScheme.darkError;
case NeonButtonType.outline:
return Colors.transparent;
}
}
Color get _foregroundColor {
switch (widget.type) {
case NeonButtonType.primary:
return AppColorScheme.darkOnPrimaryFixed;
case NeonButtonType.secondary:
return AppColorScheme.darkOnSecondary;
case NeonButtonType.tertiary:
return AppColorScheme.darkOnTertiaryFixed;
case NeonButtonType.error:
return AppColorScheme.darkOnError;
case NeonButtonType.outline:
return AppColorScheme.darkPrimary;
}
}
Color get _glowColor {
switch (widget.type) {
case NeonButtonType.primary:
return AppColorScheme.neonGlowPrimary;
case NeonButtonType.secondary:
return AppColorScheme.neonGlowSecondary;
case NeonButtonType.tertiary:
return AppColorScheme.neonGlowTertiary;
case NeonButtonType.error:
return AppColorScheme.darkError.withValues(alpha: 0.3);
case NeonButtonType.outline:
return AppColorScheme.neonGlowPrimary;
}
}
LinearGradient? get _gradient {
if (widget.type == NeonButtonType.outline) return null;
switch (widget.type) {
case NeonButtonType.primary:
return const LinearGradient(
colors: [AppColorScheme.darkPrimary, AppColorScheme.darkPrimaryContainer],
begin: Alignment(-0.7, -0.7),
end: Alignment(0.7, 0.7),
);
case NeonButtonType.secondary:
return const LinearGradient(
colors: [AppColorScheme.darkSecondary, AppColorScheme.darkSecondaryFixed],
begin: Alignment(-0.7, -0.7),
end: Alignment(0.7, 0.7),
);
case NeonButtonType.tertiary:
return AppColorScheme.buyGradient;
case NeonButtonType.error:
return AppColorScheme.sellGradient;
default:
return null;
}
}
@override
Widget build(BuildContext context) {
final button = GestureDetector(
onTapDown: widget.onPressed != null ? _onTapDown : null,
onTapUp: widget.onPressed != null ? _onTapUp : null,
onTapCancel: widget.onPressed != null ? _onTapCancel : null,
onTap: widget.isLoading ? null : widget.onPressed,
child: ScaleTransition(
scale: _scaleAnimation,
child: Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
gradient: _gradient,
color: _gradient == null ? _backgroundColor : null,
borderRadius: BorderRadius.circular(AppRadius.xxl),
border: widget.type == NeonButtonType.outline
? Border.all(
color: AppColorScheme.darkOutlineVariant.withValues(alpha: 0.3),
)
: null,
),
child: Center(
child: widget.isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(_foregroundColor),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
if (widget.icon != null) ...[
Icon(widget.icon, size: 18, color: _foregroundColor),
SizedBox(width: AppSpacing.sm),
],
Text(
widget.text,
style: TextStyle(
color: _foregroundColor,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
);
if (widget.showGlow && widget.type != NeonButtonType.outline) {
return NeonGlow(
glowColor: _glowColor,
borderRadius: BorderRadius.circular(AppRadius.xxl),
child: button,
);
}
return button;
}
}
/// 按钮类型
enum NeonButtonType {
/// 主要按钮 (青色)
primary,
/// 次要按钮 (紫色)
secondary,
/// 成功按钮 (绿色)
tertiary,
/// 危险按钮 (红色)
error,
/// 边框按钮
outline,
}
/// NeonIcon - 带霓虹光效的图标
class NeonIcon extends StatelessWidget {
/// 图标
final IconData icon;
/// 图标大小
final double size;
/// 图标颜色
final Color color;
/// 光效颜色,默认使用图标颜色
final Color? glowColor;
/// 光效模糊半径
final double glowBlur;
const NeonIcon({
super.key,
required this.icon,
this.size = 24,
required this.color,
this.glowColor,
this.glowBlur = 10,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: glowColor ?? color.withValues(alpha: 0.5),
blurRadius: glowBlur,
spreadRadius: 0,
),
],
),
child: Icon(icon, size: size, color: color),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../../core/constants/app_colors.dart'; import 'package:google_fonts/google_fonts.dart';
import '../../../core/theme/app_color_scheme.dart'; import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_spacing.dart';
import '../../../data/models/coin.dart'; import '../../../data/models/coin.dart';
import '../../../providers/market_provider.dart'; import '../../../providers/market_provider.dart';
import '../../components/glass_panel.dart';
/// 行情页面 - 使用 shadcn_ui 现代化设计 /// 行情页面 - Material Design 3 风格
class MarketPage extends StatefulWidget { class MarketPage extends StatefulWidget {
const MarketPage({super.key}); const MarketPage({super.key});
@@ -16,11 +17,11 @@ class MarketPage extends StatefulWidget {
} }
class _MarketPageState extends State<MarketPage> with AutomaticKeepAliveClientMixin { class _MarketPageState extends State<MarketPage> with AutomaticKeepAliveClientMixin {
final _searchController = TextEditingController();
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
final _searchController = TextEditingController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -38,10 +39,9 @@ class _MarketPageState extends State<MarketPage> with AutomaticKeepAliveClientMi
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
final theme = ShadTheme.of(context);
return Scaffold( return Scaffold(
backgroundColor: theme.colorScheme.background, backgroundColor: AppColorScheme.darkBackground,
body: Consumer<MarketProvider>( body: Consumer<MarketProvider>(
builder: (context, provider, _) { builder: (context, provider, _) {
return Column( return Column(
@@ -59,207 +59,300 @@ class _MarketPageState extends State<MarketPage> with AutomaticKeepAliveClientMi
} }
Widget _buildSearchBar(MarketProvider provider) { Widget _buildSearchBar(MarketProvider provider) {
final theme = ShadTheme.of(context);
return Padding( return Padding(
padding: AppSpacing.pagePadding, padding: EdgeInsets.fromLTRB(AppSpacing.md, AppSpacing.md, AppSpacing.md, 0),
child: ShadInput( child: Container(
controller: _searchController, decoration: BoxDecoration(
placeholder: const Text('搜索币种...'), color: AppColorScheme.darkSurfaceContainerLowest,
leading: Icon( borderRadius: BorderRadius.circular(AppRadius.xl),
LucideIcons.search, border: Border.all(
size: 18, color: AppColorScheme.darkOutlineVariant.withValues(alpha: 0.15),
color: theme.colorScheme.mutedForeground, ),
),
child: TextField(
controller: _searchController,
onChanged: provider.search,
style: TextStyle(color: AppColorScheme.darkOnSurface),
decoration: InputDecoration(
hintText: 'Search markets...',
hintStyle: TextStyle(color: AppColorScheme.darkOnSurfaceVariant),
prefixIcon: Icon(
LucideIcons.search,
size: 18,
color: AppColorScheme.darkOnSurfaceVariant,
),
suffixIcon: _searchController.text.isNotEmpty
? GestureDetector(
onTap: () {
_searchController.clear();
provider.clearSearch();
},
child: Icon(
LucideIcons.x,
size: 18,
color: AppColorScheme.darkOnSurfaceVariant,
),
)
: null,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md + AppSpacing.xs,
),
),
), ),
trailing: _searchController.text.isNotEmpty
? GestureDetector(
onTap: () {
_searchController.clear();
provider.clearSearch();
},
child: Icon(
LucideIcons.x,
size: 18,
color: theme.colorScheme.mutedForeground,
),
)
: null,
onChanged: provider.search,
), ),
); );
} }
Widget _buildTabs(MarketProvider provider) { Widget _buildTabs(MarketProvider provider) {
final theme = ShadTheme.of(context);
final tabs = [ final tabs = [
{'key': 'all', 'label': '全部'}, {'key': 'all', 'label': 'All'},
{'key': 'realtime', 'label': '实时'}, {'key': 'realtime', 'label': 'Real-time'},
{'key': 'hot', 'label': '热门'}, {'key': 'hot', 'label': 'Hot'},
]; ];
return Container( return Container(
height: 44, height: 48,
margin: EdgeInsets.fromLTRB(AppSpacing.md, 0, AppSpacing.md, AppSpacing.md), margin: EdgeInsets.fromLTRB(AppSpacing.md, AppSpacing.md, AppSpacing.md, 0),
child: Row( child: SingleChildScrollView(
children: tabs.asMap().entries.map((entry) { scrollDirection: Axis.horizontal,
final index = entry.key; child: Row(
final tab = entry.value; children: tabs.map((tab) {
final isActive = provider.activeTab == tab['key']; final isActive = provider.activeTab == tab['key'];
return GestureDetector( return GestureDetector(
onTap: () => provider.setTab(tab['key']!), onTap: () => provider.setTab(tab['key']!),
child: Container( child: AnimatedContainer(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.lg + AppSpacing.xs, vertical: AppSpacing.sm + AppSpacing.xs), duration: const Duration(milliseconds: 200),
margin: EdgeInsets.only(right: AppSpacing.sm), margin: EdgeInsets.only(right: AppSpacing.sm),
decoration: BoxDecoration( padding: EdgeInsets.symmetric(
color: isActive horizontal: AppSpacing.lg,
? theme.colorScheme.primary vertical: AppSpacing.sm + AppSpacing.xs,
: theme.colorScheme.card, ),
borderRadius: BorderRadius.circular(AppRadius.xl), decoration: BoxDecoration(
),
child: Text(
tab['label']!,
style: TextStyle(
color: isActive color: isActive
? Colors.white ? AppColorScheme.darkPrimary.withValues(alpha: 0.1)
: theme.colorScheme.mutedForeground, : AppColorScheme.darkSurfaceContainerHigh,
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal, borderRadius: BorderRadius.circular(AppRadius.full),
border: isActive
? Border.all(
color: AppColorScheme.darkPrimary.withValues(alpha: 0.2),
)
: null,
boxShadow: isActive
? [
BoxShadow(
color: AppColorScheme.neonGlowPrimary,
blurRadius: 15,
),
]
: null,
),
child: Text(
tab['label']!,
style: TextStyle(
color: isActive
? AppColorScheme.darkPrimary
: AppColorScheme.darkOnSurfaceVariant,
fontWeight: isActive ? FontWeight.w700 : FontWeight.normal,
fontSize: 12,
),
), ),
), ),
), );
); }).toList(),
}).toList(), ),
), ),
); );
} }
Widget _buildCoinList(MarketProvider provider) { Widget _buildCoinList(MarketProvider provider) {
final theme = ShadTheme.of(context);
if (provider.isLoading) { if (provider.isLoading) {
return Center( return Center(
child: CircularProgressIndicator( child: CircularProgressIndicator(
color: theme.colorScheme.primary, color: AppColorScheme.darkPrimary,
), ),
); );
} }
if (provider.error != null) { if (provider.error != null) {
return Center( return _buildErrorState(provider);
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
LucideIcons.circleAlert,
size: 48,
color: theme.colorScheme.destructive,
),
SizedBox(height: AppSpacing.sm + AppSpacing.xs),
Text(
provider.error!,
style: TextStyle(color: theme.colorScheme.destructive),
),
SizedBox(height: AppSpacing.md),
ShadButton(
onPressed: provider.loadCoins,
child: const Text('重试'),
),
],
),
);
} }
final coins = provider.coins; final coins = provider.coins;
if (coins.isEmpty) { if (coins.isEmpty) {
return Center( return _buildEmptyState();
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
LucideIcons.coins,
size: 48,
color: theme.colorScheme.mutedForeground,
),
SizedBox(height: AppSpacing.sm + AppSpacing.xs),
Text(
'暂无数据',
style: theme.textTheme.muted,
),
],
),
);
} }
return RefreshIndicator( return RefreshIndicator(
onRefresh: provider.refresh, onRefresh: provider.refresh,
color: theme.colorScheme.primary, color: AppColorScheme.darkPrimary,
backgroundColor: AppColorScheme.darkSurfaceContainer,
child: ListView.builder( child: ListView.builder(
padding: EdgeInsets.fromLTRB(AppSpacing.md, 0, AppSpacing.md, AppSpacing.md), padding: EdgeInsets.all(AppSpacing.md),
itemCount: coins.length, itemCount: coins.length,
itemBuilder: (context, index) => _buildCoinItem(coins[index]), itemBuilder: (context, index) => _buildCoinItem(coins[index]),
), ),
); );
} }
Widget _buildCoinItem(Coin coin) { Widget _buildErrorState(MarketProvider provider) {
final theme = ShadTheme.of(context); return Center(
child: Padding(
return Padding( padding: AppSpacing.pagePadding,
padding: EdgeInsets.only(bottom: AppSpacing.sm), child: Column(
child: ShadCard( mainAxisAlignment: MainAxisAlignment.center,
padding: AppSpacing.cardPadding,
child: Row(
children: [ children: [
// 图标 Icon(
CircleAvatar( LucideIcons.circleAlert,
radius: 22, size: 48,
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1), color: AppColorScheme.darkError,
child: Text(
coin.displayIcon,
style: TextStyle(
fontSize: 20,
color: theme.colorScheme.primary,
),
),
), ),
SizedBox(width: AppSpacing.sm + AppSpacing.xs), SizedBox(height: AppSpacing.md),
// 名称 Text(
Expanded( provider.error!,
child: Column( style: TextStyle(color: AppColorScheme.darkError),
crossAxisAlignment: CrossAxisAlignment.start, textAlign: TextAlign.center,
children: [
Text(
'${coin.code}/USDT',
style: theme.textTheme.large.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
coin.name,
style: theme.textTheme.muted,
),
],
),
), ),
// 涨跌幅 SizedBox(height: AppSpacing.lg),
Container( ShadButton(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm + AppSpacing.xs, vertical: AppSpacing.xs + AppSpacing.xs), onPressed: provider.loadCoins,
decoration: BoxDecoration( child: const Text('重试'),
color: getChangeBackgroundColor(coin.isUp),
borderRadius: BorderRadius.circular(AppRadius.sm + AppSpacing.xs),
),
child: Text(
coin.formattedChange,
style: TextStyle(
color: getChangeColor(coin.isUp),
fontWeight: FontWeight.w600,
),
),
), ),
], ],
), ),
), ),
); );
} }
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
LucideIcons.coins,
size: 48,
color: AppColorScheme.darkOnSurfaceVariant,
),
SizedBox(height: AppSpacing.md),
Text(
'暂无数据',
style: TextStyle(color: AppColorScheme.darkOnSurfaceVariant),
),
],
),
);
}
Widget _buildCoinItem(Coin coin) {
final changeColor = coin.isUp ? AppColorScheme.up : AppColorScheme.down;
final changeBgColor = coin.isUp
? AppColorScheme.darkTertiary.withValues(alpha: 0.1)
: AppColorScheme.darkError.withValues(alpha: 0.1);
return GlassCard(
margin: EdgeInsets.only(bottom: AppSpacing.sm),
child: Row(
children: [
// 图标容器
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppColorScheme.darkSurfaceContainerHighest,
borderRadius: BorderRadius.circular(AppRadius.xl),
border: Border.all(
color: AppColorScheme.darkOutlineVariant.withValues(alpha: 0.2),
),
),
child: Center(
child: Text(
coin.displayIcon,
style: TextStyle(
fontSize: 20,
color: coin.isUp ? AppColorScheme.darkPrimary : AppColorScheme.darkSecondary,
fontWeight: FontWeight.bold,
),
),
),
),
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
// 币种信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
coin.code,
style: GoogleFonts.spaceGrotesk(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColorScheme.darkOnSurface,
),
),
SizedBox(width: AppSpacing.xs),
Text(
'/USDT',
style: TextStyle(
fontSize: 12,
color: AppColorScheme.darkOnSurfaceVariant,
),
),
],
),
SizedBox(height: AppSpacing.xs / 2),
Text(
coin.name,
style: TextStyle(
fontSize: 12,
color: AppColorScheme.darkOnSurfaceVariant,
),
),
],
),
),
// 价格和涨跌幅
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'\$${coin.formattedPrice}',
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppColorScheme.darkOnSurface,
),
),
SizedBox(height: AppSpacing.xs),
Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: changeBgColor,
borderRadius: BorderRadius.circular(AppRadius.sm),
border: Border.all(
color: changeColor.withValues(alpha: 0.2),
),
),
child: Text(
coin.formattedChange,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: changeColor,
),
),
),
],
),
],
),
);
}
} }

View File

@@ -1,28 +1,33 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../core/theme/app_color_scheme.dart'; import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_spacing.dart';
import '../../../providers/auth_provider.dart'; import '../../../providers/auth_provider.dart';
import '../../../providers/theme_provider.dart'; import '../../../providers/theme_provider.dart';
import '../auth/login_page.dart'; import '../auth/login_page.dart';
import '../../components/glass_panel.dart';
import '../../components/neon_glow.dart';
/// 菜单项数据模型 /// 菜单项数据模型
class _MenuItem { class _MenuItem {
final IconData icon; final IconData icon;
final String title; final String title;
final String? subtitle; final String? subtitle;
final Color? iconColor;
final VoidCallback onTap; final VoidCallback onTap;
const _MenuItem({ const _MenuItem({
required this.icon, required this.icon,
required this.title, required this.title,
this.subtitle, this.subtitle,
this.iconColor,
required this.onTap, required this.onTap,
}); });
} }
/// 我的页面 - 使用 shadcn_ui 现代化设计 /// 我的页面 - Material Design 3 风格
class MinePage extends StatefulWidget { class MinePage extends StatefulWidget {
const MinePage({super.key}); const MinePage({super.key});
@@ -39,6 +44,7 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
super.build(context); super.build(context);
return Scaffold( return Scaffold(
backgroundColor: AppColorScheme.darkBackground,
body: Consumer<AuthProvider>( body: Consumer<AuthProvider>(
builder: (context, auth, _) { builder: (context, auth, _) {
return SingleChildScrollView( return SingleChildScrollView(
@@ -48,8 +54,18 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
_UserCard(user: auth.user), _UserCard(user: auth.user),
SizedBox(height: AppSpacing.md), SizedBox(height: AppSpacing.md),
_MenuList(onShowComingSoon: _showComingSoon, onShowAbout: _showAboutDialog), _MenuList(onShowComingSoon: _showComingSoon, onShowAbout: _showAboutDialog),
SizedBox(height: AppSpacing.lg), SizedBox(height: AppSpacing.xl),
_LogoutButton(onLogout: () => _handleLogout(auth)), _LogoutButton(onLogout: () => _handleLogout(auth)),
SizedBox(height: AppSpacing.lg),
// 版本信息
Text(
'System Build v1.0.0-Neo',
style: TextStyle(
fontSize: 10,
color: AppColorScheme.darkOnSurfaceVariant.withValues(alpha: 0.4),
letterSpacing: 0.3,
),
),
], ],
), ),
); );
@@ -64,7 +80,7 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
builder: (context) => ShadDialog.alert( builder: (context) => ShadDialog.alert(
title: Row( title: Row(
children: [ children: [
const Icon(LucideIcons.construction, color: Color(0xFFFF9800), size: 20), Icon(Icons.construction, color: AppColorScheme.warning, size: 20),
SizedBox(width: AppSpacing.sm), SizedBox(width: AppSpacing.sm),
const Text('功能开发中'), const Text('功能开发中'),
], ],
@@ -81,8 +97,6 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
} }
void _showAboutDialog() { void _showAboutDialog() {
final theme = ShadTheme.of(context);
showShadDialog( showShadDialog(
context: context, context: context,
builder: (context) => ShadDialog( builder: (context) => ShadDialog(
@@ -97,11 +111,14 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('虚拟货币模拟交易平台', style: theme.textTheme.muted), Text(
'虚拟货币模拟交易平台',
style: TextStyle(color: AppColorScheme.darkOnSurfaceVariant),
),
SizedBox(height: AppSpacing.md), SizedBox(height: AppSpacing.md),
_InfoRow(icon: LucideIcons.code, text: '版本: 1.0.0'), _InfoRow(icon: Icons.code, text: '版本: 1.0.0'),
SizedBox(height: AppSpacing.sm), SizedBox(height: AppSpacing.sm),
const _InfoRow(icon: LucideIcons.heart, text: 'Built with Flutter & shadcn_ui'), _InfoRow(icon: Icons.favorite, text: 'Built with Flutter & Material Design 3'),
], ],
), ),
actions: [ actions: [
@@ -144,7 +161,7 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
} }
} }
/// 用户卡片组件 /// 用户卡片组件 - Material Design 3 风格
class _UserCard extends StatelessWidget { class _UserCard extends StatelessWidget {
final dynamic user; final dynamic user;
@@ -152,34 +169,92 @@ class _UserCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = ShadTheme.of(context); return GlassPanel(
return ShadCard(
padding: EdgeInsets.all(AppSpacing.lg + AppSpacing.sm), padding: EdgeInsets.all(AppSpacing.lg + AppSpacing.sm),
child: Row( child: Row(
children: [ children: [
_AppLogo(radius: 32, fontSize: 24, text: user?.avatarText), // 头像 - 带霓虹边框
SizedBox(width: AppSpacing.md), Stack(
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: AppColorScheme.neonGlowPrimary,
blurRadius: 20,
),
],
),
child: _AppLogo(radius: 36, fontSize: 28, text: user?.avatarText),
),
// 验证徽章
Positioned(
bottom: 0,
right: 0,
child: Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppColorScheme.darkTertiary,
shape: BoxShape.circle,
border: Border.all(
color: AppColorScheme.darkBackground,
width: 2,
),
),
child: Icon(
Icons.verified,
size: 14,
color: AppColorScheme.darkOnTertiary,
),
),
),
],
),
SizedBox(width: AppSpacing.md + AppSpacing.xs),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
user?.username ?? '未登录', user?.username ?? '未登录',
style: theme.textTheme.h3.copyWith(fontWeight: FontWeight.bold), style: GoogleFonts.spaceGrotesk(
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppColorScheme.darkOnSurface,
),
), ),
SizedBox(height: AppSpacing.sm - AppSpacing.xs), SizedBox(height: AppSpacing.sm),
ShadBadge( // 用户等级标签
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.2), Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: AppColorScheme.darkPrimary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(AppRadius.full),
border: Border.all(
color: AppColorScheme.darkPrimary.withValues(alpha: 0.2),
),
),
child: Text( child: Text(
'普通用户', 'NORMAL USER',
style: TextStyle(fontSize: 12, color: theme.colorScheme.primary), style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
color: AppColorScheme.darkPrimary,
),
), ),
), ),
], ],
), ),
), ),
Icon(LucideIcons.chevronRight, color: theme.colorScheme.mutedForeground), Icon(
LucideIcons.chevronRight,
color: AppColorScheme.darkOnSurfaceVariant,
),
], ],
), ),
); );
@@ -196,16 +271,14 @@ class _AppLogo extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return CircleAvatar( return CircleAvatar(
radius: radius, radius: radius,
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.2), backgroundColor: AppColorScheme.darkPrimary.withValues(alpha: 0.2),
child: Text( child: Text(
text ?? '', text ?? '',
style: TextStyle( style: TextStyle(
fontSize: fontSize, fontSize: fontSize,
color: theme.colorScheme.primary, color: AppColorScheme.darkPrimary,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
@@ -222,19 +295,23 @@ class _InfoRow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return Row( return Row(
children: [ children: [
Icon(icon, size: 14, color: theme.colorScheme.mutedForeground), Icon(icon, size: 14, color: AppColorScheme.darkOnSurfaceVariant),
SizedBox(width: AppSpacing.sm - AppSpacing.xs), SizedBox(width: AppSpacing.sm),
Text(text, style: theme.textTheme.muted.copyWith(fontSize: 12)), Text(
text,
style: TextStyle(
fontSize: 12,
color: AppColorScheme.darkOnSurfaceVariant,
),
),
], ],
); );
} }
} }
/// 菜单列表组件 /// 菜单列表组件 - Glass Panel 风格
class _MenuList extends StatelessWidget { class _MenuList extends StatelessWidget {
final void Function(String) onShowComingSoon; final void Function(String) onShowComingSoon;
final VoidCallback onShowAbout; final VoidCallback onShowAbout;
@@ -243,34 +320,75 @@ class _MenuList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final themeProvider = context.watch<ThemeProvider>(); final themeProvider = context.watch<ThemeProvider>();
return ShadCard( return GlassPanel(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
borderRadius: BorderRadius.circular(AppRadius.xxl),
child: Column( child: Column(
children: [ children: [
// 主题切换开关(特殊处理) // 主题切换开关
_ThemeToggleTile(isDarkMode: themeProvider.isDarkMode), _ThemeToggleTile(isDarkMode: themeProvider.isDarkMode),
Divider(color: theme.colorScheme.border, height: 1, indent: 56), _buildDivider(),
// 普通菜单项 // 菜单项
for (var i = 0; i < _buildMenuItems().length; i++) ...[ ..._buildMenuItems(),
_MenuItemTile(item: _buildMenuItems()[i]),
if (i < _buildMenuItems().length - 1)
Divider(color: theme.colorScheme.border, height: 1, indent: 56),
],
], ],
), ),
); );
} }
List<_MenuItem> _buildMenuItems() { Widget _buildDivider() {
return Container(
margin: EdgeInsets.only(left: 56),
height: 1,
color: AppColorScheme.glassPanelBorder,
);
}
List<Widget> _buildMenuItems() {
final items = [
_MenuItem(
icon: LucideIcons.userCheck,
title: '实名认证',
subtitle: '完成实名认证,解锁更多功能',
iconColor: AppColorScheme.darkPrimary,
onTap: () => onShowComingSoon('实名认证'),
),
_MenuItem(
icon: LucideIcons.shield,
title: '安全设置',
subtitle: '密码、二次验证等安全设置',
iconColor: AppColorScheme.darkSecondary,
onTap: () => onShowComingSoon('安全设置'),
),
_MenuItem(
icon: LucideIcons.bell,
title: '消息通知',
subtitle: '管理消息推送设置',
iconColor: AppColorScheme.darkTertiary,
onTap: () => onShowComingSoon('消息通知'),
),
_MenuItem(
icon: LucideIcons.settings,
title: '系统设置',
subtitle: '主题、语言等偏好设置',
iconColor: AppColorScheme.darkPrimary,
onTap: () => onShowComingSoon('系统设置'),
),
_MenuItem(
icon: LucideIcons.info,
title: '关于我们',
subtitle: '版本信息与用户协议',
iconColor: AppColorScheme.darkOnSurfaceVariant,
onTap: onShowAbout,
),
];
return [ return [
_MenuItem(icon: LucideIcons.userCheck, title: '实名认证', subtitle: '完成实名认证,解锁更多功能', onTap: () => onShowComingSoon('实名认证')), for (var i = 0; i < items.length; i++) ...[
_MenuItem(icon: LucideIcons.shield, title: '安全设置', subtitle: '密码、二次验证等安全设置', onTap: () => onShowComingSoon('安全设置')), _MenuItemTile(item: items[i]),
_MenuItem(icon: LucideIcons.bell, title: '消息通知', subtitle: '管理消息推送设置', onTap: () => onShowComingSoon('消息通知')), if (i < items.length - 1) _buildDivider(),
_MenuItem(icon: LucideIcons.settings, title: '系统设置', subtitle: '主题、语言等偏好设置', onTap: () => onShowComingSoon('系统设置')), ],
_MenuItem(icon: LucideIcons.info, title: '关于我们', subtitle: '版本信息与用户协议', onTap: onShowAbout),
]; ];
} }
} }
@@ -283,26 +401,41 @@ class _ThemeToggleTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final themeProvider = context.read<ThemeProvider>(); final themeProvider = context.read<ThemeProvider>();
return InkWell( return InkWell(
onTap: () => themeProvider.toggleTheme(), onTap: () => themeProvider.toggleTheme(),
child: Padding( child: Padding(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: AppSpacing.sm + AppSpacing.xs), padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm + AppSpacing.xs,
),
child: Row( child: Row(
children: [ children: [
_MenuIcon(icon: isDarkMode ? LucideIcons.moon : LucideIcons.sun), _MenuIcon(
icon: isDarkMode ? LucideIcons.moon : LucideIcons.sun,
color: AppColorScheme.darkPrimary,
),
SizedBox(width: AppSpacing.sm + AppSpacing.xs), SizedBox(width: AppSpacing.sm + AppSpacing.xs),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('深色模式', style: theme.textTheme.small.copyWith(fontWeight: FontWeight.w500)), Text(
'深色模式',
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColorScheme.darkOnSurface,
),
),
SizedBox(height: AppSpacing.xs / 2), SizedBox(height: AppSpacing.xs / 2),
Text( Text(
isDarkMode ? '当前:深色主题' : '当前:浅色主题', isDarkMode ? '当前:深色主题' : '当前:浅色主题',
style: theme.textTheme.muted.copyWith(fontSize: 11), style: TextStyle(
fontSize: 11,
color: AppColorScheme.darkOnSurfaceVariant,
),
), ),
], ],
), ),
@@ -310,8 +443,8 @@ class _ThemeToggleTile extends StatelessWidget {
Switch( Switch(
value: isDarkMode, value: isDarkMode,
onChanged: (_) => themeProvider.toggleTheme(), onChanged: (_) => themeProvider.toggleTheme(),
activeTrackColor: theme.colorScheme.primary.withValues(alpha: 0.5), activeTrackColor: AppColorScheme.darkPrimary.withValues(alpha: 0.5),
activeColor: theme.colorScheme.primary, activeColor: AppColorScheme.darkPrimary,
), ),
], ],
), ),
@@ -328,29 +461,47 @@ class _MenuItemTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return InkWell( return InkWell(
onTap: item.onTap, onTap: item.onTap,
child: Padding( child: Padding(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: AppSpacing.sm + AppSpacing.xs), padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm + AppSpacing.xs,
),
child: Row( child: Row(
children: [ children: [
_MenuIcon(icon: item.icon), _MenuIcon(icon: item.icon, color: item.iconColor),
SizedBox(width: AppSpacing.sm + AppSpacing.xs), SizedBox(width: AppSpacing.sm + AppSpacing.xs),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(item.title, style: theme.textTheme.small.copyWith(fontWeight: FontWeight.w500)), Text(
item.title,
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColorScheme.darkOnSurface,
),
),
if (item.subtitle != null) ...[ if (item.subtitle != null) ...[
SizedBox(height: AppSpacing.xs / 2), SizedBox(height: AppSpacing.xs / 2),
Text(item.subtitle!, style: theme.textTheme.muted.copyWith(fontSize: 11)), Text(
item.subtitle!,
style: TextStyle(
fontSize: 11,
color: AppColorScheme.darkOnSurfaceVariant,
),
),
], ],
], ],
), ),
), ),
Icon(LucideIcons.chevronRight, size: 18, color: theme.colorScheme.mutedForeground), Icon(
LucideIcons.chevronRight,
size: 18,
color: AppColorScheme.darkOnSurfaceVariant,
),
], ],
), ),
), ),
@@ -358,29 +509,33 @@ class _MenuItemTile extends StatelessWidget {
} }
} }
/// 菜单图标组件 /// 菜单图标组件 - Material Design 3 风格
class _MenuIcon extends StatelessWidget { class _MenuIcon extends StatelessWidget {
final IconData icon; final IconData icon;
final Color? color;
const _MenuIcon({required this.icon}); const _MenuIcon({required this.icon, this.color});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = ShadTheme.of(context); final iconColor = color ?? AppColorScheme.darkPrimary;
return Container( return Container(
width: 40, width: 40,
height: 40, height: 40,
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 0.1), color: iconColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(AppRadius.md + AppSpacing.xs), borderRadius: BorderRadius.circular(AppRadius.md + AppSpacing.xs),
border: Border.all(
color: iconColor.withValues(alpha: 0.2),
),
), ),
child: Icon(icon, size: 20, color: theme.colorScheme.primary), child: Icon(icon, size: 20, color: iconColor),
); );
} }
} }
/// 退出登录按钮 /// 退出登录按钮 - 带霓虹光效
class _LogoutButton extends StatelessWidget { class _LogoutButton extends StatelessWidget {
final VoidCallback onLogout; final VoidCallback onLogout;
@@ -388,20 +543,13 @@ class _LogoutButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return NeonButton(
text: 'Logout Terminal',
type: NeonButtonType.error,
icon: Icons.logout,
onPressed: onLogout,
width: double.infinity, width: double.infinity,
height: 48, showGlow: true,
child: ShadButton.destructive(
onPressed: onLogout,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(LucideIcons.logOut, size: 18),
SizedBox(width: AppSpacing.sm),
const Text('退出登录', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
],
),
),
); );
} }
} }

View File

@@ -1,8 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart';
import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_color_scheme.dart'; import '../../../core/theme/app_color_scheme.dart';
import '../../../data/models/order_models.dart'; import '../../../data/models/order_models.dart';
import '../../../providers/asset_provider.dart';
class _FundOrderCard extends StatelessWidget { class _FundOrderCard extends StatelessWidget {
final OrderFund order; final OrderFund order;
@@ -121,7 +123,7 @@ class _FundOrderCard extends StatelessWidget {
children: [ children: [
Text('创建时间: ', style: theme.textTheme.muted), Text('创建时间: ', style: theme.textTheme.muted),
Text( Text(
order.createTime ?? '', order.createTime?.toString() ?? '',
style: theme.textTheme.small, style: theme.textTheme.small,
), ),
], ],
@@ -172,7 +174,7 @@ class _FundOrderCard extends StatelessWidget {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: const Text('已确认打款,请等待审核')), const SnackBar(content: const Text('已确认打款,请等待审核')),
); );
context.read<AssetProvider>().refreshFundOrders(); context.read<AssetProvider>().loadFundOrders();
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(response.message ?? '确认失败')), SnackBar(content: Text(response.message ?? '确认失败')),
@@ -188,7 +190,7 @@ class _FundOrderCard extends StatelessWidget {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: const Text('订单已取消')), const SnackBar(content: const Text('订单已取消')),
); );
context.read<AssetProvider>().refreshFundOrders(); context.read<AssetProvider>().loadFundOrders();
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(response.message ?? '取消失败')), SnackBar(content: Text(response.message ?? '取消失败')),

View File

@@ -4,11 +4,12 @@ import 'package:provider/provider.dart';
import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_spacing.dart';
import '../../../providers/asset_provider.dart'; import '../../../providers/asset_provider.dart';
import '../../../data/models/order_models.dart'; import '../../../data/models/order_models.dart';
import 'fund_order_card.dart';
class _FundOrdersList extends StatelessWidget { class FundOrdersList extends StatelessWidget {
final AssetProvider provider; final AssetProvider provider;
const _FundOrdersList({required this.provider}); const FundOrdersList({super.key, required this.provider});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -16,14 +17,14 @@ class _FundOrdersList extends StatelessWidget {
final orders = provider.fundOrders; final orders = provider.fundOrders;
if (orders.isEmpty) { if (orders.isEmpty) {
return const _EmptyState( return _EmptyState(
icon: LucideIcons.receipt, icon: LucideIcons.receipt,
message: '暂无充提记录', message: '暂无充提记录',
); );
} }
return RefreshIndicator( return RefreshIndicator(
onRefresh: () => provider.refreshFundOrders(), onRefresh: () => provider.loadFundOrders(),
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
child: ListView.separated( child: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
@@ -32,10 +33,177 @@ class _FundOrdersList extends StatelessWidget {
separatorBuilder: (_, __) => Divider(color: theme.colorScheme.border, height: 1), separatorBuilder: (_, __) => Divider(color: theme.colorScheme.border, height: 1),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final order = orders[index]; final order = orders[index];
return _FundOrderCard(order: order); return FundOrderCard(order: order);
}, },
), ),
); );
} }
} }
/// 空状态组件
class _EmptyState extends StatelessWidget {
final IconData icon;
final String message;
const _EmptyState({required this.icon, required this.message});
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return Center(
child: Padding(
padding: EdgeInsets.all(AppSpacing.xl),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 48, color: theme.colorScheme.mutedForeground),
SizedBox(height: AppSpacing.sm + AppSpacing.xs),
Text(message, style: theme.textTheme.muted),
],
),
),
);
}
}
/// 充值订单卡片 - 公开类
class FundOrderCard extends StatelessWidget {
final OrderFund order;
const FundOrderCard({super.key, required this.order});
@override
Widget build(BuildContext context) {
// 直接使用 _FundOrderCard 的实现
return _FundOrderCardContent(order: order);
}
}
/// 订单卡片内容
class _FundOrderCardContent extends StatelessWidget {
final OrderFund order;
const _FundOrderCardContent({required this.order});
Color _getStatusColor(int status, bool isDeposit) {
if (isDeposit) {
switch (status) {
case 1:
return const Color(0xFFFF9800);
case 2:
return const Color(0xFF2196F3);
case 3:
return const Color(0xFFafffd1);
case 4:
return const Color(0xFFff716c);
case 5:
return const Color(0xFFa9abb3);
default:
return const Color(0xFFa9abb3);
}
} else {
switch (status) {
case 1:
return const Color(0xFFFF9800);
case 2:
return const Color(0xFFafffd1);
case 3:
return const Color(0xFFff716c);
case 4:
return const Color(0xFFa9abb3);
default:
return const Color(0xFFa9abb3);
}
}
}
String _getStatusText(int status, bool isDeposit) {
if (isDeposit) {
switch (status) {
case 1:
return '待付款';
case 2:
return '待确认';
case 3:
return '已完成';
case 4:
return '已驳回';
case 5:
return '已取消';
default:
return '未知';
}
} else {
switch (status) {
case 1:
return '待审批';
case 2:
return '已完成';
case 3:
return '已驳回';
case 4:
return '已取消';
default:
return '未知';
}
}
}
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final isDeposit = order.type == 1;
final statusColor = _getStatusColor(order.status, isDeposit);
return ShadCard(
padding: AppSpacing.cardPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${isDeposit ? '+' : '-'}${order.amount} USDT',
style: theme.textTheme.large.copyWith(
color: statusColor,
fontWeight: FontWeight.bold,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: statusColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
_getStatusText(order.status, isDeposit),
style: theme.textTheme.small.copyWith(color: statusColor),
),
),
],
),
SizedBox(height: AppSpacing.sm),
Row(
children: [
Text('订单号: ', style: theme.textTheme.muted),
Text(order.orderNo, style: theme.textTheme.small),
],
),
SizedBox(height: AppSpacing.xs),
Row(
children: [
Text('创建时间: ', style: theme.textTheme.muted),
Text(
order.createTime?.toString() ?? '',
style: theme.textTheme.small,
),
],
),
],
),
);
}
}

View File

@@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
import '../../../core/theme/app_color_scheme.dart'; import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_spacing.dart';
import '../../../providers/asset_provider.dart'; import '../../../providers/asset_provider.dart';
import '../home/home_page.dart'; import 'fund_orders_list.dart';
/// 订单管理页面 /// 订单管理页面
class OrdersPage extends StatefulWidget { class OrdersPage extends StatefulWidget {
@@ -16,7 +16,7 @@ class OrdersPage extends StatefulWidget {
class _OrdersPageState extends State<OrdersPage> with AutomaticKeepAliveClientMixin { class _OrdersPageState extends State<OrdersPage> with AutomaticKeepAliveClientMixin {
int _activeTab = 0; int _activeTab = 0;
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@@ -38,7 +38,7 @@ class _OrdersPageState extends State<OrdersPage> with AutomaticKeepAliveClientMi
return Scaffold( return Scaffold(
backgroundColor: theme.colorScheme.background, backgroundColor: theme.colorScheme.background,
body: Consumer<AssetProvider>( body: Consumer<AssetProvider>(
builder: (context, provider) { builder: (context, provider, _) {
return RefreshIndicator( return RefreshIndicator(
onRefresh: () => provider.refreshAll(force: true), onRefresh: () => provider.refreshAll(force: true),
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
@@ -47,22 +47,106 @@ class _OrdersPageState extends State<OrdersPage> with AutomaticKeepAliveClientMi
padding: AppSpacing.pagePadding, padding: AppSpacing.pagePadding,
child: Column( child: Column(
children: [ children: [
_TabSelector( TabSelector(
tabs: const ['充提记录', '交易记录'], tabs: const ['充提记录', '交易记录'],
selectedIndex: _activeTab, selectedIndex: _activeTab,
onChanged: (index) => setState(() => _activeTab = index), onChanged: (index) => setState(() => _activeTab = index),
), ),
SizedBox(height: AppSpacing.md), SizedBox(height: AppSpacing.md),
_activeTab == 0 _activeTab == 0
? _FundOrdersList(provider: provider) ? FundOrdersList(provider: provider)
: _TradeOrdersList(provider: provider), : TradeOrdersList(provider: provider),
], ],
), ),
), ),
), );
), },
), ),
); );
} }
} }
/// Tab 选择器
class TabSelector extends StatelessWidget {
final List<String> tabs;
final int selectedIndex;
final ValueChanged<int> onChanged;
const TabSelector({
super.key,
required this.tabs,
required this.selectedIndex,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return Container(
padding: EdgeInsets.all(AppSpacing.xs),
decoration: BoxDecoration(
color: theme.colorScheme.card,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Row(
children: tabs.asMap().entries.map((entry) {
final index = entry.key;
final label = entry.value;
final isSelected = index == selectedIndex;
return Expanded(
child: GestureDetector(
onTap: () => onChanged(index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: EdgeInsets.symmetric(vertical: AppSpacing.sm + AppSpacing.xs),
decoration: BoxDecoration(
color: isSelected ? theme.colorScheme.primary : Colors.transparent,
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: Center(
child: Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : theme.colorScheme.mutedForeground,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
),
),
),
);
}).toList(),
),
);
}
}
/// 交易订单列表
class TradeOrdersList extends StatelessWidget {
final AssetProvider provider;
const TradeOrdersList({super.key, required this.provider});
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
// Trade orders feature not yet implemented in provider
// Using tradeAccounts (holdings) as placeholder for now
return Center(
child: Padding(
padding: EdgeInsets.all(AppSpacing.xl),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(LucideIcons.receipt, size: 48, color: theme.colorScheme.mutedForeground),
SizedBox(height: AppSpacing.sm + AppSpacing.xs),
Text('暂无交易记录', style: theme.textTheme.muted),
],
),
),
);
}
}

View File

@@ -1,14 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../core/theme/app_color_scheme.dart'; import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_spacing.dart';
import '../../../data/models/coin.dart'; import '../../../data/models/coin.dart';
import '../../../providers/market_provider.dart'; import '../../../providers/market_provider.dart';
import '../../../providers/asset_provider.dart'; import '../../../providers/asset_provider.dart';
import '../../shared/ui_constants.dart'; import '../../shared/ui_constants.dart';
import '../../components/glass_panel.dart';
import '../../components/neon_glow.dart';
/// 交易页面 - 使用 shadcn_ui 现代化设计 /// 交易页面 - Material Design 3 风格
class TradePage extends StatefulWidget { class TradePage extends StatefulWidget {
const TradePage({super.key}); const TradePage({super.key});
@@ -17,15 +20,15 @@ class TradePage extends StatefulWidget {
} }
class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixin { class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
int _tradeType = 0; // 0=买入, 1=卖出 int _tradeType = 0; // 0=买入, 1=卖出
Coin? _selectedCoin; Coin? _selectedCoin;
final _formKey = GlobalKey<ShadFormState>(); final _formKey = GlobalKey<ShadFormState>();
final _priceController = TextEditingController(); final _priceController = TextEditingController();
final _quantityController = TextEditingController(); final _quantityController = TextEditingController();
@override
bool get wantKeepAlive => true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -46,10 +49,9 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
final theme = ShadTheme.of(context);
return Scaffold( return Scaffold(
backgroundColor: theme.colorScheme.background, backgroundColor: AppColorScheme.darkBackground,
body: Consumer2<MarketProvider, AssetProvider>( body: Consumer2<MarketProvider, AssetProvider>(
builder: (context, market, asset, _) { builder: (context, market, asset, _) {
return SingleChildScrollView( return SingleChildScrollView(
@@ -77,7 +79,7 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
tradeBalance: asset.overview?.tradeBalance, tradeBalance: asset.overview?.tradeBalance,
onTradeTypeChanged: (type) => setState(() => _tradeType = type), onTradeTypeChanged: (type) => setState(() => _tradeType = type),
), ),
SizedBox(height: AppSpacing.md), SizedBox(height: AppSpacing.lg),
_TradeButton( _TradeButton(
isBuy: _tradeType == 0, isBuy: _tradeType == 0,
coinCode: _selectedCoin?.code, coinCode: _selectedCoin?.code,
@@ -121,7 +123,6 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
} }
void _showTradeResult() { void _showTradeResult() {
final theme = ShadTheme.of(context);
final isBuy = _tradeType == 0; final isBuy = _tradeType == 0;
showShadDialog( showShadDialog(
@@ -129,7 +130,11 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
builder: (ctx) => ShadDialog.alert( builder: (ctx) => ShadDialog.alert(
title: Row( title: Row(
children: [ children: [
Icon(LucideIcons.circleCheck, color: theme.colorScheme.primary, size: 24), NeonIcon(
icon: Icons.check_circle,
color: AppColorScheme.darkPrimary,
size: 24,
),
SizedBox(width: AppSpacing.sm), SizedBox(width: AppSpacing.sm),
const Text('交易成功'), const Text('交易成功'),
], ],
@@ -149,7 +154,7 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
} }
} }
/// 币种选择器 /// 币种选择器 - Glass Panel 风格
class _CoinSelector extends StatelessWidget { class _CoinSelector extends StatelessWidget {
final Coin? selectedCoin; final Coin? selectedCoin;
final List<Coin> coins; final List<Coin> coins;
@@ -163,15 +168,13 @@ class _CoinSelector extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
// 自动选择第一个币种 // 自动选择第一个币种
if (selectedCoin == null && coins.isNotEmpty) { if (selectedCoin == null && coins.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) => onCoinLoaded(coins.first)); WidgetsBinding.instance.addPostFrameCallback((_) => onCoinLoaded(coins.first));
} }
return ShadCard( return GlassCard(
padding: AppSpacing.cardPadding, showNeonGlow: false,
child: Row( child: Row(
children: [ children: [
_CoinAvatar(icon: selectedCoin?.displayIcon), _CoinAvatar(icon: selectedCoin?.displayIcon),
@@ -182,21 +185,34 @@ class _CoinSelector extends StatelessWidget {
children: [ children: [
Text( Text(
selectedCoin != null ? '${selectedCoin!.code}/USDT' : '选择币种', selectedCoin != null ? '${selectedCoin!.code}/USDT' : '选择币种',
style: theme.textTheme.large.copyWith(fontWeight: FontWeight.bold), style: GoogleFonts.spaceGrotesk(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColorScheme.darkOnSurface,
),
), ),
SizedBox(height: AppSpacing.xs), SizedBox(height: AppSpacing.xs),
Text(selectedCoin?.name ?? '点击选择交易对', style: theme.textTheme.muted), Text(
selectedCoin?.name ?? '点击选择交易对',
style: TextStyle(
fontSize: 12,
color: AppColorScheme.darkOnSurfaceVariant,
),
),
], ],
), ),
), ),
Icon(LucideIcons.chevronRight, color: theme.colorScheme.mutedForeground), Icon(
LucideIcons.chevronRight,
color: AppColorScheme.darkOnSurfaceVariant,
),
], ],
), ),
); );
} }
} }
/// 币种头像 /// 币种头像 - 带霓虹光效
class _CoinAvatar extends StatelessWidget { class _CoinAvatar extends StatelessWidget {
final String? icon; final String? icon;
@@ -204,20 +220,31 @@ class _CoinAvatar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = ShadTheme.of(context); return Container(
width: 44,
return CircleAvatar( height: 44,
radius: 22, decoration: BoxDecoration(
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1), color: AppColorScheme.darkPrimary.withValues(alpha: 0.1),
child: Text( borderRadius: BorderRadius.circular(AppRadius.md),
icon ?? '?', border: Border.all(
style: TextStyle(fontSize: 20, color: theme.colorScheme.primary), color: AppColorScheme.darkPrimary.withValues(alpha: 0.2),
),
),
child: Center(
child: Text(
icon ?? '?',
style: TextStyle(
fontSize: 20,
color: AppColorScheme.darkPrimary,
fontWeight: FontWeight.bold,
),
),
), ),
); );
} }
} }
/// 价格卡片 /// 价格卡片 - Glass Panel 风格
class _PriceCard extends StatelessWidget { class _PriceCard extends StatelessWidget {
final Coin coin; final Coin coin;
@@ -225,31 +252,56 @@ class _PriceCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final color = coin.isUp ? AppColorScheme.up : AppColorScheme.down; final color = coin.isUp ? AppColorScheme.up : AppColorScheme.down;
final bgColor = coin.isUp
? AppColorScheme.darkTertiary.withValues(alpha: 0.1)
: AppColorScheme.darkError.withValues(alpha: 0.1);
return ShadCard( return GlassCard(
padding: AppSpacing.cardPadding, showNeonGlow: false,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('最新价', style: theme.textTheme.muted), Text(
'最新价',
style: TextStyle(
fontSize: 12,
color: AppColorScheme.darkOnSurfaceVariant,
),
),
SizedBox(height: AppSpacing.xs), SizedBox(height: AppSpacing.xs),
Text('\$${coin.formattedPrice}', style: theme.textTheme.h2.copyWith(fontWeight: FontWeight.bold)), Text(
'\$${coin.formattedPrice}',
style: GoogleFonts.spaceGrotesk(
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppColorScheme.darkOnSurface,
),
),
], ],
), ),
Container( Container(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm + AppSpacing.xs, vertical: AppSpacing.xs + AppSpacing.xs), padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color.withValues(alpha: 0.2), color: bgColor,
borderRadius: BorderRadius.circular(AppRadius.md), borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(
color: color.withValues(alpha: 0.2),
),
), ),
child: Text( child: Text(
coin.formattedChange, coin.formattedChange,
style: TextStyle(fontSize: 16, color: color, fontWeight: FontWeight.w600), style: TextStyle(
fontSize: 16,
color: color,
fontWeight: FontWeight.w700,
),
), ),
), ),
], ],
@@ -258,7 +310,7 @@ class _PriceCard extends StatelessWidget {
} }
} }
/// 交易表单 /// 交易表单 - Glass Panel 风格
class _TradeForm extends StatelessWidget { class _TradeForm extends StatelessWidget {
final int tradeType; final int tradeType;
final Coin? selectedCoin; final Coin? selectedCoin;
@@ -278,10 +330,8 @@ class _TradeForm extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = ShadTheme.of(context); return GlassPanel(
padding: EdgeInsets.all(AppSpacing.lg),
return ShadCard(
padding: AppSpacing.cardPadding,
child: Column( child: Column(
children: [ children: [
// 买入/卖出切换 // 买入/卖出切换
@@ -289,45 +339,96 @@ class _TradeForm extends StatelessWidget {
tradeType: tradeType, tradeType: tradeType,
onChanged: onTradeTypeChanged, onChanged: onTradeTypeChanged,
), ),
SizedBox(height: AppSpacing.lg + AppSpacing.xs), SizedBox(height: AppSpacing.lg),
// 价格输入 // 价格输入
ShadInputFormField( _buildInputField(
id: 'price', label: '价格(USDT)',
label: const Text('价格(USDT)'),
controller: priceController, controller: priceController,
keyboardType: const TextInputType.numberWithOptions(decimal: true), placeholder: '输入价格',
placeholder: const Text('输入价格'), suffix: 'USDT',
trailing: Padding(
padding: EdgeInsets.only(right: AppSpacing.sm),
child: const Text('USDT'),
),
validator: Validators.price,
), ),
SizedBox(height: AppSpacing.md), SizedBox(height: AppSpacing.md),
// 数量输入 // 数量输入
ShadInputFormField( _buildInputField(
id: 'quantity', label: '数量',
label: const Text('数量'),
controller: quantityController, controller: quantityController,
keyboardType: const TextInputType.numberWithOptions(decimal: true), placeholder: '输入数量',
placeholder: const Text('输入数量'), suffix: selectedCoin?.code ?? '',
trailing: Padding(
padding: EdgeInsets.only(right: AppSpacing.sm),
child: Text(selectedCoin?.code ?? ''),
),
validator: Validators.quantity,
), ),
SizedBox(height: AppSpacing.md), SizedBox(height: AppSpacing.lg),
// 交易金额 // 信息行
_InfoRow(label: '交易金额', value: '${_calculateAmount()} USDT'), _InfoRow(label: '交易金额', value: '${_calculateAmount()} USDT'),
SizedBox(height: AppSpacing.sm), SizedBox(height: AppSpacing.sm),
// 可用余额
_InfoRow(label: '可用', value: '${tradeBalance ?? '0.00'} USDT'), _InfoRow(label: '可用', value: '${tradeBalance ?? '0.00'} USDT'),
], ],
), ),
); );
} }
Widget _buildInputField({
required String label,
required TextEditingController controller,
required String placeholder,
required String suffix,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
color: AppColorScheme.darkOnSurfaceVariant,
),
),
SizedBox(height: AppSpacing.xs),
Container(
decoration: BoxDecoration(
color: AppColorScheme.darkSurfaceLowest,
borderRadius: BorderRadius.circular(AppRadius.xl),
border: Border.all(
color: AppColorScheme.darkOutlineVariant.withValues(alpha: 0.3),
),
),
child: TextField(
controller: controller,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
style: GoogleFonts.spaceGrotesk(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColorScheme.darkOnSurface,
),
decoration: InputDecoration(
hintText: placeholder,
hintStyle: TextStyle(
color: AppColorScheme.darkOutlineVariant.withValues(alpha: 0.5),
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
suffixIcon: Padding(
padding: EdgeInsets.only(right: AppSpacing.sm),
child: Text(
suffix,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: AppColorScheme.darkOnSurfaceVariant,
),
),
),
suffixIconConstraints: const BoxConstraints(minWidth: 50),
),
),
),
],
);
}
String _calculateAmount() { String _calculateAmount() {
final price = double.tryParse(priceController.text) ?? 0; final price = double.tryParse(priceController.text) ?? 0;
final quantity = double.tryParse(quantityController.text) ?? 0; final quantity = double.tryParse(quantityController.text) ?? 0;
@@ -335,7 +436,7 @@ class _TradeForm extends StatelessWidget {
} }
} }
/// 交易类型选择器 /// 交易类型选择器 - Material Design 3 风格
class _TradeTypeSelector extends StatelessWidget { class _TradeTypeSelector extends StatelessWidget {
final int tradeType; final int tradeType;
final ValueChanged<int> onChanged; final ValueChanged<int> onChanged;
@@ -344,26 +445,33 @@ class _TradeTypeSelector extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Container(
children: [ padding: EdgeInsets.all(AppSpacing.xs),
Expanded( decoration: BoxDecoration(
child: _TypeButton( color: AppColorScheme.darkSurfaceLowest,
label: '买入', borderRadius: BorderRadius.circular(AppRadius.xl),
isSelected: tradeType == 0, ),
color: AppColorScheme.up, child: Row(
onTap: () => onChanged(0), children: [
Expanded(
child: _TypeButton(
label: 'Buy',
isSelected: tradeType == 0,
color: AppColorScheme.up,
onTap: () => onChanged(0),
),
), ),
), SizedBox(width: AppSpacing.sm),
SizedBox(width: AppSpacing.md), Expanded(
Expanded( child: _TypeButton(
child: _TypeButton( label: 'Sell',
label: '卖出', isSelected: tradeType == 1,
isSelected: tradeType == 1, color: AppColorScheme.down,
color: AppColorScheme.down, onTap: () => onChanged(1),
onTap: () => onChanged(1), ),
), ),
), ],
], ),
); );
} }
} }
@@ -386,19 +494,22 @@ class _TypeButton extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Container( child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: EdgeInsets.symmetric(vertical: AppSpacing.sm + AppSpacing.xs), padding: EdgeInsets.symmetric(vertical: AppSpacing.sm + AppSpacing.xs),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected ? color : Colors.transparent, color: isSelected ? color.withValues(alpha: 0.15) : Colors.transparent,
borderRadius: BorderRadius.circular(AppRadius.md), borderRadius: BorderRadius.circular(AppRadius.md),
border: isSelected ? null : Border.all(color: color), border: isSelected ? null : Border.all(color: color.withValues(alpha: 0.3)),
), ),
child: Center( child: Center(
child: Text( child: Text(
label, label,
style: TextStyle( style: TextStyle(
color: isSelected ? Colors.white : color, color: isSelected ? color : color.withValues(alpha: 0.7),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w700,
fontSize: 14,
letterSpacing: 0.5,
), ),
), ),
), ),
@@ -416,19 +527,30 @@ class _InfoRow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(label, style: theme.textTheme.muted), Text(
Text(value, style: theme.textTheme.small.copyWith(fontWeight: FontWeight.w600)), label,
style: TextStyle(
fontSize: 14,
color: AppColorScheme.darkOnSurfaceVariant,
),
),
Text(
value,
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColorScheme.darkOnSurface,
),
),
], ],
); );
} }
} }
/// 交易按钮 /// 交易按钮 - 带霓虹光效
class _TradeButton extends StatelessWidget { class _TradeButton extends StatelessWidget {
final bool isBuy; final bool isBuy;
final String? coinCode; final String? coinCode;
@@ -442,26 +564,13 @@ class _TradeButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final color = isBuy ? AppColorScheme.up : AppColorScheme.down; return NeonButton(
text: '${isBuy ? '买入' : '卖出'} ${coinCode ?? ''}',
return SizedBox( type: isBuy ? NeonButtonType.tertiary : NeonButtonType.error,
icon: isBuy ? Icons.arrow_downward : Icons.arrow_upward,
onPressed: onPressed,
width: double.infinity, width: double.infinity,
height: 48, showGlow: true,
child: ShadButton(
backgroundColor: color,
onPressed: onPressed,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(isBuy ? LucideIcons.arrowDownToLine : LucideIcons.arrowUpFromLine, size: 18, color: Colors.white),
SizedBox(width: AppSpacing.sm),
Text(
'${isBuy ? '买入' : '卖出'} ${coinCode ?? ''}',
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
),
],
),
),
); );
} }
} }