diff --git a/Doc/IM_App_架构设计.html b/Doc/IM_App_架构设计.html
index 2793eac..8d9dde1 100644
--- a/Doc/IM_App_架构设计.html
+++ b/Doc/IM_App_架构设计.html
@@ -3205,7 +3205,13 @@ flowchart LR
两大核心逻辑:
1. MVVM 分层职责:View(view/)只负责渲染和用户交互,ViewModel(presentation/)持有状态并处理业务逻辑,Model(model/ + entities/)定义数据结构 —— 三者通过 Riverpod Provider 连接,职责严格分离。
2. Riverpod 单向数据流:用户操作 → ref.read(vm.notifier).action() → ViewModel 处理逻辑 → state = newState → ref.watch(vm) 检测变化 → View 自动 rebuild。数据永远单向流动,UI 永远是状态的函数。
-3. Widget 纯展示原则:View(Widget)层对业务数据严格只读。所有逻辑(导航、CRUD、状态变更、条件判断)必须在 ViewModel 中完成,View 只调用 ViewModel 方法并渲染返回的 State。包括 demo/测试页面也不例外。
+3. Widget 纯展示原则:build() 只做一件事——把 State 属性映射成 Widget 树,不允许出现任何计算或逻辑。
+
+- 派生显示值必须是 State getter:凡是需要从 State 字段推导出另一个值(文本、颜色、样式等),一律写成 State 的 getter,Widget 只读取结果。例如:
String get buttonLabel => testStarted ? '结束' : '开始';,Widget 写 state.buttonLabel,不在 build() 里写三元。
+- 禁止
build() 内定义局部计算变量:final isSelected = current == mode 这类从构造参数/state 派生值的局部变量,不属于 build()。组件应接受已算好的 bool isSelected 参数,由父级或 State getter 负责计算。
+- 导航、CRUD、状态变更全部在 ViewModel 中完成,Widget 只转发事件:
onTap: () => ref.read(vm.notifier).doAction()。
+- demo/测试页面同样适用:demo 代码是示范,别人会照着模仿,写法必须与正式页面完全一致。
+
diff --git a/apps/im_app/lib/features/chat/presentation/.gitkeep b/apps/im_app/lib/features/chat/presentation/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/apps/im_app/lib/features/chat/presentation/chat_db_test_state.dart b/apps/im_app/lib/features/chat/presentation/chat_db_test_state.dart
new file mode 100644
index 0000000..c37e5ba
--- /dev/null
+++ b/apps/im_app/lib/features/chat/presentation/chat_db_test_state.dart
@@ -0,0 +1,39 @@
+// 数据库测试页状态(Demo,正式开发后随页面一并删除)
+
+/// 单条测试结果记录
+class TestResult {
+ final String title;
+ final String subtitle;
+ final String duration;
+
+ TestResult({
+ required this.title,
+ required this.subtitle,
+ required this.duration,
+ });
+}
+
+class ChatDbTestState {
+ final bool testStarted;
+ final List testResults;
+ final String currentState;
+
+ const ChatDbTestState({
+ this.testStarted = false,
+ this.testResults = const [],
+ this.currentState = '',
+ });
+
+ /// 按钮文案(Widget 直接读,不在 View 层做判断)
+ String get buttonLabel => testStarted ? '结束' : '开始';
+
+ ChatDbTestState copyWith({
+ bool? testStarted,
+ List? testResults,
+ String? currentState,
+ }) => ChatDbTestState(
+ testStarted: testStarted ?? this.testStarted,
+ testResults: testResults ?? this.testResults,
+ currentState: currentState ?? this.currentState,
+ );
+}
diff --git a/apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart b/apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart
index 1abaa52..4386f91 100644
--- a/apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart
+++ b/apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart
@@ -6,42 +6,12 @@ import 'package:im_app/app/di/db_provider.dart';
import 'package:im_app/data/local/drift/app_database.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
+import 'chat_db_test_state.dart';
+
+export 'chat_db_test_state.dart';
+
part 'chat_db_test_view_model.g.dart';
-class TestResult {
- final String title;
- final String subtitle;
- final String duration;
-
- TestResult({
- required this.title,
- required this.subtitle,
- required this.duration,
- });
-}
-
-class ChatDbTestState {
- final bool testStarted;
- final List testResults;
- final String currentState;
-
- const ChatDbTestState({
- this.testStarted = false,
- this.testResults = const [],
- this.currentState = '',
- });
-
- ChatDbTestState copyWith({
- bool? testStarted,
- List? testResults,
- String? currentState,
- }) => ChatDbTestState(
- testStarted: testStarted ?? this.testStarted,
- testResults: testResults ?? this.testResults,
- currentState: currentState ?? this.currentState,
- );
-}
-
@riverpod
class ChatDbTestViewModel extends _$ChatDbTestViewModel {
@override
diff --git a/apps/im_app/lib/features/chat/view/chat_db_test_page.dart b/apps/im_app/lib/features/chat/view/chat_db_test_page.dart
index 95c1f01..3c8c025 100644
--- a/apps/im_app/lib/features/chat/view/chat_db_test_page.dart
+++ b/apps/im_app/lib/features/chat/view/chat_db_test_page.dart
@@ -30,7 +30,7 @@ class ChatDbTestPage extends ConsumerWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
AppButton.inverse(
- label: state.testStarted ? '结束' : '开始',
+ label: state.buttonLabel,
onPressed: () => vm.toggleDBTest(),
),
SizedBox(width: 8),
diff --git a/apps/im_app/lib/features/settings/view/theme_view.dart b/apps/im_app/lib/features/settings/view/theme_view.dart
index e3e96a9..3151cb9 100644
--- a/apps/im_app/lib/features/settings/view/theme_view.dart
+++ b/apps/im_app/lib/features/settings/view/theme_view.dart
@@ -16,32 +16,27 @@ class ThemeView extends ConsumerWidget {
final current = ref.watch(themeViewModelProvider);
return Scaffold(
- appBar: AppBar(
- title: const Text('主题'),
- ),
+ appBar: AppBar(title: const Text('主题')),
body: ListView(
children: [
const SettingsSectionHeader(title: '外观'),
ThemeOptionTile(
label: '跟随系统',
- mode: ThemeMode.system,
- current: current,
+ isSelected: current == ThemeMode.system,
onTap: () => ref
.read(themeViewModelProvider.notifier)
.setMode(ThemeMode.system),
),
ThemeOptionTile(
label: '黑色模式',
- mode: ThemeMode.dark,
- current: current,
+ isSelected: current == ThemeMode.dark,
onTap: () => ref
.read(themeViewModelProvider.notifier)
.setMode(ThemeMode.dark),
),
ThemeOptionTile(
label: '白色模式',
- mode: ThemeMode.light,
- current: current,
+ isSelected: current == ThemeMode.light,
onTap: () => ref
.read(themeViewModelProvider.notifier)
.setMode(ThemeMode.light),
diff --git a/apps/im_app/lib/features/settings/view/widgets/theme_option_tile.dart b/apps/im_app/lib/features/settings/view/widgets/theme_option_tile.dart
index d270d9f..92b1df5 100644
--- a/apps/im_app/lib/features/settings/view/widgets/theme_option_tile.dart
+++ b/apps/im_app/lib/features/settings/view/widgets/theme_option_tile.dart
@@ -5,14 +5,13 @@ import '../../../../../core/ui/base/context_theme_ext.dart';
/// 单个主题选项行
///
/// 纯展示 + 事件透传,不感知任何 Provider。
-/// 由父级传入 [current] 判断选中状态,[onTap] 处理切换。
+/// 父级传入 [isSelected] 决定是否显示勾选图标,[onTap] 处理切换。
///
/// 用法:
/// ```dart
/// ThemeOptionTile(
/// label: '黑色模式',
-/// mode: ThemeMode.dark,
-/// current: current,
+/// isSelected: current == ThemeMode.dark,
/// onTap: () => ref.read(themeViewModelProvider.notifier).setMode(ThemeMode.dark),
/// )
/// ```
@@ -20,20 +19,17 @@ class ThemeOptionTile extends StatelessWidget {
const ThemeOptionTile({
super.key,
required this.label,
- required this.mode,
- required this.current,
+ required this.isSelected,
required this.onTap,
});
final String label;
- final ThemeMode mode;
- final ThemeMode current;
+ final bool isSelected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final s = context.styles;
- final isSelected = current == mode;
return ListTile(
title: Text(label),