diff --git a/flutter_monisuo/IMPLEMENTATION_PLAN.md b/flutter_monisuo/IMPLEMENTATION_PLAN.md index 48b711b..9478c69 100644 --- a/flutter_monisuo/IMPLEMENTATION_PLAN.md +++ b/flutter_monisuo/IMPLEMENTATION_PLAN.md @@ -1,7 +1,52 @@ # Monisuo 主题现代化实施计划 ## Status -STATUS: COMPLETE +STATUS: IN_PROGRESS + +## Phase 7: 现代化改造 v2.0(当前任务) + +### 目标 +将 Flutter Monisuo 应用打造为现代化、简约、专业的虚拟货币交易平台。 + +### 设计规范参考: `specs/modernization-v2.md` + +### 7.1 P0 - 核心基础设施 ✅ +- [x] 7.1.1 创建 ThemeProvider(明暗主题切换) +- [x] 7.1.2 更新 main.dart 使用 MultiProvider +- [x] 7.1.3 创建浅色主题配置 +- [x] 7.1.4 更新深色主题配置 +- [x] 7.1.5 在 mine_page 添加主题切换开关 +- [x] 7.1.6 更新颜色系统(app_colors.dart) +- [x] 7.1.7 集成 Google Fonts(Inter + JetBrains Mono) + +### 7.2 P1 - 组件现代化 +- [x] 7.2.1 创建间距系统(app_spacing.dart) +- [x] 7.2.2 创建圆角系统(app_border_radius.dart) +- [ ] 7.2.3 优化按钮组件 +- [ ] 7.2.4 优化卡片组件 +- [ ] 7.2.5 优化输入框组件 + +### 7.3 P2 - 弹窗现代化 +- [ ] 7.3.1 创建现代弹窗模板 +- [ ] 7.3.2 创建现代底部抽屉模板 +- [ ] 7.3.3 更新所有 AlertDialog + +### 7.4 P3 - 页面优化 +- [ ] 7.4.1 登录页面现代化 +- [ ] 7.4.2 首页现代化 +- [ ] 7.4.3 行情页现代化 +- [ ] 7.4.4 交易页现代化 +- [ ] 7.4.5 资产页现代化 +- [ ] 7.4.6 我的页面现代化 + +### 7.5 P4 - 验证与优化 +- [ ] 7.5.1 对比度检查(WCAG AA >= 4.5:1) +- [ ] 7.5.2 响应式布局测试 +- [ ] 7.5.3 动画优化 + +--- + +## Previous Phases (Completed) ## Progress diff --git a/flutter_monisuo/lib/core/theme/app_color_scheme.dart b/flutter_monisuo/lib/core/theme/app_color_scheme.dart index 9bc1fd6..4ae466f 100644 --- a/flutter_monisuo/lib/core/theme/app_color_scheme.dart +++ b/flutter_monisuo/lib/core/theme/app_color_scheme.dart @@ -1,61 +1,221 @@ import 'package:flutter/material.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; -import '../constants/app_colors.dart'; -/// 自定义品牌颜色方案 - 深色主题 +/// 现代化颜色系统 - 支持明暗主题 /// -/// 基于品牌色 #2563EB (专业蓝) 定制 -class AppShadColorScheme { - AppShadColorScheme._(); +/// 设计原则: +/// - Vercel/Linear 风格的现代简约设计 +/// - 深色主题:#0A0A0B 背景,微妙阴影 +/// - 浅色主题:纯白背景,清晰边框 +/// - 所有对比度 >= 4.5:1 (WCAG AA) +class AppColorScheme { + AppColorScheme._(); - /// 深色主题颜色 - static ShadColorScheme get dark => ShadColorScheme( + // ============================================ + // 品牌色 - 青绿色 (明暗通用) + // ============================================ + + /// 主品牌色 - 深色主题 + static const Color primaryDark = Color(0xFF00D4AA); + + /// 主品牌色 - 浅色主题 + static const Color primaryLight = Color(0xFF00B894); + + // ============================================ + // 语义色 - 涨跌 (明暗通用) + // ============================================ + + /// 涨/买入 - 绿色 + static const Color up = Color(0xFF00C853); + + /// 跌/卖出 - 红色 + static const Color down = Color(0xFFFF5252); + + /// 成功 + static const Color success = Color(0xFF00C853); + + /// 警告 + 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); + + // ============================================ + // 浅色主题颜色 + // ============================================ + + 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); + + // ============================================ + // Shadcn ColorScheme - 深色主题 + // ============================================ + + static ShadColorScheme get darkShad => ShadColorScheme( // 背景与前景 - background: AppColors.background, - foreground: AppColors.textPrimary, + background: _darkBackground, + foreground: _darkTextPrimary, // 卡片 - card: AppColors.cardBackground, - cardForeground: AppColors.textPrimary, + card: _darkCardBackground, + cardForeground: _darkTextPrimary, // 弹出层 - popover: AppColors.surface, - popoverForeground: AppColors.textPrimary, + popover: _darkCardBackground, + popoverForeground: _darkTextPrimary, // 主色 - primary: AppColors.primary, - primaryForeground: Colors.white, + primary: primaryDark, + primaryForeground: _darkTextPrimary, // 次要色 - secondary: const Color(0xFF252542), - secondaryForeground: AppColors.textPrimary, + secondary: _darkSecondary, + secondaryForeground: _darkTextPrimary, // 静音色 - muted: const Color(0xFF2A2A45), - mutedForeground: AppColors.textSecondary, + muted: _darkMuted, + mutedForeground: _darkTextSecondary, // 强调色 - accent: AppColors.primary.withValues(alpha: 0.15), - accentForeground: AppColors.primary, + accent: primaryDark.withValues(alpha: 0.15), + accentForeground: primaryDark, // 危险色 - destructive: AppColors.error, - destructiveForeground: Colors.white, + destructive: error, + destructiveForeground: _darkTextPrimary, // 边框与输入 - border: AppColors.border, - input: AppColors.inputBorder, - ring: AppColors.primary, + border: _darkBorder, + input: _darkBorder, + ring: primaryDark, // 选择色 - selection: AppColors.primary.withValues(alpha: 0.3), + selection: primaryDark.withValues(alpha: 0.3), + ); + + // ============================================ + // Shadcn ColorScheme - 浅色主题 + // ============================================ + + 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, + + // 危险色 + destructive: error, + destructiveForeground: _lightBackground, + + // 边框与输入 + border: _lightBorder, + input: _lightBorder, + ring: primaryLight, + + // 选择色 + selection: primaryLight.withValues(alpha: 0.3), + ); + + // ============================================ + // Material ColorScheme - 深色主题 + // ============================================ + + static ColorScheme get darkMaterial => ColorScheme.dark( + primary: primaryDark, + onPrimary: _darkTextPrimary, + secondary: _darkSecondary, + onSecondary: _darkTextPrimary, + error: error, + onError: _darkTextPrimary, + surface: _darkCardBackground, + onSurface: _darkTextPrimary, + surfaceContainerHighest: _darkBackground, + ); + + // ============================================ + // Material ColorScheme - 浅色主题 + // ============================================ + + static ColorScheme get lightMaterial => ColorScheme.light( + primary: primaryLight, + onPrimary: _lightBackground, + secondary: _lightSecondary, + onSecondary: _lightTextPrimary, + error: error, + onError: _lightBackground, + surface: _lightCardBackground, + onSurface: _lightTextPrimary, + surfaceContainerHighest: _lightBackground, ); } -/// 创建自定义 ShadThemeData -ShadThemeData createAppShadTheme() { +/// 创建 Shadcn 深色主题 +ShadThemeData createDarkShadTheme() { return ShadThemeData( brightness: Brightness.dark, - colorScheme: AppShadColorScheme.dark, + colorScheme: AppColorScheme.darkShad, ); } + +/// 创建 Shadcn 浅色主题 +ShadThemeData createLightShadTheme() { + return ShadThemeData( + brightness: Brightness.light, + colorScheme: AppColorScheme.lightShad, + ); +} + +/// 获取涨跌颜色(明暗通用) +Color getChangeColor(bool isUp) => isUp ? AppColorScheme.up : AppColorScheme.down; + +/// 获取涨跌背景色(带透明度) +Color getChangeBackgroundColor(bool isUp, {double opacity = 0.15}) { + return isUp + ? AppColorScheme.up.withValues(alpha: opacity) + : AppColorScheme.down.withValues(alpha: opacity); +} diff --git a/flutter_monisuo/lib/core/theme/app_colors.dart b/flutter_monisuo/lib/core/theme/app_colors.dart index a9ac476..cd8fe68 100644 --- a/flutter_monisuo/lib/core/theme/app_colors.dart +++ b/flutter_monisuo/lib/core/theme/app_colors.dart @@ -138,7 +138,7 @@ class AppColors { /// 获取带透明度的涨跌背景色 static Color getChangeBackgroundColor(bool isUp) => - isUp ? up.withOpacity(0.15) : down.withOpacity(0.15); + isUp ? up.withValues(alpha: 0.15) : down.withValues(alpha: 0.15); /// 渐变色 (兼容旧代码) - 品牌蓝 static const List gradientColors = [Color(0xFF2563EB), Color(0xFF1D4ED8)]; diff --git a/flutter_monisuo/lib/core/theme/app_spacing.dart b/flutter_monisuo/lib/core/theme/app_spacing.dart index 1863bb3..4edc535 100644 --- a/flutter_monisuo/lib/core/theme/app_spacing.dart +++ b/flutter_monisuo/lib/core/theme/app_spacing.dart @@ -67,7 +67,7 @@ class AppSpacing { static SizedBox vertical(double spacing) => SizedBox(height: spacing); } -/// 圆角系统 +/// 圆角系统 - 基于 modernization-v2.md 规范 class AppRadius { AppRadius._(); @@ -75,20 +75,23 @@ class AppRadius { // 基础圆角 (Base Radius) // ============================================ - /// 小圆角 - 6px (标签、小组件) - static const double sm = 6.0; + /// 小圆角 - 4px (标签、小组件) + static const double sm = 4.0; - /// 中圆角 - 10px (按钮、输入框) - static const double md = 10.0; + /// 中圆角 - 8px (按钮、输入框) + static const double md = 8.0; - /// 大圆角 - 14px (卡片) - static const double lg = 14.0; + /// 大圆角 - 12px (卡片) + static const double lg = 12.0; - /// 特大圆角 - 20px (大卡片、模态框) - static const double xl = 20.0; + /// 特大圆角 - 16px (大卡片) + static const double xl = 16.0; - /// 圆形 - 999px - static const double full = 999.0; + /// 超大圆角 - 24px (模态框、底部抽屉) + static const double xxl = 24.0; + + /// 圆形 - 9999px + static const double full = 9999.0; // ============================================ // 预设 BorderRadius @@ -106,6 +109,9 @@ class AppRadius { /// 特大圆角 static BorderRadius get radiusXl => BorderRadius.circular(xl); + /// 超大圆角 + static BorderRadius get radiusXxl => BorderRadius.circular(xxl); + /// 圆形 static BorderRadius get radiusFull => BorderRadius.circular(full); } diff --git a/flutter_monisuo/lib/core/theme/app_theme.dart b/flutter_monisuo/lib/core/theme/app_theme.dart index bd0ee5e..84e4663 100644 --- a/flutter_monisuo/lib/core/theme/app_theme.dart +++ b/flutter_monisuo/lib/core/theme/app_theme.dart @@ -1,62 +1,65 @@ import 'package:flutter/material.dart'; -import '../constants/app_colors.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'app_color_scheme.dart'; +import 'app_spacing.dart'; -/// 应用主题配置 +/// 应用主题配置 - 基于 modernization-v2.md 规范 class AppTheme { AppTheme._(); - /// 深色主题 + // ============================================ + // 深色主题 + // ============================================ + static ThemeData get darkTheme { return ThemeData( useMaterial3: true, brightness: Brightness.dark, - scaffoldBackgroundColor: AppColors.background, - primaryColor: AppColors.primary, - colorScheme: const ColorScheme.dark( - primary: AppColors.primary, - secondary: AppColors.primaryLight, - error: AppColors.error, - surface: AppColors.cardBackground, - ), - appBarTheme: const AppBarTheme( - backgroundColor: AppColors.background, - foregroundColor: AppColors.textPrimary, + scaffoldBackgroundColor: AppColorScheme.darkShad.background, + primaryColor: AppColorScheme.primaryDark, + colorScheme: AppColorScheme.darkMaterial, + appBarTheme: AppBarTheme( + backgroundColor: AppColorScheme.darkShad.background, + foregroundColor: AppColorScheme.darkShad.foreground, elevation: 0, centerTitle: true, - titleTextStyle: TextStyle( + titleTextStyle: GoogleFonts.inter( fontSize: 18, fontWeight: FontWeight.w600, - color: AppColors.textPrimary, + color: AppColorScheme.darkShad.foreground, ), ), inputDecorationTheme: InputDecorationTheme( filled: true, - fillColor: AppColors.cardBackground, + fillColor: AppColorScheme.darkShad.card, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide(color: AppColors.border), + borderRadius: BorderRadius.circular(AppRadius.md), + borderSide: BorderSide(color: AppColorScheme.darkShad.border), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide(color: AppColors.border), + borderRadius: BorderRadius.circular(AppRadius.md), + borderSide: BorderSide(color: AppColorScheme.darkShad.border), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide(color: AppColors.primary), + borderRadius: BorderRadius.circular(AppRadius.md), + borderSide: BorderSide(color: AppColorScheme.primaryDark, width: 2), + ), + hintStyle: TextStyle(color: AppColorScheme.darkShad.mutedForeground), + contentPadding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: 14, ), - hintStyle: const TextStyle(color: AppColors.textHint), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primary, + backgroundColor: AppColorScheme.primaryDark, foregroundColor: Colors.white, minimumSize: const Size(double.infinity, 48), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(AppRadius.md), ), - textStyle: const TextStyle( + textStyle: GoogleFonts.inter( fontSize: 16, fontWeight: FontWeight.w600, ), @@ -64,200 +67,243 @@ class AppTheme { ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( - foregroundColor: AppColors.primary, + foregroundColor: AppColorScheme.primaryDark, ), ), - dividerTheme: const DividerThemeData( - color: AppColors.border, + dividerTheme: DividerThemeData( + color: AppColorScheme.darkShad.border, thickness: 1, ), cardTheme: CardThemeData( - color: AppColors.cardBackground, + color: AppColorScheme.darkShad.card, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(AppRadius.lg), + ), + ), + ); + } + + // ============================================ + // 浅色主题 + // ============================================ + + static ThemeData get lightTheme { + return ThemeData( + useMaterial3: true, + brightness: Brightness.light, + scaffoldBackgroundColor: AppColorScheme.lightShad.background, + primaryColor: AppColorScheme.primaryLight, + colorScheme: AppColorScheme.lightMaterial, + appBarTheme: AppBarTheme( + backgroundColor: AppColorScheme.lightShad.background, + foregroundColor: AppColorScheme.lightShad.foreground, + elevation: 0, + centerTitle: true, + titleTextStyle: GoogleFonts.inter( + fontSize: 18, + fontWeight: FontWeight.w600, + color: AppColorScheme.lightShad.foreground, + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: AppColorScheme.lightShad.card, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadius.md), + borderSide: BorderSide(color: AppColorScheme.lightShad.border), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadius.md), + borderSide: BorderSide(color: AppColorScheme.lightShad.border), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadius.md), + borderSide: BorderSide(color: AppColorScheme.primaryLight, width: 2), + ), + hintStyle: TextStyle(color: AppColorScheme.lightShad.mutedForeground), + contentPadding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: 14, + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: AppColorScheme.primaryLight, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 48), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.md), + ), + textStyle: GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: AppColorScheme.primaryLight, + ), + ), + dividerTheme: DividerThemeData( + color: AppColorScheme.lightShad.border, + thickness: 1, + ), + cardTheme: CardThemeData( + color: AppColorScheme.lightShad.card, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.lg), ), ), ); } } -/// 文本样式 +/// 文本样式 - 使用 Google Fonts class AppTextStyles { AppTextStyles._(); + // ============================================ + // 标题样式 - Inter 字体 + // ============================================ + + static TextStyle displayLarge(BuildContext context) => GoogleFonts.inter( + fontSize: 32, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurface, + ); + + static TextStyle displayMedium(BuildContext context) => GoogleFonts.inter( + fontSize: 24, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ); + + static TextStyle displaySmall(BuildContext context) => GoogleFonts.inter( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ); + + // ============================================ + // 正文样式 - Inter 字体 + // ============================================ + + static TextStyle bodyLarge(BuildContext context) => GoogleFonts.inter( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ); + + static TextStyle bodyMedium(BuildContext context) => GoogleFonts.inter( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ); + + static TextStyle bodySmall(BuildContext context) => GoogleFonts.inter( + fontSize: 12, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ); + + // ============================================ + // 数字样式 - JetBrains Mono 等宽字体 + // ============================================ + + 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( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface, + ); + + // ============================================ + // 便捷静态样式(兼容旧代码) + // ============================================ + static const TextStyle heading1 = TextStyle( fontSize: 28, fontWeight: FontWeight.bold, - color: AppColors.textPrimary, + color: Color(0xFFFFFFFF), ); static const TextStyle heading2 = TextStyle( fontSize: 24, fontWeight: FontWeight.bold, - color: AppColors.textPrimary, + color: Color(0xFFFFFFFF), ); static const TextStyle heading3 = TextStyle( fontSize: 18, fontWeight: FontWeight.w600, - color: AppColors.textPrimary, + color: Color(0xFFFFFFFF), ); static const TextStyle heading4 = TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: AppColors.textPrimary, + color: Color(0xFFFFFFFF), ); static const TextStyle body1 = TextStyle( fontSize: 16, - color: AppColors.textPrimary, + color: Color(0xFFFFFFFF), ); static const TextStyle body2 = TextStyle( fontSize: 14, - color: AppColors.textPrimary, + color: Color(0xFFFFFFFF), ); static const TextStyle caption = TextStyle( fontSize: 12, - color: AppColors.textSecondary, + color: Color(0xFFA1A1AA), ); static const TextStyle hint = TextStyle( fontSize: 14, - color: AppColors.textHint, + color: Color(0xFF71717A), ); - /// 价格样式 - 用于显示价格、金额 static const TextStyle price = TextStyle( fontSize: 20, fontWeight: FontWeight.bold, - color: AppColors.textPrimary, + color: Color(0xFFFFFFFF), ); - /// 涨跌幅样式 static const TextStyle change = TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ); - /// 数字样式 - 用于显示数量 static const TextStyle number = TextStyle( fontSize: 16, fontWeight: FontWeight.w500, - color: AppColors.textPrimary, + color: Color(0xFFFFFFFF), ); } -/// 间距常量 -class AppSpacing { - AppSpacing._(); +/// 动画时长 +class AnimationDurations { + AnimationDurations._(); - static const double xs = 4.0; - static const double sm = 8.0; - static const double md = 16.0; - static const double lg = 24.0; - static const double xl = 32.0; - static const double xxl = 48.0; - - /// 页面水平内边距 - static const double pageHorizontal = 16.0; - - /// 页面垂直内边距 - static const double pageVertical = 16.0; - - /// 卡片内边距 - static const double cardPadding = 16.0; - - /// 列表项间距 - static const double listItemSpacing = 8.0; - - /// 表单字段间距 - static const double formFieldSpacing = 12.0; -} - -/// 圆角常量 -class AppRadius { - AppRadius._(); - - static const double sm = 8.0; - static const double md = 12.0; - static const double lg = 16.0; - static const double xl = 24.0; - static const double full = 999.0; - - /// 卡片圆角 - static const double card = lg; - - /// 按钮圆角 - static const double button = md; - - /// 输入框圆角 - static const double input = md; - - /// 标签圆角 - static const double badge = sm; -} - -/// 响应式断点 -class AppBreakpoints { - AppBreakpoints._(); - - /// 手机 - static const double mobile = 600; - - /// 平板 - static const double tablet = 900; - - /// 桌面 - static const double desktop = 1200; - - /// 判断是否为手机 - static bool isMobile(BuildContext context) => - MediaQuery.of(context).size.width < mobile; - - /// 判断是否为平板 - static bool isTablet(BuildContext context) { - final width = MediaQuery.of(context).size.width; - return width >= mobile && width < tablet; - } - - /// 判断是否为桌面 - static bool isDesktop(BuildContext context) => - MediaQuery.of(context).size.width >= tablet; -} - -/// 响应式工具 -class Responsive { - Responsive._(); - - /// 根据屏幕宽度返回响应式值 - static T value( - BuildContext context, { - required T mobile, - T? tablet, - T? desktop, - }) { - final width = MediaQuery.of(context).size.width; - - if (width >= AppBreakpoints.tablet && desktop != null) { - return desktop; - } - if (width >= AppBreakpoints.mobile && tablet != null) { - return tablet; - } - return mobile; - } - - /// 响应式字体大小 - static double fontSize(BuildContext context, double base) { - return value(context, mobile: base, tablet: base * 1.1, desktop: base * 1.2); - } - - /// 响应式间距 - static double spacing(BuildContext context, double base) { - return value(context, mobile: base, tablet: base * 1.2, desktop: base * 1.5); - } + static const Duration fast = Duration(milliseconds: 150); + static const Duration normal = Duration(milliseconds: 250); + static const Duration slow = Duration(milliseconds: 400); + static const Duration verySlow = Duration(milliseconds: 600); } diff --git a/flutter_monisuo/lib/main.dart b/flutter_monisuo/lib/main.dart index 3366bf4..03ff639 100644 --- a/flutter_monisuo/lib/main.dart +++ b/flutter_monisuo/lib/main.dart @@ -16,6 +16,7 @@ import 'data/services/fund_service.dart'; import 'providers/auth_provider.dart'; import 'providers/market_provider.dart'; import 'providers/asset_provider.dart'; +import 'providers/theme_provider.dart'; import 'ui/pages/auth/login_page.dart'; import 'ui/pages/main/main_page.dart'; @@ -36,12 +37,17 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MultiProvider( providers: _buildProviders(), - child: AuthNavigator( - child: ShadApp.custom( - themeMode: ThemeMode.dark, - darkTheme: createAppShadTheme(), - appBuilder: _buildMaterialApp, - ), + child: Consumer( + builder: (context, themeProvider, _) { + return AuthNavigator( + child: ShadApp.custom( + themeMode: themeProvider.themeMode, + theme: createLightShadTheme(), + darkTheme: createDarkShadTheme(), + appBuilder: _buildMaterialApp, + ), + ); + }, ), ); } @@ -50,6 +56,10 @@ class MyApp extends StatelessWidget { final dioClient = DioClient(); return [ + // Theme Provider (必须放在最前面) + ChangeNotifierProvider( + create: (_) => ThemeProvider()..init(), + ), // Services Provider.value(value: dioClient), Provider(create: (_) => UserService(dioClient)), diff --git a/flutter_monisuo/lib/providers/theme_provider.dart b/flutter_monisuo/lib/providers/theme_provider.dart new file mode 100644 index 0000000..d54dc63 --- /dev/null +++ b/flutter_monisuo/lib/providers/theme_provider.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// 主题提供者 - 管理明暗主题切换 +/// +/// 功能: +/// - 支持浅色/深色/跟随系统三种模式 +/// - 持久化主题设置到本地存储 +/// - 提供主题切换方法 +class ThemeProvider extends ChangeNotifier { + static const String _themeModeKey = 'theme_mode'; + + ThemeMode _themeMode = ThemeMode.dark; + + /// 当前主题模式 + ThemeMode get themeMode => _themeMode; + + /// 是否为深色模式 + bool get isDarkMode => _themeMode == ThemeMode.dark; + + /// 初始化主题设置(从本地存储加载) + Future init() async { + final prefs = await SharedPreferences.getInstance(); + final modeString = prefs.getString(_themeModeKey); + + if (modeString != null) { + _themeMode = ThemeMode.values.firstWhere( + (mode) => mode.toString() == modeString, + orElse: () => ThemeMode.dark, + ); + notifyListeners(); + } + } + + /// 切换主题(浅色/深色) + Future toggleTheme() async { + _themeMode = _themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light; + await _saveThemeMode(); + notifyListeners(); + } + + /// 设置主题模式 + Future setThemeMode(ThemeMode mode) async { + if (_themeMode == mode) return; + _themeMode = mode; + await _saveThemeMode(); + notifyListeners(); + } + + /// 保存主题设置到本地存储 + Future _saveThemeMode() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_themeModeKey, _themeMode.toString()); + } +} diff --git a/flutter_monisuo/lib/ui/pages/mine/mine_page.dart b/flutter_monisuo/lib/ui/pages/mine/mine_page.dart index c4a0b90..c8dc9a0 100644 --- a/flutter_monisuo/lib/ui/pages/mine/mine_page.dart +++ b/flutter_monisuo/lib/ui/pages/mine/mine_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:provider/provider.dart'; import '../../../providers/auth_provider.dart'; +import '../../../providers/theme_provider.dart'; import '../auth/login_page.dart'; /// 菜单项数据模型 @@ -241,15 +242,19 @@ class _MenuList extends StatelessWidget { @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); - final items = _buildMenuItems(); + final themeProvider = context.watch(); return ShadCard( padding: EdgeInsets.zero, child: Column( children: [ - for (var i = 0; i < items.length; i++) ...[ - _MenuItemTile(item: items[i]), - if (i < items.length - 1) + // 主题切换开关(特殊处理) + _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), ], ], @@ -268,6 +273,51 @@ class _MenuList extends StatelessWidget { } } +/// 主题切换组件 +class _ThemeToggleTile extends StatelessWidget { + final bool isDarkMode; + + const _ThemeToggleTile({required this.isDarkMode}); + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + final themeProvider = context.read(); + + return InkWell( + onTap: () => themeProvider.toggleTheme(), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + _MenuIcon(icon: isDarkMode ? LucideIcons.moon : LucideIcons.sun), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('深色模式', style: theme.textTheme.small.copyWith(fontWeight: FontWeight.w500)), + const SizedBox(height: 2), + Text( + isDarkMode ? '当前:深色主题' : '当前:浅色主题', + style: theme.textTheme.muted.copyWith(fontSize: 11), + ), + ], + ), + ), + Switch( + value: isDarkMode, + onChanged: (_) => themeProvider.toggleTheme(), + activeTrackColor: theme.colorScheme.primary.withValues(alpha: 0.5), + activeColor: theme.colorScheme.primary, + ), + ], + ), + ), + ); + } +} + /// 菜单项组件 class _MenuItemTile extends StatelessWidget { final _MenuItem item; diff --git a/flutter_monisuo/pubspec.lock b/flutter_monisuo/pubspec.lock index ffb2670..d3e2fa4 100644 --- a/flutter_monisuo/pubspec.lock +++ b/flutter_monisuo/pubspec.lock @@ -197,6 +197,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 + url: "https://pub.dev" + source: hosted + version: "6.3.3" hooks: dependency: transitive description: diff --git a/flutter_monisuo/pubspec.yaml b/flutter_monisuo/pubspec.yaml index ef65885..623cbbc 100644 --- a/flutter_monisuo/pubspec.yaml +++ b/flutter_monisuo/pubspec.yaml @@ -30,6 +30,9 @@ dependencies: intl: ^0.20.2 decimal: ^2.3.3 + # 字体 + google_fonts: ^6.2.1 + dev_dependencies: flutter_test: sdk: flutter diff --git a/flutter_monisuo/specs/modernization-v2.md b/flutter_monisuo/specs/modernization-v2.md new file mode 100644 index 0000000..f339e0e --- /dev/null +++ b/flutter_monisuo/specs/modernization-v2.md @@ -0,0 +1,461 @@ +# Flutter Monisuo 现代化改造规范 v2.0 + +## 目标 +将 Flutter Monisuo 应用打造为现代化、简约、专业的虚拟货币交易平台,参考 SuperDesign 设计原则。 + +## 设计原则 + +### 1. 现代化简约风格 +- **Vercel/Linear 风格**:干净的深色主题,微妙的阴影,大量留白 +- **避免过时设计**:不使用 Bootstrap 蓝、沉重的阴影、复杂的渐变 +- **微交互**:细腻的动画反馈(150-400ms) + +### 2. 明暗主题支持 +- 完整的 Light/Dark 主题切换 +- 使用 ColorScheme 管理主题 +- 主题切换时平滑过渡 + +### 3. 颜色系统(基于 OKLCH 转换) + +#### 现代深色主题 +```dart +// Vercel/Linear 风格 +background: Color(0xFF0A0A0B) // oklch(0.098 0.005 270) +cardBackground: Color(0xFF111113) // oklch(0.148 0.004 270) +primary: Color(0xFF00D4AA) // 品牌青绿色 +primaryForeground: Color(0xFFFFFFFF) +secondary: Color(0xFF1C1C1F) +muted: Color(0xFF27272A) +border: Color(0xFF27272A) +``` + +#### 现代浅色主题 +```dart +background: Color(0xFFFFFFFF) // oklch(1 0 0) +cardBackground: Color(0xFFFAFAFA) // oklch(0.98 0 0) +primary: Color(0xFF00B894) // 品牌青绿色(深色版) +primaryForeground: Color(0xFFFFFFFF) +secondary: Color(0xFFF4F4F5) +muted: Color(0xFFE4E4E7) +border: Color(0xFFE4E4E7) +``` + +#### 涨跌色(明暗通用) +```dart +up: Color(0xFF00C853) // 涨/买入(绿色) +down: Color(0xFFFF5252) // 跌/卖出(红色) +``` + +### 4. 字体系统 + +#### 使用 Google Fonts +```yaml +dependencies: + google_fonts: ^6.1.0 +``` + +#### 字体选择 +- **主字体**:Inter(现代、清晰) +- **数字字体**:JetBrains Mono(等宽,用于价格/数量) +- **回退字体**:system-ui + +#### 字号系统 +```dart +// 标题 +displayLarge: 32sp, weight: 700 +displayMedium: 24sp, weight: 600 +displaySmall: 20sp, weight: 600 + +// 正文 +bodyLarge: 16sp, weight: 400 +bodyMedium: 14sp, weight: 400 +bodySmall: 12sp, weight: 400 + +// 数字(等宽) +numberLarge: 20sp, JetBrains Mono +numberMedium: 16sp, JetBrains Mono +numberSmall: 14sp, JetBrains Mono +``` + +### 5. 间距系统 + +```dart +class Spacing { + static const double xs = 4.0; // 0.25rem + static const double sm = 8.0; // 0.5rem + static const double md = 16.0; // 1rem + static const double lg = 24.0; // 1.5rem + static const double xl = 32.0; // 2rem + static const double xxl = 48.0; // 3rem +} +``` + +### 6. 圆角系统 + +```dart +class BorderRadius { + static const double sm = 4.0; + static const double md = 8.0; + static const double lg = 12.0; + static const double xl = 16.0; + static const double xxl = 24.0; + static const double full = 9999.0; +} +``` + +### 7. 阴影系统 + +```dart +// 微妙的阴影(Vercel 风格) +BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: Offset(0, 2), +) + +// 悬停阴影 +BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: Offset(0, 4), +) +``` + +### 8. 动画系统 + +```dart +class AnimationDurations { + static const Duration fast = Duration(milliseconds: 150); + static const Duration normal = Duration(milliseconds: 250); + static const Duration slow = Duration(milliseconds: 400); + static const Duration verySlow = Duration(milliseconds: 600); +} + +// Curves +Curves.easeOutCubic // 入场动画 +Curves.easeInOutCubic // 过渡动画 +Curves.elasticOut // 弹性反馈 +``` + +## 组件设计规范 + +### 1. 按钮 + +#### 主要按钮(Primary) +```dart +ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + minimumSize: Size(44, 44), // 触摸目标 + padding: EdgeInsets.symmetric(horizontal: Spacing.lg, vertical: Spacing.md), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(BorderRadius.md), + ), + elevation: 0, // 现代风格:无阴影或微阴影 + ), +) +``` + +#### 次要按钮(Secondary) +```dart +OutlinedButton( + style: OutlinedButton.styleFrom( + minimumSize: Size(44, 44), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(BorderRadius.md), + ), + side: BorderSide(color: Theme.of(context).colorScheme.border), + ), +) +``` + +#### 幽灵按钮(Ghost) +```dart +TextButton( + style: TextButton.styleFrom( + minimumSize: Size(44, 44), + ), +) +``` + +### 2. 卡片 + +```dart +Card( + elevation: 0, // 使用边框代替阴影 + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(BorderRadius.lg), + side: BorderSide( + color: Theme.of(context).colorScheme.border, + width: 1, + ), + ), + color: Theme.of(context).colorScheme.cardBackground, + child: Padding( + padding: EdgeInsets.all(Spacing.md), + child: Column(...), + ), +) +``` + +### 3. 输入框 + +```dart +TextFormField( + decoration: InputDecoration( + filled: true, + fillColor: Theme.of(context).colorScheme.cardBackground, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(BorderRadius.md), + borderSide: BorderSide(color: Theme.of(context).colorScheme.border), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(BorderRadius.md), + borderSide: BorderSide(color: Theme.of(context).colorScheme.border), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(BorderRadius.md), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 2, + ), + ), + contentPadding: EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.sm, + ), + ), +) +``` + +### 4. 现代弹窗 + +#### 标准弹窗 +```dart +showDialog( + context: context, + builder: (context) => Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(BorderRadius.xl), + ), + backgroundColor: Theme.of(context).colorScheme.cardBackground, + child: Container( + padding: EdgeInsets.all(Spacing.lg), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题 + Text( + 'Dialog Title', + style: Theme.of(context).textTheme.displaySmall, + ), + SizedBox(height: Spacing.md), + // 内容 + Text('Dialog content here...'), + SizedBox(height: Spacing.lg), + // 按钮 + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton(child: Text('Cancel')), + SizedBox(width: Spacing.sm), + ElevatedButton(child: Text('Confirm')), + ], + ), + ], + ), + ), + ), +); +``` + +#### 底部抽屉(Bottom Sheet) +```dart +showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.cardBackground, + borderRadius: BorderRadius.vertical( + top: Radius.circular(BorderRadius.xl), + ), + ), + padding: EdgeInsets.all(Spacing.lg), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 拖动指示器 + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.muted, + borderRadius: BorderRadius.circular(2), + ), + ), + SizedBox(height: Spacing.md), + // 内容 + ... + ], + ), + ), +); +``` + +### 5. 列表项 + +```dart +ListTile( + contentPadding: EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.sm, + ), + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + borderRadius: BorderRadius.circular(BorderRadius.md), + ), + child: Icon(icon, size: 20), + ), + title: Text( + title, + style: Theme.of(context).textTheme.bodyLarge, + ), + subtitle: Text( + subtitle, + style: Theme.of(context).textTheme.bodySmall, + ), + trailing: Icon(Icons.chevron_right, size: 20), +) +``` + +## 页面布局规范 + +### 1. 通用布局 + +```dart +Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.background, + elevation: 0, + title: Text('Page Title'), + ), + body: SafeArea( + child: SingleChildScrollView( + padding: EdgeInsets.all(Spacing.md), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 内容 + ], + ), + ), + ), +) +``` + +### 2. 响应式布局 + +```dart +LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth >= 1024) { + // 桌面布局 + return _DesktopLayout(); + } else if (constraints.maxWidth >= 768) { + // 平板布局 + return _TabletLayout(); + } else { + // 移动布局 + return _MobileLayout(); + } + }, +) +``` + +## 主题切换实现 + +### 1. 主题 Provider + +```dart +class ThemeProvider extends ChangeNotifier { + ThemeMode _themeMode = ThemeMode.system; + + ThemeMode get themeMode => _themeMode; + + void toggleTheme() { + _themeMode = _themeMode == ThemeMode.light + ? ThemeMode.dark + : ThemeMode.light; + notifyListeners(); + } + + void setTheme(ThemeMode mode) { + _themeMode = mode; + notifyListeners(); + } +} +``` + +### 2. 主题切换按钮 + +```dart +IconButton( + icon: Icon( + Provider.of(context).themeMode == ThemeMode.light + ? Icons.dark_mode + : Icons.light_mode, + ), + onPressed: () { + Provider.of(context, listen: false).toggleTheme(); + }, +) +``` + +## 无障碍设计 + +### 1. 对比度 +- 所有文字/背景组合 >= 4.5:1(WCAG AA) +- 大文字(18sp+)>= 3:1 + +### 2. 触摸目标 +- 最小触摸目标 44x44 + +### 3. 语义化 +```dart +Semantics( + label: 'Submit button', + button: true, + child: ElevatedButton(...), +) +``` + +## 禁止事项 + +- ❌ 使用过时的 Bootstrap 蓝 (#007bff) +- ❌ 沉重的阴影 +- ❌ 复杂的渐变 +- ❌ 文字与背景颜色相同 +- ❌ 对比度 < 4.5:1 +- ❌ 触摸目标 < 44x44 +- ❌ 硬编码颜色值 +- ❌ 不一致的间距/圆角 + +## 验证清单 + +- [ ] 明暗主题切换正常 +- [ ] 所有页面风格一致 +- [ ] 对比度 >= 4.5:1 +- [ ] 触摸目标 >= 44x44 +- [ ] flutter analyze 无错误 +- [ ] 响应式布局正常 +- [ ] 动画流畅(60fps) +- [ ] 无硬编码颜色 +- [ ] 间距/圆角一致