flu-cli 2.0.3 → 2.0.5
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.
- package/CHANGELOG.md +28 -6
- package/CLI.md +1 -0
- package/README.md +139 -67
- package/config/templates.js +6 -127
- package/index.js +100 -50
- package/lib/commands/add.js +8 -8
- package/lib/commands/assets.js +183 -0
- package/lib/commands/config.js +41 -1
- package/lib/commands/newClack.js +89 -116
- package/lib/commands/snippets.js +50 -9
- package/lib/commands/template.js +1 -1
- package/lib/templates/templateCopier.js +14 -275
- package/lib/templates/templateManager.js +20 -163
- package/lib/utils/config.js +13 -51
- package/lib/utils/flutterHelper.js +7 -82
- package/lib/utils/i18n.js +7 -0
- package/lib/utils/templateSelectorEnquirer.js +70 -43
- package/locales/en-US.json +59 -0
- package/locales/zh-CN.json +59 -0
- package/package.json +2 -2
- package/release.sh +380 -32
- package/scripts/sync-base-to-templates.js +6 -6
- package/scripts/workspace-clone-all.sh +4 -4
- package/scripts/workspace-status-all.sh +3 -3
- package/lib/generators/project_generator.js +0 -96
- package/lib/generators/state_manager_generator.js +0 -402
- package/templates/README.md +0 -138
- package/templates/base_files/base_list_page.dart.template +0 -174
- package/templates/base_files/base_list_viewmodel.dart.template +0 -134
- package/templates/base_files/base_page.dart.template +0 -251
- package/templates/base_files/base_viewmodel.dart.template +0 -77
- package/templates/base_files/theme/status_views_theme.dart.template +0 -46
- package/templates/snippets/dart.code-snippets +0 -392
|
@@ -1,174 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,251 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import 'package:flutter/foundation.dart';
|
|
2
|
-
|
|
3
|
-
/// 页面状态定义
|
|
4
|
-
enum ViewState { idle, loading, success, error }
|
|
5
|
-
|
|
6
|
-
class BaseViewModel extends ChangeNotifier {
|
|
7
|
-
ViewState _state = ViewState.idle;
|
|
8
|
-
String? _errorMessage;
|
|
9
|
-
bool _isRefreshing = false;
|
|
10
|
-
// ==================== Getters ====================
|
|
11
|
-
/// 当前视图状态
|
|
12
|
-
ViewState get state => _state;
|
|
13
|
-
|
|
14
|
-
/// 错误信息
|
|
15
|
-
String? get errorMessage => _errorMessage;
|
|
16
|
-
|
|
17
|
-
/// 是否正在加载
|
|
18
|
-
bool get isLoading => _state == ViewState.loading;
|
|
19
|
-
|
|
20
|
-
/// 是否加载失败
|
|
21
|
-
bool get isError => _state == ViewState.error;
|
|
22
|
-
|
|
23
|
-
/// 是否加载成功
|
|
24
|
-
bool get isSuccess => _state == ViewState.success;
|
|
25
|
-
|
|
26
|
-
/// 是否空闲状态
|
|
27
|
-
bool get isIdle => _state == ViewState.idle;
|
|
28
|
-
|
|
29
|
-
/// 是否正在刷新
|
|
30
|
-
|
|
31
|
-
bool get isRefreshing => _isRefreshing;
|
|
32
|
-
// ==================== 状态设置 ====================
|
|
33
|
-
/// 设置视图状态
|
|
34
|
-
///
|
|
35
|
-
/// [state] 新的状态
|
|
36
|
-
/// [error] 错误信息(仅在 error 状态时有效)
|
|
37
|
-
void setState(ViewState state, {String? error}) {
|
|
38
|
-
_state = state;
|
|
39
|
-
_errorMessage = error;
|
|
40
|
-
notifyListeners();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/// 设置刷新状态
|
|
44
|
-
void setRefreshing(bool refreshing) {
|
|
45
|
-
_isRefreshing = refreshing;
|
|
46
|
-
notifyListeners();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
@override
|
|
50
|
-
void notifyListeners() {
|
|
51
|
-
super.notifyListeners();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/// 通知数据变化(函数级注释)
|
|
55
|
-
///
|
|
56
|
-
/// - 仅用于局部刷新,避免整页重建
|
|
57
|
-
void notifyDataChange() {
|
|
58
|
-
notifyListeners();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ==================== 生命周期 ====================
|
|
62
|
-
/// 初始化钩子(函数级注释)
|
|
63
|
-
Future<void> onInit() async {
|
|
64
|
-
// 子类可以重写此方法进行初始化
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/// 刷新钩子(函数级注释)
|
|
68
|
-
Future<void> refreshData() async {
|
|
69
|
-
// 子类可以重写此方法实现刷新逻辑
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/// 释放资源(函数级注释)
|
|
73
|
-
@override
|
|
74
|
-
void dispose() {
|
|
75
|
-
super.dispose();
|
|
76
|
-
}
|
|
77
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import 'package:flutter/material.dart';
|
|
2
|
-
|
|
3
|
-
/// 状态占位页全局配置(函数级注释)
|
|
4
|
-
///
|
|
5
|
-
/// - 统一的 ThemeExtension,用于 Loading/Error/Empty/LoadMore 视图
|
|
6
|
-
class StatusViewsTheme extends ThemeExtension<StatusViewsTheme> {
|
|
7
|
-
final Widget Function(BuildContext ctx)? loadingBuilder;
|
|
8
|
-
final Widget Function(BuildContext ctx, String? msg)? errorBuilder;
|
|
9
|
-
final Widget Function(BuildContext ctx)? emptyBuilder;
|
|
10
|
-
final Color? refreshIndicatorColor;
|
|
11
|
-
final Widget Function(BuildContext ctx, bool hasMore)? loadMoreBuilder;
|
|
12
|
-
|
|
13
|
-
const StatusViewsTheme({
|
|
14
|
-
this.loadingBuilder,
|
|
15
|
-
this.errorBuilder,
|
|
16
|
-
this.emptyBuilder,
|
|
17
|
-
this.refreshIndicatorColor,
|
|
18
|
-
this.loadMoreBuilder,
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
@override
|
|
22
|
-
StatusViewsTheme copyWith({
|
|
23
|
-
Widget Function(BuildContext)? loadingBuilder,
|
|
24
|
-
Widget Function(BuildContext, String?)? errorBuilder,
|
|
25
|
-
Widget Function(BuildContext)? emptyBuilder,
|
|
26
|
-
Color? refreshIndicatorColor,
|
|
27
|
-
Widget Function(BuildContext, bool)? loadMoreBuilder,
|
|
28
|
-
}) {
|
|
29
|
-
return StatusViewsTheme(
|
|
30
|
-
loadingBuilder: loadingBuilder ?? this.loadingBuilder,
|
|
31
|
-
errorBuilder: errorBuilder ?? this.errorBuilder,
|
|
32
|
-
emptyBuilder: emptyBuilder ?? this.emptyBuilder,
|
|
33
|
-
refreshIndicatorColor:
|
|
34
|
-
refreshIndicatorColor ?? this.refreshIndicatorColor,
|
|
35
|
-
loadMoreBuilder: loadMoreBuilder ?? this.loadMoreBuilder,
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
@override
|
|
40
|
-
ThemeExtension<StatusViewsTheme> lerp(
|
|
41
|
-
ThemeExtension<StatusViewsTheme>? other,
|
|
42
|
-
double t,
|
|
43
|
-
) {
|
|
44
|
-
return this;
|
|
45
|
-
}
|
|
46
|
-
}
|