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: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" - 高端金融科技风格
///
/// 设计原则:
/// - 无边框规则: 禁止 1px solid 边框,使用层次色变化
/// - 层次化设计: 通过 surface-container 层次而非阴影
/// - Material Design 3 配色方案
/// - Glass Panel 毛玻璃效果
/// - Neon Glow 霓虹光效
/// - 渐变 CTA: primary → primary_container (135度)
class AppColorScheme {
AppColorScheme._();
// ============================================
// 深色主题 - "The Kinetic Vault"
// 深色主题 - "The Kinetic Vault" (Material Design 3)
// ============================================
/// 背景基色 - 最深
static const Color darkBackground = Color(0xFF0b0e14);
/// Surface 层次 (从低到高)
static const Color darkSurfaceLowest = Color(0xFF0d1017);
/// Surface 层次 (从低到高) - Material Design 3 规范
static const Color darkSurfaceDim = Color(0xFF0b0e14);
static const Color darkSurfaceLowest = Color(0xFF000000);
static const Color darkSurfaceLow = Color(0xFF10131a);
static const Color darkSurface = Color(0xFF151921);
static const Color darkSurfaceHigh = Color(0xFF1a1f2a);
static const Color darkSurfaceHighest = Color(0xFF22262f);
static const Color darkSurface = Color(0xFF0b0e14);
static const Color darkSurfaceContainer = Color(0xFF161a21);
static const Color darkSurfaceContainerHigh = Color(0xFF1c2028);
static const Color darkSurfaceContainerHighest = Color(0xFF22262f);
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);
/// Primary - Neon Blue (主要交互)
/// Primary - Neon Cyan (青色 - 主要交互)
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 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 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 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 darkInversePrimary = Color(0xFF00687f);
static const Color darkSurfaceTint = Color(0xFF72dcff);
// ============================================
// 浅色主题 - "The Ethereal Terminal"
@@ -56,11 +100,11 @@ class AppColorScheme {
static const Color lightBackground = Color(0xFFf5f7f9);
/// Surface 层次 (从低到高)
static const Color lightSurfaceLowest = Color(0xFFffffff); // Elevated (pop)
static const Color lightSurfaceLow = Color(0xFFeef1f3); // Softly recessed
static const Color lightSurface = Color(0xFFf5f7f9); // Base canvas
static const Color lightSurfaceHigh = Color(0xFFe8ebef); // Elevated
static const Color lightSurfaceHighest = Color(0xFFd9dde0); // Navigation anchor
static const Color lightSurfaceLowest = Color(0xFFffffff);
static const Color lightSurfaceLow = Color(0xFFeef1f3);
static const Color lightSurface = Color(0xFFf5f7f9);
static const Color lightSurfaceHigh = Color(0xFFe8ebef);
static const Color lightSurfaceHighest = Color(0xFFd9dde0);
/// Ghost Border
static const Color lightOutlineVariant = Color(0xFFc4c8cc);
@@ -82,6 +126,29 @@ class AppColorScheme {
static const Color lightOnSurfaceVariant = Color(0xFF5a5d60);
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 success = darkTertiary;
/// 跌/卖出/错误
static const Color down = Color(0xFFFF5252);
/// 跌/卖出/错误 - 使用 Material Design 3 error
static const Color down = Color(0xFFff716c);
static const Color error = down;
/// 静默/禁用/次要
static const Color muted = darkOnSurfaceVariant;
/// 警告
static const Color warning = Color(0xFFFF9800);
@@ -123,14 +193,14 @@ class AppColorScheme {
/// 买入按钮渐变
static const LinearGradient buyGradient = LinearGradient(
colors: [darkTertiary, Color(0xFF7de8b8)],
colors: [darkTertiary, darkTertiaryContainer],
begin: Alignment(-0.7, -0.7),
end: Alignment(0.7, 0.7),
);
/// 卖出按钮渐变
static const LinearGradient sellGradient = LinearGradient(
colors: [down, Color(0xFFe84545)],
colors: [darkError, darkErrorDim],
begin: Alignment(-0.7, -0.7),
end: Alignment(0.7, 0.7),
);
@@ -149,15 +219,15 @@ class AppColorScheme {
static ShadColorScheme get darkShad => ShadColorScheme(
background: darkBackground,
foreground: darkOnSurface,
card: darkSurface,
card: darkSurfaceContainer,
cardForeground: darkOnSurface,
popover: darkSurfaceHigh,
popover: darkSurfaceContainerHigh,
popoverForeground: darkOnSurface,
primary: darkPrimary,
primaryForeground: darkBackground,
primaryForeground: darkOnPrimary,
secondary: darkSecondary,
secondaryForeground: darkOnSurface,
muted: darkSurfaceHigh,
muted: darkSurfaceContainerHigh,
mutedForeground: darkOnSurfaceVariant,
accent: darkPrimary.withValues(alpha: 0.15),
accentForeground: darkPrimary,
@@ -181,7 +251,7 @@ class AppColorScheme {
popover: lightSurfaceLowest,
popoverForeground: lightOnSurface,
primary: lightPrimary,
primaryForeground: Color(0xFFFFFFFF),
primaryForeground: const Color(0xFFFFFFFF),
secondary: lightSecondary,
secondaryForeground: lightOnSurface,
muted: lightSurfaceHigh,
@@ -189,7 +259,7 @@ class AppColorScheme {
accent: lightPrimary.withValues(alpha: 0.1),
accentForeground: lightPrimary,
destructive: error,
destructiveForeground: Color(0xFFFFFFFF),
destructiveForeground: const Color(0xFFFFFFFF),
border: lightOutlineVariant.withValues(alpha: 0.5),
input: lightOutlineVariant.withValues(alpha: 0.3),
ring: lightPrimary,
@@ -197,26 +267,42 @@ class AppColorScheme {
);
// ============================================
// Material ColorScheme - 深色主题
// Material ColorScheme - 深色主题 (Material Design 3)
// ============================================
static ColorScheme get darkMaterial => ColorScheme.dark(
primary: darkPrimary,
onPrimary: darkBackground,
onPrimary: darkOnPrimary,
primaryContainer: darkPrimaryContainer,
onPrimaryContainer: darkOnPrimaryContainer,
secondary: darkSecondary,
onSecondary: darkOnSurface,
onSecondary: darkOnSecondary,
secondaryContainer: darkSecondaryContainer,
onSecondaryContainer: darkOnSecondaryContainer,
tertiary: darkTertiary,
onTertiary: darkBackground,
error: error,
onError: darkOnSurface,
onTertiary: darkOnTertiary,
tertiaryContainer: darkTertiaryContainer,
onTertiaryContainer: darkOnTertiaryContainer,
error: darkError,
onError: darkOnError,
errorContainer: darkErrorContainer,
onErrorContainer: darkOnErrorContainer,
surface: darkSurface,
onSurface: darkOnSurface,
surfaceDim: darkSurfaceDim,
surfaceBright: darkSurfaceBright,
surfaceContainerLowest: darkSurfaceLowest,
surfaceContainerLow: darkSurfaceLow,
surfaceContainer: darkSurface,
surfaceContainerHigh: darkSurfaceHigh,
surfaceContainerHighest: darkSurfaceHighest,
surfaceContainer: darkSurfaceContainer,
surfaceContainerHigh: darkSurfaceContainerHigh,
surfaceContainerHighest: darkSurfaceContainerHighest,
onSurfaceVariant: darkOnSurfaceVariant,
outline: darkOutline,
outlineVariant: darkOutlineVariant,
inverseSurface: darkInverseSurface,
onInverseSurface: darkInverseOnSurface,
inversePrimary: darkInversePrimary,
surfaceTint: darkSurfaceTint,
);
// ============================================
@@ -225,13 +311,13 @@ class AppColorScheme {
static ColorScheme get lightMaterial => ColorScheme.light(
primary: lightPrimary,
onPrimary: Color(0xFFFFFFFF),
onPrimary: const Color(0xFFFFFFFF),
secondary: lightSecondary,
onSecondary: Color(0xFFFFFFFF),
onSecondary: const Color(0xFFFFFFFF),
tertiary: lightTertiary,
onTertiary: Color(0xFFFFFFFF),
onTertiary: const Color(0xFFFFFFFF),
error: error,
onError: Color(0xFFFFFFFF),
onError: const Color(0xFFFFFFFF),
surface: lightSurface,
onSurface: lightOnSurface,
surfaceContainerLowest: lightSurfaceLowest,
@@ -255,14 +341,14 @@ class AppColorScheme {
@Deprecated('Use darkBackground instead')
static const Color _darkBackground = darkBackground;
@Deprecated('Use darkSurface instead')
static const Color _darkCardBackground = darkSurface;
@Deprecated('Use darkSurfaceContainer instead')
static const Color _darkCardBackground = darkSurfaceContainer;
@Deprecated('Use darkSurfaceHigh instead')
static const Color _darkSecondary = darkSurfaceHigh;
@Deprecated('Use darkSurfaceContainerHigh instead')
static const Color _darkSecondary = darkSurfaceContainerHigh;
@Deprecated('Use darkSurfaceHigh instead')
static const Color _darkMuted = darkSurfaceHigh;
@Deprecated('Use darkSurfaceContainerHigh instead')
static const Color _darkMuted = darkSurfaceContainerHigh;
@Deprecated('Use darkOutlineVariant instead')
static const Color _darkBorder = darkOutlineVariant;

View File

@@ -4,3 +4,5 @@ library components;
export 'coin_card.dart';
export 'trade_button.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:shadcn_ui/shadcn_ui.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_spacing.dart';
import '../../../data/models/coin.dart';
import '../../../providers/market_provider.dart';
import '../../components/glass_panel.dart';
/// 行情页面 - 使用 shadcn_ui 现代化设计
/// 行情页面 - Material Design 3 风格
class MarketPage extends StatefulWidget {
const MarketPage({super.key});
@@ -16,11 +17,11 @@ class MarketPage extends StatefulWidget {
}
class _MarketPageState extends State<MarketPage> with AutomaticKeepAliveClientMixin {
final _searchController = TextEditingController();
@override
bool get wantKeepAlive => true;
final _searchController = TextEditingController();
@override
void initState() {
super.initState();
@@ -38,10 +39,9 @@ class _MarketPageState extends State<MarketPage> with AutomaticKeepAliveClientMi
@override
Widget build(BuildContext context) {
super.build(context);
final theme = ShadTheme.of(context);
return Scaffold(
backgroundColor: theme.colorScheme.background,
backgroundColor: AppColorScheme.darkBackground,
body: Consumer<MarketProvider>(
builder: (context, provider, _) {
return Column(
@@ -59,207 +59,300 @@ class _MarketPageState extends State<MarketPage> with AutomaticKeepAliveClientMi
}
Widget _buildSearchBar(MarketProvider provider) {
final theme = ShadTheme.of(context);
return Padding(
padding: AppSpacing.pagePadding,
child: ShadInput(
controller: _searchController,
placeholder: const Text('搜索币种...'),
leading: Icon(
LucideIcons.search,
size: 18,
color: theme.colorScheme.mutedForeground,
padding: EdgeInsets.fromLTRB(AppSpacing.md, AppSpacing.md, AppSpacing.md, 0),
child: Container(
decoration: BoxDecoration(
color: AppColorScheme.darkSurfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.xl),
border: Border.all(
color: AppColorScheme.darkOutlineVariant.withValues(alpha: 0.15),
),
),
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) {
final theme = ShadTheme.of(context);
final tabs = [
{'key': 'all', 'label': '全部'},
{'key': 'realtime', 'label': '实时'},
{'key': 'hot', 'label': '热门'},
{'key': 'all', 'label': 'All'},
{'key': 'realtime', 'label': 'Real-time'},
{'key': 'hot', 'label': 'Hot'},
];
return Container(
height: 44,
margin: EdgeInsets.fromLTRB(AppSpacing.md, 0, AppSpacing.md, AppSpacing.md),
child: Row(
children: tabs.asMap().entries.map((entry) {
final index = entry.key;
final tab = entry.value;
final isActive = provider.activeTab == tab['key'];
height: 48,
margin: EdgeInsets.fromLTRB(AppSpacing.md, AppSpacing.md, AppSpacing.md, 0),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: tabs.map((tab) {
final isActive = provider.activeTab == tab['key'];
return GestureDetector(
onTap: () => provider.setTab(tab['key']!),
child: Container(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.lg + AppSpacing.xs, vertical: AppSpacing.sm + AppSpacing.xs),
margin: EdgeInsets.only(right: AppSpacing.sm),
decoration: BoxDecoration(
color: isActive
? theme.colorScheme.primary
: theme.colorScheme.card,
borderRadius: BorderRadius.circular(AppRadius.xl),
),
child: Text(
tab['label']!,
style: TextStyle(
return GestureDetector(
onTap: () => provider.setTab(tab['key']!),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: EdgeInsets.only(right: AppSpacing.sm),
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.lg,
vertical: AppSpacing.sm + AppSpacing.xs,
),
decoration: BoxDecoration(
color: isActive
? Colors.white
: theme.colorScheme.mutedForeground,
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
? AppColorScheme.darkPrimary.withValues(alpha: 0.1)
: AppColorScheme.darkSurfaceContainerHigh,
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) {
final theme = ShadTheme.of(context);
if (provider.isLoading) {
return Center(
child: CircularProgressIndicator(
color: theme.colorScheme.primary,
color: AppColorScheme.darkPrimary,
),
);
}
if (provider.error != null) {
return Center(
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('重试'),
),
],
),
);
return _buildErrorState(provider);
}
final coins = provider.coins;
if (coins.isEmpty) {
return Center(
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 _buildEmptyState();
}
return RefreshIndicator(
onRefresh: provider.refresh,
color: theme.colorScheme.primary,
color: AppColorScheme.darkPrimary,
backgroundColor: AppColorScheme.darkSurfaceContainer,
child: ListView.builder(
padding: EdgeInsets.fromLTRB(AppSpacing.md, 0, AppSpacing.md, AppSpacing.md),
padding: EdgeInsets.all(AppSpacing.md),
itemCount: coins.length,
itemBuilder: (context, index) => _buildCoinItem(coins[index]),
),
);
}
Widget _buildCoinItem(Coin coin) {
final theme = ShadTheme.of(context);
return Padding(
padding: EdgeInsets.only(bottom: AppSpacing.sm),
child: ShadCard(
padding: AppSpacing.cardPadding,
child: Row(
Widget _buildErrorState(MarketProvider provider) {
return Center(
child: Padding(
padding: AppSpacing.pagePadding,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 图标
CircleAvatar(
radius: 22,
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1),
child: Text(
coin.displayIcon,
style: TextStyle(
fontSize: 20,
color: theme.colorScheme.primary,
),
),
Icon(
LucideIcons.circleAlert,
size: 48,
color: AppColorScheme.darkError,
),
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
// 名称
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${coin.code}/USDT',
style: theme.textTheme.large.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
coin.name,
style: theme.textTheme.muted,
),
],
),
SizedBox(height: AppSpacing.md),
Text(
provider.error!,
style: TextStyle(color: AppColorScheme.darkError),
textAlign: TextAlign.center,
),
// 涨跌幅
Container(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm + AppSpacing.xs, vertical: AppSpacing.xs + AppSpacing.xs),
decoration: BoxDecoration(
color: getChangeBackgroundColor(coin.isUp),
borderRadius: BorderRadius.circular(AppRadius.sm + AppSpacing.xs),
),
child: Text(
coin.formattedChange,
style: TextStyle(
color: getChangeColor(coin.isUp),
fontWeight: FontWeight.w600,
),
),
SizedBox(height: AppSpacing.lg),
ShadButton(
onPressed: provider.loadCoins,
child: const Text('重试'),
),
],
),
),
);
}
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: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 '../../../providers/auth_provider.dart';
import '../../../providers/theme_provider.dart';
import '../auth/login_page.dart';
import '../../components/glass_panel.dart';
import '../../components/neon_glow.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,
});
}
/// 我的页面 - 使用 shadcn_ui 现代化设计
/// 我的页面 - Material Design 3 风格
class MinePage extends StatefulWidget {
const MinePage({super.key});
@@ -39,6 +44,7 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
super.build(context);
return Scaffold(
backgroundColor: AppColorScheme.darkBackground,
body: Consumer<AuthProvider>(
builder: (context, auth, _) {
return SingleChildScrollView(
@@ -48,8 +54,18 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
_UserCard(user: auth.user),
SizedBox(height: AppSpacing.md),
_MenuList(onShowComingSoon: _showComingSoon, onShowAbout: _showAboutDialog),
SizedBox(height: AppSpacing.lg),
SizedBox(height: AppSpacing.xl),
_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(
title: Row(
children: [
const Icon(LucideIcons.construction, color: Color(0xFFFF9800), size: 20),
Icon(Icons.construction, color: AppColorScheme.warning, size: 20),
SizedBox(width: AppSpacing.sm),
const Text('功能开发中'),
],
@@ -81,8 +97,6 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
}
void _showAboutDialog() {
final theme = ShadTheme.of(context);
showShadDialog(
context: context,
builder: (context) => ShadDialog(
@@ -97,11 +111,14 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('虚拟货币模拟交易平台', style: theme.textTheme.muted),
Text(
'虚拟货币模拟交易平台',
style: TextStyle(color: AppColorScheme.darkOnSurfaceVariant),
),
SizedBox(height: AppSpacing.md),
_InfoRow(icon: LucideIcons.code, text: '版本: 1.0.0'),
_InfoRow(icon: Icons.code, text: '版本: 1.0.0'),
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: [
@@ -144,7 +161,7 @@ class _MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin
}
}
/// 用户卡片组件
/// 用户卡片组件 - Material Design 3 风格
class _UserCard extends StatelessWidget {
final dynamic user;
@@ -152,34 +169,92 @@ class _UserCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return ShadCard(
return GlassPanel(
padding: EdgeInsets.all(AppSpacing.lg + AppSpacing.sm),
child: Row(
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(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
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),
ShadBadge(
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.2),
SizedBox(height: AppSpacing.sm),
// 用户等级标签
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(
'普通用户',
style: TextStyle(fontSize: 12, color: theme.colorScheme.primary),
'NORMAL USER',
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
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return CircleAvatar(
radius: radius,
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.2),
backgroundColor: AppColorScheme.darkPrimary.withValues(alpha: 0.2),
child: Text(
text ?? '',
style: TextStyle(
fontSize: fontSize,
color: theme.colorScheme.primary,
color: AppColorScheme.darkPrimary,
fontWeight: FontWeight.bold,
),
),
@@ -222,19 +295,23 @@ class _InfoRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return Row(
children: [
Icon(icon, size: 14, color: theme.colorScheme.mutedForeground),
SizedBox(width: AppSpacing.sm - AppSpacing.xs),
Text(text, style: theme.textTheme.muted.copyWith(fontSize: 12)),
Icon(icon, size: 14, color: AppColorScheme.darkOnSurfaceVariant),
SizedBox(width: AppSpacing.sm),
Text(
text,
style: TextStyle(
fontSize: 12,
color: AppColorScheme.darkOnSurfaceVariant,
),
),
],
);
}
}
/// 菜单列表组件
/// 菜单列表组件 - Glass Panel 风格
class _MenuList extends StatelessWidget {
final void Function(String) onShowComingSoon;
final VoidCallback onShowAbout;
@@ -243,34 +320,75 @@ class _MenuList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final themeProvider = context.watch<ThemeProvider>();
return ShadCard(
return GlassPanel(
padding: EdgeInsets.zero,
borderRadius: BorderRadius.circular(AppRadius.xxl),
child: Column(
children: [
// 主题切换开关(特殊处理)
// 主题切换开关
_ThemeToggleTile(isDarkMode: themeProvider.isDarkMode),
Divider(color: theme.colorScheme.border, height: 1, indent: 56),
// 普通菜单项
for (var i = 0; i < _buildMenuItems().length; i++) ...[
_MenuItemTile(item: _buildMenuItems()[i]),
if (i < _buildMenuItems().length - 1)
Divider(color: theme.colorScheme.border, height: 1, indent: 56),
],
_buildDivider(),
// 菜单项
..._buildMenuItems(),
],
),
);
}
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 [
_MenuItem(icon: LucideIcons.userCheck, title: '实名认证', subtitle: '完成实名认证,解锁更多功能', onTap: () => onShowComingSoon('实名认证')),
_MenuItem(icon: LucideIcons.shield, title: '安全设置', subtitle: '密码、二次验证等安全设置', onTap: () => onShowComingSoon('安全设置')),
_MenuItem(icon: LucideIcons.bell, title: '消息通知', subtitle: '管理消息推送设置', onTap: () => onShowComingSoon('消息通知')),
_MenuItem(icon: LucideIcons.settings, title: '系统设置', subtitle: '主题、语言等偏好设置', onTap: () => onShowComingSoon('系统设置')),
_MenuItem(icon: LucideIcons.info, title: '关于我们', subtitle: '版本信息与用户协议', onTap: onShowAbout),
for (var i = 0; i < items.length; i++) ...[
_MenuItemTile(item: items[i]),
if (i < items.length - 1) _buildDivider(),
],
];
}
}
@@ -283,26 +401,41 @@ class _ThemeToggleTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final themeProvider = context.read<ThemeProvider>();
return InkWell(
onTap: () => themeProvider.toggleTheme(),
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(
children: [
_MenuIcon(icon: isDarkMode ? LucideIcons.moon : LucideIcons.sun),
_MenuIcon(
icon: isDarkMode ? LucideIcons.moon : LucideIcons.sun,
color: AppColorScheme.darkPrimary,
),
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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),
Text(
isDarkMode ? '当前:深色主题' : '当前:浅色主题',
style: theme.textTheme.muted.copyWith(fontSize: 11),
style: TextStyle(
fontSize: 11,
color: AppColorScheme.darkOnSurfaceVariant,
),
),
],
),
@@ -310,8 +443,8 @@ class _ThemeToggleTile extends StatelessWidget {
Switch(
value: isDarkMode,
onChanged: (_) => themeProvider.toggleTheme(),
activeTrackColor: theme.colorScheme.primary.withValues(alpha: 0.5),
activeColor: theme.colorScheme.primary,
activeTrackColor: AppColorScheme.darkPrimary.withValues(alpha: 0.5),
activeColor: AppColorScheme.darkPrimary,
),
],
),
@@ -328,29 +461,47 @@ class _MenuItemTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return InkWell(
onTap: item.onTap,
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(
children: [
_MenuIcon(icon: item.icon),
_MenuIcon(icon: item.icon, color: item.iconColor),
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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) ...[
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 {
final IconData icon;
final Color? color;
const _MenuIcon({required this.icon});
const _MenuIcon({required this.icon, this.color});
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
final iconColor = color ?? AppColorScheme.darkPrimary;
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 0.1),
color: iconColor.withValues(alpha: 0.1),
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 {
final VoidCallback onLogout;
@@ -388,20 +543,13 @@ class _LogoutButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
return NeonButton(
text: 'Logout Terminal',
type: NeonButtonType.error,
icon: Icons.logout,
onPressed: onLogout,
width: double.infinity,
height: 48,
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)),
],
),
),
showGlow: true,
);
}
}

View File

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

View File

@@ -4,11 +4,12 @@ import 'package:provider/provider.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../providers/asset_provider.dart';
import '../../../data/models/order_models.dart';
import 'fund_order_card.dart';
class _FundOrdersList extends StatelessWidget {
class FundOrdersList extends StatelessWidget {
final AssetProvider provider;
const _FundOrdersList({required this.provider});
const FundOrdersList({super.key, required this.provider});
@override
Widget build(BuildContext context) {
@@ -16,14 +17,14 @@ class _FundOrdersList extends StatelessWidget {
final orders = provider.fundOrders;
if (orders.isEmpty) {
return const _EmptyState(
return _EmptyState(
icon: LucideIcons.receipt,
message: '暂无充提记录',
);
}
return RefreshIndicator(
onRefresh: () => provider.refreshFundOrders(),
onRefresh: () => provider.loadFundOrders(),
color: theme.colorScheme.primary,
child: ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
@@ -32,10 +33,177 @@ class _FundOrdersList extends StatelessWidget {
separatorBuilder: (_, __) => Divider(color: theme.colorScheme.border, height: 1),
itemBuilder: (context, 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_spacing.dart';
import '../../../providers/asset_provider.dart';
import '../home/home_page.dart';
import 'fund_orders_list.dart';
/// 订单管理页面
class OrdersPage extends StatefulWidget {
@@ -16,7 +16,7 @@ class OrdersPage extends StatefulWidget {
class _OrdersPageState extends State<OrdersPage> with AutomaticKeepAliveClientMixin {
int _activeTab = 0;
@override
bool get wantKeepAlive => true;
@@ -38,7 +38,7 @@ class _OrdersPageState extends State<OrdersPage> with AutomaticKeepAliveClientMi
return Scaffold(
backgroundColor: theme.colorScheme.background,
body: Consumer<AssetProvider>(
builder: (context, provider) {
builder: (context, provider, _) {
return RefreshIndicator(
onRefresh: () => provider.refreshAll(force: true),
color: theme.colorScheme.primary,
@@ -47,22 +47,106 @@ class _OrdersPageState extends State<OrdersPage> with AutomaticKeepAliveClientMi
padding: AppSpacing.pagePadding,
child: Column(
children: [
_TabSelector(
TabSelector(
tabs: const ['充提记录', '交易记录'],
selectedIndex: _activeTab,
onChanged: (index) => setState(() => _activeTab = index),
),
SizedBox(height: AppSpacing.md),
_activeTab == 0
? _FundOrdersList(provider: provider)
: _TradeOrdersList(provider: provider),
? FundOrdersList(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: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 '../../../data/models/coin.dart';
import '../../../providers/market_provider.dart';
import '../../../providers/asset_provider.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 {
const TradePage({super.key});
@@ -17,15 +20,15 @@ class TradePage extends StatefulWidget {
}
class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
int _tradeType = 0; // 0=买入, 1=卖出
Coin? _selectedCoin;
final _formKey = GlobalKey<ShadFormState>();
final _priceController = TextEditingController();
final _quantityController = TextEditingController();
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
@@ -46,10 +49,9 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
@override
Widget build(BuildContext context) {
super.build(context);
final theme = ShadTheme.of(context);
return Scaffold(
backgroundColor: theme.colorScheme.background,
backgroundColor: AppColorScheme.darkBackground,
body: Consumer2<MarketProvider, AssetProvider>(
builder: (context, market, asset, _) {
return SingleChildScrollView(
@@ -77,7 +79,7 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
tradeBalance: asset.overview?.tradeBalance,
onTradeTypeChanged: (type) => setState(() => _tradeType = type),
),
SizedBox(height: AppSpacing.md),
SizedBox(height: AppSpacing.lg),
_TradeButton(
isBuy: _tradeType == 0,
coinCode: _selectedCoin?.code,
@@ -121,7 +123,6 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
}
void _showTradeResult() {
final theme = ShadTheme.of(context);
final isBuy = _tradeType == 0;
showShadDialog(
@@ -129,7 +130,11 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
builder: (ctx) => ShadDialog.alert(
title: Row(
children: [
Icon(LucideIcons.circleCheck, color: theme.colorScheme.primary, size: 24),
NeonIcon(
icon: Icons.check_circle,
color: AppColorScheme.darkPrimary,
size: 24,
),
SizedBox(width: AppSpacing.sm),
const Text('交易成功'),
],
@@ -149,7 +154,7 @@ class _TradePageState extends State<TradePage> with AutomaticKeepAliveClientMixi
}
}
/// 币种选择器
/// 币种选择器 - Glass Panel 风格
class _CoinSelector extends StatelessWidget {
final Coin? selectedCoin;
final List<Coin> coins;
@@ -163,15 +168,13 @@ class _CoinSelector extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
// 自动选择第一个币种
if (selectedCoin == null && coins.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) => onCoinLoaded(coins.first));
}
return ShadCard(
padding: AppSpacing.cardPadding,
return GlassCard(
showNeonGlow: false,
child: Row(
children: [
_CoinAvatar(icon: selectedCoin?.displayIcon),
@@ -182,21 +185,34 @@ class _CoinSelector extends StatelessWidget {
children: [
Text(
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),
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 {
final String? icon;
@@ -204,20 +220,31 @@ class _CoinAvatar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return CircleAvatar(
radius: 22,
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1),
child: Text(
icon ?? '?',
style: TextStyle(fontSize: 20, color: theme.colorScheme.primary),
return Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: AppColorScheme.darkPrimary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(
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 {
final Coin coin;
@@ -225,31 +252,56 @@ class _PriceCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
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(
padding: AppSpacing.cardPadding,
return GlassCard(
showNeonGlow: false,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('最新价', style: theme.textTheme.muted),
Text(
'最新价',
style: TextStyle(
fontSize: 12,
color: AppColorScheme.darkOnSurfaceVariant,
),
),
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(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm + AppSpacing.xs, vertical: AppSpacing.xs + AppSpacing.xs),
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.2),
color: bgColor,
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(
color: color.withValues(alpha: 0.2),
),
),
child: Text(
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 {
final int tradeType;
final Coin? selectedCoin;
@@ -278,10 +330,8 @@ class _TradeForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return ShadCard(
padding: AppSpacing.cardPadding,
return GlassPanel(
padding: EdgeInsets.all(AppSpacing.lg),
child: Column(
children: [
// 买入/卖出切换
@@ -289,45 +339,96 @@ class _TradeForm extends StatelessWidget {
tradeType: tradeType,
onChanged: onTradeTypeChanged,
),
SizedBox(height: AppSpacing.lg + AppSpacing.xs),
SizedBox(height: AppSpacing.lg),
// 价格输入
ShadInputFormField(
id: 'price',
label: const Text('价格(USDT)'),
_buildInputField(
label: '价格(USDT)',
controller: priceController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
placeholder: const Text('输入价格'),
trailing: Padding(
padding: EdgeInsets.only(right: AppSpacing.sm),
child: const Text('USDT'),
),
validator: Validators.price,
placeholder: '输入价格',
suffix: 'USDT',
),
SizedBox(height: AppSpacing.md),
// 数量输入
ShadInputFormField(
id: 'quantity',
label: const Text('数量'),
_buildInputField(
label: '数量',
controller: quantityController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
placeholder: const Text('输入数量'),
trailing: Padding(
padding: EdgeInsets.only(right: AppSpacing.sm),
child: Text(selectedCoin?.code ?? ''),
),
validator: Validators.quantity,
placeholder: '输入数量',
suffix: selectedCoin?.code ?? '',
),
SizedBox(height: AppSpacing.md),
// 交易金额
SizedBox(height: AppSpacing.lg),
// 信息行
_InfoRow(label: '交易金额', value: '${_calculateAmount()} USDT'),
SizedBox(height: AppSpacing.sm),
// 可用余额
_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() {
final price = double.tryParse(priceController.text) ?? 0;
final quantity = double.tryParse(quantityController.text) ?? 0;
@@ -335,7 +436,7 @@ class _TradeForm extends StatelessWidget {
}
}
/// 交易类型选择器
/// 交易类型选择器 - Material Design 3 风格
class _TradeTypeSelector extends StatelessWidget {
final int tradeType;
final ValueChanged<int> onChanged;
@@ -344,26 +445,33 @@ class _TradeTypeSelector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: _TypeButton(
label: '买入',
isSelected: tradeType == 0,
color: AppColorScheme.up,
onTap: () => onChanged(0),
return Container(
padding: EdgeInsets.all(AppSpacing.xs),
decoration: BoxDecoration(
color: AppColorScheme.darkSurfaceLowest,
borderRadius: BorderRadius.circular(AppRadius.xl),
),
child: Row(
children: [
Expanded(
child: _TypeButton(
label: 'Buy',
isSelected: tradeType == 0,
color: AppColorScheme.up,
onTap: () => onChanged(0),
),
),
),
SizedBox(width: AppSpacing.md),
Expanded(
child: _TypeButton(
label: '卖出',
isSelected: tradeType == 1,
color: AppColorScheme.down,
onTap: () => onChanged(1),
SizedBox(width: AppSpacing.sm),
Expanded(
child: _TypeButton(
label: 'Sell',
isSelected: tradeType == 1,
color: AppColorScheme.down,
onTap: () => onChanged(1),
),
),
),
],
],
),
);
}
}
@@ -386,19 +494,22 @@ class _TypeButton extends StatelessWidget {
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: EdgeInsets.symmetric(vertical: AppSpacing.sm + AppSpacing.xs),
decoration: BoxDecoration(
color: isSelected ? color : Colors.transparent,
color: isSelected ? color.withValues(alpha: 0.15) : Colors.transparent,
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: Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : color,
fontWeight: FontWeight.w600,
color: isSelected ? color : color.withValues(alpha: 0.7),
fontWeight: FontWeight.w700,
fontSize: 14,
letterSpacing: 0.5,
),
),
),
@@ -416,19 +527,30 @@ class _InfoRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: theme.textTheme.muted),
Text(value, style: theme.textTheme.small.copyWith(fontWeight: FontWeight.w600)),
Text(
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 {
final bool isBuy;
final String? coinCode;
@@ -442,26 +564,13 @@ class _TradeButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final color = isBuy ? AppColorScheme.up : AppColorScheme.down;
return SizedBox(
return NeonButton(
text: '${isBuy ? '买入' : '卖出'} ${coinCode ?? ''}',
type: isBuy ? NeonButtonType.tertiary : NeonButtonType.error,
icon: isBuy ? Icons.arrow_downward : Icons.arrow_upward,
onPressed: onPressed,
width: double.infinity,
height: 48,
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),
),
],
),
),
showGlow: true,
);
}
}