Merge pull request '颜色,基础组件重新封装,降低理解难度,分层更明显。入口更抽象' (#11) from cody/UI_base_Refactor into dev
Reviewed-on: https://gitea.winwayinfo.com/CUS-IM/customer-im-client/pulls/11
This commit is contained in:
@@ -330,6 +330,20 @@
|
|||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 章节最后更新时间标签 */
|
||||||
|
.updated-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #999;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border: 1px solid #e0e3e8;
|
||||||
|
padding: 1px 8px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-left: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
</head>
|
</head>
|
||||||
@@ -2674,9 +2688,15 @@ final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 4. UseCase(按需)
|
/// 4. UseCase(登录有多步编排 — 校验 + 登录 + WS 连接 + DB 开库 + 用户持久化)
|
||||||
final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
|
final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
|
||||||
return LoginUseCase(authRepository: ref.read(authRepositoryProvider));
|
return LoginUseCase(
|
||||||
|
authRepository: ref.read(authRepositoryProvider),
|
||||||
|
socketManager: ref.read(socketManagerProvider),
|
||||||
|
apiConfig: ref.read(apiConfigProvider),
|
||||||
|
storageApi: ref.read(storageSdkProvider),
|
||||||
|
userRepository: ref.read(userRepositoryProvider),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
</code></pre>
|
</code></pre>
|
||||||
|
|
||||||
@@ -2977,7 +2997,7 @@ flowchart TD
|
|||||||
style CoreUI fill:#fff4e6,stroke:#f57c00
|
style CoreUI fill:#fff4e6,stroke:#f57c00
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 id="2-2-整体目录图">2.2 整体目录图</h3>
|
<h3 id="2-2-整体目录图">2.2 整体目录图<span class="updated-tag">最后更新 2026-03-10-10-17</span></h3>
|
||||||
|
|
||||||
<h4>图表说明</h4>
|
<h4>图表说明</h4>
|
||||||
|
|
||||||
@@ -2996,10 +3016,13 @@ flowchart TD
|
|||||||
│ │ └── guards/
|
│ │ └── guards/
|
||||||
│ │ └── auth_guard.dart # 登录守卫(switch AppRouteName,穷举防漏路由)
|
│ │ └── auth_guard.dart # 登录守卫(switch AppRouteName,穷举防漏路由)
|
||||||
│ │
|
│ │
|
||||||
│ └── di/ # 全局 DI — 手动装配的 Provider
|
│ ├── di/ # 全局 DI — 手动装配的 Provider
|
||||||
│ ├── network_provider.dart # NetworkMonitor + ApiConfig + NetworksSdkApi + SocketConfig + SocketClient + SocketManager
|
│ │ ├── network_provider.dart # NetworkMonitor + ApiConfig + NetworksSdkApi + SocketConfig + SocketClient + SocketManager
|
||||||
│ ├── db_provider.dart # StorageSdkApi(注入 AppDatabase factory)
|
│ │ ├── db_provider.dart # storageSdkProvider + storageSdkLifecycleProvider
|
||||||
│ └── app_providers.dart # AppInitializer + ThemeModeNotifier + AuthNotifier
|
│ │ ├── app_providers.dart # AppInitializer + ThemeModeNotifier + AuthNotifier
|
||||||
|
│ │ └── user_provider.dart # userRepositoryProvider / CRUD UseCase Providers / users + allUsers Stream
|
||||||
|
│ └── notifiers/ # 全局 Notifier(@riverpod,跨模块共用)
|
||||||
|
│ └── user_notifier.dart # UserNotifier — 单用户状态(family per-uid,含 watch + CRUD)
|
||||||
│
|
│
|
||||||
├── features/ # 功能模块(垂直切片):Feature 间禁止直接 import
|
├── features/ # 功能模块(垂直切片):Feature 间禁止直接 import
|
||||||
│ │
|
│ │
|
||||||
@@ -3012,18 +3035,33 @@ flowchart TD
|
|||||||
│ │ │ └── auth_providers.dart # authRepositoryProvider / loginUseCaseProvider
|
│ │ │ └── auth_providers.dart # authRepositoryProvider / loginUseCaseProvider
|
||||||
│ │ ├── presentation/
|
│ │ ├── presentation/
|
||||||
│ │ │ ├── login_view_model.dart # @riverpod ViewModel(生成 login_view_model.g.dart)
|
│ │ │ ├── login_view_model.dart # @riverpod ViewModel(生成 login_view_model.g.dart)
|
||||||
│ │ │ └── login_state.dart # @freezed State(生成 login_state.freezed.dart)
|
│ │ │ └── login_state.dart # @freezed State(step / contact / isLoading / error)
|
||||||
│ │ ├── usecases/
|
│ │ ├── usecases/
|
||||||
│ │ │ └── login_usecase.dart # 格式校验 → Repository → User Entity
|
│ │ │ └── login_usecase.dart # 校验 → OTP → 登录 → WS 连接 → DB 开库 → 用户持久化
|
||||||
│ │ └── view/
|
│ │ └── view/
|
||||||
│ │ └── login_page.dart # 登录页
|
│ │ ├── login_page.dart # 登录页(装配 + 控制器 + 回调)
|
||||||
|
│ │ └── widgets/
|
||||||
|
│ │ ├── login_phone_step.dart # 手机号输入步骤(纯展示)
|
||||||
|
│ │ └── login_otp_step.dart # 验证码输入步骤(纯展示)
|
||||||
│ │
|
│ │
|
||||||
│ ├── chat/ # 聊天 ── 开发中
|
│ ├── chat/ # 聊天 ── 开发中
|
||||||
|
│ │ ├── di/
|
||||||
|
│ │ │ └── chat_bot_provider.dart # chatBotRepositoryProvider / allChatBots / chatBot(id)
|
||||||
|
│ │ ├── call/ # 通话子模块
|
||||||
|
│ │ │ └── di/
|
||||||
|
│ │ │ └── call_log_provider.dart # callLogRepositoryProvider / allCallLogs / callLog(id)
|
||||||
│ │ ├── presentation/
|
│ │ ├── presentation/
|
||||||
│ │ │ └── chat_view_model.dart # @riverpod ViewModel
|
│ │ │ ├── chat_view_model.dart # @riverpod ViewModel(会话列表)
|
||||||
|
│ │ │ ├── chat_db_test_state.dart # Drift DB 测试 State(手写,含分页字段)
|
||||||
|
│ │ │ └── chat_db_test_view_model.dart # Drift DB 测试 ViewModel(startTest / loadMore)
|
||||||
|
│ │ ├── usecases/
|
||||||
|
│ │ │ ├── insert_users_use_case.dart # 批量插入用户(200 条/批,去重)
|
||||||
|
│ │ │ ├── update_users_use_case.dart # 更新前 10 条用户昵称
|
||||||
|
│ │ │ └── delete_users_use_case.dart # 删除前 10 条用户
|
||||||
│ │ └── view/
|
│ │ └── view/
|
||||||
│ │ ├── chat_page.dart # 会话列表页(Tab 1)
|
│ │ ├── chat_page.dart # 会话列表页(Tab 1)
|
||||||
│ │ └── chat_detail_page.dart # 聊天详情页
|
│ │ ├── chat_detail_page.dart # 聊天详情页
|
||||||
|
│ │ └── chat_db_test_page.dart # Drift DB 测试页
|
||||||
│ │
|
│ │
|
||||||
│ ├── contact/ # 通讯录 ── 骨架
|
│ ├── contact/ # 通讯录 ── 骨架
|
||||||
│ │ └── view/
|
│ │ └── view/
|
||||||
@@ -3031,12 +3069,12 @@ flowchart TD
|
|||||||
│ │
|
│ │
|
||||||
│ └── settings/ # 设置 ── 已实现(主题切换)
|
│ └── settings/ # 设置 ── 已实现(主题切换)
|
||||||
│ ├── di/
|
│ ├── di/
|
||||||
│ │ └── settings_providers.dart # settingsRepositoryProvider(待 storage_sdk 接入)
|
│ │ └── settings_providers.dart # setThemeUseCaseProvider
|
||||||
│ ├── presentation/
|
│ ├── presentation/
|
||||||
│ │ ├── settings_view_model.dart # @riverpod ViewModel(设置页导航)
|
│ │ ├── settings_view_model.dart # @riverpod ViewModel(设置页导航)
|
||||||
│ │ └── theme_view_model.dart # @riverpod ViewModel(生成 theme_view_model.g.dart)
|
│ │ └── theme_view_model.dart # @riverpod ViewModel(主题切换)
|
||||||
│ ├── usecases/
|
│ ├── usecases/
|
||||||
│ │ └── set_theme_usecase.dart # 主题切换用例
|
│ │ └── set_theme_usecase.dart # 主题切换用例(幂等校验,防重复切换)
|
||||||
│ └── view/
|
│ └── view/
|
||||||
│ ├── settings_page.dart # 设置主页(Tab 3)
|
│ ├── settings_page.dart # 设置主页(Tab 3)
|
||||||
│ ├── theme_view.dart # 主题选择页
|
│ ├── theme_view.dart # 主题选择页
|
||||||
@@ -3045,34 +3083,60 @@ flowchart TD
|
|||||||
│ └── theme_option_tile.dart
|
│ └── theme_option_tile.dart
|
||||||
│
|
│
|
||||||
├── domain/ # Domain 层(纯 Dart,零 Flutter / 零网络依赖)
|
├── domain/ # Domain 层(纯 Dart,零 Flutter / 零网络依赖)
|
||||||
│ ├── entities/
|
│ ├── entities/ # 21 个实体(每实体一文件)
|
||||||
│ │ └── user.dart # 用户实体
|
│ │ ├── user.dart # 用户
|
||||||
│ │ # message / conversation / contact 待开发
|
│ │ ├── message.dart # 消息
|
||||||
│ └── repositories/
|
│ │ ├── chat.dart # 会话
|
||||||
│ └── auth_repository.dart # abstract interface
|
│ │ ├── call_log.dart # 通话记录
|
||||||
│ # message / chat / contact_repository 待开发
|
│ │ ├── chat_bot.dart # 聊天机器人
|
||||||
|
│ │ ├── chat_category.dart # 会话分类
|
||||||
|
│ │ ├── group.dart # 群组
|
||||||
|
│ │ ├── group_member.dart # 群成员
|
||||||
|
│ │ ├── workspace.dart # 工作区
|
||||||
|
│ │ ├── company_member.dart # 企业成员
|
||||||
|
│ │ ├── favourite.dart / favourite_detail.dart
|
||||||
|
│ │ ├── sound.dart / tag.dart / retry.dart
|
||||||
|
│ │ ├── pending_friend_request_history.dart
|
||||||
|
│ │ ├── user_request_history.dart
|
||||||
|
│ │ └── {discover,explore,favorite,recent}_mini_app.dart
|
||||||
|
│ └── repositories/ # 4 个抽象接口
|
||||||
|
│ ├── auth_repository.dart # sendOtp / verifyOtp / login / logout
|
||||||
|
│ ├── user_repository.dart # watch / get / insert / update / upsert / delete
|
||||||
|
│ ├── call_log_repository.dart # 通话记录 CRUD + watch
|
||||||
|
│ └── chat_bot_repository.dart # 聊天机器人 CRUD + watch
|
||||||
│
|
│
|
||||||
├── data/ # Data 层(implements domain 接口)
|
├── data/ # Data 层(implements domain 接口)
|
||||||
│ ├── repositories/
|
│ ├── repositories/ # 4 个实现类
|
||||||
│ │ └── auth_repository_impl.dart # 认证仓库
|
│ │ ├── auth_repository_impl.dart # 认证(登录 / 登出 / Token 回调)
|
||||||
│ │ # message / chat / contact 待开发
|
│ │ ├── user_repository_impl.dart # 用户(Drift CRUD + Stream watch)
|
||||||
|
│ │ ├── call_log_repository_impl.dart # 通话记录
|
||||||
|
│ │ └── chat_bot_repository_impl.dart # 聊天机器人
|
||||||
│ ├── local/
|
│ ├── local/
|
||||||
│ │ └── drift/ # Drift 本地数据库
|
│ │ └── drift/ # Drift 本地数据库
|
||||||
│ │ ├── app_database.dart # @DriftDatabase 定义 + onUpgrade 自动补列
|
│ │ ├── app_database.dart # @DriftDatabase(19 张表)+ onUpgrade 自动补列
|
||||||
│ │ # database_connection.dart 已迁移至 storage_sdk(数据库生命周期统一在 SDK 层管理)
|
|
||||||
│ │ ├── mapper/
|
│ │ ├── mapper/
|
||||||
│ │ │ └── drift_path_mapper.dart # Drift 路径映射工具
|
│ │ │ └── drift_path_mapper.dart # Drift 路径映射工具
|
||||||
│ │ └── tables/
|
│ │ └── tables/ # 19 个表定义(每表一文件)
|
||||||
│ │ └── users.dart # Users 表定义
|
│ │ ├── users.dart / messages.dart / chats.dart / call_logs.dart
|
||||||
|
│ │ ├── chat_bots.dart / chat_categories.dart / groups.dart / workspaces.dart
|
||||||
|
│ │ ├── favourites.dart / favourite_details.dart
|
||||||
|
│ │ ├── sounds.dart / tags.dart / retries.dart
|
||||||
|
│ │ ├── pending_friend_request_histories.dart / user_request_histories.dart
|
||||||
|
│ │ └── {discover,explore,favorite,recent}_mini_apps.dart
|
||||||
│ ├── remote/ # Request 文件(一个端点一个文件)
|
│ ├── remote/ # Request 文件(一个端点一个文件)
|
||||||
|
│ │ ├── send_otp_request.dart # 发送验证码
|
||||||
|
│ │ ├── verify_otp_request.dart # 校验验证码
|
||||||
│ │ ├── login_request.dart # 登录
|
│ │ ├── login_request.dart # 登录
|
||||||
│ │ ├── logout_request.dart # 登出
|
│ │ ├── logout_request.dart # 登出
|
||||||
│ │ ├── get_profile_request.dart # 获取用户信息
|
│ │ ├── get_profile_request.dart # 获取用户信息
|
||||||
│ │ └── upload_file_request.dart # 文件上传
|
│ │ └── upload_file_request.dart # 文件上传
|
||||||
│ │ # send_message / 其他业务端点 待开发
|
│ └── models/ # 持久化 DTO(17 个,与表一一对应)
|
||||||
│ └── models/ # 持久化 DTO(@JsonSerializable)
|
│ ├── user_dto.dart / message_dto.dart / chat_dto.dart
|
||||||
│ └── user_dto.dart # 用户持久化 DTO
|
│ ├── chat_bot_dto.dart / chat_category_dto.dart / group_dto.dart / workspace_dto.dart
|
||||||
│ # message / conversation / contact_dto 待开发
|
│ ├── favourite_dto.dart / favourite_detail_dto.dart
|
||||||
|
│ ├── sound_dto.dart / tag_dto.dart / retry_dto.dart
|
||||||
|
│ ├── pending_friend_request_history_dto.dart / user_request_history_dto.dart
|
||||||
|
│ └── {discover,explore,favorite,recent}_mini_app_dto.dart
|
||||||
│
|
│
|
||||||
└── core/ # Core 层:零业务逻辑,禁止反向依赖 features / domain / data
|
└── core/ # Core 层:零业务逻辑,禁止反向依赖 features / domain / data
|
||||||
├── foundation/ # 基础配置(各为单独文件,非子目录)
|
├── foundation/ # 基础配置(各为单独文件,非子目录)
|
||||||
@@ -3092,12 +3156,12 @@ flowchart TD
|
|||||||
│ └── socket_manager.dart # WebSocket 生命周期(连接/断开/重连编排)
|
│ └── socket_manager.dart # WebSocket 生命周期(连接/断开/重连编排)
|
||||||
│
|
│
|
||||||
└── ui/ # Core UI(设计系统 + 可复用组件)
|
└── ui/ # Core UI(设计系统 + 可复用组件)
|
||||||
├── base/ # 设计 Token
|
├── base/ # 设计基础(颜色 / 字体 / 间距 / 圆角 / 阴影)
|
||||||
│ ├── assets.dart # 静态资源路径常量(AppAssets:logo / 占位图)
|
│ ├── assets.dart # 静态资源路径常量(AppAssets:logo / 占位图)
|
||||||
│ ├── icons.dart # 图标常量(AppIcons:导航 / 操作 / 聊天 / 用户 / 状态)
|
│ ├── icons.dart # 图标常量(AppIcons:导航 / 操作 / 聊天 / 用户 / 状态)
|
||||||
│ ├── app_theme.dart # ThemeData 组装(Light / Dark)
|
│ ├── app_theme.dart # ThemeData 组装(Light / Dark)
|
||||||
│ ├── colors.dart # 颜色体系(品牌色 / 语义色 / 灰阶)
|
│ ├── colors.dart # 颜色体系(品牌色 / 语义色 / 灰阶)
|
||||||
│ ├── context_theme_ext.dart # BuildContext 主题扩展(context.theme / context.colors)
|
│ ├── context_theme_ext.dart # BuildContext 扩展(context.colors / context.styles / context.shadows)
|
||||||
│ └── font.dart # 字体(TextStyle 定义 + textTheme(brightness))
|
│ └── font.dart # 字体(TextStyle 定义 + textTheme(brightness))
|
||||||
├── components/ # 原子组件
|
├── components/ # 原子组件
|
||||||
│ └── app_button.dart # 按钮
|
│ └── app_button.dart # 按钮
|
||||||
@@ -3106,7 +3170,7 @@ flowchart TD
|
|||||||
# app_dialog / app_toast / app_empty_state 等
|
# app_dialog / app_toast / app_empty_state 等
|
||||||
</code></pre>
|
</code></pre>
|
||||||
|
|
||||||
<h3 id="2-3-整体分层图">2.3 整体分层图(MVVM + Riverpod 数据流)</h3>
|
<h3 id="2-3-整体分层图">2.3 整体分层图(MVVM + Riverpod 数据流)<span class="updated-tag">最后更新 2026-03-10-10-17</span></h3>
|
||||||
|
|
||||||
<h4>图表说明</h4>
|
<h4>图表说明</h4>
|
||||||
|
|
||||||
@@ -3211,7 +3275,7 @@ flowchart LR
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<p><strong>两大核心逻辑</strong>:</p>
|
<p><strong>三大核心逻辑</strong>:</p>
|
||||||
<p>1. <strong>MVVM 分层职责</strong>:View(view/)只负责渲染和用户交互,ViewModel(presentation/)持有状态并处理业务逻辑,Model(model/ + entities/)定义数据结构 —— 三者通过 Riverpod Provider 连接,职责严格分离。</p>
|
<p>1. <strong>MVVM 分层职责</strong>:View(view/)只负责渲染和用户交互,ViewModel(presentation/)持有状态并处理业务逻辑,Model(model/ + entities/)定义数据结构 —— 三者通过 Riverpod Provider 连接,职责严格分离。</p>
|
||||||
<p>2. <strong>Riverpod 单向数据流</strong>:用户操作 → <code>ref.read(vm.notifier).action()</code> → ViewModel 处理逻辑 → <code>state = newState</code> → <code>ref.watch(vm)</code> 检测变化 → View 自动 rebuild。数据永远单向流动,UI 永远是状态的函数。</p>
|
<p>2. <strong>Riverpod 单向数据流</strong>:用户操作 → <code>ref.read(vm.notifier).action()</code> → ViewModel 处理逻辑 → <code>state = newState</code> → <code>ref.watch(vm)</code> 检测变化 → View 自动 rebuild。数据永远单向流动,UI 永远是状态的函数。</p>
|
||||||
<p>3. <strong>Widget 纯展示原则</strong>:<code>build()</code> 只做一件事——把 State 属性映射成 Widget 树,不允许出现任何计算或逻辑。</p>
|
<p>3. <strong>Widget 纯展示原则</strong>:<code>build()</code> 只做一件事——把 State 属性映射成 Widget 树,不允许出现任何计算或逻辑。</p>
|
||||||
@@ -3461,7 +3525,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 +3586,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 +3685,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<BoxShadow> get bs4; // Elevation 4 — 小卡片
|
||||||
|
List<BoxShadow> get bs8; // Elevation 8 — Card
|
||||||
|
List<BoxShadow> get bs12; // Elevation 12 — Dropdown
|
||||||
|
List<BoxShadow> 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,
|
||||||
|
onPrimary: c.textOnPrimary,
|
||||||
|
surface: c.bgSecondary,
|
||||||
|
onSurface: c.textPrimary,
|
||||||
|
error: c.statusError,
|
||||||
|
onError: c.textOnPrimary,
|
||||||
|
// ...
|
||||||
),
|
),
|
||||||
textTheme: TextTheme(
|
appBarTheme: AppBarTheme(backgroundColor: c.bgPrimary, foregroundColor: c.textPrimary),
|
||||||
displayLarge: AppTypography.displayLarge.copyWith(color: AppColors.white),
|
elevatedButtonTheme: ElevatedButtonThemeData(style: ElevatedButton.styleFrom(
|
||||||
headlineMedium: AppTypography.headlineMedium.copyWith(color: AppColors.white),
|
backgroundColor: c.brandPrimary, foregroundColor: c.textOnPrimary,
|
||||||
bodyLarge: AppTypography.bodyLarge.copyWith(color: AppColors.gray100),
|
)),
|
||||||
|
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 +3777,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 +3787,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 +3848,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 +3877,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 +3902,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 +3973,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 +4214,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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -5171,19 +5242,19 @@ flowchart TD
|
|||||||
|
|
||||||
<div class="mermaid">
|
<div class="mermaid">
|
||||||
flowchart TD
|
flowchart TD
|
||||||
UI[UI Layer] -->|用户操作: vm.method()| VM[ViewModel]
|
UI["UI Layer"] -->|"用户操作 vm.method"| VM["ViewModel"]
|
||||||
VM -->|调用| Repo[Repository]
|
VM -->|"调用"| Repo["Repository"]
|
||||||
VM -.复杂场景.-> UC[UseCase(按需)]
|
VM -.->|"复杂场景"| UC["UseCase(按需)"]
|
||||||
UC -.-> Repo
|
UC -.-> Repo
|
||||||
Repo -->|返回 Entity| VM
|
Repo -->|"返回 Entity"| VM
|
||||||
VM -->|state = newState| State[UI State]
|
VM -->|"state = newState"| UIState["UI State"]
|
||||||
State -->|ref.watch 自动刷新| UI
|
UIState -->|"ref.watch 自动刷新"| UI
|
||||||
|
|
||||||
style UI fill:#e1f5ff,stroke:#0288d1
|
style UI fill:#e1f5ff,stroke:#0288d1
|
||||||
style VM fill:#fff4e6,stroke:#f57c00
|
style VM fill:#fff4e6,stroke:#f57c00
|
||||||
style Repo fill:#e8f5e9,stroke:#388e3c
|
style Repo fill:#e8f5e9,stroke:#388e3c
|
||||||
style UC fill:#f3e5f5,stroke:#7b1fa2,stroke-dasharray: 5 5
|
style UC fill:#f3e5f5,stroke:#7b1fa2
|
||||||
style State fill:#fff4e6,stroke:#f57c00
|
style UIState fill:#fff4e6,stroke:#f57c00
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4>Riverpod ViewModel 实现方式</h4>
|
<h4>Riverpod ViewModel 实现方式</h4>
|
||||||
@@ -6199,9 +6270,15 @@ final authRepositoryProvider = Provider<AuthRepository>((ref) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// UseCase(按需 — 登录有多步编排,所以需要 UseCase)
|
// UseCase(登录有多步编排 — 校验 + 登录 + WS 连接 + DB 开库 + 用户持久化)
|
||||||
final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
|
final loginUseCaseProvider = Provider<LoginUseCase>((ref) {
|
||||||
return LoginUseCase(authRepository: ref.read(authRepositoryProvider));
|
return LoginUseCase(
|
||||||
|
authRepository: ref.read(authRepositoryProvider),
|
||||||
|
socketManager: ref.read(socketManagerProvider),
|
||||||
|
apiConfig: ref.read(apiConfigProvider),
|
||||||
|
storageApi: ref.read(storageSdkProvider),
|
||||||
|
userRepository: ref.read(userRepositoryProvider),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
</code></pre>
|
</code></pre>
|
||||||
|
|
||||||
@@ -6870,7 +6947,7 @@ final user = await db.selectFirst(appDb.users, (t) => t.uid.equals(uid));
|
|||||||
<p><strong>为什么独立 Package</strong>:国际化服务于 core/ui(组件内置文案)和 Feature 层(页面文案、错误提示展示),作为独立 SDK 可跨项目复用翻译基础设施。<strong>注意</strong>:foundation 本身不依赖 l10n_sdk —— 错误映射仅产出错误码/错误键,由 Presentation / UI 层通过 l10n_sdk 转为本地化文案,从而避免 foundation ↔ l10n 双向依赖。</p>
|
<p><strong>为什么独立 Package</strong>:国际化服务于 core/ui(组件内置文案)和 Feature 层(页面文案、错误提示展示),作为独立 SDK 可跨项目复用翻译基础设施。<strong>注意</strong>:foundation 本身不依赖 l10n_sdk —— 错误映射仅产出错误码/错误键,由 Presentation / UI 层通过 l10n_sdk 转为本地化文案,从而避免 foundation ↔ l10n 双向依赖。</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
|
||||||
<h3 id="7-5-core-ui">7.5 Core UI(core/ui/)</h3>
|
<h3 id="7-5-core-ui">7.5 Core UI(core/ui/)<span class="updated-tag">最后更新 2026-03-10-10-17</span></h3>
|
||||||
|
|
||||||
<p>UI 基础设施,为所有 Feature 提供统一的视觉规范和可复用组件。三层结构自底向上构建:</p>
|
<p>UI 基础设施,为所有 Feature 提供统一的视觉规范和可复用组件。三层结构自底向上构建:</p>
|
||||||
|
|
||||||
@@ -6879,10 +6956,13 @@ final user = await db.selectFirst(appDb.users, (t) => 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>
|
||||||
@@ -9826,7 +9906,7 @@ flowchart TD
|
|||||||
|
|
||||||
<p>本章定义颜色、字体、组件、弹框、图标的命名与使用规则,明确设计与研发的协作约定。Figma 按此命名,代码按此封装,两端名称一一对应。</p>
|
<p>本章定义颜色、字体、组件、弹框、图标的命名与使用规则,明确设计与研发的协作约定。Figma 按此命名,代码按此封装,两端名称一一对应。</p>
|
||||||
|
|
||||||
<h3 id="8-0-核心约定">8.0 核心约定</h3>
|
<h3 id="8-0-核心约定">8.0 核心约定<span class="updated-tag">最后更新 2026-03-10-10-17</span></h3>
|
||||||
|
|
||||||
<div style="background: #e3f2fd; padding: 20px; border-radius: 8px; border-left: 4px solid #1565c0; margin: 20px 0;">
|
<div style="background: #e3f2fd; padding: 20px; border-radius: 8px; border-left: 4px solid #1565c0; margin: 20px 0;">
|
||||||
<p style="margin-top: 0; font-weight: 700; color: #1565c0;">全局只有一份</p>
|
<p style="margin-top: 0; font-weight: 700; color: #1565c0;">全局只有一份</p>
|
||||||
@@ -9853,39 +9933,76 @@ flowchart TD
|
|||||||
<strong>图片和组件是重灾区:</strong>没有统一来源时,不同研发各自导出同一张图,文件名不同、尺寸不同,最终项目里堆满重复文件。Figma 统一命名、代码统一注册,才能从源头堵住。
|
<strong>图片和组件是重灾区:</strong>没有统一来源时,不同研发各自导出同一张图,文件名不同、尺寸不同,最终项目里堆满重复文件。Figma 统一命名、代码统一注册,才能从源头堵住。
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 id="8-1-颜色体系">8.1 颜色体系</h3>
|
<h3 id="8-1-颜色体系">8.1 颜色体系<span class="updated-tag">最后更新 2026-03-10-10-17</span></h3>
|
||||||
|
|
||||||
<p>所有颜色通过抽象名称引用。抽象名在亮色 / 暗色两套主题下对应不同色值,修改主题只需改映射表,不需逐个找组件。</p>
|
<p>颜色采用 5 层架构:<strong>ColorBases(颜色常量)→ AppColors(语义接口)→ LightColors/DarkColors(亮暗实现)→ AppTheme._build(AppColors c)(主题组装)→ context.colors(Widget 消费)</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>
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,18 @@ 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(实现)
|
||||||
|
/// → ★ AppTheme._build(AppColors c) ★ ← 你在这里
|
||||||
/// → MaterialApp(theme: AppTheme.theme, darkTheme: AppTheme.darkTheme)
|
/// → MaterialApp(theme: AppTheme.theme, darkTheme: AppTheme.darkTheme)
|
||||||
/// → Theme.of(context) → 所有 Widget 自动响应主题变化
|
/// → 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,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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.colors(Widget 消费)
|
||||||
|
// ```
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
@@ -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 Extension(context.shadows)
|
/// → context.shadows(View 层消费)
|
||||||
/// → 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
|
|
||||||
color: _shadowColor,
|
|
||||||
|
|
||||||
/// 模糊半径(影响阴影扩散范围)
|
|
||||||
blurRadius: blur,
|
|
||||||
|
|
||||||
/// 阴影偏移
|
|
||||||
offset: Offset(0, dy),
|
|
||||||
)
|
|
||||||
];
|
];
|
||||||
}
|
|
||||||
|
|
||||||
/// Elevation 4
|
/// Elevation 4 — 小卡片 / List Item
|
||||||
///
|
List<BoxShadow> get bs4 => _shadow(blur: 4, dy: 2);
|
||||||
/// 适用场景:
|
|
||||||
/// - List Item
|
|
||||||
/// - 小卡片
|
|
||||||
List<BoxShadow> get bs4 =>
|
|
||||||
_shadow(
|
|
||||||
blur: 4,
|
|
||||||
dy: 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Elevation 8
|
/// Elevation 8 — Card / 商品卡片
|
||||||
///
|
List<BoxShadow> get bs8 => _shadow(blur: 8, dy: 4);
|
||||||
/// 适用场景:
|
|
||||||
/// - Card
|
|
||||||
/// - 商品卡片
|
|
||||||
List<BoxShadow> get bs8 =>
|
|
||||||
_shadow(
|
|
||||||
blur: 8,
|
|
||||||
dy: 4,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Elevation 12
|
/// Elevation 12 — Dropdown / Popover
|
||||||
///
|
List<BoxShadow> get bs12 => _shadow(blur: 12, dy: 8);
|
||||||
/// 适用场景:
|
|
||||||
/// - Dropdown
|
|
||||||
/// - Popover
|
|
||||||
List<BoxShadow> get bs12 =>
|
|
||||||
_shadow(
|
|
||||||
blur: 12,
|
|
||||||
dy: 8,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Elevation 16
|
/// Elevation 16 — Dialog / Modal / 悬浮面板
|
||||||
///
|
List<BoxShadow> get bs16 => _shadow(blur: 16, dy: 8);
|
||||||
/// 适用场景:
|
|
||||||
/// - 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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('返回修改手机号'),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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('获取验证码'),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user