From df0e8beba94ae9eb4979e58659d7b505477535c7 Mon Sep 17 00:00:00 2001 From: sion <450702724@qq.com> Date: Tue, 24 Mar 2026 02:16:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E5=BA=94=E7=94=A8=E6=96=B0?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E7=B3=BB=E7=BB=9F=E5=88=B0=20Flutter=20?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新颜色系统为 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 个新组件 --- .../lib/core/theme/app_color_scheme.dart | 188 ++- .../lib/ui/components/components.dart | 2 + .../lib/ui/components/glass_panel.dart | 315 +++++ .../lib/ui/components/neon_glow.dart | 403 ++++++ .../lib/ui/pages/asset/asset_page.dart | 1088 ++++++++++++----- .../lib/ui/pages/market/market_page.dart | 407 +++--- .../lib/ui/pages/mine/mine_page.dart | 308 +++-- .../lib/ui/pages/orders/fund_order_card.dart | 8 +- .../lib/ui/pages/orders/fund_orders_list.dart | 178 ++- .../lib/ui/pages/orders/orders_page.dart | 100 +- .../lib/ui/pages/trade/trade_page.dart | 333 +++-- 11 files changed, 2625 insertions(+), 705 deletions(-) create mode 100644 flutter_monisuo/lib/ui/components/glass_panel.dart create mode 100644 flutter_monisuo/lib/ui/components/neon_glow.dart diff --git a/flutter_monisuo/lib/core/theme/app_color_scheme.dart b/flutter_monisuo/lib/core/theme/app_color_scheme.dart index b4c1b28..d32ca2c 100644 --- a/flutter_monisuo/lib/core/theme/app_color_scheme.dart +++ b/flutter_monisuo/lib/core/theme/app_color_scheme.dart @@ -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; diff --git a/flutter_monisuo/lib/ui/components/components.dart b/flutter_monisuo/lib/ui/components/components.dart index f293bbb..c845710 100644 --- a/flutter_monisuo/lib/ui/components/components.dart +++ b/flutter_monisuo/lib/ui/components/components.dart @@ -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'; diff --git a/flutter_monisuo/lib/ui/components/glass_panel.dart b/flutter_monisuo/lib/ui/components/glass_panel.dart new file mode 100644 index 0000000..5704bef --- /dev/null +++ b/flutter_monisuo/lib/ui/components/glass_panel.dart @@ -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 show({ + required BuildContext context, + required Widget Function(BuildContext) builder, + bool isScrollControlled = false, + bool isDismissible = true, + bool enableDrag = true, + }) { + return showModalBottomSheet( + context: context, + isScrollControlled: isScrollControlled, + isDismissible: isDismissible, + enableDrag: enableDrag, + backgroundColor: Colors.transparent, + builder: builder, + ); + } +} diff --git a/flutter_monisuo/lib/ui/components/neon_glow.dart b/flutter_monisuo/lib/ui/components/neon_glow.dart new file mode 100644 index 0000000..d832df5 --- /dev/null +++ b/flutter_monisuo/lib/ui/components/neon_glow.dart @@ -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 createState() => _NeonButtonState(); +} + +class _NeonButtonState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + bool _isPressed = false; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 150), + vsync: this, + ); + _scaleAnimation = Tween(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(_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), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/asset/asset_page.dart b/flutter_monisuo/lib/ui/pages/asset/asset_page.dart index 624becf..89ee97f 100644 --- a/flutter_monisuo/lib/ui/pages/asset/asset_page.dart +++ b/flutter_monisuo/lib/ui/pages/asset/asset_page.dart @@ -2,13 +2,16 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:provider/provider.dart'; +import 'package:google_fonts/google_fonts.dart'; import '../../../core/theme/app_color_scheme.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../providers/asset_provider.dart'; import '../../shared/ui_constants.dart'; +import '../../components/glass_panel.dart'; +import '../../components/neon_glow.dart'; import '../orders/fund_orders_page.dart'; -/// 资产页面 - 使用 shadcn_ui 现代化设计 +/// 资产页面 - Material Design 3 风格 class AssetPage extends StatefulWidget { const AssetPage({super.key}); @@ -17,11 +20,11 @@ class AssetPage extends StatefulWidget { } class _AssetPageState extends State with AutomaticKeepAliveClientMixin { + int _activeTab = 0; + @override bool get wantKeepAlive => true; - int _activeTab = 0; - @override void initState() { super.initState(); @@ -35,15 +38,15 @@ class _AssetPageState extends State 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: Consumer( builder: (context, provider, _) { return RefreshIndicator( onRefresh: () => provider.refreshAll(force: true), - color: theme.colorScheme.primary, + color: AppColorScheme.darkPrimary, + backgroundColor: AppColorScheme.darkSurfaceContainer, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: AppSpacing.pagePadding, @@ -70,7 +73,7 @@ class _AssetPageState extends State with AutomaticKeepAliveClientMixi } } -/// 资产总览卡片 +/// 资产总览卡片 - Material Design 3 风格 class _AssetCard extends StatelessWidget { final dynamic overview; @@ -78,40 +81,67 @@ class _AssetCard extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = ShadTheme.of(context); - return Container( width: double.infinity, - padding: EdgeInsets.all(AppSpacing.lg), + padding: EdgeInsets.all(AppSpacing.lg + AppSpacing.sm), decoration: BoxDecoration( gradient: AppColorScheme.assetCardGradient, borderRadius: BorderRadius.circular(AppRadius.xl), + boxShadow: [ + BoxShadow( + color: AppColorScheme.neonGlowPrimary, + blurRadius: 20, + ), + ], ), child: Column( children: [ Text( - '总资产估值(USDT)', - style: theme.textTheme.small.copyWith(color: Colors.white70), + 'PORTFOLIO VALUE', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + letterSpacing: 0.2, + color: Colors.white.withValues(alpha: 0.7), + ), ), SizedBox(height: AppSpacing.sm), Text( - overview?.totalAsset ?? '0.00', - style: theme.textTheme.h1.copyWith( + '\$${overview?.totalAsset ?? '0.00'}', + style: GoogleFonts.spaceGrotesk( + fontSize: 36, fontWeight: FontWeight.bold, color: Colors.white, ), ), SizedBox(height: AppSpacing.md), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(LucideIcons.trendingUp, color: Colors.white70, size: 16), - SizedBox(width: AppSpacing.xs), - Text( - '总盈亏: ${overview?.totalProfit ?? '0.00'} USDT', - style: theme.textTheme.small.copyWith(color: Colors.white70), - ), - ], + Container( + padding: EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.xs + AppSpacing.xs, + ), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppRadius.full), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + LucideIcons.trendingUp, + color: Colors.white.withValues(alpha: 0.7), + size: 14, + ), + SizedBox(width: AppSpacing.xs), + Text( + '总盈亏: ${overview?.totalProfit ?? '0.00'} USDT', + style: TextStyle( + fontSize: 12, + color: Colors.white.withValues(alpha: 0.7), + ), + ), + ], + ), ), ], ), @@ -119,7 +149,7 @@ class _AssetCard extends StatelessWidget { } } -/// Tab 选择器 +/// Tab 选择器 - Material Design 3 风格 class _TabSelector extends StatelessWidget { final List tabs; final int selectedIndex; @@ -133,12 +163,10 @@ class _TabSelector extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = ShadTheme.of(context); - return Container( padding: EdgeInsets.all(AppSpacing.xs), decoration: BoxDecoration( - color: theme.colorScheme.card, + color: AppColorScheme.darkSurfaceContainer, borderRadius: BorderRadius.circular(AppRadius.lg), ), child: Row( @@ -150,17 +178,26 @@ class _TabSelector extends StatelessWidget { return Expanded( child: GestureDetector( onTap: () => onChanged(index), - child: Container( + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), padding: EdgeInsets.symmetric(vertical: AppSpacing.sm + AppSpacing.xs), decoration: BoxDecoration( - color: isSelected ? theme.colorScheme.primary : Colors.transparent, + color: isSelected ? AppColorScheme.darkPrimary : Colors.transparent, borderRadius: BorderRadius.circular(AppRadius.md), + boxShadow: isSelected + ? [ + BoxShadow( + color: AppColorScheme.neonGlowPrimary, + blurRadius: 10, + ), + ] + : null, ), child: Center( child: Text( label, style: TextStyle( - color: isSelected ? Colors.white : theme.colorScheme.mutedForeground, + color: isSelected ? AppColorScheme.darkOnPrimaryFixed : AppColorScheme.darkOnSurfaceVariant, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), ), @@ -174,7 +211,7 @@ class _TabSelector extends StatelessWidget { } } -/// 资金账户卡片 +/// 资金账户卡片 - Glass Panel 风格 class _FundAccountCard extends StatelessWidget { final AssetProvider provider; @@ -182,10 +219,9 @@ class _FundAccountCard extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = ShadTheme.of(context); final fund = provider.fundAccount; - return ShadCard( + return GlassPanel( padding: EdgeInsets.all(AppSpacing.lg + AppSpacing.xs), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -193,7 +229,13 @@ class _FundAccountCard extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('USDT余额', style: theme.textTheme.muted), + Text( + 'USDT 余额', + style: TextStyle( + fontSize: 12, + color: AppColorScheme.darkOnSurfaceVariant, + ), + ), GestureDetector( onTap: () => Navigator.push( context, @@ -201,23 +243,67 @@ class _FundAccountCard extends StatelessWidget { ), child: Row( children: [ - Text('充提记录', style: TextStyle(color: theme.colorScheme.primary, fontSize: 12)), - Icon(LucideIcons.chevronRight, size: 14, color: theme.colorScheme.primary), + Text( + '充提记录', + style: TextStyle( + color: AppColorScheme.darkPrimary, + fontSize: 12, + ), + ), + Icon( + LucideIcons.chevronRight, + size: 14, + color: AppColorScheme.darkPrimary, + ), ], ), ), ], ), SizedBox(height: AppSpacing.sm), - Text(fund?.balance ?? '0.00', style: theme.textTheme.h2.copyWith(fontWeight: FontWeight.bold)), + Text( + fund?.balance ?? '0.00', + style: GoogleFonts.spaceGrotesk( + fontSize: 28, + fontWeight: FontWeight.bold, + color: AppColorScheme.darkOnSurface, + ), + ), SizedBox(height: AppSpacing.lg), Row( children: [ - Expanded(child: _ActionButton(label: '充值', icon: LucideIcons.plus, color: AppColorScheme.success, onTap: () => _showDepositDialog(context))), - SizedBox(width: AppSpacing.sm + AppSpacing.xs), - Expanded(child: _ActionButton(label: '提现', icon: LucideIcons.minus, color: AppColorScheme.warning, onTap: () => _showWithdrawDialog(context, fund?.balance))), - SizedBox(width: AppSpacing.sm + AppSpacing.xs), - Expanded(child: _ActionButton.outline(label: '划转', icon: LucideIcons.arrowRightLeft, onTap: () => _showTransferDialog(context))), + Expanded( + child: NeonButton( + text: '充值', + type: NeonButtonType.tertiary, + icon: Icons.add, + onPressed: () => _showDepositDialog(context), + height: 44, + showGlow: false, + ), + ), + SizedBox(width: AppSpacing.sm), + Expanded( + child: NeonButton( + text: '提现', + type: NeonButtonType.secondary, + icon: Icons.remove, + onPressed: () => _showWithdrawDialog(context, fund?.balance), + height: 44, + showGlow: false, + ), + ), + SizedBox(width: AppSpacing.sm), + Expanded( + child: NeonButton( + text: '划转', + type: NeonButtonType.outline, + icon: Icons.swap_horiz, + onPressed: () => _showTransferDialog(context), + height: 44, + showGlow: false, + ), + ), ], ), ], @@ -226,45 +312,7 @@ class _FundAccountCard extends StatelessWidget { } } -/// 操作按钮 -class _ActionButton extends StatelessWidget { - final String label; - final IconData icon; - final Color? color; - final bool isOutline; - final VoidCallback onTap; - - const _ActionButton({ - required this.label, - required this.icon, - required this.color, - required this.onTap, - }) : isOutline = false; - - const _ActionButton.outline({ - required this.label, - required this.icon, - required this.onTap, - }) : color = null, isOutline = true; - - @override - Widget build(BuildContext context) { - final child = Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(icon, size: 18, color: isOutline ? null : Colors.white), - SizedBox(width: AppSpacing.xs), - Text(label), - ], - ); - - return isOutline - ? ShadButton.outline(onPressed: onTap, child: child) - : ShadButton(backgroundColor: color, onPressed: onTap, child: child); - } -} - -/// 交易账户卡片 +/// 交易账户卡片 - Glass Panel 风格 class _TradeAccountCard extends StatelessWidget { final List holdings; @@ -272,14 +320,19 @@ class _TradeAccountCard extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = ShadTheme.of(context); - - return ShadCard( + return GlassPanel( padding: AppSpacing.cardPadding, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('持仓列表', style: theme.textTheme.large.copyWith(fontWeight: FontWeight.bold)), + Text( + '持仓列表', + style: GoogleFonts.spaceGrotesk( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColorScheme.darkOnSurface, + ), + ), SizedBox(height: AppSpacing.md), if (holdings.isEmpty) const _EmptyState(icon: LucideIcons.wallet, message: '暂无持仓') @@ -288,7 +341,11 @@ class _TradeAccountCard extends StatelessWidget { shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: holdings.length, - separatorBuilder: (_, __) => Divider(color: theme.colorScheme.border, height: 1), + separatorBuilder: (_, __) => Container( + margin: EdgeInsets.only(left: 56), + height: 1, + color: AppColorScheme.glassPanelBorder, + ), itemBuilder: (context, index) => _HoldingItem(holding: holdings[index]), ), ], @@ -306,16 +363,21 @@ class _EmptyState extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = ShadTheme.of(context); - return Center( child: Padding( padding: EdgeInsets.all(AppSpacing.xl), child: Column( children: [ - Icon(icon, size: 48, color: theme.colorScheme.mutedForeground), + Icon( + icon, + size: 48, + color: AppColorScheme.darkOnSurfaceVariant, + ), SizedBox(height: AppSpacing.sm + AppSpacing.xs), - Text(message, style: theme.textTheme.muted), + Text( + message, + style: TextStyle(color: AppColorScheme.darkOnSurfaceVariant), + ), ], ), ), @@ -331,18 +393,25 @@ class _HoldingItem extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = ShadTheme.of(context); - return Padding( padding: EdgeInsets.symmetric(vertical: AppSpacing.sm), child: Row( children: [ - CircleAvatar( - radius: 20, - backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1), - child: Text( - holding.coinCode.substring(0, 1), - style: TextStyle(color: theme.colorScheme.primary, fontWeight: FontWeight.bold), + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColorScheme.darkPrimary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppRadius.md), + ), + child: Center( + child: Text( + holding.coinCode.substring(0, 1), + style: TextStyle( + color: AppColorScheme.darkPrimary, + fontWeight: FontWeight.bold, + ), + ), ), ), SizedBox(width: AppSpacing.sm + AppSpacing.xs), @@ -350,20 +419,40 @@ class _HoldingItem extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(holding.coinCode, style: theme.textTheme.large.copyWith(fontWeight: FontWeight.w600)), - Text('数量: ${holding.quantity}', style: theme.textTheme.muted.copyWith(fontSize: 12)), + Text( + holding.coinCode, + style: GoogleFonts.spaceGrotesk( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColorScheme.darkOnSurface, + ), + ), + Text( + '数量: ${holding.quantity}', + style: TextStyle( + fontSize: 12, + color: AppColorScheme.darkOnSurfaceVariant, + ), + ), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text('${holding.currentValue} USDT', style: theme.textTheme.small), + Text( + '${holding.currentValue} USDT', + style: TextStyle( + fontSize: 12, + color: AppColorScheme.darkOnSurface, + ), + ), Text( holding.formattedProfitRate, style: TextStyle( color: holding.isProfit ? AppColorScheme.up : AppColorScheme.down, fontSize: 12, + fontWeight: FontWeight.w600, ), ), ], @@ -374,44 +463,116 @@ class _HoldingItem extends StatelessWidget { } } -// Dialogs +// ============================================ +// Dialogs - Glass Panel 风格 +// ============================================ + void _showDepositDialog(BuildContext context) { final amountController = TextEditingController(); final formKey = GlobalKey(); showShadDialog( context: context, - builder: (ctx) => ShadDialog( - title: const Text('充值'), - child: ShadForm( - key: formKey, - child: ShadInputFormField( - id: 'amount', - controller: amountController, - placeholder: const Text('请输入充值金额(USDT)'), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - validator: Validators.amount, + builder: (ctx) => Dialog( + backgroundColor: Colors.transparent, + child: GlassPanel( + borderRadius: BorderRadius.circular(AppRadius.xxl), + padding: EdgeInsets.all(AppSpacing.lg), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Deposit (充值)', + style: GoogleFonts.spaceGrotesk( + fontSize: 24, + fontWeight: FontWeight.bold, + color: AppColorScheme.darkOnSurface, + ), + ), + SizedBox(height: AppSpacing.xs), + Text( + 'Asset: USDT', + style: TextStyle( + fontSize: 12, + letterSpacing: 0.1, + color: AppColorScheme.darkOnSurfaceVariant, + ), + ), + ], + ), + Container( + padding: EdgeInsets.all(AppSpacing.sm), + decoration: BoxDecoration( + color: AppColorScheme.darkSurfaceContainerHigh, + borderRadius: BorderRadius.circular(AppRadius.md), + ), + child: Icon( + LucideIcons.wallet, + color: AppColorScheme.darkSecondary, + ), + ), + ], + ), + SizedBox(height: AppSpacing.lg), + ShadForm( + key: formKey, + child: ShadInputFormField( + id: 'amount', + controller: amountController, + label: const Text('Amount to Transfer'), + placeholder: const Text('0.00'), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + validator: Validators.amount, + ), + ), + SizedBox(height: AppSpacing.lg), + Row( + children: [ + Expanded( + child: NeonButton( + text: '取消', + type: NeonButtonType.outline, + onPressed: () => Navigator.of(ctx).pop(), + height: 48, + showGlow: false, + ), + ), + SizedBox(width: AppSpacing.sm), + Expanded( + child: NeonButton( + text: '下一步', + type: NeonButtonType.primary, + onPressed: () async { + if (formKey.currentState!.saveAndValidate()) { + Navigator.of(ctx).pop(); + final response = await context.read().deposit( + amount: amountController.text, + ); + if (context.mounted) { + if (response.success && response.data != null) { + _showDepositResultDialog(context, response.data!); + } else { + _showResultDialog(context, '申请失败', response.message); + } + } + } + }, + height: 48, + showGlow: true, + ), + ), + ], + ), + ], ), ), - actions: [ - ShadButton.outline(child: const Text('取消'), onPressed: () => Navigator.of(ctx).pop()), - ShadButton( - child: const Text('下一步'), - onPressed: () async { - if (formKey.currentState!.saveAndValidate()) { - Navigator.of(ctx).pop(); - final response = await context.read().deposit(amount: amountController.text); - if (context.mounted) { - if (response.success && response.data != null) { - _showDepositResultDialog(context, response.data!); - } else { - _showResultDialog(context, '申请失败', response.message); - } - } - } - }, - ), - ], ), ); } @@ -424,44 +585,143 @@ void _showDepositResultDialog(BuildContext context, Map data) { showShadDialog( context: context, - builder: (ctx) => ShadDialog( - title: const Text('充值申请成功'), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('订单号: $orderNo', style: const TextStyle(fontSize: 12)), - SizedBox(height: AppSpacing.sm), - Text('充值金额: $amount USDT', style: const TextStyle(fontWeight: FontWeight.bold)), - SizedBox(height: AppSpacing.md), - const Text('请向以下地址转账:', style: TextStyle(fontSize: 12)), - SizedBox(height: AppSpacing.sm), - _WalletAddressCard(address: walletAddress, network: walletNetwork), - SizedBox(height: AppSpacing.sm + AppSpacing.xs), - const Text('转账完成后请点击"已打款"按钮确认', style: TextStyle(fontSize: 12, color: Colors.orange)), - ], - ), - actions: [ - ShadButton.outline(child: const Text('稍后确认'), onPressed: () => Navigator.of(ctx).pop()), - ShadButton( - child: const Text('已打款'), - onPressed: () async { - Navigator.of(ctx).pop(); - final response = await context.read().confirmPay(orderNo); - if (context.mounted) { - _showResultDialog( - context, - response.success ? '确认成功' : '确认失败', - response.success ? '请等待管理员审核' : response.message, - ); - } - }, + builder: (ctx) => Dialog( + backgroundColor: Colors.transparent, + child: GlassPanel( + borderRadius: BorderRadius.circular(AppRadius.xxl), + padding: EdgeInsets.all(AppSpacing.lg), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + NeonIcon( + icon: Icons.check_circle, + color: AppColorScheme.darkTertiary, + size: 24, + ), + SizedBox(width: AppSpacing.sm), + Text( + '充值申请成功', + style: GoogleFonts.spaceGrotesk( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppColorScheme.darkOnSurface, + ), + ), + ], + ), + SizedBox(height: AppSpacing.lg), + _InfoRow(label: '订单号', value: orderNo), + SizedBox(height: AppSpacing.sm), + _InfoRow(label: '充值金额', value: '$amount USDT', isBold: true), + SizedBox(height: AppSpacing.lg), + Text( + '请向以下地址转账:', + style: TextStyle( + fontSize: 12, + color: AppColorScheme.darkOnSurfaceVariant, + ), + ), + SizedBox(height: AppSpacing.sm), + _WalletAddressCard(address: walletAddress, network: walletNetwork), + SizedBox(height: AppSpacing.md), + Container( + padding: EdgeInsets.all(AppSpacing.sm), + decoration: BoxDecoration( + color: AppColorScheme.warning.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppRadius.md), + border: Border.all( + color: AppColorScheme.warning.withValues(alpha: 0.2), + ), + ), + child: Row( + children: [ + Icon(Icons.info_outline, size: 16, color: AppColorScheme.warning), + SizedBox(width: AppSpacing.sm), + Expanded( + child: Text( + '转账完成后请点击"已打款"按钮确认', + style: TextStyle(fontSize: 12, color: AppColorScheme.warning), + ), + ), + ], + ), + ), + SizedBox(height: AppSpacing.lg), + Row( + children: [ + Expanded( + child: NeonButton( + text: '稍后确认', + type: NeonButtonType.outline, + onPressed: () => Navigator.of(ctx).pop(), + height: 44, + showGlow: false, + ), + ), + SizedBox(width: AppSpacing.sm), + Expanded( + child: NeonButton( + text: '已打款', + type: NeonButtonType.primary, + onPressed: () async { + Navigator.of(ctx).pop(); + final response = await context.read().confirmPay(orderNo); + if (context.mounted) { + _showResultDialog( + context, + response.success ? '确认成功' : '确认失败', + response.success ? '请等待管理员审核' : response.message, + ); + } + }, + height: 44, + showGlow: true, + ), + ), + ], + ), + ], ), - ], + ), ), ); } +class _InfoRow extends StatelessWidget { + final String label; + final String value; + final bool isBold; + + const _InfoRow({required this.label, required this.value, this.isBold = false}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12, + color: AppColorScheme.darkOnSurfaceVariant, + ), + ), + Text( + value, + style: TextStyle( + fontSize: 12, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal, + color: AppColorScheme.darkOnSurface, + ), + ), + ], + ); + } +} + class _WalletAddressCard extends StatelessWidget { final String address; final String network; @@ -471,10 +731,13 @@ class _WalletAddressCard extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: EdgeInsets.all(AppSpacing.sm + AppSpacing.xs), + padding: EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: Colors.grey[100], + color: AppColorScheme.darkSurfaceContainerHigh, borderRadius: BorderRadius.circular(AppRadius.md), + border: Border.all( + color: AppColorScheme.darkOutlineVariant.withValues(alpha: 0.3), + ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -482,22 +745,45 @@ class _WalletAddressCard extends StatelessWidget { Row( children: [ Expanded( - child: Text(address, style: const TextStyle(fontFamily: 'monospace', fontSize: 12)), + child: Text( + address, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 12, + color: AppColorScheme.darkOnSurface, + ), + ), ), - IconButton( - icon: const Icon(LucideIcons.copy, size: 18), - onPressed: () { + GestureDetector( + onTap: () { Clipboard.setData(ClipboardData(text: address)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('地址已复制到剪贴板')), ); }, - tooltip: '复制地址', + child: Container( + padding: EdgeInsets.all(AppSpacing.xs), + decoration: BoxDecoration( + color: AppColorScheme.darkPrimary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + child: Icon( + LucideIcons.copy, + size: 16, + color: AppColorScheme.darkPrimary, + ), + ), ), ], ), - SizedBox(height: AppSpacing.xs), - Text('网络: $network', style: const TextStyle(fontSize: 12, color: Colors.grey)), + SizedBox(height: AppSpacing.sm), + Text( + '网络: $network', + style: TextStyle( + fontSize: 11, + color: AppColorScheme.darkOnSurfaceVariant, + ), + ), ], ), ); @@ -512,67 +798,182 @@ void _showWithdrawDialog(BuildContext context, String? balance) { showShadDialog( context: context, - builder: (ctx) => ShadDialog( - title: const Text('提现'), - child: SingleChildScrollView( - child: ShadForm( - key: formKey, + builder: (ctx) => Dialog( + backgroundColor: Colors.transparent, + child: GlassPanel( + borderRadius: BorderRadius.circular(AppRadius.xxl), + padding: EdgeInsets.all(AppSpacing.lg), + child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (balance != null) - Padding( - padding: EdgeInsets.only(bottom: AppSpacing.sm + AppSpacing.xs), - child: Text('可用余额: $balance USDT', style: const TextStyle(color: Colors.grey)), + Row( + children: [ + Container( + padding: EdgeInsets.all(AppSpacing.sm), + decoration: BoxDecoration( + color: AppColorScheme.darkPrimary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppRadius.md), + ), + child: Icon( + LucideIcons.wallet, + color: AppColorScheme.darkPrimary, + ), + ), + SizedBox(width: AppSpacing.sm), + Text( + '提现 (Withdraw)', + style: GoogleFonts.spaceGrotesk( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppColorScheme.darkOnSurface, + ), + ), + ], + ), + SizedBox(height: AppSpacing.xs), + Text( + 'Securely transfer your assets to an external wallet address.', + style: TextStyle( + fontSize: 12, + color: AppColorScheme.darkOnSurfaceVariant, ), - ShadInputFormField( - id: 'amount', - controller: amountController, - placeholder: const Text('请输入提现金额(USDT)'), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - validator: Validators.amount, ), - SizedBox(height: AppSpacing.sm + AppSpacing.xs), - ShadInputFormField( - id: 'address', - controller: addressController, - placeholder: const Text('请输入提现地址'), - validator: (v) => Validators.required(v, '提现地址'), + if (balance != null) ...[ + SizedBox(height: AppSpacing.md), + Container( + padding: EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ), + decoration: BoxDecoration( + color: AppColorScheme.darkTertiary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(AppRadius.full), + border: Border.all( + color: AppColorScheme.darkTertiary.withValues(alpha: 0.2), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '可用余额: ', + style: TextStyle( + fontSize: 10, + letterSpacing: 0.1, + color: AppColorScheme.darkOnSurfaceVariant, + ), + ), + Text( + '$balance USDT', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: AppColorScheme.darkTertiary, + ), + ), + ], + ), + ), + ], + SizedBox(height: AppSpacing.lg), + ShadForm( + key: formKey, + child: Column( + children: [ + ShadInputFormField( + id: 'amount', + controller: amountController, + label: const Text('Withdrawal Amount'), + placeholder: const Text('Enter withdrawal amount (USDT)'), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + validator: Validators.amount, + ), + SizedBox(height: AppSpacing.md), + ShadInputFormField( + id: 'address', + controller: addressController, + label: const Text('Destination Address'), + placeholder: const Text('Enter withdrawal address'), + validator: (v) => Validators.required(v, '提现地址'), + ), + SizedBox(height: AppSpacing.md), + ShadInputFormField( + id: 'contact', + controller: contactController, + label: const Text('Contact Info (Optional)'), + placeholder: const Text('Contact info'), + ), + ], + ), ), - SizedBox(height: AppSpacing.sm + AppSpacing.xs), - ShadInputFormField( - id: 'contact', - controller: contactController, - placeholder: const Text('联系方式(可选)'), + SizedBox(height: AppSpacing.lg), + Row( + children: [ + Expanded( + child: NeonButton( + text: '取消', + type: NeonButtonType.outline, + onPressed: () => Navigator.of(ctx).pop(), + height: 44, + showGlow: false, + ), + ), + SizedBox(width: AppSpacing.sm), + Expanded( + child: NeonButton( + text: '提交', + type: NeonButtonType.primary, + onPressed: () async { + if (formKey.currentState!.saveAndValidate()) { + Navigator.of(ctx).pop(); + final response = await context.read().withdraw( + amount: amountController.text, + withdrawAddress: addressController.text, + withdrawContact: contactController.text.isNotEmpty + ? contactController.text + : null, + ); + if (context.mounted) { + _showResultDialog( + context, + response.success ? '申请成功' : '申请失败', + response.success ? '请等待管理员审批' : response.message, + ); + } + } + }, + height: 44, + showGlow: true, + ), + ), + ], + ), + SizedBox(height: AppSpacing.md), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.verified_user, + size: 12, + color: AppColorScheme.darkOnSurfaceVariant.withValues(alpha: 0.5), + ), + SizedBox(width: AppSpacing.xs), + Text( + 'End-to-End Encrypted Transaction', + style: TextStyle( + fontSize: 10, + letterSpacing: 0.1, + color: AppColorScheme.darkOnSurfaceVariant.withValues(alpha: 0.5), + ), + ), + ], ), ], ), ), ), - actions: [ - ShadButton.outline(child: const Text('取消'), onPressed: () => Navigator.of(ctx).pop()), - ShadButton( - child: const Text('提交'), - onPressed: () async { - if (formKey.currentState!.saveAndValidate()) { - Navigator.of(ctx).pop(); - final response = await context.read().withdraw( - amount: amountController.text, - withdrawAddress: addressController.text, - withdrawContact: contactController.text.isNotEmpty ? contactController.text : null, - ); - if (context.mounted) { - _showResultDialog( - context, - response.success ? '申请成功' : '申请失败', - response.success ? '请等待管理员审批' : response.message, - ); - } - } - }, - ), - ], ), ); } @@ -585,59 +986,97 @@ void _showTransferDialog(BuildContext context) { showShadDialog( context: context, builder: (ctx) => StatefulBuilder( - builder: (ctx, setState) => ShadDialog( - title: const Text('划转'), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: AppSpacing.sm), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _DirectionButton( - label: '资金→交易', - isSelected: direction == 1, - onTap: () => setState(() => direction = 1), + builder: (ctx, setState) => Dialog( + backgroundColor: Colors.transparent, + child: GlassPanel( + borderRadius: BorderRadius.circular(AppRadius.xxl), + padding: EdgeInsets.all(AppSpacing.lg), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '划转', + style: GoogleFonts.spaceGrotesk( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppColorScheme.darkOnSurface, ), - SizedBox(width: AppSpacing.sm), - _DirectionButton( - label: '交易→资金', - isSelected: direction == 2, - onTap: () => setState(() => direction = 2), - ), - ], - ), - SizedBox(height: AppSpacing.md), - ShadForm( - key: formKey, - child: ShadInputFormField( - id: 'amount', - controller: controller, - placeholder: const Text('请输入划转金额(USDT)'), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - validator: Validators.amount, ), - ), - ], - ), - actions: [ - ShadButton.outline(child: const Text('取消'), onPressed: () => Navigator.of(ctx).pop()), - ShadButton( - child: const Text('确认'), - onPressed: () async { - if (formKey.currentState!.saveAndValidate()) { - Navigator.of(ctx).pop(); - final response = await context.read().transfer( - direction: direction, - amount: controller.text, - ); - if (context.mounted) { - _showResultDialog(context, response.success ? '划转成功' : '划转失败', response.message); - } - } - }, + SizedBox(height: AppSpacing.lg), + Row( + children: [ + Expanded( + child: _DirectionButton( + label: '资金→交易', + isSelected: direction == 1, + onTap: () => setState(() => direction = 1), + ), + ), + SizedBox(width: AppSpacing.sm), + Expanded( + child: _DirectionButton( + label: '交易→资金', + isSelected: direction == 2, + onTap: () => setState(() => direction = 2), + ), + ), + ], + ), + SizedBox(height: AppSpacing.lg), + ShadForm( + key: formKey, + child: ShadInputFormField( + id: 'amount', + controller: controller, + label: const Text('划转金额'), + placeholder: const Text('请输入划转金额(USDT)'), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + validator: Validators.amount, + ), + ), + SizedBox(height: AppSpacing.lg), + Row( + children: [ + Expanded( + child: NeonButton( + text: '取消', + type: NeonButtonType.outline, + onPressed: () => Navigator.of(ctx).pop(), + height: 44, + showGlow: false, + ), + ), + SizedBox(width: AppSpacing.sm), + Expanded( + child: NeonButton( + text: '确认', + type: NeonButtonType.primary, + onPressed: () async { + if (formKey.currentState!.saveAndValidate()) { + Navigator.of(ctx).pop(); + final response = await context.read().transfer( + direction: direction, + amount: controller.text, + ); + if (context.mounted) { + _showResultDialog( + context, + response.success ? '划转成功' : '划转失败', + response.message, + ); + } + } + }, + height: 44, + showGlow: true, + ), + ), + ], + ), + ], ), - ], + ), ), ), ); @@ -648,19 +1087,56 @@ class _DirectionButton extends StatelessWidget { final bool isSelected; final VoidCallback onTap; - const _DirectionButton({required this.label, required this.isSelected, required this.onTap}); + const _DirectionButton({ + required this.label, + required this.isSelected, + required this.onTap, + }); @override Widget build(BuildContext context) { - return ShadButton.outline( - size: ShadButtonSize.sm, - onPressed: onTap, - child: Row( - children: [ - if (isSelected) const Icon(LucideIcons.check, size: 14) else const SizedBox(width: 14), - SizedBox(width: AppSpacing.xs), - Text(label), - ], + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: EdgeInsets.symmetric(vertical: AppSpacing.sm + AppSpacing.xs), + decoration: BoxDecoration( + color: isSelected + ? AppColorScheme.darkPrimary.withValues(alpha: 0.15) + : AppColorScheme.darkSurfaceContainerHigh, + borderRadius: BorderRadius.circular(AppRadius.md), + border: isSelected + ? Border.all( + color: AppColorScheme.darkPrimary.withValues(alpha: 0.3), + ) + : null, + ), + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (isSelected) + Icon( + LucideIcons.check, + size: 14, + color: AppColorScheme.darkPrimary, + ) + else + SizedBox(width: 14), + SizedBox(width: AppSpacing.xs), + Text( + label, + style: TextStyle( + color: isSelected + ? AppColorScheme.darkPrimary + : AppColorScheme.darkOnSurfaceVariant, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + fontSize: 12, + ), + ), + ], + ), + ), ), ); } @@ -669,12 +1145,46 @@ class _DirectionButton extends StatelessWidget { void _showResultDialog(BuildContext context, String title, String? message) { showShadDialog( context: context, - builder: (ctx) => ShadDialog.alert( - title: Text(title), - description: message != null ? Text(message) : null, - actions: [ - ShadButton(child: const Text('确定'), onPressed: () => Navigator.of(ctx).pop()), - ], + builder: (ctx) => Dialog( + backgroundColor: Colors.transparent, + child: GlassPanel( + borderRadius: BorderRadius.circular(AppRadius.xxl), + padding: EdgeInsets.all(AppSpacing.lg), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: GoogleFonts.spaceGrotesk( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppColorScheme.darkOnSurface, + ), + ), + if (message != null) ...[ + SizedBox(height: AppSpacing.sm), + Text( + message, + style: TextStyle( + color: AppColorScheme.darkOnSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + SizedBox(height: AppSpacing.lg), + SizedBox( + width: double.infinity, + child: NeonButton( + text: '确定', + type: NeonButtonType.primary, + onPressed: () => Navigator.of(ctx).pop(), + height: 44, + showGlow: false, + ), + ), + ], + ), + ), ), ); } diff --git a/flutter_monisuo/lib/ui/pages/market/market_page.dart b/flutter_monisuo/lib/ui/pages/market/market_page.dart index 3282420..24e9abe 100644 --- a/flutter_monisuo/lib/ui/pages/market/market_page.dart +++ b/flutter_monisuo/lib/ui/pages/market/market_page.dart @@ -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 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 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( builder: (context, provider, _) { return Column( @@ -59,207 +59,300 @@ class _MarketPageState extends State 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, + ), + ), + ), + ], + ), + ], + ), + ); + } } diff --git a/flutter_monisuo/lib/ui/pages/mine/mine_page.dart b/flutter_monisuo/lib/ui/pages/mine/mine_page.dart index fe455eb..7f4f65c 100644 --- a/flutter_monisuo/lib/ui/pages/mine/mine_page.dart +++ b/flutter_monisuo/lib/ui/pages/mine/mine_page.dart @@ -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 with AutomaticKeepAliveClientMixin super.build(context); return Scaffold( + backgroundColor: AppColorScheme.darkBackground, body: Consumer( builder: (context, auth, _) { return SingleChildScrollView( @@ -48,8 +54,18 @@ class _MinePageState extends State 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 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 with AutomaticKeepAliveClientMixin } void _showAboutDialog() { - final theme = ShadTheme.of(context); - showShadDialog( context: context, builder: (context) => ShadDialog( @@ -97,11 +111,14 @@ class _MinePageState extends State 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 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(); - 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 _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(); 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, ); } } diff --git a/flutter_monisuo/lib/ui/pages/orders/fund_order_card.dart b/flutter_monisuo/lib/ui/pages/orders/fund_order_card.dart index 099c4c3..9a9d684 100644 --- a/flutter_monisuo/lib/ui/pages/orders/fund_order_card.dart +++ b/flutter_monisuo/lib/ui/pages/orders/fund_order_card.dart @@ -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().refreshFundOrders(); + context.read().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().refreshFundOrders(); + context.read().loadFundOrders(); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(response.message ?? '取消失败')), diff --git a/flutter_monisuo/lib/ui/pages/orders/fund_orders_list.dart b/flutter_monisuo/lib/ui/pages/orders/fund_orders_list.dart index d8c2d87..d8a2770 100644 --- a/flutter_monisuo/lib/ui/pages/orders/fund_orders_list.dart +++ b/flutter_monisuo/lib/ui/pages/orders/fund_orders_list.dart @@ -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, + ), + ], + ), + ], + ), + ); + } +} + diff --git a/flutter_monisuo/lib/ui/pages/orders/orders_page.dart b/flutter_monisuo/lib/ui/pages/orders/orders_page.dart index cda4f3a..0289714 100644 --- a/flutter_monisuo/lib/ui/pages/orders/orders_page.dart +++ b/flutter_monisuo/lib/ui/pages/orders/orders_page.dart @@ -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 with AutomaticKeepAliveClientMixin { int _activeTab = 0; - + @override bool get wantKeepAlive => true; @@ -38,7 +38,7 @@ class _OrdersPageState extends State with AutomaticKeepAliveClientMi return Scaffold( backgroundColor: theme.colorScheme.background, body: Consumer( - 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 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 tabs; + final int selectedIndex; + final ValueChanged 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), + ], + ), + ), + ); + } +} diff --git a/flutter_monisuo/lib/ui/pages/trade/trade_page.dart b/flutter_monisuo/lib/ui/pages/trade/trade_page.dart index 632206f..f3fbf80 100644 --- a/flutter_monisuo/lib/ui/pages/trade/trade_page.dart +++ b/flutter_monisuo/lib/ui/pages/trade/trade_page.dart @@ -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 with AutomaticKeepAliveClientMixin { - @override - bool get wantKeepAlive => true; - int _tradeType = 0; // 0=买入, 1=卖出 Coin? _selectedCoin; final _formKey = GlobalKey(); final _priceController = TextEditingController(); final _quantityController = TextEditingController(); + @override + bool get wantKeepAlive => true; + @override void initState() { super.initState(); @@ -46,10 +49,9 @@ class _TradePageState extends State 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( builder: (context, market, asset, _) { return SingleChildScrollView( @@ -77,7 +79,7 @@ class _TradePageState extends State 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 with AutomaticKeepAliveClientMixi } void _showTradeResult() { - final theme = ShadTheme.of(context); final isBuy = _tradeType == 0; showShadDialog( @@ -129,7 +130,11 @@ class _TradePageState extends State 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 with AutomaticKeepAliveClientMixi } } -/// 币种选择器 +/// 币种选择器 - Glass Panel 风格 class _CoinSelector extends StatelessWidget { final Coin? selectedCoin; final List 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 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, ); } }