Files
monisuo/flutter_monisuo/specs/modernization-v2.md
sion c4cf23a4a1 feat(ui): 添加明暗主题切换支持
- 创建 ThemeProvider 管理主题状态
- 配置浅色和深色主题(Vercel/Linear 风格)
- 集成 Google Fonts(Inter + JetBrains Mono)
- 在我的页面添加主题切换开关
- 更新颜色系统符合 modernization-v2.md 规范
- 优化间距和圆角系统

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 14:12:00 +08:00

10 KiB
Raw Blame History

Flutter Monisuo 现代化改造规范 v2.0

目标

将 Flutter Monisuo 应用打造为现代化、简约、专业的虚拟货币交易平台,参考 SuperDesign 设计原则。

设计原则

1. 现代化简约风格

  • Vercel/Linear 风格:干净的深色主题,微妙的阴影,大量留白
  • 避免过时设计:不使用 Bootstrap 蓝、沉重的阴影、复杂的渐变
  • 微交互细腻的动画反馈150-400ms

2. 明暗主题支持

  • 完整的 Light/Dark 主题切换
  • 使用 ColorScheme 管理主题
  • 主题切换时平滑过渡

3. 颜色系统(基于 OKLCH 转换)

现代深色主题

// 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)

现代浅色主题

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)

涨跌色(明暗通用)

up:    Color(0xFF00C853)  // 涨/买入(绿色)
down:  Color(0xFFFF5252)  // 跌/卖出(红色)

4. 字体系统

使用 Google Fonts

dependencies:
  google_fonts: ^6.1.0

字体选择

  • 主字体Inter现代、清晰
  • 数字字体JetBrains Mono等宽用于价格/数量)
  • 回退字体system-ui

字号系统

// 标题
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. 间距系统

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. 圆角系统

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. 阴影系统

// 微妙的阴影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. 动画系统

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

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

OutlinedButton(
  style: OutlinedButton.styleFrom(
    minimumSize: Size(44, 44),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(BorderRadius.md),
    ),
    side: BorderSide(color: Theme.of(context).colorScheme.border),
  ),
)

幽灵按钮Ghost

TextButton(
  style: TextButton.styleFrom(
    minimumSize: Size(44, 44),
  ),
)

2. 卡片

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. 输入框

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. 现代弹窗

标准弹窗

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

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. 列表项

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. 通用布局

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. 响应式布局

LayoutBuilder(
  builder: (context, constraints) {
    if (constraints.maxWidth >= 1024) {
      // 桌面布局
      return _DesktopLayout();
    } else if (constraints.maxWidth >= 768) {
      // 平板布局
      return _TabletLayout();
    } else {
      // 移动布局
      return _MobileLayout();
    }
  },
)

主题切换实现

1. 主题 Provider

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. 主题切换按钮

IconButton(
  icon: Icon(
    Provider.of<ThemeProvider>(context).themeMode == ThemeMode.light
        ? Icons.dark_mode
        : Icons.light_mode,
  ),
  onPressed: () {
    Provider.of<ThemeProvider>(context, listen: false).toggleTheme();
  },
)

无障碍设计

1. 对比度

  • 所有文字/背景组合 >= 4.5:1WCAG AA
  • 大文字18sp+>= 3:1

2. 触摸目标

  • 最小触摸目标 44x44

3. 语义化

Semantics(
  label: 'Submit button',
  button: true,
  child: ElevatedButton(...),
)

禁止事项

  • 使用过时的 Bootstrap 蓝 (#007bff)
  • 沉重的阴影
  • 复杂的渐变
  • 文字与背景颜色相同
  • 对比度 < 4.5:1
  • 触摸目标 < 44x44
  • 硬编码颜色值
  • 不一致的间距/圆角

验证清单

  • 明暗主题切换正常
  • 所有页面风格一致
  • 对比度 >= 4.5:1
  • 触摸目标 >= 44x44
  • flutter analyze 无错误
  • 响应式布局正常
  • 动画流畅60fps
  • 无硬编码颜色
  • 间距/圆角一致