颜色,基础组件重新封装,降低理解难度,分层更明显。入口更抽象

This commit is contained in:
Cody
2026-03-10 09:38:07 +08:00
parent ea3192bd65
commit ed93d556d6
13 changed files with 590 additions and 537 deletions

View File

@@ -3092,7 +3092,7 @@ flowchart TD
│ └── socket_manager.dart # WebSocket 生命周期(连接/断开/重连编排) │ └── socket_manager.dart # WebSocket 生命周期(连接/断开/重连编排)
└── ui/ # Core UI设计系统 + 可复用组件) └── ui/ # Core UI设计系统 + 可复用组件)
├── base/ # 设计 Token ├── base/ # 设计基础(颜色 / 字体 / 间距 / 圆角 / 阴影)
│ ├── assets.dart # 静态资源路径常量AppAssetslogo / 占位图) │ ├── assets.dart # 静态资源路径常量AppAssetslogo / 占位图)
│ ├── icons.dart # 图标常量AppIcons导航 / 操作 / 聊天 / 用户 / 状态) │ ├── icons.dart # 图标常量AppIcons导航 / 操作 / 聊天 / 用户 / 状态)
│ ├── app_theme.dart # ThemeData 组装Light / Dark │ ├── app_theme.dart # ThemeData 组装Light / Dark
@@ -3461,7 +3461,7 @@ flowchart TD
DesignSystem --> Colors[Colors 颜色] DesignSystem --> Colors[Colors 颜色]
DesignSystem --> Typography[Typography 字体] DesignSystem --> Typography[Typography 字体]
DesignSystem --> Tokens[Design Tokens 基础定义] DesignSystem --> Tokens[间距 / 圆角 / 阴影]
DesignSystem --> Theme[Theme 主题] DesignSystem --> Theme[Theme 主题]
Foundation --> Atoms[Atoms 原子组件] Foundation --> Atoms[Atoms 原子组件]
@@ -3522,39 +3522,44 @@ flowchart TD
<p><strong>核心原则</strong>:与 Figma 设计稿完全对应,确保设计与实现一致。</p> <p><strong>核心原则</strong>:与 Figma 设计稿完全对应,确保设计与实现一致。</p>
<h5>1.1 Colors颜色</h5> <h5>1.1 Colors颜色— 5 层架构</h5>
<pre><code class="language-dart">/// 颜色定义 - 与 Figma 设计稿对应 <pre><code class="language-dart">/// Layer 1: 颜色常量,不带语义
class AppColors { abstract class ColorBases {
// Primary Colors - 主色 static const primary400 = Color(0xFF5BA3F5);
static const primary = Color(0xFF667EEA); static const primary500 = Color(0xFF2F80ED);
static const primaryDark = Color(0xFF5568D3); static const primary700 = Color(0xFF1A6BD4);
static const primaryLight = Color(0xFF8B9FFF); static const neutral0 = Color(0xFFFFFFFF);
static const neutral50 = Color(0xFFF8F9FA);
// ... neutral100~950, error500, success500, warning500
}
// Secondary Colors - 辅助色 /// Layer 2: 语义颜色接口
static const secondary = Color(0xFF764BA2); abstract class AppColors {
static const secondaryDark = Color(0xFF5E3882); Color get bgPrimary; // Scaffold 底色
static const secondaryLight = Color(0xFF9B6FC4); Color get bgSecondary; // Card / Surface
Color get textPrimary; // 主文本
Color get textSecondary; // 辅助文本
Color get brandPrimary; // 主品牌色
Color get statusError; // 错误
// ... 完整定义见 colors.dart
}
// Neutral Colors - 中性色 /// Layer 3: 亮暗实现
static const black = Color(0xFF000000); class LightColors implements AppColors {
static const white = Color(0xFFFFFFFF); const LightColors();
static const gray900 = Color(0xFF1A1A1A); @override Color get bgPrimary => ColorBases.neutral50;
static const gray800 = Color(0xFF2D2D2D); @override Color get textPrimary => ColorBases.neutral900;
static const gray700 = Color(0xFF404040); @override Color get brandPrimary => ColorBases.primary500;
static const gray600 = Color(0xFF5C5C5C); // ...
static const gray500 = Color(0xFF737373); }
static const gray400 = Color(0xFF999999);
static const gray300 = Color(0xFFBFBFBF);
static const gray200 = Color(0xFFE6E6E6);
static const gray100 = Color(0xFFF5F5F5);
static const gray50 = Color(0xFFFAFAFA);
// Semantic Colors - 语义色 class DarkColors implements AppColors {
static const success = Color(0xFF10B981); const DarkColors();
static const warning = Color(0xFFF59E0B); @override Color get bgPrimary => ColorBases.neutral900;
static const error = Color(0xFFEF4444); @override Color get textPrimary => ColorBases.neutral0;
static const info = Color(0xFF3B82F6); @override Color get brandPrimary => ColorBases.primary400;
// ...
} }
</code></pre> </code></pre>
@@ -3616,77 +3621,79 @@ class AppTypography {
} }
</code></pre> </code></pre>
<h5>1.3 Design Tokens基础定义</h5> <h5>1.3 间距 / 圆角 / 阴影</h5>
<pre><code class="language-dart">/// 基础定义 - 间距、圆角、阴影等 <pre><code class="language-dart">/// 间距常量 — spacing.dart
class AppTokens { class AppSpacing {
// Spacing - 间距8pt 网格系统) static const double s4 = 4;
static const spacing4 = 4.0; static const double s8 = 8;
static const spacing8 = 8.0; static const double s12 = 12;
static const spacing12 = 12.0; static const double s16 = 16;
static const spacing16 = 16.0; static const double s24 = 24;
static const spacing20 = 20.0; static const double s32 = 32;
static const spacing24 = 24.0; }
static const spacing32 = 32.0;
static const spacing40 = 40.0;
static const spacing48 = 48.0;
// Border Radius - 圆角 /// 圆角常量 — radius.dart
static const radiusSmall = 4.0; class AppRadius {
static const radiusMedium = 8.0; // 基础 Radius
static const radiusLarge = 12.0; static const Radius r4 = Radius.circular(4);
static const radiusXLarge = 16.0; static const Radius r8 = Radius.circular(8);
static const radiusFull = 9999.0; static const Radius r12 = Radius.circular(12);
static const Radius r16 = Radius.circular(16);
// Elevation - 阴影 // 组件级圆角
static const elevationNone = 0.0; static const BorderRadius card = BorderRadius.all(r12);
static const elevationLow = 2.0; static const BorderRadius button = BorderRadius.all(r8);
static const elevationMedium = 4.0; static const BorderRadius dialog = BorderRadius.all(r16);
static const elevationHigh = 8.0; }
/// 阴影常量 — shadows.dart
/// 颜色走 AppColors.shadow 自动适配亮暗模式
class AppShadows {
List&lt;BoxShadow&gt; get bs4; // Elevation 4 — 小卡片
List&lt;BoxShadow&gt; get bs8; // Elevation 8 — Card
List&lt;BoxShadow&gt; get bs12; // Elevation 12 — Dropdown
List&lt;BoxShadow&gt; get bs16; // Elevation 16 — Dialog
} }
</code></pre> </code></pre>
<h5>1.4 Theme主题 - 黑暗模式</h5> <h5>1.4 Theme主题组装</h5>
<pre><code class="language-dart">/// 主题定义 - 支持亮色/暗色模式 <pre><code class="language-dart">/// Layer 4: 主题组装 — 接收 AppColors 实例,零 isDark 分支
class AppTheme { class AppTheme {
// Light Theme AppTheme._();
static ThemeData light = ThemeData(
brightness: Brightness.light,
primaryColor: AppColors.primary,
scaffoldBackgroundColor: AppColors.white,
colorScheme: const ColorScheme.light(
primary: AppColors.primary,
secondary: AppColors.secondary,
error: AppColors.error,
surface: AppColors.white,
background: AppColors.gray50,
),
textTheme: TextTheme(
displayLarge: AppTypography.displayLarge,
headlineMedium: AppTypography.headlineMedium,
bodyLarge: AppTypography.bodyLarge,
),
);
// Dark Theme static ThemeData get theme => _build(const LightColors());
static ThemeData dark = ThemeData( static ThemeData get darkTheme => _build(const DarkColors());
brightness: Brightness.dark,
primaryColor: AppColors.primary, static ThemeData _build(AppColors c) {
scaffoldBackgroundColor: AppColors.gray900, final brightness = c is DarkColors ? Brightness.dark : Brightness.light;
colorScheme: const ColorScheme.dark( return ThemeData(
primary: AppColors.primary, useMaterial3: true,
secondary: AppColors.secondary, brightness: brightness,
error: AppColors.error, scaffoldBackgroundColor: c.bgPrimary,
surface: AppColors.gray800, colorScheme: ColorScheme(
background: AppColors.black, brightness: brightness,
), primary: c.brandPrimary,
textTheme: TextTheme( onPrimary: c.textOnPrimary,
displayLarge: AppTypography.displayLarge.copyWith(color: AppColors.white), surface: c.bgSecondary,
headlineMedium: AppTypography.headlineMedium.copyWith(color: AppColors.white), onSurface: c.textPrimary,
bodyLarge: AppTypography.bodyLarge.copyWith(color: AppColors.gray100), error: c.statusError,
), onError: c.textOnPrimary,
); // ...
),
appBarTheme: AppBarTheme(backgroundColor: c.bgPrimary, foregroundColor: c.textPrimary),
elevatedButtonTheme: ElevatedButtonThemeData(style: ElevatedButton.styleFrom(
backgroundColor: c.brandPrimary, foregroundColor: c.textOnPrimary,
)),
inputDecorationTheme: InputDecorationTheme(fillColor: c.bgSecondary, filled: true),
cardTheme: CardThemeData(color: c.bgSecondary),
dividerTheme: DividerThemeData(color: c.divider),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: c.navBarBg, selectedItemColor: c.navBarSelected,
),
);
}
} }
</code></pre> </code></pre>
@@ -3706,8 +3713,8 @@ class AppTheme {
<tbody> <tbody>
<tr> <tr>
<td><code>Primary/Main</code></td> <td><code>Primary/Main</code></td>
<td><code>AppColors.primary</code></td> <td><code>context.colors.brandPrimary</code></td>
<td>主色</td> <td>品牌</td>
</tr> </tr>
<tr> <tr>
<td><code>Button/Primary</code></td> <td><code>Button/Primary</code></td>
@@ -3716,18 +3723,18 @@ class AppTheme {
</tr> </tr>
<tr> <tr>
<td><code>Text/Headline/Large</code></td> <td><code>Text/Headline/Large</code></td>
<td><code>AppTypography.headlineLarge</code></td> <td><code>context.styles.headlineLarge</code></td>
<td>大标题</td> <td>大标题</td>
</tr> </tr>
<tr> <tr>
<td><code>Spacing/16</code></td> <td><code>Spacing/16</code></td>
<td><code>AppTokens.spacing16</code></td> <td><code>AppSpacing.s16</code></td>
<td>16pt 间距</td> <td>16pt 间距</td>
</tr> </tr>
<tr> <tr>
<td><code>Radius/Medium</code></td> <td><code>Radius/Card</code></td>
<td><code>AppTokens.radiusMedium</code></td> <td><code>AppRadius.card</code></td>
<td>中等圆角</td> <td>卡片圆角</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -3777,7 +3784,7 @@ class AppButton extends StatelessWidget {
foregroundColor: _getForegroundColor(), foregroundColor: _getForegroundColor(),
padding: _getPadding(), padding: _getPadding(),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppTokens.radiusMedium), borderRadius: AppRadius.button,
), ),
), ),
child: Text(text, style: _getTextStyle()), child: Text(text, style: _getTextStyle()),
@@ -3806,7 +3813,7 @@ class AppTextField extends StatelessWidget {
labelText: label, labelText: label,
hintText: hint, hintText: hint,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppTokens.radiusMedium), borderRadius: AppRadius.button,
), ),
), ),
); );
@@ -3831,11 +3838,11 @@ class SearchBar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: EdgeInsets.all(AppTokens.spacing12), padding: EdgeInsets.all(AppSpacing.s12),
child: Row( child: Row(
children: [ children: [
Icon(Icons.search, color: AppColors.gray500), Icon(Icons.search, color: context.colors.textSecondary),
SizedBox(width: AppTokens.spacing8), SizedBox(width: AppSpacing.s8),
Expanded( Expanded(
child: AppTextField( child: AppTextField(
hint: hint, hint: hint,
@@ -3902,18 +3909,18 @@ class MessageBubble extends ConsumerWidget {
alignment: isSender ? Alignment.centerRight : Alignment.centerLeft, alignment: isSender ? Alignment.centerRight : Alignment.centerLeft,
child: Container( child: Container(
margin: EdgeInsets.symmetric( margin: EdgeInsets.symmetric(
horizontal: AppTokens.spacing16, horizontal: AppSpacing.s16,
vertical: AppTokens.spacing8, vertical: AppSpacing.s8,
), ),
padding: EdgeInsets.all(AppTokens.spacing12), padding: EdgeInsets.all(AppSpacing.s12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSender ? AppColors.primary : AppColors.gray200, color: isSender ? context.colors.brandPrimary : context.colors.bgTertiary,
borderRadius: BorderRadius.circular(AppTokens.radiusLarge), borderRadius: AppRadius.card,
), ),
child: Text( child: Text(
message.content, message.content,
style: AppTypography.bodyMedium.copyWith( style: context.styles.bodyMedium?.copyWith(
color: isSender ? AppColors.white : AppColors.black, color: isSender ? context.colors.textOnPrimary : context.colors.textPrimary,
), ),
), ),
), ),
@@ -4143,7 +4150,7 @@ class PlatformButton extends StatelessWidget {
if (PlatformAdapter.isIOS) { if (PlatformAdapter.isIOS) {
return CupertinoButton( return CupertinoButton(
onPressed: onPressed, onPressed: onPressed,
color: AppColors.primary, color: context.colors.brandPrimary,
child: Text(text), child: Text(text),
); );
} }
@@ -6879,10 +6886,13 @@ final user = await db.selectFirst(appDb.users, (t) =&gt; t.uid.equals(uid));
<p>最底层的视觉规范定义,<strong>不含任何 Widget</strong>,只输出颜色/字体常量和 ThemeData</p> <p>最底层的视觉规范定义,<strong>不含任何 Widget</strong>,只输出颜色/字体常量和 ThemeData</p>
<ul> <ul>
<li><code>colors.dart</code>(已实现):颜色体系 —— 品牌色、语义色success / warning / error、中性灰阶</li> <li><code>colors.dart</code>(已实现):5 层颜色体系 —— ColorBases颜色常量+ AppColors语义接口+ LightColors / DarkColors亮暗实现</li>
<li><code>font.dart</code>(已实现):字体 —— TextStyle 定义 + <code>textTheme(brightness)</code>(统一字族/字号/行高)</li> <li><code>font.dart</code>(已实现):字体 —— TextStyle 定义 + <code>textTheme(brightness)</code>(统一字族/字号/行高)</li>
<li><code>app_theme.dart</code>(已实现):主题组装 —— 将以上令牌组合为 Light / Dark ThemeData</li> <li><code>shadows.dart</code>(已实现):阴影常量 —— bs4 / bs8 / bs12 / bs16颜色走 AppColors.shadow 自动适配亮暗</li>
<li>spacing / radius / shadows 等(待开发,按需添加)</li> <li><code>spacing.dart</code>(已实现):间距常量 —— s4 / s8 / s12 / s16 / s24 / s32</li>
<li><code>radius.dart</code>(已实现):圆角常量 —— 基础 r4~r20 + 组件级 card / button / dialog</li>
<li><code>app_theme.dart</code>(已实现):主题组装 —— <code>_build(AppColors c)</code> 零 isDark 分支</li>
<li><code>context_theme_ext.dart</code>(已实现):统一 context 扩展 —— context.colors + context.styles + context.shadows</li>
</ul> </ul>
<h4>第二层基础组件core/ui/components/</h4> <h4>第二层基础组件core/ui/components/</h4>
@@ -9855,37 +9865,74 @@ flowchart TD
<h3 id="8-1-颜色体系">8.1 颜色体系</h3> <h3 id="8-1-颜色体系">8.1 颜色体系</h3>
<p>所有颜色通过抽象名称引用。抽象名在亮色 / 暗色两套主题下对应不同色值,修改主题只需改映射表,不需逐个找组件</p> <p>颜色采用 5 层架构:<strong>ColorBases颜色常量→ AppColors语义接口→ LightColors/DarkColors亮暗实现→ AppTheme._build(AppColors c)(主题组装)→ context.colorsWidget 消费)</strong>。AppTheme 内部零 isDark 分支,亮暗差异完全由实现类决定</p>
<h4>语义色(随主题变化)</h4> <h4>Layer 1: ColorBases — 颜色常量</h4>
<table> <table>
<thead> <thead><tr><th>色相</th><th>常量名</th><th>色值</th></tr></thead>
<tr><th>抽象名</th><th>Figma 名</th><th>亮色</th><th>暗色</th><th>用途</th></tr>
</thead>
<tbody> <tbody>
<tr><td><code>primary</code></td><td>Primary</td><td>#2F80ED</td><td>#5BA3F5</td><td>主操作、链接、选中态</td></tr> <tr><td rowspan="3">Brand</td><td>primary400</td><td>#5BA3F5</td></tr>
<tr><td><code>background</code></td><td>Background</td><td>#F8F9FA</td><td>#202124</td><td>页面底色</td></tr> <tr><td>primary500</td><td>#2F80ED</td></tr>
<tr><td><code>surface</code></td><td>Surface</td><td>#FFFFFF</td><td>#3C4043</td><td>卡片、弹框、输入框</td></tr> <tr><td>primary700</td><td>#1A6BD4</td></tr>
<tr><td><code>onSurface</code></td><td>On Surface</td><td>#202124</td><td>#FFFFFF</td><td>surface 上的文字</td></tr> <tr><td rowspan="10">Neutral</td><td>neutral0</td><td>#FFFFFF</td></tr>
<tr><td><code>error</code></td><td>Error</td><td colspan="2">#EB5757</td><td>错误状态</td></tr> <tr><td>neutral50</td><td>#F8F9FA</td></tr>
<tr><td><code>success</code></td><td>Success</td><td colspan="2">#27AE60</td><td>成功状态</td></tr> <tr><td>neutral100</td><td>#F1F3F4</td></tr>
<tr><td><code>warning</code></td><td>Warning</td><td colspan="2">#F2C94C</td><td>警告状态</td></tr> <tr><td>neutral200</td><td>#E8EAED</td></tr>
<tr><td>neutral400</td><td>#BDC1C6</td></tr>
<tr><td>neutral600</td><td>#80868B</td></tr>
<tr><td>neutral800</td><td>#3C4043</td></tr>
<tr><td>neutral850</td><td>#2C2C2E</td></tr>
<tr><td>neutral900</td><td>#202124</td></tr>
<tr><td>neutral950</td><td>#000000</td></tr>
<tr><td rowspan="2">Neutral Alpha</td><td>black12</td><td>12% 透明度黑色</td></tr>
<tr><td>black60</td><td>60% 透明度黑色</td></tr>
<tr><td rowspan="3">Status</td><td>error500</td><td>#EB5757</td></tr>
<tr><td>success500</td><td>#27AE60</td></tr>
<tr><td>warning500</td><td>#F2C94C</td></tr>
</tbody> </tbody>
</table> </table>
<h4>灰阶(固定值,不随主题变化)</h4> <h4>Layer 2-3: AppColors — 语义接口 + 亮暗实现</h4>
<table> <table>
<thead><tr><th></th><th>色值</th><th>名称</th><th>色值</th><th>名称</th><th>色值</th></tr></thead> <thead><tr><th>语义</th><th>用途</th><th>亮色</th><th>暗色</th></tr></thead>
<tbody> <tbody>
<tr><td>white</td><td>#FFFFFF</td><td>gray50</td><td>#F8F9FA</td><td>gray100</td><td>#F1F3F4</td></tr> <tr><td><code>bgPrimary</code></td><td>Scaffold 底色</td><td>neutral50</td><td>neutral900</td></tr>
<tr><td>gray200</td><td>#E8EAED</td><td>gray400</td><td>#BDC1C6</td><td>gray600</td><td>#80868B</td></tr> <tr><td><code>bgSecondary</code></td><td>Card / Surface</td><td>neutral0</td><td>neutral800</td></tr>
<tr><td>gray800</td><td>#3C4043</td><td>gray900</td><td>#202124</td><td>black</td><td>#000000</td></tr> <tr><td><code>bgTertiary</code></td><td>次要区块</td><td>neutral100</td><td>neutral850</td></tr>
<tr><td><code>textPrimary</code></td><td>主文本</td><td>neutral900</td><td>neutral0</td></tr>
<tr><td><code>textSecondary</code></td><td>辅助文本</td><td>neutral600</td><td>neutral400</td></tr>
<tr><td><code>textDisabled</code></td><td>禁用态</td><td>neutral400</td><td>neutral600</td></tr>
<tr><td><code>textOnPrimary</code></td><td>品牌色上的文字</td><td colspan="2">neutral0</td></tr>
<tr><td><code>borderDefault</code></td><td>默认边框</td><td>neutral200</td><td>neutral800</td></tr>
<tr><td><code>borderFocused</code></td><td>聚焦边框</td><td>primary500</td><td>primary400</td></tr>
<tr><td><code>brandPrimary</code></td><td>主品牌色</td><td>primary500</td><td>primary400</td></tr>
<tr><td><code>brandPrimaryHover</code></td><td>Hover 态</td><td>primary700</td><td>primary500</td></tr>
<tr><td><code>statusError</code></td><td>错误</td><td colspan="2">error500</td></tr>
<tr><td><code>statusSuccess</code></td><td>成功</td><td colspan="2">success500</td></tr>
<tr><td><code>statusWarning</code></td><td>警告</td><td colspan="2">warning500</td></tr>
<tr><td><code>navBarBg</code></td><td>底部导航背景</td><td>neutral0</td><td>neutral900</td></tr>
<tr><td><code>navBarSelected</code></td><td>导航选中色</td><td>primary500</td><td>primary400</td></tr>
<tr><td><code>divider</code></td><td>分割线</td><td>neutral100</td><td>neutral800</td></tr>
<tr><td><code>shadow</code></td><td>阴影颜色</td><td>black12</td><td>black60</td></tr>
</tbody> </tbody>
</table> </table>
<p><strong>使用原则</strong>:需随主题切换 → 用语义色(<code>primary</code><code>surface</code>);亮暗保持不变 → 用灰阶固定值。</p> <h4>Widget 侧使用(双入口)</h4>
<p>三个 context 扩展:<strong><code>context.colors</code></strong> — 语义颜色;<strong><code>context.styles</code></strong> — 字体 + 预组合样式;<strong><code>context.shadows</code></strong> — 阴影常量。</p>
<pre><code>final c = context.colors;
final s = context.styles;
Container(color: c.bgSecondary) // 语义颜色
Text('标题', style: s.headlineMedium) // 字体
Text('分组', style: s.sectionLabel) // 预组合:字体 + 品牌色
Icon(Icons.check, color: c.brandPrimary) // 图标颜色
Container(decoration: BoxDecoration(boxShadow: context.shadows.bs8)) // 阴影
</code></pre>
<p><strong>使用原则</strong>:颜色一律用 <code>context.colors.xxx</code>,禁止写 <code>Color(0xFF...)</code><code>Theme.of(context).colorScheme.xxx</code>。新增语义色先在 <code>AppColors</code> 加 getter再在 LightColors / DarkColors 各实现一遍。</p>
<h3 id="8-2-字体体系">8.2 字体体系</h3> <h3 id="8-2-字体体系">8.2 字体体系</h3>

View File

@@ -3,18 +3,20 @@ import 'package:flutter/material.dart';
import 'package:im_app/core/ui/base/colors.dart'; import 'package:im_app/core/ui/base/colors.dart';
import 'package:im_app/core/ui/base/font.dart'; import 'package:im_app/core/ui/base/font.dart';
/// 主题组装 -- 将 AppColors / AppFont 组装为 ThemeData /// 主题组装 将 AppColors / AppFont 组装为 ThemeData
/// ///
/// 同时提供 Light / Dark 双主题,按钮形状/颜色/字体统一在此定义, /// 接收 [AppColors] 语义颜色实例,零 isDark 分支。
/// AppButton 只负责变体切换和 loading 逻辑,不硬编码颜色和字体。 /// 亮暗色差异完全由 [LightColors] / [DarkColors] 实现决定,
/// 本类只做「语义 → ThemeData 属性」的映射。
/// ///
/// ## 数据流位置 /// ## 数据流位置
/// ///
/// ``` /// ```
/// AppColors + AppFont (L1 常量) /// ColorBases颜色常量→ AppColors语义接口
/// → ★ AppTheme ★ (L1 组装) ← 你在这里 /// → LightColors / DarkColors实现
/// → MaterialApp(theme: AppTheme.theme, darkTheme: AppTheme.darkTheme) /// → ★ AppTheme._build(AppColors c) ★ ← 你在这里
/// → Theme.of(context) → 所有 Widget 自动响应主题变化 /// → MaterialApp(theme: AppTheme.theme, darkTheme: AppTheme.darkTheme)
/// → Theme.of(context) → 所有 Widget 自动响应主题变化
/// ``` /// ```
/// ///
/// ## 使用 /// ## 使用
@@ -22,7 +24,7 @@ import 'package:im_app/core/ui/base/font.dart';
/// ```dart /// ```dart
/// // app/app.dart /// // app/app.dart
/// MaterialApp( /// MaterialApp(
/// theme: AppTheme.theme, // getter 名与 MaterialApp 参数名一一对应 /// theme: AppTheme.theme,
/// darkTheme: AppTheme.darkTheme, /// darkTheme: AppTheme.darkTheme,
/// ) /// )
/// ``` /// ```
@@ -30,62 +32,128 @@ class AppTheme {
AppTheme._(); AppTheme._();
/// 亮色主题 — 对应 MaterialApp `theme:` 参数 /// 亮色主题 — 对应 MaterialApp `theme:` 参数
static ThemeData get theme => _build(Brightness.light); static ThemeData get theme => _build(const LightColors());
/// 暗色主题 — 对应 MaterialApp `darkTheme:` 参数 /// 暗色主题 — 对应 MaterialApp `darkTheme:` 参数
static ThemeData get darkTheme => _build(Brightness.dark); static ThemeData get darkTheme => _build(const DarkColors());
static ThemeData _build(Brightness brightness) { static ThemeData _build(AppColors c) {
final isDark = brightness == Brightness.dark; final brightness = c is DarkColors ? Brightness.dark : Brightness.light;
final primary = isDark ? AppColors.primaryLight : AppColors.primary;
return ThemeData( return ThemeData(
useMaterial3: true, useMaterial3: true,
brightness: brightness, brightness: brightness,
// ── ColorScheme ────────────────────────────────────────────────────
colorScheme: ColorScheme( colorScheme: ColorScheme(
brightness: brightness, brightness: brightness,
primary: primary, primary: c.brandPrimary,
onPrimary: AppColors.white, onPrimary: c.textOnPrimary,
secondary: primary, secondary: c.brandPrimary,
onSecondary: AppColors.white, onSecondary: c.textOnPrimary,
error: AppColors.error, error: c.statusError,
onError: AppColors.white, onError: c.textOnPrimary,
surface: isDark ? AppColors.gray800 : AppColors.white, surface: c.bgSecondary,
onSurface: isDark ? AppColors.white : AppColors.gray900, onSurface: c.textPrimary,
), ),
scaffoldBackgroundColor: isDark ? AppColors.gray900 : AppColors.gray50, scaffoldBackgroundColor: c.bgPrimary,
// 字体 // ── Typography ─────────────────────────────────────────────────────
textTheme: AppFont.textTheme(brightness), textTheme: AppFont.textTheme(brightness),
// ElevatedButton → AppButton.primary // ── AppBar ─────────────────────────────────────────────────────────
appBarTheme: AppBarTheme(
backgroundColor: c.bgPrimary,
foregroundColor: c.textPrimary,
elevation: 0,
centerTitle: true,
),
// ── ElevatedButton → AppButton.primary ─────────────────────────────
elevatedButtonTheme: ElevatedButtonThemeData( elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary, backgroundColor: c.brandPrimary,
foregroundColor: AppColors.white, foregroundColor: c.textOnPrimary,
disabledBackgroundColor: AppColors.gray400, disabledBackgroundColor: c.borderDefault,
disabledForegroundColor: c.textDisabled,
minimumSize: const Size.fromHeight(48), minimumSize: const Size.fromHeight(48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
), ),
), ),
// OutlinedButton → AppButton.secondary // ── OutlinedButton → AppButton.secondary ──────────────────────────
outlinedButtonTheme: OutlinedButtonThemeData( outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: primary, foregroundColor: c.brandPrimary,
side: BorderSide(color: primary), side: BorderSide(color: c.brandPrimary),
minimumSize: const Size.fromHeight(48), minimumSize: const Size.fromHeight(48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
), ),
), ),
// TextButton → AppButton.text // ── TextButton → AppButton.text ────────────────────────────────────
textButtonTheme: TextButtonThemeData( textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: primary, foregroundColor: c.brandPrimary,
minimumSize: const Size.fromHeight(48), minimumSize: const Size.fromHeight(48),
), ),
), ),
// ── InputDecoration ────────────────────────────────────────────────
inputDecorationTheme: InputDecorationTheme(
fillColor: c.bgSecondary,
filled: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
border: OutlineInputBorder(
borderSide: BorderSide(color: c.borderDefault),
borderRadius: BorderRadius.circular(8),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: c.borderDefault),
borderRadius: BorderRadius.circular(8),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: c.borderFocused, width: 2),
borderRadius: BorderRadius.circular(8),
),
errorBorder: OutlineInputBorder(
borderSide: BorderSide(color: c.statusError),
borderRadius: BorderRadius.circular(8),
),
hintStyle: TextStyle(color: c.textDisabled),
),
// ── Card ───────────────────────────────────────────────────────────
cardTheme: CardThemeData(
color: c.bgSecondary,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: c.borderDefault),
),
),
// ── Divider ────────────────────────────────────────────────────────
dividerTheme: DividerThemeData(
color: c.divider,
thickness: 1,
),
// ── BottomNavigationBar ────────────────────────────────────────────
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: c.navBarBg,
selectedItemColor: c.navBarSelected,
unselectedItemColor: c.textDisabled,
showUnselectedLabels: true,
type: BottomNavigationBarType.fixed,
),
); );
} }
} }

View File

@@ -1,7 +0,0 @@
import 'package:flutter/material.dart';
import 'package:im_app/core/ui/base/shadows.dart';
extension AppThemeExt on BuildContext {
AppShadows get shadows => AppShadows(this);
}

View File

@@ -1,44 +1,158 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// 颜色体系 — 与 Figma 设计稿对应 // =============================================================================
/// // LAYER 1: ColorBases — 颜色常量,不带语义
/// L1 基础常量 -- 不含任何 Widget只输出颜色常量。 // =============================================================================
/// View 层不直接引用 AppColors通过 Theme.of(context) 访问语义色; //
/// 有特殊硬编码需求(插图、固定品牌色)时可直接引用。 // 色值与 Figma 设计稿一一对应。命名用 {色相}{明度} 格式(如 primary500
/// // 不暗示任何使用场景。
/// ## 数据流位置 //
/// // View 层不直接引用 ColorBases通过 AppColors语义接口间接使用。
/// ```
/// AppColors颜色常量← 你在这里
/// → AppTheme组装为 ThemeData
/// → MaterialApp注入
/// → Theme.of(context)View 层消费)
/// ```
class AppColors {
AppColors._();
// ── Brand Primary ────────────────────────────────────────────────────────── abstract class ColorBases {
static const primary = Color(0xFF2F80ED); ColorBases._();
static const primaryDark = Color(0xFF1A6BD4);
static const primaryLight = Color(0xFF5BA3F5);
// ── Semantic ─────────────────────────────────────────────────────────────── // ── Brand ────────────────────────────────────────────────────────────────
static const success = Color(0xFF27AE60); static const primary400 = Color(0xFF5BA3F5);
static const warning = Color(0xFFF2C94C); static const primary500 = Color(0xFF2F80ED);
static const error = Color(0xFFEB5757); static const primary700 = Color(0xFF1A6BD4);
// ── Neutral Gray Scale ───────────────────────────────────────────────────── // ── Neutral ──────────────────────────────────────────────────────────────
static const white = Color(0xFFFFFFFF); static const neutral0 = Color(0xFFFFFFFF);
static const gray50 = Color(0xFFF8F9FA); static const neutral50 = Color(0xFFF8F9FA);
static const gray100 = Color(0xFFF1F3F4); static const neutral100 = Color(0xFFF1F3F4);
static const gray200 = Color(0xFFE8EAED); static const neutral200 = Color(0xFFE8EAED);
static const gray400 = Color(0xFFBDC1C6); static const neutral400 = Color(0xFFBDC1C6);
static const gray600 = Color(0xFF80868B); static const neutral600 = Color(0xFF80868B);
static const gray800 = Color(0xFF3C4043); static const neutral800 = Color(0xFF3C4043);
static const gray900 = Color(0xFF202124); static const neutral850 = Color(0xFF2C2C2E);
static const black = Color(0xFF000000); static const neutral900 = Color(0xFF202124);
static const neutral950 = Color(0xFF000000);
// ── Neutral black Scale ───────────────────────────────────────────────────── // ── Neutral Alpha带透明度阴影等场景──────────────────────────────────
static const black12 = Color(0x1F000000); // 12% opacity static const black12 = Color(0x1F000000);
static const black60 = Color(0x99000000); // 60% opacity static const black60 = Color(0x99000000);
// ── Status ───────────────────────────────────────────────────────────────
static const error500 = Color(0xFFEB5757);
static const success500 = Color(0xFF27AE60);
static const warning500 = Color(0xFFF2C94C);
}
// =============================================================================
// LAYER 2: AppColors — 语义颜色抽象接口
// =============================================================================
//
// 按用途命名bgPrimary、textSecondary不关心具体色值。
// LightColors / DarkColors 各自实现AppTheme._build() 消费此接口。
// Widget 侧通过 context.colors 访问(见 context_theme_ext.dart
//
// ## 数据流
//
// ```
// ColorBases颜色常量
// → AppColors语义接口← 你在这里
// → LightColors / DarkColors实现
// → AppTheme._build(AppColors c)(组装 ThemeData
// → context.colorsWidget 消费)
// ```
abstract class AppColors {
// ── Background ───────────────────────────────────────────────────────────
Color get bgPrimary;
Color get bgSecondary;
Color get bgTertiary;
// ── Text ─────────────────────────────────────────────────────────────────
Color get textPrimary;
Color get textSecondary;
Color get textDisabled;
Color get textOnPrimary;
// ── Border ───────────────────────────────────────────────────────────────
Color get borderDefault;
Color get borderFocused;
// ── Brand ────────────────────────────────────────────────────────────────
Color get brandPrimary;
Color get brandPrimaryHover;
// ── Status ───────────────────────────────────────────────────────────────
Color get statusError;
Color get statusSuccess;
Color get statusWarning;
// ── Component ────────────────────────────────────────────────────────────
Color get navBarBg;
Color get navBarSelected;
Color get divider;
// ── Effect ───────────────────────────────────────────────────────────────
Color get shadow;
}
// =============================================================================
// LAYER 3a: LightColors — 亮色实现
// =============================================================================
class LightColors implements AppColors {
const LightColors();
@override Color get bgPrimary => ColorBases.neutral50;
@override Color get bgSecondary => ColorBases.neutral0;
@override Color get bgTertiary => ColorBases.neutral100;
@override Color get textPrimary => ColorBases.neutral900;
@override Color get textSecondary => ColorBases.neutral600;
@override Color get textDisabled => ColorBases.neutral400;
@override Color get textOnPrimary => ColorBases.neutral0;
@override Color get borderDefault => ColorBases.neutral200;
@override Color get borderFocused => ColorBases.primary500;
@override Color get brandPrimary => ColorBases.primary500;
@override Color get brandPrimaryHover => ColorBases.primary700;
@override Color get statusError => ColorBases.error500;
@override Color get statusSuccess => ColorBases.success500;
@override Color get statusWarning => ColorBases.warning500;
@override Color get navBarBg => ColorBases.neutral0;
@override Color get navBarSelected => ColorBases.primary500;
@override Color get divider => ColorBases.neutral100;
@override Color get shadow => ColorBases.black12;
}
// =============================================================================
// LAYER 3b: DarkColors — 暗色实现
// =============================================================================
class DarkColors implements AppColors {
const DarkColors();
@override Color get bgPrimary => ColorBases.neutral900;
@override Color get bgSecondary => ColorBases.neutral800;
@override Color get bgTertiary => ColorBases.neutral850;
@override Color get textPrimary => ColorBases.neutral0;
@override Color get textSecondary => ColorBases.neutral400;
@override Color get textDisabled => ColorBases.neutral600;
@override Color get textOnPrimary => ColorBases.neutral0;
@override Color get borderDefault => ColorBases.neutral800;
@override Color get borderFocused => ColorBases.primary400;
@override Color get brandPrimary => ColorBases.primary400;
@override Color get brandPrimaryHover => ColorBases.primary500;
@override Color get statusError => ColorBases.error500;
@override Color get statusSuccess => ColorBases.success500;
@override Color get statusWarning => ColorBases.warning500;
@override Color get navBarBg => ColorBases.neutral900;
@override Color get navBarSelected => ColorBases.primary400;
@override Color get divider => ColorBases.neutral800;
@override Color get shadow => ColorBases.black60;
} }

View File

@@ -1,27 +1,40 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:im_app/core/ui/base/colors.dart';
import 'package:im_app/core/ui/base/font.dart'; import 'package:im_app/core/ui/base/font.dart';
import 'package:im_app/core/ui/base/shadows.dart';
/// 主题样式快捷封装 /// 主题样式快捷封装 — 字体 + 预组合样式
/// ///
/// `context.styles` 返回此对象build 方法里一行获取所有样式, /// `context.styles` 返回此对象build 方法里一行获取字体样式,
/// 之后直接用 `s.bodySmall`、`s.primary`,不再写 Theme.of(context)。 /// 之后直接用 `s.bodySmall`、`s.sectionLabel`,不再写 Theme.of(context)。
///
/// 颜色访问走 `context.colors`(见下方 [AppColorsX] 扩展),
/// 本类只负责字体和「字体+颜色」的预组合。
/// ///
/// ```dart /// ```dart
/// final s = context.styles; /// final s = context.styles;
/// final c = context.colors;
/// ///
/// Text('标题', style: s.titleMedium) /// Text('标题', style: s.titleMedium)
/// Text('描述', style: s.bodySmall) /// Text('描述', style: s.bodySmall)
/// Icon(Icons.home, color: s.primary) /// Text('分组', style: s.sectionLabel) // 预组合:字体 + 品牌色
/// Text('改色', style: s.bodySmall?.copyWith(color: s.primary)) /// Icon(Icons.home, color: c.brandPrimary)
/// ``` /// ```
class AppStyles { class AppStyles {
AppStyles(BuildContext context) AppStyles(BuildContext context)
: _t = Theme.of(context).textTheme, : _t = Theme.of(context).textTheme,
_c = Theme.of(context).colorScheme; _colors = Theme.of(context).brightness == Brightness.dark
? const DarkColors()
: const LightColors();
final TextTheme _t; final TextTheme _t;
final ColorScheme _c; final AppColors _colors;
// ── 亮暗判断 ──────────────────────────────────────────────────────────────
bool get isDark => _colors is DarkColors;
Brightness get brightness => isDark ? Brightness.dark : Brightness.light;
// ── 字体 ────────────────────────────────────────────────────────────────── // ── 字体 ──────────────────────────────────────────────────────────────────
@@ -45,45 +58,58 @@ class AppStyles {
TextStyle? get labelMedium => _t.labelMedium; TextStyle? get labelMedium => _t.labelMedium;
TextStyle? get labelSmall => _t.labelSmall; TextStyle? get labelSmall => _t.labelSmall;
// ── 颜色 + 亮暗 ───────────────────────────────────────────────────────────
Brightness get brightness => _c.brightness;
bool get isDark => _c.brightness == Brightness.dark;
Color get primary => _c.primary;
Color get onPrimary => _c.onPrimary;
Color get secondary => _c.secondary;
Color get onSecondary => _c.onSecondary;
Color get error => _c.error;
Color get onError => _c.onError;
Color get surface => _c.surface;
Color get onSurface => _c.onSurface;
Color get outline => _c.outline;
Color get outlineVariant => _c.outlineVariant;
// ── 预组合样式(字体 + 颜色,开箱即用)────────────────────────────────────── // ── 预组合样式(字体 + 颜色,开箱即用)──────────────────────────────────────
// //
// 与 AppButton 变体理念一致:按语义选用,无需手动拼 TextStyle 或 copyWith。 // 与 AppButton 变体理念一致:按语义选用,无需手动拼 TextStyle 或 copyWith。
// 新增场景时在此扩展,保持全局一致。 // 新增场景时在此扩展,保持全局一致。
/// 分组标题 — 列表 Section、设置分组等sectionLabel 字体 + primary 色) /// 分组标题 — 列表 Section、设置分组等sectionLabel 字体 + 品牌色)
TextStyle get sectionLabel => AppFont.sectionLabel.copyWith(color: primary); TextStyle get sectionLabel =>
AppFont.sectionLabel.copyWith(color: _colors.brandPrimary);
/// 辅助文字 — 元数据、次要信息、时间戳等labelMedium + outline 色) /// 辅助文字 — 元数据、次要信息、时间戳等labelMedium + 次要文字色)
TextStyle? get labelMuted => labelMedium?.copyWith(color: outline); TextStyle? get labelMuted =>
labelMedium?.copyWith(color: _colors.textSecondary);
/// 正文次要 — 描述、提示等bodySmall + outline 色) /// 正文次要 — 描述、提示等bodySmall + 次要文字色)
TextStyle? get bodyMuted => bodySmall?.copyWith(color: outline); TextStyle? get bodyMuted =>
bodySmall?.copyWith(color: _colors.textSecondary);
/// 错误提示 — 表单错误、警告等bodySmall + error 色) /// 错误提示 — 表单错误、警告等bodySmall + 错误色)
TextStyle? get bodyError => bodySmall?.copyWith(color: error); TextStyle? get bodyError =>
bodySmall?.copyWith(color: _colors.statusError);
} }
/// BuildContext 主题入口 /// 语义颜色入口 — 按亮暗模式返回对应的 [AppColors] 实例
///
/// ```dart
/// final c = context.colors;
/// Container(color: c.bgSecondary)
/// Text('hello', style: TextStyle(color: c.textPrimary))
/// ```
extension AppColorsX on BuildContext {
AppColors get colors => Theme.of(this).brightness == Brightness.dark
? const DarkColors()
: const LightColors();
}
/// 字体样式入口
/// ///
/// ```dart /// ```dart
/// final s = context.styles; /// final s = context.styles;
/// Text('标题', style: s.headlineMedium)
/// ``` /// ```
extension AppThemeX on BuildContext { extension AppThemeX on BuildContext {
AppStyles get styles => AppStyles(this); AppStyles get styles => AppStyles(this);
} }
/// 阴影入口
///
/// ```dart
/// Container(
/// decoration: BoxDecoration(boxShadow: context.shadows.bs8),
/// )
/// ```
extension AppShadowsX on BuildContext {
AppShadows get shadows => AppShadows(this);
}

View File

@@ -1,131 +1,57 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
/// 圆角设计 Token /// 圆角常量 — 统一管理项目中的圆角规范
/// ///
/// 统一管理项目中的圆角规范,避免在业务代码中直接写 /// 避免在业务代码中直接写 `BorderRadius.circular()`。
/// `Radius.circular()` 或 `BorderRadius.circular()`
///
/// 使用方式:
/// ///
/// ```dart /// ```dart
/// Container( /// Container(decoration: BoxDecoration(borderRadius: AppRadius.card))
/// decoration: BoxDecoration(
/// borderRadius: AppRadius.card,
/// ),
/// )
/// ``` /// ```
/// ///
/// 设计规范来源: /// 基础档位4 / 6 / 8 / 10 / 12 / 14 / 16 / 18 / 20
/// 通常来自 UI 设计系统,例如
/// 4 / 8 / 12 / 16 / 20
class AppRadius { class AppRadius {
/// 私有构造函数,防止被实例化
AppRadius._(); AppRadius._();
// ================================ // ── 基础 Radius ────────────────────────────────────────────────────────
// 基础 Radius Token
// ================================
// 用于组合 BorderRadius
/// 4px 圆角
static const Radius r4 = Radius.circular(4); static const Radius r4 = Radius.circular(4);
/// 6px 圆角
static const Radius r6 = Radius.circular(6); static const Radius r6 = Radius.circular(6);
/// 8px 圆角(常用于按钮)
static const Radius r8 = Radius.circular(8); static const Radius r8 = Radius.circular(8);
/// 10px 圆角
static const Radius r10 = Radius.circular(10); static const Radius r10 = Radius.circular(10);
/// 12px 圆角(常用于卡片)
static const Radius r12 = Radius.circular(12); static const Radius r12 = Radius.circular(12);
/// 14px 圆角
static const Radius r14 = Radius.circular(14); static const Radius r14 = Radius.circular(14);
/// 16px 圆角(常用于弹窗)
static const Radius r16 = Radius.circular(16); static const Radius r16 = Radius.circular(16);
/// 18px 圆角
static const Radius r18 = Radius.circular(18); static const Radius r18 = Radius.circular(18);
/// 20px 圆角
static const Radius r20 = Radius.circular(20); static const Radius r20 = Radius.circular(20);
// ================================ // ── 组件级圆角(优先使用)─────────────────────────────────────────────
// 组件级设计 Token
// ================================
// 推荐优先使用这些,而不是直接使用 brXX
/// 卡片圆角 /// 卡片 — Card / 商品卡片 / 信息卡片
///
/// 示例Card / 商品卡片 / 信息卡片
static const BorderRadius card = BorderRadius.all(r12); static const BorderRadius card = BorderRadius.all(r12);
/// 按钮圆角 /// 按钮 — PrimaryButton / SecondaryButton
///
/// 示例PrimaryButton / SecondaryButton
static const BorderRadius button = BorderRadius.all(r8); static const BorderRadius button = BorderRadius.all(r8);
/// 弹窗圆角 /// 弹窗 — Dialog / Modal
///
/// 示例Dialog / Modal
static const BorderRadius dialog = BorderRadius.all(r16); static const BorderRadius dialog = BorderRadius.all(r16);
// ================================ // ── 通用 BorderRadius组件级圆角不满足时使用──────────────────────────
// 通用 BorderRadius
// ================================
// 当组件 Token 不满足需求时使用
static const BorderRadius br4 = BorderRadius.all(r4); static const BorderRadius br4 = BorderRadius.all(r4);
static const BorderRadius br6 = BorderRadius.all(r6); static const BorderRadius br6 = BorderRadius.all(r6);
static const BorderRadius br8 = BorderRadius.all(r8); static const BorderRadius br8 = BorderRadius.all(r8);
static const BorderRadius br10 = BorderRadius.all(r10); static const BorderRadius br10 = BorderRadius.all(r10);
static const BorderRadius br12 = BorderRadius.all(r12); static const BorderRadius br12 = BorderRadius.all(r12);
static const BorderRadius br14 = BorderRadius.all(r14); static const BorderRadius br14 = BorderRadius.all(r14);
static const BorderRadius br16 = BorderRadius.all(r16); static const BorderRadius br16 = BorderRadius.all(r16);
static const BorderRadius br18 = BorderRadius.all(r18); static const BorderRadius br18 = BorderRadius.all(r18);
static const BorderRadius br20 = BorderRadius.all(r20); static const BorderRadius br20 = BorderRadius.all(r20);
// ================================ // ── 方向性圆角 ──────────────────────────────────────────────────────────
// 辅助方法
// ================================
// 用于生成顶部或底部圆角
/// 生成顶部圆角 /// 顶部圆角 — BottomSheet、底部弹窗
/// static BorderRadius top(Radius r) => BorderRadius.vertical(top: r);
/// 常用于:
/// - BottomSheet
/// - 底部弹窗
/// - 半屏弹层
///
/// 示例:
/// ```dart
/// borderRadius: AppRadius.top(AppRadius.r16)
/// ```
static BorderRadius top(Radius r) =>
BorderRadius.vertical(top: r);
/// 生成底部圆角 /// 底部圆角 — Header、顶部卡片
/// static BorderRadius bottom(Radius r) => BorderRadius.vertical(bottom: r);
/// 常用于:
/// - Header
/// - 顶部卡片
///
/// 示例:
/// ```dart
/// borderRadius: AppRadius.bottom(AppRadius.r16)
/// ```
static BorderRadius bottom(Radius r) =>
BorderRadius.vertical(bottom: r);
} }

View File

@@ -1,129 +1,59 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'colors.dart';
/// 阴影 Design Token import 'package:im_app/core/ui/base/colors.dart';
/// 阴影常量 — 统一管理项目中的阴影规范
/// ///
/// 统一管理项目中的阴影规范,避免在业务代码中直接书写 `BoxShadow`。 /// 阴影颜色通过 [AppColors.shadow] 自动适配亮暗模式,
/// 所有阴影通过 Design Token 提供,保证: /// 无需手动判断 Brightness。
/// ///
/// - UI 风格统一 /// ## 数据流
/// - 支持 Dark / Light Mode
/// - 与设计稿Figma保持一致
///
/// ## 数据流位置
/// ///
/// ``` /// ```
/// AppColors颜色常量 /// ColorBases颜色常量→ AppColors.shadow语义色
/// → AppShadows阴影 Token /// → AppShadows阴影常量
/// → Context Extensioncontext.shadows /// → context.shadowsView 层消费
/// → View 层消费
/// ```
///
/// ## 使用示例
///
/// ```dart
/// Container(
/// decoration: BoxDecoration(
/// color: Colors.white,
/// boxShadow: context.shadows.bs8,
/// ),
/// )
/// ``` /// ```
/// ///
/// ## Elevation 体系 /// ## Elevation 体系
/// ///
/// 阴影遵循常见 UI 设计系统的层级规范:
///
/// - **4** : 小卡片 / List Item /// - **4** : 小卡片 / List Item
/// - **8** : Card / 商品卡片 /// - **8** : Card / 商品卡片
/// - **12** : Dropdown / Popover /// - **12** : Dropdown / Popover
/// - **16** : Dialog / Modal / 悬浮面板 /// - **16** : Dialog / Modal / 悬浮面板
///
/// ## 使用
///
/// ```dart
/// Container(
/// decoration: BoxDecoration(
/// color: context.colors.bgSecondary,
/// boxShadow: context.shadows.bs8,
/// ),
/// )
/// ```
class AppShadows { class AppShadows {
/// 构造函数,通过 BuildContext 获取当前主题 AppShadows(BuildContext context)
AppShadows(this.context); : _color = (Theme.of(context).brightness == Brightness.dark
? const DarkColors()
: const LightColors())
.shadow;
/// 当前 Widget 的 BuildContext final Color _color;
///
/// 用于根据 Theme 判断 Light / Dark Mode
/// 从而动态获取阴影颜色。
final BuildContext context;
/// 内部统一阴影生成方法 List<BoxShadow> _shadow({required double blur, required double dy}) => [
/// BoxShadow(color: _color, blurRadius: blur, offset: Offset(0, dy)),
/// 避免重复创建 `BoxShadow` 逻辑, ];
/// 所有阴影 Token 都通过该方法生成。
List<BoxShadow> _shadow({
required double blur,
required double dy,
}) {
return [
BoxShadow(
/// 阴影颜色来自 Design Token /// Elevation 4 — 小卡片 / List Item
color: _shadowColor, List<BoxShadow> get bs4 => _shadow(blur: 4, dy: 2);
/// 模糊半径(影响阴影扩散范围) /// Elevation 8 — Card / 商品卡片
blurRadius: blur, List<BoxShadow> get bs8 => _shadow(blur: 8, dy: 4);
/// 阴影偏移 /// Elevation 12 — Dropdown / Popover
offset: Offset(0, dy), List<BoxShadow> get bs12 => _shadow(blur: 12, dy: 8);
)
];
}
/// Elevation 4 /// Elevation 16 — Dialog / Modal / 悬浮面板
/// List<BoxShadow> get bs16 => _shadow(blur: 16, dy: 8);
/// 适用场景:
/// - List Item
/// - 小卡片
List<BoxShadow> get bs4 =>
_shadow(
blur: 4,
dy: 2,
);
/// Elevation 8
///
/// 适用场景:
/// - Card
/// - 商品卡片
List<BoxShadow> get bs8 =>
_shadow(
blur: 8,
dy: 4,
);
/// Elevation 12
///
/// 适用场景:
/// - Dropdown
/// - Popover
List<BoxShadow> get bs12 =>
_shadow(
blur: 12,
dy: 8,
);
/// Elevation 16
///
/// 适用场景:
/// - Dialog
/// - Modal
/// - Floating Panel
List<BoxShadow> get bs16 =>
_shadow(
blur: 16,
dy: 8,
);
/// 阴影颜色 Token
Color get _shadowColor {
final brightness = Theme
.of(context)
.brightness;
return brightness == Brightness.dark
? AppColors.black60
: AppColors.black12;
}
} }

View File

@@ -1,72 +1,32 @@
/// 间距设计 Token /// 间距常量 — 统一管理项目中的间距规范
/// ///
/// 统一管理项目中的间距规范,避免在业务代码中直接写 magic number,例如: /// 避免在业务代码中直接写 magic number
/// ///
/// ❌ 不推荐
/// ```dart
/// Padding(padding: EdgeInsets.all(16))
/// ```
///
/// ✅ 推荐
/// ```dart /// ```dart
/// // 用常量,不写裸数字
/// Padding(padding: EdgeInsets.all(AppSpacing.s16)) /// Padding(padding: EdgeInsets.all(AppSpacing.s16))
/// SizedBox(height: AppSpacing.s8)
/// ``` /// ```
/// ///
/// 常用于: /// 基础档位4 / 8 / 12 / 16 / 24 / 32
/// - Padding
/// - Margin
/// - SizedBox
/// - Sliver 间距
///
/// 设计规范通常来源于 UI 设计系统,例如:
/// 4 / 8 / 12 / 16 / 24 / 32
class AppSpacing { class AppSpacing {
/// 私有构造函数,防止实例化
AppSpacing._(); AppSpacing._();
// ================================ /// 4px — icon 与文字之间、紧凑布局
// 基础间距 Token
// ================================
/// 4px 间距(最小间距)
///
/// 常用于:
/// - icon 与文字之间
/// - 紧凑布局
static const double s4 = 4; static const double s4 = 4;
/// 8px 间距(小间距) /// 8px — 列表 item 内间距、小组件之间
///
/// 常用于:
/// - 列表 item 内间距
/// - 小组件之间
static const double s8 = 8; static const double s8 = 8;
/// 12px 间距(中小间距) /// 12px — 表单组件、信息块之间
///
/// 常用于:
/// - 表单组件
/// - 信息块之间
static const double s12 = 12; static const double s12 = 12;
/// 16px 间距(标准间距) /// 16px — 页面 Padding、Card 内边距
///
/// 常用于:
/// - 页面 Padding
/// - Card 内边距
static const double s16 = 16; static const double s16 = 16;
/// 24px 间距(大间距) /// 24px — 模块之间、Section 分隔
///
/// 常用于:
/// - 模块之间
/// - Section 分隔
static const double s24 = 24; static const double s24 = 24;
/// 32px 间距(超大间距) /// 32px — 页面大区块、顶部/底部留白
///
/// 常用于:
/// - 页面大区块
/// - 顶部/底部留白
static const double s32 = 32; static const double s32 = 32;
} }

View File

@@ -116,10 +116,10 @@ class AppButton extends StatelessWidget {
} }
Widget _buildInverse(BuildContext context, Widget label) { Widget _buildInverse(BuildContext context, Widget label) {
final s = context.styles; final c = context.colors;
final style = FilledButton.styleFrom( final style = FilledButton.styleFrom(
backgroundColor: s.onSurface, backgroundColor: c.textPrimary,
foregroundColor: s.surface, foregroundColor: c.bgSecondary,
); );
if (icon != null) { if (icon != null) {
return FilledButton.icon( return FilledButton.icon(

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:im_app/core/ui/base/context_theme_ext.dart';
import 'package:im_app/core/ui/components/app_button.dart';
import 'package:im_app/features/login/presentation/login_state.dart'; import 'package:im_app/features/login/presentation/login_state.dart';
/// 登录步骤 2 — 输入验证码并完成登录 /// 登录步骤 2 — 输入验证码并完成登录
@@ -31,21 +33,22 @@ class LoginOtpStep extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = context.styles;
final c = context.colors;
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Text( Text(
'输入验证码', '输入验证码',
style: Theme.of(context).textTheme.headlineSmall, style: s.headlineSmall,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'验证码已发送至 ${state.countryCode} ${state.maskedContact}', '验证码已发送至 ${state.countryCode} ${state.maskedContact}',
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: s.bodyMedium?.copyWith(color: c.textSecondary),
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
@@ -55,7 +58,6 @@ class LoginOtpStep extends StatelessWidget {
maxLength: 4, maxLength: 4,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: '4 位验证码', labelText: '4 位验证码',
border: OutlineInputBorder(),
counterText: '', counterText: '',
), ),
autofillHints: const [AutofillHints.oneTimeCode], autofillHints: const [AutofillHints.oneTimeCode],
@@ -66,24 +68,19 @@ class LoginOtpStep extends StatelessWidget {
padding: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.only(bottom: 16),
child: Text( child: Text(
state.error!, state.error!,
style: TextStyle(color: Theme.of(context).colorScheme.error), style: s.bodyError,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
FilledButton( AppButton.primary(
label: '登录',
onPressed: state.isLoading ? null : onVerifyAndLogin, onPressed: state.isLoading ? null : onVerifyAndLogin,
child: state.isLoading isLoading: state.isLoading,
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('登录'),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
TextButton( AppButton.text(
label: '返回修改手机号',
onPressed: state.isLoading ? null : onBackToPhone, onPressed: state.isLoading ? null : onBackToPhone,
child: const Text('返回修改手机号'),
), ),
], ],
); );

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:im_app/core/ui/base/context_theme_ext.dart';
import 'package:im_app/core/ui/components/app_button.dart';
import 'package:im_app/features/login/presentation/login_state.dart'; import 'package:im_app/features/login/presentation/login_state.dart';
/// 登录步骤 1 — 输入国家代码 + 手机号 /// 登录步骤 1 — 输入国家代码 + 手机号
@@ -28,13 +30,16 @@ class LoginPhoneStep extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = context.styles;
final c = context.colors;
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Text( Text(
'手机号登录', '手机号登录',
style: Theme.of(context).textTheme.headlineSmall, style: s.headlineSmall,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 40), const SizedBox(height: 40),
@@ -43,25 +48,17 @@ class LoginPhoneStep extends StatelessWidget {
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(color: c.borderDefault),
color: Theme.of(context).colorScheme.outline,
),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: Text( child: Text(state.countryCode, style: s.bodyLarge),
state.countryCode,
style: Theme.of(context).textTheme.bodyLarge,
),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: TextField( child: TextField(
controller: phoneCtrl, controller: phoneCtrl,
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
decoration: const InputDecoration( decoration: const InputDecoration(labelText: '手机号'),
labelText: '手机号',
border: OutlineInputBorder(),
),
autofillHints: const [AutofillHints.telephoneNumber], autofillHints: const [AutofillHints.telephoneNumber],
), ),
), ),
@@ -73,19 +70,14 @@ class LoginPhoneStep extends StatelessWidget {
padding: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.only(bottom: 16),
child: Text( child: Text(
state.error!, state.error!,
style: TextStyle(color: Theme.of(context).colorScheme.error), style: s.bodyError,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
FilledButton( AppButton.primary(
label: '获取验证码',
onPressed: state.isLoading ? null : onSendOtp, onPressed: state.isLoading ? null : onSendOtp,
child: state.isLoading isLoading: state.isLoading,
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('获取验证码'),
), ),
], ],
); );

View File

@@ -29,11 +29,11 @@ class ThemeOptionTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = context.styles; final c = context.colors;
return ListTile( return ListTile(
title: Text(label), title: Text(label),
trailing: isSelected ? Icon(Icons.check, color: s.primary) : null, trailing: isSelected ? Icon(Icons.check, color: c.brandPrimary) : null,
onTap: onTap, onTap: onTap,
); );
} }

View File

@@ -33,7 +33,7 @@ melos:
scripts: scripts:
analyze: analyze:
description: "Run flutter analyze in all packages" description: "Run flutter analyze in all packages"
run: melos exec -- flutter analyze . run: melos exec --concurrency=1 -- flutter analyze .
test: test:
description: "Run flutter test in all packages" description: "Run flutter test in all packages"