feat(theme): 重构颜色系统为双主题设计并添加渐变组件

- 引入 "The Kinetic Vault"(深色)和 "The Ethereal Terminal"(浅色)双主题系统
- 重构颜色方案,使用层次化 surface 容器替代边框,遵循无边框设计规则
- 添加渐变预设(CTA、买入/卖出、资产卡片)和渐变按钮组件
- 更新圆角系统,明确按钮、卡片、输入框和标签的圆角规范
- 统一文本样式系统,使用 Space Grotesk(标题)和 Manrope(正文)字体
- 更新现有组件(AssetCard、CoinCard、TradeButton)以遵循新设计规范
- 添加向后兼容的已废弃常量,确保现有代码正常运行
This commit is contained in:
2026-03-23 22:11:26 +08:00
parent e01f3330a2
commit a7bec4cfde
7 changed files with 775 additions and 266 deletions

View File

@@ -1,118 +1,171 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
/// 现代化颜色系统 - 支持明暗主题
/// "The Kinetic Vault" & "The Ethereal Terminal" 双主题颜色系统
///
/// 深色主题: "The Kinetic Vault" - 高端加密编辑风格
/// 浅色主题: "The Ethereal Terminal" - 高端金融科技风格
///
/// 设计原则:
/// - Vercel/Linear 风格的现代简约设计
/// - 深色主题:#0A0A0B 背景,微妙阴影
/// - 浅色主题:纯白背景,清晰边框
/// - 所有对比度 >= 4.5:1 (WCAG AA)
/// - 无边框规则: 禁止 1px solid 边框,使用层次色变化
/// - 层次化设计: 通过 surface-container 层次而非阴影
/// - 渐变 CTA: primary → primary_container (135度)
class AppColorScheme {
AppColorScheme._();
// ============================================
// 品牌色 - 青绿色 (明暗通用)
// 深色主题 - "The Kinetic Vault"
// ============================================
/// 主品牌色 - 深色主题
static const Color primaryDark = Color(0xFF00D4AA);
/// 背景基色 -
static const Color darkBackground = Color(0xFF0b0e14);
/// 主品牌色 - 浅色主题
static const Color primaryLight = Color(0xFF00B894);
/// Surface 层次 (从低到高)
static const Color darkSurfaceLowest = Color(0xFF0d1017);
static const Color darkSurfaceLow = Color(0xFF10131a);
static const Color darkSurface = Color(0xFF151921);
static const Color darkSurfaceHigh = Color(0xFF1a1f2a);
static const Color darkSurfaceHighest = Color(0xFF22262f);
/// Ghost Border - 后备边框
static const Color darkOutlineVariant = Color(0xFF45484f);
/// Primary - Neon Blue (主要交互)
static const Color darkPrimary = Color(0xFF72dcff);
static const Color darkPrimaryContainer = Color(0xFF4ac8f0);
/// Secondary - Electric Purple (次要强调)
static const Color darkSecondary = Color(0xFFdd8bfb);
static const Color darkSecondaryContainer = Color(0xFF2d1f3d);
/// Tertiary - Emerald Green (仅用于成功/盈利/买入)
static const Color darkTertiary = Color(0xFFafffd1);
static const Color darkTertiaryContainer = Color(0xFF1a3d2d);
/// 文本色
static const Color darkOnSurface = Color(0xFFecedf6);
static const Color darkOnSurfaceVariant = Color(0xFFa9abb3);
static const Color darkOnSurfaceMuted = Color(0xFF6b6d75);
// ============================================
// 语义色 - 涨跌 (明暗通用)
// 浅色主题 - "The Ethereal Terminal"
// ============================================
/// 涨/买入 - 绿色
static const Color up = Color(0xFF00C853);
/// 背景基色 - 珍珠白
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
/// Ghost Border
static const Color lightOutlineVariant = Color(0xFFc4c8cc);
/// Primary - Plasma (主要交互)
static const Color lightPrimary = Color(0xFF0050d4);
static const Color lightPrimaryContainer = Color(0xFF7b9cff);
/// Secondary - Pulse indicator
static const Color lightSecondary = Color(0xFF8319da);
static const Color lightSecondaryContainer = Color(0xFFe8d4fa);
/// Tertiary - Success (买入/盈利)
static const Color lightTertiary = Color(0xFF00a878);
static const Color lightTertiaryContainer = Color(0xFFd4f5e9);
/// 文本色
static const Color lightOnSurface = Color(0xFF2c2f31);
static const Color lightOnSurfaceVariant = Color(0xFF5a5d60);
static const Color lightOnSurfaceMuted = Color(0xFF8a8d90);
// ============================================
// 语义色 - 明暗通用
// ============================================
/// 涨/买入/成功
static const Color up = darkTertiary;
static const Color success = darkTertiary;
/// 跌/卖出/错误
static const Color down = Color(0xFFFF5252);
/// 成功
static const Color success = Color(0xFF00C853);
static const Color error = down;
/// 警告
static const Color warning = Color(0xFFFF9800);
/// 错误
static const Color error = Color(0xFFFF5252);
/// 信息
static const Color info = Color(0xFF2196F3);
// ============================================
// 深色主题颜色 (Vercel/Linear 风格)
// 渐变预设
// ============================================
static const Color _darkBackground = Color(0xFF0A0A0B);
static const Color _darkCardBackground = Color(0xFF111113);
static const Color _darkSecondary = Color(0xFF1C1C1F);
static const Color _darkMuted = Color(0xFF27272A);
static const Color _darkBorder = Color(0xFF27272A);
static const Color _darkTextPrimary = Color(0xFFFFFFFF);
static const Color _darkTextSecondary = Color(0xFFA1A1AA);
static const Color _darkTextHint = Color(0xFF71717A);
/// 深色 CTA 渐变 (primary → primary_container, 135度)
static const LinearGradient darkCtaGradient = LinearGradient(
colors: [darkPrimary, darkPrimaryContainer],
begin: Alignment(-0.7, -0.7),
end: Alignment(0.7, 0.7),
);
// ============================================
// 浅色主题颜色
// ============================================
/// 浅色 CTA 渐变 (Plasma)
static const LinearGradient lightCtaGradient = LinearGradient(
colors: [lightPrimary, lightPrimaryContainer],
begin: Alignment(-0.7, -0.7),
end: Alignment(0.7, 0.7),
);
static const Color _lightBackground = Color(0xFFFFFFFF);
static const Color _lightCardBackground = Color(0xFFFAFAFA);
static const Color _lightSecondary = Color(0xFFF4F4F5);
static const Color _lightMuted = Color(0xFFE4E4E7);
static const Color _lightBorder = Color(0xFFE4E4E7);
static const Color _lightTextPrimary = Color(0xFF0A0A0B);
static const Color _lightTextSecondary = Color(0xFF52525B);
static const Color _lightTextHint = Color(0xFF71717A);
/// 兼容别名
static const LinearGradient ctaGradient = darkCtaGradient;
/// 买入按钮渐变
static const LinearGradient buyGradient = LinearGradient(
colors: [darkTertiary, Color(0xFF7de8b8)],
begin: Alignment(-0.7, -0.7),
end: Alignment(0.7, 0.7),
);
/// 卖出按钮渐变
static const LinearGradient sellGradient = LinearGradient(
colors: [down, Color(0xFFe84545)],
begin: Alignment(-0.7, -0.7),
end: Alignment(0.7, 0.7),
);
/// 资产卡片渐变
static const LinearGradient assetCardGradient = LinearGradient(
colors: [darkPrimary, darkSecondary],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
// ============================================
// Shadcn ColorScheme - 深色主题
// ============================================
static ShadColorScheme get darkShad => ShadColorScheme(
// 背景与前景
background: _darkBackground,
foreground: _darkTextPrimary,
// 卡片
card: _darkCardBackground,
cardForeground: _darkTextPrimary,
// 弹出层
popover: _darkCardBackground,
popoverForeground: _darkTextPrimary,
// 主色
primary: primaryDark,
primaryForeground: _darkTextPrimary,
// 次要色
secondary: _darkSecondary,
secondaryForeground: _darkTextPrimary,
// 静音色
muted: _darkMuted,
mutedForeground: _darkTextSecondary,
// 强调色
accent: primaryDark.withValues(alpha: 0.15),
accentForeground: primaryDark,
// 危险色
background: darkBackground,
foreground: darkOnSurface,
card: darkSurface,
cardForeground: darkOnSurface,
popover: darkSurfaceHigh,
popoverForeground: darkOnSurface,
primary: darkPrimary,
primaryForeground: darkBackground,
secondary: darkSecondary,
secondaryForeground: darkOnSurface,
muted: darkSurfaceHigh,
mutedForeground: darkOnSurfaceVariant,
accent: darkPrimary.withValues(alpha: 0.15),
accentForeground: darkPrimary,
destructive: error,
destructiveForeground: _darkTextPrimary,
// 边框与输入
border: _darkBorder,
input: _darkBorder,
ring: primaryDark,
// 选择色
selection: primaryDark.withValues(alpha: 0.3),
destructiveForeground: darkOnSurface,
border: darkOutlineVariant.withValues(alpha: 0.15),
input: darkOutlineVariant.withValues(alpha: 0.15),
ring: darkPrimary,
selection: darkPrimary.withValues(alpha: 0.3),
);
// ============================================
@@ -120,45 +173,26 @@ class AppColorScheme {
// ============================================
static ShadColorScheme get lightShad => ShadColorScheme(
// 背景与前景
background: _lightBackground,
foreground: _lightTextPrimary,
// 卡片
card: _lightCardBackground,
cardForeground: _lightTextPrimary,
// 弹出层
popover: _lightBackground,
popoverForeground: _lightTextPrimary,
// 主色
primary: primaryLight,
primaryForeground: _lightBackground,
// 次要色
secondary: _lightSecondary,
secondaryForeground: _lightTextPrimary,
// 静音色
muted: _lightMuted,
mutedForeground: _lightTextSecondary,
// 强调色
accent: primaryLight.withValues(alpha: 0.15),
accentForeground: primaryLight,
// 危险色
background: lightBackground,
foreground: lightOnSurface,
card: lightSurfaceLowest,
cardForeground: lightOnSurface,
popover: lightSurfaceLowest,
popoverForeground: lightOnSurface,
primary: lightPrimary,
primaryForeground: Color(0xFFFFFFFF),
secondary: lightSecondary,
secondaryForeground: lightOnSurface,
muted: lightSurfaceHigh,
mutedForeground: lightOnSurfaceVariant,
accent: lightPrimary.withValues(alpha: 0.1),
accentForeground: lightPrimary,
destructive: error,
destructiveForeground: _lightBackground,
// 边框与输入
border: _lightBorder,
input: _lightBorder,
ring: primaryLight,
// 选择色
selection: primaryLight.withValues(alpha: 0.3),
destructiveForeground: Color(0xFFFFFFFF),
border: lightOutlineVariant.withValues(alpha: 0.5),
input: lightOutlineVariant.withValues(alpha: 0.3),
ring: lightPrimary,
selection: lightPrimary.withValues(alpha: 0.2),
);
// ============================================
@@ -166,15 +200,22 @@ class AppColorScheme {
// ============================================
static ColorScheme get darkMaterial => ColorScheme.dark(
primary: primaryDark,
onPrimary: _darkTextPrimary,
secondary: _darkSecondary,
onSecondary: _darkTextPrimary,
primary: darkPrimary,
onPrimary: darkBackground,
secondary: darkSecondary,
onSecondary: darkOnSurface,
tertiary: darkTertiary,
onTertiary: darkBackground,
error: error,
onError: _darkTextPrimary,
surface: _darkCardBackground,
onSurface: _darkTextPrimary,
surfaceContainerHighest: _darkBackground,
onError: darkOnSurface,
surface: darkSurface,
onSurface: darkOnSurface,
surfaceContainerLowest: darkSurfaceLowest,
surfaceContainerLow: darkSurfaceLow,
surfaceContainer: darkSurface,
surfaceContainerHigh: darkSurfaceHigh,
surfaceContainerHighest: darkSurfaceHighest,
outlineVariant: darkOutlineVariant,
);
// ============================================
@@ -182,16 +223,81 @@ class AppColorScheme {
// ============================================
static ColorScheme get lightMaterial => ColorScheme.light(
primary: primaryLight,
onPrimary: _lightBackground,
secondary: _lightSecondary,
onSecondary: _lightTextPrimary,
primary: lightPrimary,
onPrimary: Color(0xFFFFFFFF),
secondary: lightSecondary,
onSecondary: Color(0xFFFFFFFF),
tertiary: lightTertiary,
onTertiary: Color(0xFFFFFFFF),
error: error,
onError: _lightBackground,
surface: _lightCardBackground,
onSurface: _lightTextPrimary,
surfaceContainerHighest: _lightBackground,
onError: Color(0xFFFFFFFF),
surface: lightSurface,
onSurface: lightOnSurface,
surfaceContainerLowest: lightSurfaceLowest,
surfaceContainerLow: lightSurfaceLow,
surfaceContainer: lightSurface,
surfaceContainerHigh: lightSurfaceHigh,
surfaceContainerHighest: lightSurfaceHighest,
outlineVariant: lightOutlineVariant,
);
// ============================================
// 兼容性常量 (已废弃,保留向后兼容)
// ============================================
@Deprecated('Use darkPrimary instead')
static const Color primaryDark = darkPrimary;
@Deprecated('Use lightPrimary instead')
static const Color primaryLight = lightPrimary;
@Deprecated('Use darkBackground instead')
static const Color _darkBackground = darkBackground;
@Deprecated('Use darkSurface instead')
static const Color _darkCardBackground = darkSurface;
@Deprecated('Use darkSurfaceHigh instead')
static const Color _darkSecondary = darkSurfaceHigh;
@Deprecated('Use darkSurfaceHigh instead')
static const Color _darkMuted = darkSurfaceHigh;
@Deprecated('Use darkOutlineVariant instead')
static const Color _darkBorder = darkOutlineVariant;
@Deprecated('Use darkOnSurface instead')
static const Color _darkTextPrimary = darkOnSurface;
@Deprecated('Use darkOnSurfaceVariant instead')
static const Color _darkTextSecondary = darkOnSurfaceVariant;
@Deprecated('Use darkOnSurfaceMuted instead')
static const Color _darkTextHint = darkOnSurfaceMuted;
@Deprecated('Use lightBackground instead')
static const Color _lightBackground = lightBackground;
@Deprecated('Use lightSurfaceLowest instead')
static const Color _lightCardBackground = lightSurfaceLowest;
@Deprecated('Use lightSurfaceHigh instead')
static const Color _lightSecondary = lightSurfaceHigh;
@Deprecated('Use lightSurfaceHigh instead')
static const Color _lightMuted = lightSurfaceHigh;
@Deprecated('Use lightOutlineVariant instead')
static const Color _lightBorder = lightOutlineVariant;
@Deprecated('Use lightOnSurface instead')
static const Color _lightTextPrimary = lightOnSurface;
@Deprecated('Use lightOnSurfaceVariant instead')
static const Color _lightTextSecondary = lightOnSurfaceVariant;
@Deprecated('Use lightOnSurfaceMuted instead')
static const Color _lightTextHint = lightOnSurfaceMuted;
}
/// 创建 Shadcn 深色主题

View File

@@ -67,7 +67,13 @@ class AppSpacing {
static SizedBox vertical(double spacing) => SizedBox(height: spacing);
}
/// 圆角系统 - 基于 modernization-v2.md 规范
/// 圆角系统 - "The Kinetic Vault" & "The Ethereal Terminal" 规范
///
/// 设计规则:
/// - 按钮: xxl (24px / 1.5rem)
/// - 卡片: xl (16px)
/// - 输入框: md (8px)
/// - 标签: sm (4px)
class AppRadius {
AppRadius._();
@@ -78,19 +84,19 @@ class AppRadius {
/// 小圆角 - 4px (标签、小组件)
static const double sm = 4.0;
/// 中圆角 - 8px (按钮、输入框)
/// 中圆角 - 8px (输入框)
static const double md = 8.0;
/// 大圆角 - 12px (卡片)
/// 大圆角 - 12px (卡片 - 旧版)
static const double lg = 12.0;
/// 特大圆角 - 16px (大卡片)
static const double xl = 16.0;
/// 超大圆角 - 24px (模态框、底部抽屉)
/// 超大圆角 - 24px (按钮、模态框、底部抽屉)
static const double xxl = 24.0;
/// 圆形 - 9999px
/// 圆形 - 9999px (Pill buttons)
static const double full = 9999.0;
// ============================================

View File

@@ -3,294 +3,429 @@ import 'package:google_fonts/google_fonts.dart';
import 'app_color_scheme.dart';
import 'app_spacing.dart';
/// 应用主题配置 - 基于 modernization-v2.md 规范
/// "The Kinetic Vault" & "The Ethereal Terminal" 主题配置
class AppTheme {
AppTheme._();
// ============================================
// 深色主题
// 深色主题 - "The Kinetic Vault"
// ============================================
static ThemeData get darkTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
scaffoldBackgroundColor: AppColorScheme.darkShad.background,
primaryColor: AppColorScheme.primaryDark,
scaffoldBackgroundColor: AppColorScheme.darkBackground,
primaryColor: AppColorScheme.darkPrimary,
colorScheme: AppColorScheme.darkMaterial,
// AppBar - 无边框规则
appBarTheme: AppBarTheme(
backgroundColor: AppColorScheme.darkShad.background,
foregroundColor: AppColorScheme.darkShad.foreground,
backgroundColor: AppColorScheme.darkBackground,
foregroundColor: AppColorScheme.darkOnSurface,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
titleTextStyle: GoogleFonts.inter(
titleTextStyle: GoogleFonts.spaceGrotesk(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColorScheme.darkShad.foreground,
color: AppColorScheme.darkOnSurface,
),
),
// 卡片 - 无边框,使用 surface 层次
cardTheme: CardThemeData(
color: AppColorScheme.darkSurface,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.xl),
),
),
// 输入框 - Ghost Border 风格
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColorScheme.darkShad.card,
fillColor: AppColorScheme.darkSurfaceLow,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.md),
borderSide: BorderSide(color: AppColorScheme.darkShad.border),
borderSide: BorderSide(
color: AppColorScheme.darkOutlineVariant.withValues(alpha: 0.15),
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.md),
borderSide: BorderSide(color: AppColorScheme.darkShad.border),
borderSide: BorderSide(
color: AppColorScheme.darkOutlineVariant.withValues(alpha: 0.15),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.md),
borderSide: BorderSide(color: AppColorScheme.primaryDark, width: 2),
borderSide: const BorderSide(color: AppColorScheme.darkPrimary, width: 2),
),
hintStyle: TextStyle(color: AppColorScheme.darkShad.mutedForeground),
hintStyle: TextStyle(color: AppColorScheme.darkOnSurfaceMuted),
contentPadding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: 14,
),
),
// 按钮 - 渐变 CTA大圆角
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColorScheme.primaryDark,
foregroundColor: Colors.white,
backgroundColor: AppColorScheme.darkPrimary,
foregroundColor: AppColorScheme.darkBackground,
minimumSize: const Size(double.infinity, 48),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.md),
borderRadius: BorderRadius.circular(AppRadius.xxl),
),
textStyle: GoogleFonts.inter(
elevation: 0,
textStyle: GoogleFonts.manrope(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: AppColorScheme.primaryDark,
foregroundColor: AppColorScheme.darkPrimary,
),
),
// 分割线 - 使用层次而非边框
dividerTheme: DividerThemeData(
color: AppColorScheme.darkShad.border,
color: AppColorScheme.darkOutlineVariant.withValues(alpha: 0.1),
thickness: 1,
),
cardTheme: CardThemeData(
color: AppColorScheme.darkShad.card,
// 底部导航
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: AppColorScheme.darkSurface,
selectedItemColor: AppColorScheme.darkPrimary,
unselectedItemColor: AppColorScheme.darkOnSurfaceVariant,
type: BottomNavigationBarType.fixed,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.lg),
),
),
);
}
// ============================================
// 浅色主题
// 浅色主题 - "The Ethereal Terminal"
// ============================================
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
scaffoldBackgroundColor: AppColorScheme.lightShad.background,
primaryColor: AppColorScheme.primaryLight,
scaffoldBackgroundColor: AppColorScheme.lightBackground,
primaryColor: AppColorScheme.lightPrimary,
colorScheme: AppColorScheme.lightMaterial,
// AppBar - 珍珠白
appBarTheme: AppBarTheme(
backgroundColor: AppColorScheme.lightShad.background,
foregroundColor: AppColorScheme.lightShad.foreground,
backgroundColor: AppColorScheme.lightBackground,
foregroundColor: AppColorScheme.lightOnSurface,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
titleTextStyle: GoogleFonts.inter(
titleTextStyle: GoogleFonts.spaceGrotesk(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColorScheme.lightShad.foreground,
color: AppColorScheme.lightOnSurface,
),
),
// 卡片 - Elevated (pop) on pearl background
cardTheme: CardThemeData(
color: AppColorScheme.lightSurfaceLowest,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.xl),
),
),
// 输入框 - 底部线条风格
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColorScheme.lightShad.card,
border: OutlineInputBorder(
fillColor: AppColorScheme.lightSurfaceLow,
border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.md),
borderSide: BorderSide(color: AppColorScheme.lightShad.border),
borderSide: BorderSide(
color: AppColorScheme.lightOutlineVariant.withValues(alpha: 0.5),
width: 2,
),
),
enabledBorder: OutlineInputBorder(
enabledBorder: UnderlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.md),
borderSide: BorderSide(color: AppColorScheme.lightShad.border),
borderSide: BorderSide(
color: AppColorScheme.lightOutlineVariant.withValues(alpha: 0.5),
width: 2,
),
),
focusedBorder: OutlineInputBorder(
focusedBorder: UnderlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.md),
borderSide: BorderSide(color: AppColorScheme.primaryLight, width: 2),
borderSide: const BorderSide(color: AppColorScheme.lightPrimary, width: 2),
),
hintStyle: TextStyle(color: AppColorScheme.lightShad.mutedForeground),
hintStyle: TextStyle(color: AppColorScheme.lightOnSurfaceMuted),
contentPadding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: 14,
),
),
// 按钮 - Plasma 渐变full 圆角
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColorScheme.primaryLight,
foregroundColor: Colors.white,
backgroundColor: AppColorScheme.lightPrimary,
foregroundColor: const Color(0xFFFFFFFF),
minimumSize: const Size(double.infinity, 48),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.md),
borderRadius: BorderRadius.circular(AppRadius.xxl),
),
textStyle: GoogleFonts.inter(
elevation: 0,
textStyle: GoogleFonts.manrope(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: AppColorScheme.primaryLight,
foregroundColor: AppColorScheme.lightPrimary,
),
),
// 分割线 - 不使用,用留白代替
dividerTheme: DividerThemeData(
color: AppColorScheme.lightShad.border,
color: AppColorScheme.lightOutlineVariant.withValues(alpha: 0.2),
thickness: 1,
),
cardTheme: CardThemeData(
color: AppColorScheme.lightShad.card,
// 底部导航
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: AppColorScheme.lightSurfaceHighest,
selectedItemColor: AppColorScheme.lightPrimary,
unselectedItemColor: AppColorScheme.lightOnSurfaceVariant,
type: BottomNavigationBarType.fixed,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.lg),
),
),
);
}
}
/// 文本样式 - 使用 Google Fonts
/// "The Kinetic Vault" & "The Ethereal Terminal" 文字样式系统
///
/// Display/Headlines: Space Grotesk (编辑风格)
/// Body/Labels: Manrope (实用风格)
class AppTextStyles {
AppTextStyles._();
// ============================================
// 标题样式 - Inter 字体
// Display - 编辑风格 (Space Grotesk)
// ============================================
static TextStyle displayLarge(BuildContext context) => GoogleFonts.inter(
fontSize: 32,
/// D1 - 大标题 (48px) - Hero moments
static TextStyle displayLarge(BuildContext context) => GoogleFonts.spaceGrotesk(
fontSize: 48,
fontWeight: FontWeight.w700,
color: Theme.of(context).colorScheme.onSurface,
height: 1.1,
letterSpacing: -0.02,
);
static TextStyle displayMedium(BuildContext context) => GoogleFonts.inter(
/// D2 - 中标题 (36px)
static TextStyle displayMedium(BuildContext context) => GoogleFonts.spaceGrotesk(
fontSize: 36,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
height: 1.2,
letterSpacing: -0.01,
);
/// D3 - 小标题 (28px)
static TextStyle displaySmall(BuildContext context) => GoogleFonts.spaceGrotesk(
fontSize: 28,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
height: 1.25,
);
// ============================================
// Headline - 编辑风格 (Space Grotesk)
// ============================================
static TextStyle headlineLarge(BuildContext context) => GoogleFonts.spaceGrotesk(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
height: 1.3,
);
static TextStyle displaySmall(BuildContext context) => GoogleFonts.inter(
static TextStyle headlineMedium(BuildContext context) => GoogleFonts.spaceGrotesk(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
height: 1.35,
);
static TextStyle headlineSmall(BuildContext context) => GoogleFonts.spaceGrotesk(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,
height: 1.4,
);
// ============================================
// 正文样式 - Inter 字体
// Body - 实用风格 (Manrope)
// ============================================
static TextStyle bodyLarge(BuildContext context) => GoogleFonts.inter(
static TextStyle bodyLarge(BuildContext context) => GoogleFonts.manrope(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface,
height: 1.5,
);
static TextStyle bodyMedium(BuildContext context) => GoogleFonts.inter(
static TextStyle bodyMedium(BuildContext context) => GoogleFonts.manrope(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface,
height: 1.5,
);
static TextStyle bodySmall(BuildContext context) => GoogleFonts.inter(
static TextStyle bodySmall(BuildContext context) => GoogleFonts.manrope(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface,
color: Theme.of(context).colorScheme.onSurfaceVariant,
height: 1.45,
);
// ============================================
// 数字样式 - JetBrains Mono 等宽字体
// Label - 实用风格 (Manrope)
// ============================================
static TextStyle numberLarge(BuildContext context) => GoogleFonts.jetBrainsMono(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
);
static TextStyle numberMedium(BuildContext context) => GoogleFonts.jetBrainsMono(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,
);
static TextStyle numberSmall(BuildContext context) => GoogleFonts.jetBrainsMono(
static TextStyle labelLarge(BuildContext context) => GoogleFonts.manrope(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,
height: 1.4,
);
static TextStyle labelMedium(BuildContext context) => GoogleFonts.manrope(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant,
height: 1.4,
);
static TextStyle labelSmall(BuildContext context) => GoogleFonts.manrope(
fontSize: 11,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant,
height: 1.4,
);
// ============================================
// 便捷静态样式(兼容旧代码)
// 数字/金额 - Space Grotesk (等宽特性)
// ============================================
static TextStyle numberLarge(BuildContext context) => GoogleFonts.spaceGrotesk(
fontSize: 24,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
height: 1.2,
fontFeatures: const [FontFeature.tabularFigures()],
);
static TextStyle numberMedium(BuildContext context) => GoogleFonts.spaceGrotesk(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,
height: 1.25,
fontFeatures: const [FontFeature.tabularFigures()],
);
static TextStyle numberSmall(BuildContext context) => GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,
height: 1.3,
fontFeatures: const [FontFeature.tabularFigures()],
);
// ============================================
// 兼容旧代码的静态样式 (已废弃)
// ============================================
@Deprecated('Use displaySmall instead')
static const TextStyle heading1 = TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Color(0xFFFFFFFF),
);
@Deprecated('Use headlineLarge instead')
static const TextStyle heading2 = TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Color(0xFFFFFFFF),
);
@Deprecated('Use headlineMedium instead')
static const TextStyle heading3 = TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFFFFFFFF),
);
@Deprecated('Use headlineSmall instead')
static const TextStyle heading4 = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFFFFFFFF),
);
@Deprecated('Use bodyLarge instead')
static const TextStyle body1 = TextStyle(
fontSize: 16,
color: Color(0xFFFFFFFF),
);
@Deprecated('Use bodyMedium instead')
static const TextStyle body2 = TextStyle(
fontSize: 14,
color: Color(0xFFFFFFFF),
);
@Deprecated('Use labelSmall instead')
static const TextStyle caption = TextStyle(
fontSize: 12,
color: Color(0xFFA1A1AA),
);
@Deprecated('Use labelSmall instead')
static const TextStyle hint = TextStyle(
fontSize: 14,
color: Color(0xFF71717A),
);
@Deprecated('Use numberMedium instead')
static const TextStyle price = TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFFFFFFFF),
);
@Deprecated('Use numberSmall with color instead')
static const TextStyle change = TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
);
@Deprecated('Use numberSmall instead')
static const TextStyle number = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,

View File

@@ -1,8 +1,16 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:flutter_animate/flutter_animate.dart';
import '../../core/theme/app_color_scheme.dart';
import '../../core/theme/app_spacing.dart';
/// 资产卡片组件 - 用于显示资产总览
///
/// 设计规则 ("The Kinetic Vault"):
/// - 渐变背景: Neon Blue → Electric Purple
/// - 圆角: xl (16px)
/// - 无边框,使用渐变层次
/// - 微妙阴影: 10% black, blur 10px
class AssetCard extends StatelessWidget {
final String title;
final String balance;
@@ -13,16 +21,16 @@ class AssetCard extends StatelessWidget {
final Gradient? gradient;
final VoidCallback? onTap;
// 默认渐变色 - 使用品牌青绿色
/// 默认渐变色 - Neon Blue → Electric Purple
static const defaultGradient = LinearGradient(
colors: [AppColorScheme.primaryDark, Color(0xFF00B894)],
colors: [AppColorScheme.darkPrimary, AppColorScheme.darkSecondary],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
// 深色渐变
static const darkGradient = LinearGradient(
colors: [Color(0xFF1E3A5F), Color(0xFF0D253F)],
/// 翡翠渐变 - 用于盈利展示
static const emeraldGradient = LinearGradient(
colors: [AppColorScheme.darkTertiary, Color(0xFF7de8b8)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
@@ -48,10 +56,10 @@ class AssetCard extends StatelessWidget {
onTap: onTap,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
padding: const EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
gradient: cardGradient,
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(AppRadius.xl),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
@@ -79,18 +87,19 @@ class AssetCard extends StatelessWidget {
),
],
),
const SizedBox(height: 8),
// 余额
const SizedBox(height: AppSpacing.sm),
// 余额 - 大标题
Text(
balance,
style: theme.textTheme.h1.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: 32,
),
),
// 盈亏信息
if (profit != null) ...[
const SizedBox(height: 12),
const SizedBox(height: AppSpacing.md),
Row(
children: [
Icon(
@@ -108,7 +117,7 @@ class AssetCard extends StatelessWidget {
],
// 子项
if (items != null && items!.isNotEmpty) ...[
const SizedBox(height: 20),
const SizedBox(height: AppSpacing.lg),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: items!.map((item) => _buildItem(item)).toList(),
@@ -154,6 +163,11 @@ class AssetItem {
}
/// 简洁资产卡片 - 用于列表中显示
///
/// 设计规则:
/// - 使用 surface 层次而非边框
/// - 圆角: xl (16px)
/// - 涨跌标签: 15% 透明度背景
class AssetCardCompact extends StatelessWidget {
final String title;
final String balance;
@@ -176,7 +190,7 @@ class AssetCardCompact extends StatelessWidget {
final isValueUp = isUp ?? true;
return ShadCard(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(AppSpacing.md),
child: InkWell(
onTap: onTap,
child: Row(
@@ -204,7 +218,7 @@ class AssetCardCompact extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: getChangeBackgroundColor(isValueUp),
borderRadius: BorderRadius.circular(6),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
change!,

View File

@@ -1,8 +1,14 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../core/theme/app_color_scheme.dart';
import '../../core/theme/app_spacing.dart';
/// 币种卡片组件 - 用于显示币种信息
///
/// 设计规则:
/// - 使用 surface 层次而非边框
/// - 圆角: xl (16px) 卡片, sm (4px) 涨跌标签
/// - 涨跌标签: 15% 透明度背景
class CoinCard extends StatelessWidget {
final String code;
final String name;
@@ -28,12 +34,12 @@ class CoinCard extends StatelessWidget {
final theme = ShadTheme.of(context);
return ShadCard(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(AppSpacing.md),
child: InkWell(
onTap: onTap,
child: Row(
children: [
// 图标
// 图标 - 圆形
Container(
width: 44,
height: 44,
@@ -52,7 +58,7 @@ class CoinCard extends StatelessWidget {
),
),
),
const SizedBox(width: 12),
const SizedBox(width: AppSpacing.sm),
// 名称
Expanded(
child: Column(
@@ -71,12 +77,12 @@ class CoinCard extends StatelessWidget {
],
),
),
// 涨跌幅
// 涨跌幅 - Dynamic Chip
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: getChangeBackgroundColor(isUp),
borderRadius: BorderRadius.circular(6),
borderRadius: BorderRadius.circular(AppRadius.sm),
),
child: Text(
change,

View File

@@ -0,0 +1,223 @@
import 'package:flutter/material.dart';
import '../../core/theme/app_color_scheme.dart';
import '../../core/theme/app_spacing.dart';
/// 渐变按钮组件 - 支持 CTA 渐变效果
///
/// 设计规则:
/// - 渐变方向: 135度 (primary → primary_container)
/// - 圆角: xxl (24px / 1.5rem)
/// - 无边框
class GradientButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool isLoading;
final bool isFullWidth;
final LinearGradient? gradient;
final IconData? icon;
final double height;
const GradientButton({
super.key,
required this.text,
this.onPressed,
this.isLoading = false,
this.isFullWidth = true,
this.gradient,
this.icon,
this.height = 48,
});
/// CTA 按钮 - 使用主题渐变
factory GradientButton.cta({
Key? key,
required String text,
VoidCallback? onPressed,
bool isLoading = false,
bool isFullWidth = true,
IconData? icon,
bool isDark = true,
}) {
return GradientButton(
key: key,
text: text,
onPressed: onPressed,
isLoading: isLoading,
isFullWidth: isFullWidth,
icon: icon,
gradient: isDark ? AppColorScheme.darkCtaGradient : AppColorScheme.lightCtaGradient,
);
}
/// 买入按钮 - 翡翠绿渐变
factory GradientButton.buy({
Key? key,
required String text,
VoidCallback? onPressed,
bool isLoading = false,
bool isFullWidth = true,
IconData? icon,
}) {
return GradientButton(
key: key,
text: text,
onPressed: onPressed,
isLoading: isLoading,
isFullWidth: isFullWidth,
icon: icon ?? Icons.arrow_downward_rounded,
gradient: AppColorScheme.buyGradient,
);
}
/// 卖出按钮 - 红色渐变
factory GradientButton.sell({
Key? key,
required String text,
VoidCallback? onPressed,
bool isLoading = false,
bool isFullWidth = true,
IconData? icon,
}) {
return GradientButton(
key: key,
text: text,
onPressed: onPressed,
isLoading: isLoading,
isFullWidth: isFullWidth,
icon: icon ?? Icons.arrow_upward_rounded,
gradient: AppColorScheme.sellGradient,
);
}
@override
Widget build(BuildContext context) {
final buttonGradient = gradient ?? AppColorScheme.darkCtaGradient;
return Container(
width: isFullWidth ? double.infinity : null,
height: height,
decoration: BoxDecoration(
gradient: buttonGradient,
borderRadius: BorderRadius.circular(AppRadius.xxl),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: isLoading ? null : onPressed,
borderRadius: BorderRadius.circular(AppRadius.xxl),
child: Center(
child: Row(
mainAxisSize: isFullWidth ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (isLoading)
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
else ...[
if (icon != null) ...[
Icon(icon, size: 18, color: Colors.white),
SizedBox(width: AppSpacing.sm),
],
Text(
text,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
],
),
),
),
),
);
}
}
/// Ghost 按钮 - 次要操作
///
/// 设计规则:
/// - Ghost Border: 15% opacity outline-variant
/// - 圆角: xxl (24px)
/// - 主色文字
class GhostButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool isLoading;
final bool isFullWidth;
final IconData? icon;
const GhostButton({
super.key,
required this.text,
this.onPressed,
this.isLoading = false,
this.isFullWidth = true,
this.icon,
});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final textColor = isDark ? AppColorScheme.darkPrimary : AppColorScheme.lightPrimary;
final borderColor = isDark
? AppColorScheme.darkOutlineVariant.withValues(alpha: 0.15)
: AppColorScheme.lightOutlineVariant.withValues(alpha: 0.3);
return Container(
width: isFullWidth ? double.infinity : null,
height: 48,
decoration: BoxDecoration(
color: Colors.transparent,
border: Border.all(color: borderColor, width: 1),
borderRadius: BorderRadius.circular(AppRadius.xxl),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: isLoading ? null : onPressed,
borderRadius: BorderRadius.circular(AppRadius.xxl),
child: Center(
child: Row(
mainAxisSize: isFullWidth ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (isLoading)
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: textColor,
),
)
else ...[
if (icon != null) ...[
Icon(icon, size: 18, color: textColor),
SizedBox(width: AppSpacing.sm),
],
Text(
text,
style: TextStyle(
color: textColor,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
],
),
),
),
),
);
}
}

View File

@@ -1,8 +1,14 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import '../../core/theme/app_color_scheme.dart';
import '../../core/theme/app_spacing.dart';
/// 交易按钮组件 - 买入/卖出按钮
///
/// 设计规则:
/// - 渐变按钮: 135度渐变
/// - 圆角: xxl (24px / 1.5rem)
/// - 买入: 翡翠绿渐变
/// - 卖出: 红色渐变
class TradeButton extends StatelessWidget {
final bool isBuy;
final String? coinCode;
@@ -39,44 +45,57 @@ class TradeButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final color = isBuy ? AppColorScheme.up : AppColorScheme.down;
final text = isBuy ? '买入${coinCode != null ? ' $coinCode' : ''}' : '卖出${coinCode != null ? ' $coinCode' : ''}';
final icon = isBuy ? LucideIcons.arrowDownToLine : LucideIcons.arrowUpFromLine;
final gradient = isBuy ? AppColorScheme.buyGradient : AppColorScheme.sellGradient;
final text = isBuy
? '买入${coinCode != null ? ' $coinCode' : ''}'
: '卖出${coinCode != null ? ' $coinCode' : ''}';
final button = ShadButton(
backgroundColor: color,
onPressed: isLoading ? null : onPressed,
child: Row(
mainAxisSize: fullWidth ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (isLoading)
const SizedBox.square(
dimension: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
else
Icon(icon, size: 18, color: Colors.white),
const SizedBox(width: 8),
Text(
text,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
return Container(
width: fullWidth ? double.infinity : null,
height: 48,
decoration: BoxDecoration(
gradient: gradient,
borderRadius: BorderRadius.circular(AppRadius.xxl),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: isLoading ? null : onPressed,
borderRadius: BorderRadius.circular(AppRadius.xxl),
child: Center(
child: Row(
mainAxisSize: fullWidth ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (isLoading)
const SizedBox.square(
dimension: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
else
Icon(
isBuy ? Icons.arrow_downward_rounded : Icons.arrow_upward_rounded,
size: 18,
color: Colors.white,
),
const SizedBox(width: 8),
Text(
text,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
),
);
if (fullWidth) {
return SizedBox(width: double.infinity, child: button);
}
return button;
}
}
@@ -108,7 +127,7 @@ class TradeButtonGroup extends StatelessWidget {
isLoading: isBuyLoading,
),
),
const SizedBox(width: 12),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: TradeButton.sell(
coinCode: coinCode,