flu-cli 0.0.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CLI.md +349 -0
  2. package/README.md +59 -276
  3. package/config/dev.config.js +56 -0
  4. package/config/templates.js +147 -0
  5. package/index.js +128 -81
  6. package/lib/commands/add.js +472 -0
  7. package/lib/commands/cache.js +99 -0
  8. package/lib/commands/completion.js +94 -0
  9. package/lib/commands/generate.js +26 -0
  10. package/lib/commands/newClack.js +396 -0
  11. package/lib/commands/snippets.js +39 -0
  12. package/lib/commands/templates.js +84 -0
  13. package/lib/generators/component_generator.js +93 -0
  14. package/lib/generators/model_generator.js +303 -0
  15. package/lib/generators/module_generator.js +141 -0
  16. package/lib/generators/page_generator.js +322 -0
  17. package/lib/generators/project_generator.js +96 -0
  18. package/lib/generators/service_generator.js +408 -0
  19. package/lib/generators/state_manager_generator.js +402 -0
  20. package/lib/generators/viewmodel_generator.js +115 -0
  21. package/lib/generators/widget_generator.js +104 -0
  22. package/lib/templates/templateCopier.js +296 -0
  23. package/lib/templates/templateManager.js +191 -0
  24. package/lib/utils/config.js +99 -0
  25. package/lib/utils/flutterHelper.js +85 -0
  26. package/lib/utils/index_updater.js +69 -0
  27. package/lib/utils/logger.js +57 -0
  28. package/lib/utils/project_detector.js +227 -0
  29. package/lib/utils/snippet_loader.js +32 -0
  30. package/lib/utils/string_helper.js +56 -0
  31. package/lib/utils/templateSelectorEnquirer.js +200 -0
  32. package/package.json +31 -6
  33. package/release.sh +107 -0
  34. package/scripts/e2e-state-tests.js +116 -0
  35. package/scripts/sync-base-to-templates.js +108 -0
  36. package/scripts/workspace-clone-all.sh +101 -0
  37. package/scripts/workspace-status-all.sh +112 -0
  38. package/templates/README.md +138 -0
  39. package/templates/base_files/base_list_page.dart.template +174 -0
  40. package/templates/base_files/base_list_viewmodel.dart.template +134 -0
  41. package/templates/base_files/base_page.dart.template +251 -0
  42. package/templates/base_files/base_viewmodel.dart.template +77 -0
  43. package/templates/base_files/theme/status_views_theme.dart.template +46 -0
  44. package/templates/snippets/dart.code-snippets +487 -0
  45. package/lib/createProject.js +0 -220
  46. package/lib/flutterProjectCreator.js +0 -80
  47. package/lib/libCopier.js +0 -368
  48. package/lib/userInteraction.js +0 -274
  49. package/lib/utils.js +0 -200
  50. package/publish.sh +0 -29
@@ -0,0 +1,138 @@
1
+ # Flutter 模板
2
+
3
+ ## 📁 独立仓库架构
4
+
5
+ 从 V2 开始,每个模板都有自己的独立 Git 仓库:
6
+
7
+ | 模板 | 仓库地址 | 说明 |
8
+ | ----------- | -------------------------------------------------------------- | --------------------------------------- |
9
+ | **lite** | [template-lite](https://gitee.com/flu-cli/template-lite) | 轻量级模板,适合快速原型开发 |
10
+ | **modular** | [template-modular](https://gitee.com/flu-cli/template-modular) | 模块化模板,适合中大型项目 |
11
+ | **clean** | [template-clean](https://gitee.com/flu-cli/template-clean) | Clean Architecture 模板,适合企业级应用 |
12
+
13
+ ## 🎯 为什么使用独立仓库?
14
+
15
+ ### 优势
16
+
17
+ 1. **独立维护**
18
+
19
+ - 每个模板可以独立更新和发版
20
+ - 不同团队可以维护不同模板
21
+ - 版本控制更清晰
22
+
23
+ 2. **减小主仓库体积**
24
+
25
+ - flu-cli 主仓库不包含模板代码
26
+ - 下载和克隆更快
27
+
28
+ 3. **灵活的版本控制**
29
+
30
+ - 用户可以选择特定版本的模板
31
+ - 支持模板的独立版本号
32
+
33
+ 4. **更好的协作**
34
+ - 模板贡献者不需要访问主仓库
35
+ - 降低协作门槛
36
+
37
+ ## 🚀 使用方式
38
+
39
+ ### 通过 flu-cli 使用(推荐)
40
+
41
+ ```bash
42
+ # flu-cli 会自动从远程下载模板
43
+ flu-cli new my_app --template lite
44
+
45
+ # 或使用本地模板(如果已克隆)
46
+ flu-cli new my_app --template lite
47
+ ```
48
+
49
+ ### 直接克隆模板
50
+
51
+ ```bash
52
+ # 克隆模板仓库
53
+ git clone https://gitee.com/flu-cli/template-lite.git my_app
54
+ cd my_app
55
+
56
+ # 初始化(创建 pubspec.yaml)
57
+ ./init.sh
58
+
59
+ # 运行
60
+ flutter run
61
+ ```
62
+
63
+ ## 📝 本地开发模板
64
+
65
+ 如果你想在本地开发和测试模板:
66
+
67
+ ```bash
68
+ # 1. 克隆模板到 templates/ 目录
69
+ cd templates/
70
+ git clone https://gitee.com/flu-cli/template-lite.git lite
71
+ git clone https://gitee.com/flu-cli/template-modular.git modular
72
+ git clone https://gitee.com/flu-cli/template-clean.git clean
73
+
74
+ # 2. 进入模板目录开发
75
+ cd lite
76
+ ./init.sh
77
+ flutter run
78
+
79
+ # 3. 提交更改到模板仓库
80
+ git add .
81
+ git commit -m "Update template"
82
+ git push
83
+ ```
84
+
85
+ ## 🔄 模板更新流程
86
+
87
+ ### 更新模板
88
+
89
+ ```bash
90
+ # 进入模板目录
91
+ cd templates/lite
92
+
93
+ # 拉取最新代码
94
+ git pull
95
+
96
+ # 测试
97
+ ./init.sh
98
+ flutter run
99
+
100
+ # 提交更改
101
+ git add .
102
+ git commit -m "Update: xxx"
103
+ git push
104
+ ```
105
+
106
+ ### 用户更新模板
107
+
108
+ ```bash
109
+ # flu-cli 会自动检查更新
110
+ flu-cli update
111
+
112
+ # 或强制更新
113
+ flu-cli new my_app --template lite --no-cache
114
+ ```
115
+
116
+ ## 📚 相关文档
117
+
118
+ - [双文件策略说明](../MIGRATION_PLAN.md#双文件策略)
119
+ - [迁移到 Gitee 组织](../MIGRATION_PLAN.md)
120
+ - [模板开发指南](https://gitee.com/flu-cli/flu-cli-docs)
121
+
122
+ ## 💡 注意事项
123
+
124
+ 1. **flu-cli 主仓库不跟踪 templates/ 内容**
125
+
126
+ - templates/ 已添加到 .gitignore
127
+ - 只保留此 README.md
128
+
129
+ 2. **每个模板都是独立的 Git 仓库**
130
+
131
+ - 有自己的 .git 目录
132
+ - 独立的提交历史
133
+ - 独立的版本号
134
+
135
+ 3. **本地开发时**
136
+ - 可以在 templates/ 下克隆模板仓库
137
+ - 修改后提交到对应的模板仓库
138
+ - 不会影响 flu-cli 主仓库
@@ -0,0 +1,174 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ import 'base_list_viewmodel.dart';
4
+ import 'base_page.dart';
5
+ import 'theme/status_views_theme.dart';
6
+
7
+ /// 列表页面基类:在 BasePage 基础上扩展列表体与触底加载能力
8
+ abstract class BaseListPage<T, VM extends BaseListViewModel<T>>
9
+ extends BasePage<VM> {
10
+ /// 构造函数
11
+ const BaseListPage({super.key});
12
+
13
+ /// 子类必须实现:创建 ViewModel 实例
14
+ VM createListViewModel();
15
+
16
+ /// 渲染单个条目
17
+ Widget buildItem(BuildContext context, VM vm, T item, int index);
18
+
19
+ /// 可选:头部、尾部、状态页覆盖(函数级注释)
20
+
21
+ /// 构建成功态的列表体(函数级注释)
22
+ /// 返回 null 使用默认 ListView.builder;可自定义 SliverList/GridView 等容器
23
+ Widget? buildListWidget(BuildContext context, VM vm) => null;
24
+ Widget? buildHeader(BuildContext context, VM vm) => null;
25
+ Widget? buildFooter(BuildContext context, VM vm) => null;
26
+ Widget? buildLoading(BuildContext context, VM vm) => null;
27
+ Widget? buildError(BuildContext context, VM vm, String? msg) => null;
28
+ Widget? buildEmpty(BuildContext context, VM vm) => null;
29
+
30
+ /// 可选:替换底部“加载更多”视图
31
+ Widget? loadMoreFooterBuilder(
32
+ BuildContext context,
33
+ VM vm,
34
+ bool hasMore,
35
+ bool isLoadingMore,
36
+ ) =>
37
+ null;
38
+
39
+ /// 刷新容器包裹(用于接入第三方刷新库)
40
+ Widget refreshWrapperBuilder(BuildContext context, Widget child, VM vm) =>
41
+ child;
42
+
43
+ /// 列表体外层包裹(函数级注释)
44
+ /// 用于添加 Scrollbar、NotificationListener、吸顶 Header 等额外结构
45
+ Widget listWrapperBuilder(BuildContext context, Widget list, VM vm) => list;
46
+
47
+ /// 提供自定义 ScrollController(函数级注释)
48
+ /// 返回 null 使用内部控制器;非空时由外部管理其生命周期
49
+ ScrollController? provideScrollController(BuildContext context, VM vm) =>
50
+ null;
51
+
52
+ /// 滚动事件回调(函数级注释)
53
+ /// 可用于自定义触底策略或滚动埋点
54
+ void onScroll(BuildContext context, ScrollPosition position, VM vm) {}
55
+
56
+ /// 是否启用自动触底加载
57
+ bool get enableAutoLoadMore => true;
58
+
59
+ /// 触底阈值
60
+ double get loadMoreThreshold => 100;
61
+
62
+ /// 是否启用下拉刷新(透传到 BasePageState)
63
+ bool get enableRefresh => false;
64
+
65
+ @override
66
+ State<BaseListPage<T, VM>> createState() => _BaseListPageState<T, VM>();
67
+ }
68
+
69
+ class _BaseListPageState<T, VM extends BaseListViewModel<T>>
70
+ extends BasePageState<VM, BaseListPage<T, VM>> {
71
+ late final ScrollController _controller =
72
+ widget.provideScrollController(context, viewModel) ?? ScrollController();
73
+
74
+ /// 创建 ViewModel(使用注入的实例)
75
+ @override
76
+ VM createViewModel() => widget.createListViewModel();
77
+
78
+ /// 页面是否为空:依据列表数据判断
79
+ @override
80
+ bool get isEmptyContent => viewModel.items.isEmpty;
81
+
82
+ @override
83
+ void initState() {
84
+ super.initState();
85
+ _controller.addListener(_onScroll);
86
+ }
87
+
88
+ @override
89
+ void dispose() {
90
+ _controller.removeListener(_onScroll);
91
+ _controller.dispose();
92
+ super.dispose();
93
+ }
94
+
95
+ /// 触底检测:接近底部时尝试加载更多
96
+ void _onScroll() {
97
+ widget.onScroll(context, _controller.position, viewModel);
98
+ if (!widget.enableAutoLoadMore) return;
99
+ if (!_controller.hasClients) return;
100
+ final max = _controller.position.maxScrollExtent;
101
+ final offset = _controller.offset;
102
+ if (offset >= max - widget.loadMoreThreshold) {
103
+ viewModel.loadMore();
104
+ }
105
+ }
106
+
107
+ /// 构建成功态内容:列表 + 底部 Footer
108
+ @override
109
+ Widget buildContent(BuildContext context) {
110
+ Widget body = _buildListWidget();
111
+ if (widget.enableRefresh) {
112
+ body = widget.refreshWrapperBuilder(context, body, viewModel);
113
+ }
114
+ return body;
115
+ }
116
+
117
+ /// 构建列表视图:支持自定义或默认实现
118
+ Widget _buildListWidget() {
119
+ final vm = viewModel;
120
+ final theme = Theme.of(context).extension<StatusViewsTheme>();
121
+ final list = widget.buildListWidget(context, vm) ??
122
+ ListView.builder(
123
+ controller: _controller,
124
+ itemCount: vm.items.length + 1,
125
+ itemBuilder: (ctx, i) {
126
+ if (i == 0) {
127
+ final header = widget.buildHeader(ctx, vm);
128
+ if (header != null) return header;
129
+ }
130
+ final last = i == vm.items.length;
131
+ if (last) {
132
+ final localFooter = widget.buildFooter(ctx, vm);
133
+ if (localFooter != null) return localFooter;
134
+
135
+ if (vm.loadMoreFailed) {
136
+ return Padding(
137
+ padding: const EdgeInsets.all(16),
138
+ child: Column(
139
+ mainAxisSize: MainAxisSize.min,
140
+ children: [
141
+ Text(
142
+ vm.loadMoreError?.toString() ?? '加载更多失败',
143
+ style: const TextStyle(color: Colors.red),
144
+ ),
145
+ const SizedBox(height: 8),
146
+ ElevatedButton(
147
+ onPressed: vm.loadMore,
148
+ child: const Text('重试'),
149
+ ),
150
+ ],
151
+ ),
152
+ );
153
+ }
154
+
155
+ final globalFooter =
156
+ theme?.loadMoreBuilder?.call(ctx, vm.hasMore);
157
+ if (globalFooter != null) return globalFooter;
158
+
159
+ return Padding(
160
+ padding: const EdgeInsets.all(16),
161
+ child: Center(
162
+ child: vm.hasMore
163
+ ? const CircularProgressIndicator()
164
+ : const Text('没有更多了'),
165
+ ),
166
+ );
167
+ }
168
+ final item = vm.items[i];
169
+ return widget.buildItem(ctx, vm, item, i);
170
+ },
171
+ );
172
+ return widget.listWrapperBuilder(context, list, vm);
173
+ }
174
+ }
@@ -0,0 +1,134 @@
1
+ import 'base_viewmodel.dart';
2
+
3
+ /// 列表基础 ViewModel(轻量版)
4
+ ///
5
+ /// - 提供分页/刷新/加载更多的最小实现
6
+ abstract class BaseListViewModel<T> extends BaseViewModel {
7
+ final List<T> items = <T>[];
8
+ int page = 1;
9
+ int pageSize = 20;
10
+ bool hasMore = true;
11
+ bool isLoadingMore = false;
12
+ bool loadMoreFailed = false;
13
+ Object? loadMoreError;
14
+
15
+ /// 子类必须实现:拉取一页数据(函数级注释)
16
+ Future<List<T>> fetchPage({required int page, required int pageSize});
17
+
18
+ /// 初始化加载(函数级注释)
19
+ @override
20
+ Future<void> onInit() async {
21
+ super.onInit();
22
+ await loadData();
23
+ }
24
+
25
+ /// 首屏加载(函数级注释)
26
+ Future<void> loadData() async {
27
+ setState(ViewState.loading);
28
+ page = 1;
29
+ hasMore = true;
30
+ try {
31
+ final list = await fetchPage(page: page, pageSize: pageSize);
32
+ items
33
+ ..clear()
34
+ ..addAll(list);
35
+ _configHasMore(list);
36
+ setState(ViewState.success);
37
+ } catch (e) {
38
+ setState(ViewState.error, error: e.toString());
39
+ }
40
+ }
41
+
42
+ /// 下拉刷新(函数级注释)
43
+ @override
44
+ Future<void> refreshData() async {
45
+ if (isRefreshing) return;
46
+ setRefreshing(true);
47
+ final old = state;
48
+ try {
49
+ page = 1;
50
+ hasMore = true;
51
+ final list = await fetchPage(page: page, pageSize: pageSize);
52
+ items
53
+ ..clear()
54
+ ..addAll(list);
55
+ _configHasMore(list);
56
+ setState(ViewState.success);
57
+ } catch (_) {
58
+ // 刷新失败回退原状态
59
+ setState(old);
60
+ } finally {
61
+ setRefreshing(false);
62
+ }
63
+ }
64
+
65
+ /// 加载更多(函数级注释)
66
+ Future<void> loadMore() async {
67
+ if (!hasMore || isLoadingMore) return;
68
+ isLoadingMore = true;
69
+ loadMoreFailed = false;
70
+ loadMoreError = null;
71
+ try {
72
+ final next = page + 1;
73
+ final list = await fetchPage(page: next, pageSize: pageSize);
74
+ if (list.isNotEmpty) {
75
+ items.addAll(list);
76
+ page = next;
77
+ }
78
+ _configHasMore(list);
79
+ } catch (e) {
80
+ loadMoreFailed = true;
81
+ loadMoreError = e;
82
+ } finally {
83
+ isLoadingMore = false;
84
+ notifyDataChange();
85
+ }
86
+ }
87
+
88
+ /// 重置分页(函数级注释)
89
+ void resetPaging({int initialPage = 1, int pageSize = 20}) {
90
+ page = initialPage;
91
+ this.pageSize = pageSize;
92
+ hasMore = true;
93
+ items.clear();
94
+ loadMoreFailed = false;
95
+ loadMoreError = null;
96
+ notifyListeners();
97
+ setState(ViewState.idle);
98
+ }
99
+
100
+ /// 计算是否还有更多(函数级注释)
101
+ void _configHasMore(List<T> data) {
102
+ hasMore = data.length >= pageSize;
103
+ loadMoreFailed = false;
104
+ loadMoreError = null;
105
+ }
106
+
107
+ /// 清空列表
108
+ void clearItems() {
109
+ items.clear();
110
+ page = 1;
111
+ hasMore = true;
112
+ notifyListeners();
113
+ }
114
+
115
+ /// 数据操作:添加(函数级注释)
116
+ void addItem(T item) {
117
+ items.add(item);
118
+ notifyListeners();
119
+ }
120
+
121
+ /// 数据操作:移除(函数级注释)
122
+ void removeItem(T item) {
123
+ items.remove(item);
124
+ notifyListeners();
125
+ }
126
+
127
+ /// 数据操作:更新(函数级注释)
128
+ void updateItem(int index, T item) {
129
+ if (index >= 0 && index < items.length) {
130
+ items[index] = item;
131
+ notifyListeners();
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,251 @@
1
+ import 'package:flutter/material.dart';
2
+
3
+ import 'base_viewmodel.dart';
4
+ import 'theme/status_views_theme.dart';
5
+
6
+ /// Page 基类
7
+ ///
8
+ /// 提供统一的页面结构和状态处理
9
+ ///
10
+ /// 示例:
11
+ /// ```dart
12
+ /// class HomePage extends BasePage<HomeViewModel> {
13
+ /// const HomePage({super.key});
14
+ ///
15
+ /// @override
16
+ /// State<HomePage> createState() => _HomePageState();
17
+ /// }
18
+ ///
19
+ /// class _HomePageState extends BasePageState<HomeViewModel, HomePage> {
20
+ /// @override
21
+ /// HomeViewModel createViewModel() => HomeViewModel();
22
+ ///
23
+ /// @override
24
+ /// String get title => '首页';
25
+ ///
26
+ /// @override
27
+ /// Widget buildContent(BuildContext context) {
28
+ /// return Center(
29
+ /// child: Text('Counter: ${viewModel.counter}'),
30
+ /// );
31
+ /// }
32
+ /// }
33
+ /// ```
34
+ abstract class BasePage<VM extends BaseViewModel> extends StatefulWidget {
35
+ const BasePage({super.key});
36
+
37
+ /// 是否显示 AppBar(默认 true)
38
+ bool get showAppBar => true;
39
+
40
+ /// AppBar 标题(默认空串)
41
+ String get title => '';
42
+
43
+ /// AppBar 操作按钮(默认 null)
44
+ List<Widget>? get actions => null;
45
+
46
+ /// 是否显示返回按钮(默认自动判断)
47
+ bool? get showBackButton => null;
48
+
49
+ /// 背景颜色(默认 null)
50
+ Color? get backgroundColor => null;
51
+
52
+ /// 是否启用 SafeArea(默认 true)
53
+ bool get useSafeArea => true;
54
+
55
+ @override
56
+ State<BasePage<VM>> createState();
57
+ }
58
+
59
+ /// Page State 基类
60
+ abstract class BasePageState<VM extends BaseViewModel, T extends BasePage<VM>>
61
+ extends State<T> {
62
+ /// ViewModel 实例
63
+ late final VM viewModel;
64
+
65
+ // ==================== 子类需要实现的方法 ====================
66
+
67
+ /// 创建 ViewModel
68
+ VM createViewModel();
69
+
70
+ /// 构建内容区域
71
+ Widget buildContent(BuildContext context);
72
+
73
+ // ==================== 可选重写的方法 ====================
74
+
75
+ /// 是否显示 AppBar(透传自 Widget 层)
76
+ bool get showAppBar => widget.showAppBar;
77
+
78
+ /// AppBar 标题(透传自 Widget 层)
79
+ String get title => widget.title;
80
+
81
+ /// AppBar 操作按钮(透传自 Widget 层)
82
+ List<Widget>? get actions => widget.actions;
83
+
84
+ /// 是否显示返回按钮(透传自 Widget 层)
85
+ bool? get showBackButton => widget.showBackButton;
86
+
87
+ /// 背景颜色(透传自 Widget 层)
88
+ Color? get backgroundColor => widget.backgroundColor;
89
+
90
+ /// 是否安全区域(透传自 Widget 层)
91
+ bool get useSafeArea => widget.useSafeArea;
92
+
93
+ // ==================== 生命周期 ====================
94
+
95
+ @override
96
+ void initState() {
97
+ super.initState();
98
+ viewModel = createViewModel();
99
+ _setupListener();
100
+ viewModel.onInit();
101
+ }
102
+
103
+ /// 设置监听器
104
+ void _setupListener() {
105
+ // 所有状态管理器都使用统一的 addListener 方式
106
+ viewModel.addListener(_onViewModelChanged);
107
+ }
108
+
109
+ @override
110
+ void dispose() {
111
+ viewModel.removeListener(_onViewModelChanged);
112
+ viewModel.dispose();
113
+ super.dispose();
114
+ }
115
+
116
+ /// ViewModel 变化回调
117
+ void _onViewModelChanged() {
118
+ if (mounted) {
119
+ setState(() {});
120
+ }
121
+ }
122
+
123
+ // ==================== UI 构建 ====================
124
+
125
+ @override
126
+ Widget build(BuildContext context) {
127
+ Widget body = _buildBody();
128
+
129
+ if (useSafeArea) {
130
+ body = SafeArea(child: body);
131
+ }
132
+
133
+ return Scaffold(
134
+ appBar: showAppBar ? _buildAppBar() : null,
135
+ backgroundColor: backgroundColor,
136
+ body: body,
137
+ );
138
+ }
139
+
140
+ /// 构建 AppBar
141
+ PreferredSizeWidget? _buildAppBar() {
142
+ return AppBar(
143
+ title: Text(title),
144
+ actions: actions,
145
+ automaticallyImplyLeading: showBackButton ?? true,
146
+ );
147
+ }
148
+
149
+ /// 构建 Body
150
+ Widget _buildBody() {
151
+ final theme = Theme.of(context).extension<StatusViewsTheme>();
152
+
153
+ if (viewModel.isLoading) {
154
+ final global = theme?.loadingBuilder?.call(context);
155
+ return global ?? buildLoading();
156
+ }
157
+
158
+ if (viewModel.isError) {
159
+ final global = theme?.errorBuilder?.call(context, viewModel.errorMessage);
160
+ return global ?? buildError();
161
+ }
162
+
163
+ if (isEmptyContent) {
164
+ final global = theme?.emptyBuilder?.call(context);
165
+ return global ?? buildEmpty();
166
+ }
167
+
168
+ return buildContent(context);
169
+ }
170
+
171
+ // ==================== 可重写的 UI 组件 ====================
172
+
173
+ /// 构建 Loading 视图
174
+ Widget buildLoading() {
175
+ return const Center(
176
+ child: CircularProgressIndicator(),
177
+ );
178
+ }
179
+
180
+ /// 构建错误视图
181
+ Widget buildError() {
182
+ return Center(
183
+ child: Column(
184
+ mainAxisAlignment: MainAxisAlignment.center,
185
+ children: [
186
+ const Icon(
187
+ Icons.error_outline,
188
+ size: 64,
189
+ color: Colors.red,
190
+ ),
191
+ const SizedBox(height: 16),
192
+ Text(
193
+ viewModel.errorMessage ?? '加载失败',
194
+ style: const TextStyle(fontSize: 16),
195
+ ),
196
+ const SizedBox(height: 24),
197
+ ElevatedButton.icon(
198
+ onPressed: viewModel.refreshData,
199
+ icon: const Icon(Icons.refresh),
200
+ label: const Text('重试'),
201
+ ),
202
+ ],
203
+ ),
204
+ );
205
+ }
206
+
207
+ /// 构建空视图
208
+ Widget buildEmpty() {
209
+ return const Center(
210
+ child: Text('暂无数据'),
211
+ );
212
+ }
213
+
214
+ /// 页面是否为空(默认 false,子类可重写)
215
+ bool get isEmptyContent => false;
216
+
217
+ /// 显示 SnackBar
218
+ void showSnackBar(String message, {Duration? duration}) {
219
+ if (!mounted) return;
220
+ ScaffoldMessenger.of(context).showSnackBar(
221
+ SnackBar(
222
+ content: Text(message),
223
+ duration: duration ?? const Duration(seconds: 2),
224
+ ),
225
+ );
226
+ }
227
+
228
+ /// 显示 Loading Dialog
229
+ void showLoadingDialog({String? message}) {
230
+ showDialog(
231
+ context: context,
232
+ barrierDismissible: false,
233
+ builder: (context) => AlertDialog(
234
+ content: Row(
235
+ children: [
236
+ const CircularProgressIndicator(),
237
+ const SizedBox(width: 16),
238
+ Text(message ?? '加载中...'),
239
+ ],
240
+ ),
241
+ ),
242
+ );
243
+ }
244
+
245
+ /// 隐藏 Loading Dialog
246
+ void hideLoadingDialog() {
247
+ if (mounted) {
248
+ Navigator.of(context).pop();
249
+ }
250
+ }
251
+ }