flu-cli-core 1.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.
- package/README.md +60 -0
- package/dist/chunk-FOMWV2YP.js +378 -0
- package/dist/chunk-SW6YDKXI.js +112 -0
- package/dist/factory-6DDXZYQP.js +6 -0
- package/dist/index.cjs +4668 -0
- package/dist/index.d.cts +644 -0
- package/dist/index.d.ts +644 -0
- package/dist/index.js +4037 -0
- package/dist/upgrade_snippets-XFR7Q444.js +8 -0
- package/locales/en-US.json +59 -0
- package/locales/zh-CN.json +59 -0
- package/package.json +52 -0
- package/templates/README.md +129 -0
- package/templates/core_files/base/base_list_page.dart.template +225 -0
- package/templates/core_files/base/base_list_viewmodel.dart.template +164 -0
- package/templates/core_files/base/base_page.dart.template +252 -0
- package/templates/core_files/base/base_viewmodel.dart.template +68 -0
- package/templates/core_files/base/index.dart.template +5 -0
- package/templates/core_files/config/app_config.dart.template +142 -0
- package/templates/core_files/config/app_initializer.dart.template +74 -0
- package/templates/core_files/config/index.dart.template +3 -0
- package/templates/core_files/index.dart.template +8 -0
- package/templates/core_files/network/README.md +378 -0
- package/templates/core_files/network/app_error_code.dart.template +49 -0
- package/templates/core_files/network/app_http.dart.template +306 -0
- package/templates/core_files/network/app_response.dart.template +81 -0
- package/templates/core_files/network/index.dart.template +12 -0
- package/templates/core_files/network/interceptors/app_response_interceptor.dart.template +44 -0
- package/templates/core_files/network/interceptors/auth_interceptor.dart.template +30 -0
- package/templates/core_files/network/interceptors/error_interceptor.dart.template +48 -0
- package/templates/core_files/network/interceptors/index.dart.template +6 -0
- package/templates/core_files/network/interceptors/log_interceptor.dart.template +97 -0
- package/templates/core_files/network/interceptors/network_error_interceptor.dart.template +58 -0
- package/templates/core_files/network/interceptors/retry_interceptor.dart.template +69 -0
- package/templates/core_files/network/response_adapter.dart.template +69 -0
- package/templates/core_files/router/app_routes.dart.template +32 -0
- package/templates/core_files/router/index.dart.template +3 -0
- package/templates/core_files/router/navigator_util_getx.dart.template +131 -0
- package/templates/core_files/router/navigator_util_material.dart.template +191 -0
- package/templates/core_files/storage/index.dart.template +3 -0
- package/templates/core_files/storage/storage_keys.dart.template +34 -0
- package/templates/core_files/storage/storage_util.dart.template +102 -0
- package/templates/core_files/theme/app_theme.dart.template +37 -0
- package/templates/core_files/theme/index.dart.template +3 -0
- package/templates/core_files/theme/status_views_theme.dart.template +40 -0
- package/templates/core_files/utils/index.dart.template +2 -0
- package/templates/core_files/utils/loading_util.dart.template +55 -0
- package/templates/core_files/utils/toast_util.dart.template +128 -0
- package/templates/examples/eg_list_page.dart.template +340 -0
- package/templates/examples/eg_list_viewmodel.dart.template +31 -0
- package/templates/examples/eg_service.dart.template +78 -0
- package/templates/examples/mock_data.dart.template +50388 -0
- package/templates/examples/tu_chong_model.dart.template +633 -0
- package/templates/request_helper.dart.template +59 -0
- package/templates/snippets/flu-cli.code-snippets +268 -0
- package/templates/snippets/flu-cli.code-snippets.backup +268 -0
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# @flu-cli/core (V6.0)
|
|
2
|
+
|
|
3
|
+
核心逻辑库,遵循 **Task Pipeline (任务流水线)** 架构,为 flu-cli 和 VSCode 扩展提供标准化的 Flutter 代码生成与增强能力。
|
|
4
|
+
|
|
5
|
+
## 🏗 核心架构:Task Pipeline
|
|
6
|
+
|
|
7
|
+
从 V6.0 开始,核心生成器由传统的命令式逻辑转变为 **原子化任务流水线** 架构。
|
|
8
|
+
|
|
9
|
+
### 1. 乐高式搭建 (The Lego Model)
|
|
10
|
+
`ProjectGenerator` 不再直接操作文件,而是作为一个 **Orchestrator (指挥官)**。它通过 `ProjectPipeline` 编排一系列 `IProjectTask`。
|
|
11
|
+
|
|
12
|
+
```mermaid
|
|
13
|
+
graph LR
|
|
14
|
+
A[ProjectGenerator] --> B(ProjectPipeline)
|
|
15
|
+
B --> T1[FlutterInitTask]
|
|
16
|
+
B --> T2[TemplateCopyTask]
|
|
17
|
+
B --> T3[VariablesReplaceTask]
|
|
18
|
+
B --> T4[NetworkEnrichTask]
|
|
19
|
+
B --> T5[RouteMappingTask]
|
|
20
|
+
B --> T6[HomePatchTask]
|
|
21
|
+
B --> T7[CleanupTask]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 2. 骨架 + 增强 (Skeleton + Enrichment)
|
|
25
|
+
- **Skeleton (骨架)**: 从 Git 或本地加载的纯净 Flutter 代码模板。
|
|
26
|
+
- **Enrichment (增强)**: CLI 通过任务流水线动态注入的功能层(如:`NetworkEnrichTask` 注入 Dio 封装,`StateManagerEnrichTask` 注入状态管理适配器)。
|
|
27
|
+
|
|
28
|
+
## 💡 为什么这么牛?
|
|
29
|
+
|
|
30
|
+
- **极简核心**: `ProjectGenerator.ts` 从 1500 行精简至 100 行,逻辑高度内聚于各个 Task。
|
|
31
|
+
- **高可扩展性**: 想要支持一个新的功能(如 Sentry 监控、WeChat SDK)?只需编写一个新的 `EnrichTask` 并加入流水线。
|
|
32
|
+
- **高度解耦**: 状态管理、路由注册、环境修复各司其职,互不干扰。
|
|
33
|
+
- **声明式配置**: 开发者可以清晰地看到项目生成的每一个步骤。
|
|
34
|
+
|
|
35
|
+
## 🚀 核心组件
|
|
36
|
+
|
|
37
|
+
### 生成器 (Generators)
|
|
38
|
+
- **ProjectGenerator**: 采用 Pipeline 模式的项目实例化引擎。
|
|
39
|
+
- **Page/ViewModel/Model Generator**: 针对不同架构模板的高级代码生成器。
|
|
40
|
+
|
|
41
|
+
### 任务体系 (Tasks)
|
|
42
|
+
- `FlutterInitTask`: 确保原生 Flutter 环境就绪。
|
|
43
|
+
- `TemplateCopyTask`: 智能处理模板复制与冗余清理。
|
|
44
|
+
- `NetworkEnrichTask`: 声明式注入网络层基础设施。
|
|
45
|
+
- `RouteMappingTask`: 自动扫描业务路径并注册路由表。
|
|
46
|
+
- `HomePatchTask`: 自动化首页示例入口侵入。
|
|
47
|
+
|
|
48
|
+
## 🛠 开发与扩展
|
|
49
|
+
|
|
50
|
+
### 如何添加一个新任务?
|
|
51
|
+
1. 在 `src/generators/tasks/` 创建实现 `IProjectTask` 的类。
|
|
52
|
+
2. 在 `ProjectGenerator.generate` 中通过 `.addTask()` 链式调用。
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
pipeline.addTask(new YourAwesomeTask());
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 📜 许可证
|
|
59
|
+
|
|
60
|
+
MIT - Copyright © 2025 火叶工作室
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// src/generators/tasks/adapters/provider_adapter.ts
|
|
2
|
+
var ProviderAdapter = class {
|
|
3
|
+
name;
|
|
4
|
+
constructor(name = "provider") {
|
|
5
|
+
this.name = name;
|
|
6
|
+
}
|
|
7
|
+
getDependencies() {
|
|
8
|
+
return this.name === "provider" ? ["provider: ^6.1.1"] : [];
|
|
9
|
+
}
|
|
10
|
+
getBaseViewModelTemplate(context) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
getBasePageParent(isListPage) {
|
|
14
|
+
return isListPage ? "BaseListPage" : "BasePage";
|
|
15
|
+
}
|
|
16
|
+
getBaseViewModelParent(isListPage) {
|
|
17
|
+
return isListPage ? "BaseListViewModel" : "BaseViewModel";
|
|
18
|
+
}
|
|
19
|
+
getImports(relativeCorePath) {
|
|
20
|
+
if (this.name === "provider") {
|
|
21
|
+
return [
|
|
22
|
+
`import 'package:provider/provider.dart';`,
|
|
23
|
+
`import '${relativeCorePath}';`
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
return [`import '${relativeCorePath}';`];
|
|
27
|
+
}
|
|
28
|
+
patchAppEntry(content, context) {
|
|
29
|
+
return content;
|
|
30
|
+
}
|
|
31
|
+
getCommonInfo() {
|
|
32
|
+
return ` ViewState _state = ViewState.idle;
|
|
33
|
+
String? _errorMessage;
|
|
34
|
+
bool _isRefreshing = false;
|
|
35
|
+
|
|
36
|
+
// ==================== Getters ====================
|
|
37
|
+
// \u5F53\u524D\u89C6\u56FE\u72B6\u6001
|
|
38
|
+
ViewState get state => _state;
|
|
39
|
+
// \u9519\u8BEF\u4FE1\u606F
|
|
40
|
+
String? get errorMessage => _errorMessage;
|
|
41
|
+
// \u662F\u5426\u6B63\u5728\u52A0\u8F7D
|
|
42
|
+
bool get isLoading => _state == ViewState.loading;
|
|
43
|
+
// \u662F\u5426\u52A0\u8F7D\u5931\u8D25
|
|
44
|
+
bool get isError => _state == ViewState.error;
|
|
45
|
+
// \u662F\u5426\u52A0\u8F7D\u6210\u529F
|
|
46
|
+
bool get isSuccess => _state == ViewState.success;
|
|
47
|
+
// \u662F\u5426\u7A7A\u95F2\u72B6\u6001
|
|
48
|
+
bool get isIdle => _state == ViewState.idle;
|
|
49
|
+
// \u662F\u5426\u6B63\u5728\u5237\u65B0
|
|
50
|
+
bool get isRefreshing => _isRefreshing;
|
|
51
|
+
|
|
52
|
+
// ==================== \u72B6\u6001\u8BBE\u7F6E ====================
|
|
53
|
+
|
|
54
|
+
/// \u8BBE\u7F6E\u89C6\u56FE\u72B6\u6001
|
|
55
|
+
void setState(ViewState state, {String? error}) {
|
|
56
|
+
_state = state;
|
|
57
|
+
_errorMessage = error;
|
|
58
|
+
notifyListeners();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// \u8BBE\u7F6E\u5237\u65B0\u72B6\u6001
|
|
62
|
+
void setRefreshing(bool refreshing) {
|
|
63
|
+
_isRefreshing = refreshing;
|
|
64
|
+
notifyListeners();
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/generators/tasks/adapters/getx_adapter.ts
|
|
71
|
+
var GetXAdapter = class {
|
|
72
|
+
name = "getx";
|
|
73
|
+
getDependencies() {
|
|
74
|
+
return ["get: ^4.6.6"];
|
|
75
|
+
}
|
|
76
|
+
getBaseViewModelTemplate(context) {
|
|
77
|
+
return `import 'package:get/get.dart';
|
|
78
|
+
|
|
79
|
+
enum ViewState { idle, loading, success, error }
|
|
80
|
+
|
|
81
|
+
class BaseViewModel extends GetxController {
|
|
82
|
+
ViewState _state = ViewState.idle;
|
|
83
|
+
String? _errorMessage;
|
|
84
|
+
bool _isRefreshing = false;
|
|
85
|
+
bool _disposed = false;
|
|
86
|
+
|
|
87
|
+
// ==================== Getters ====================
|
|
88
|
+
ViewState get state => _state;
|
|
89
|
+
String? get errorMessage => _errorMessage;
|
|
90
|
+
bool get isLoading => _state == ViewState.loading;
|
|
91
|
+
bool get isError => _state == ViewState.error;
|
|
92
|
+
bool get isSuccess => _state == ViewState.success;
|
|
93
|
+
bool get isIdle => _state == ViewState.idle;
|
|
94
|
+
bool get isRefreshing => _isRefreshing;
|
|
95
|
+
// GetX \u4F7F\u7528 isClosed \u5224\u65AD\u662F\u5426\u5DF2\u91CA\u653E
|
|
96
|
+
bool get isDisposed => _disposed || isClosed;
|
|
97
|
+
|
|
98
|
+
// ==================== \u72B6\u6001\u8BBE\u7F6E ====================
|
|
99
|
+
void setState(ViewState state, {String? error}) {
|
|
100
|
+
if (_disposed || isClosed) return;
|
|
101
|
+
_state = state;
|
|
102
|
+
_errorMessage = error;
|
|
103
|
+
update(); // GetX \u4F7F\u7528 update() \u800C\u975E notifyListeners()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
void setRefreshing(bool refreshing) {
|
|
107
|
+
if (_disposed || isClosed) return;
|
|
108
|
+
_isRefreshing = refreshing;
|
|
109
|
+
update();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// \u901A\u77E5\u6570\u636E\u53D8\u5316(\u4EC5\u7528\u4E8E\u5C40\u90E8\u5237\u65B0)
|
|
113
|
+
void notifyDataChange() {
|
|
114
|
+
if (_disposed || isClosed) return;
|
|
115
|
+
update();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ==================== \u751F\u547D\u5468\u671F ====================
|
|
119
|
+
/// \u521D\u59CB\u5316\u94A9\u5B50
|
|
120
|
+
@override
|
|
121
|
+
Future<void> onInit() async {
|
|
122
|
+
super.onInit();
|
|
123
|
+
// \u5B50\u7C7B\u53EF\u4EE5\u91CD\u5199\u6B64\u65B9\u6CD5\u8FDB\u884C\u521D\u59CB\u5316
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// \u5237\u65B0\u6570\u636E(\u94A9\u5B50)
|
|
127
|
+
Future<void> refreshData() async {
|
|
128
|
+
// \u5B50\u7C7B\u53EF\u4EE5\u91CD\u5199\u6B64\u65B9\u6CD5\u5B9E\u73B0\u5237\u65B0\u903B\u8F91
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@override
|
|
132
|
+
void onClose() {
|
|
133
|
+
_disposed = true;
|
|
134
|
+
super.onClose();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
`;
|
|
138
|
+
}
|
|
139
|
+
getBasePageParent(isListPage) {
|
|
140
|
+
return isListPage ? "BaseListPage" : "BasePage";
|
|
141
|
+
}
|
|
142
|
+
getBaseViewModelParent(isListPage) {
|
|
143
|
+
return isListPage ? "BaseListViewModel" : "BaseViewModel";
|
|
144
|
+
}
|
|
145
|
+
getImports(relativeCorePath) {
|
|
146
|
+
return [
|
|
147
|
+
`import '${relativeCorePath}';`
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 修改 app.dart
|
|
152
|
+
*/
|
|
153
|
+
patchAppEntry(content, context) {
|
|
154
|
+
let raw = content;
|
|
155
|
+
if (!raw.includes("GetMaterialApp(")) {
|
|
156
|
+
raw = raw.replace(/MaterialApp\s*\(/, "GetMaterialApp(");
|
|
157
|
+
}
|
|
158
|
+
if (raw.includes("GetMaterialApp(") && !raw.includes("import 'package:get/get.dart'")) {
|
|
159
|
+
if (raw.includes("import 'package:flutter/material.dart';")) {
|
|
160
|
+
raw = raw.replace(
|
|
161
|
+
"import 'package:flutter/material.dart';",
|
|
162
|
+
"import 'package:flutter/material.dart';\nimport 'package:get/get.dart';"
|
|
163
|
+
);
|
|
164
|
+
} else {
|
|
165
|
+
raw = `import 'package:get/get.dart';
|
|
166
|
+
` + raw;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (raw.includes("routes: AppRoutes.routes")) {
|
|
170
|
+
raw = raw.replace("routes: AppRoutes.routes", "getPages: AppRoutes.pages");
|
|
171
|
+
}
|
|
172
|
+
if (raw.includes("navigatorKey: NavigatorUtil.navigatorKey")) {
|
|
173
|
+
raw = raw.replace(/navigatorKey:\s*NavigatorUtil\.navigatorKey\s*,?/g, "");
|
|
174
|
+
}
|
|
175
|
+
if (raw.includes("scaffoldMessengerKey: NavigatorUtil.scaffoldMessengerKey")) {
|
|
176
|
+
raw = raw.replace(/scaffoldMessengerKey:\s*NavigatorUtil\.scaffoldMessengerKey\s*,?\s*\n?/g, "");
|
|
177
|
+
}
|
|
178
|
+
return raw;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 修改 app_routes.dart
|
|
182
|
+
*/
|
|
183
|
+
patchRoutes(content, context) {
|
|
184
|
+
let raw = content;
|
|
185
|
+
if (raw.includes("static List<GetPage> get pages")) {
|
|
186
|
+
return raw;
|
|
187
|
+
}
|
|
188
|
+
if (!raw.includes("import 'package:get/get.dart'")) {
|
|
189
|
+
if (raw.includes("import 'package:flutter/material.dart';")) {
|
|
190
|
+
raw = raw.replace(
|
|
191
|
+
"import 'package:flutter/material.dart';",
|
|
192
|
+
"import 'package:get/get.dart';"
|
|
193
|
+
);
|
|
194
|
+
} else {
|
|
195
|
+
raw = `import 'package:get/get.dart';
|
|
196
|
+
` + raw;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const startMarker = "// __ROUTE_CONFIG_START__";
|
|
200
|
+
const endMarker = "// __ROUTE_CONFIG_END__";
|
|
201
|
+
const pagesGetter = `
|
|
202
|
+
/// GetX \u8DEF\u7531\u6620\u5C04
|
|
203
|
+
/// \u5728 app.dart \u4E2D\u4F7F\u7528: GetMaterialApp(getPages: AppRoutes.pages, ...)
|
|
204
|
+
static List<GetPage> get pages {
|
|
205
|
+
return [
|
|
206
|
+
GetPage(name: home, page: () => const HomePage()),
|
|
207
|
+
GetPage(name: userList, page: () => const UserListPage()),
|
|
208
|
+
// --- \u5728\u6B64\u4E0B\u65B9\u6DFB\u52A0\u60A8\u7684\u81EA\u5B9A\u4E49\u8DEF\u7531 ---
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
`;
|
|
212
|
+
const startIndex = raw.indexOf(startMarker);
|
|
213
|
+
const endIndex = raw.indexOf(endMarker);
|
|
214
|
+
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
215
|
+
const before = raw.substring(0, startIndex);
|
|
216
|
+
const after = raw.substring(endIndex + endMarker.length);
|
|
217
|
+
raw = before + pagesGetter.trim() + after;
|
|
218
|
+
} else {
|
|
219
|
+
const standardRoutesPattern = /\s*\/\/\/\s*Material\s+路由映射[\s\S]*?static\s+Map<String,\s*WidgetBuilder>\s*get\s*routes\s*\{\s*return\s*\{[\s\S]*?\}\s*;\s*\}/;
|
|
220
|
+
if (standardRoutesPattern.test(raw)) {
|
|
221
|
+
raw = raw.replace(standardRoutesPattern, pagesGetter);
|
|
222
|
+
} else {
|
|
223
|
+
const lastBraceIndex = raw.lastIndexOf("}");
|
|
224
|
+
if (lastBraceIndex !== -1) {
|
|
225
|
+
raw = raw.slice(0, lastBraceIndex) + "\n" + pagesGetter + raw.slice(lastBraceIndex);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return raw;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// src/generators/tasks/adapters/riverpod_adapter.ts
|
|
234
|
+
import { join } from "path";
|
|
235
|
+
import fsx from "fs-extra";
|
|
236
|
+
var RiverpodAdapter = class {
|
|
237
|
+
name = "riverpod";
|
|
238
|
+
getDependencies() {
|
|
239
|
+
return ["flutter_riverpod: ^2.4.9"];
|
|
240
|
+
}
|
|
241
|
+
getBasePageParent(isListPage) {
|
|
242
|
+
return isListPage ? "BaseListPageRiverpod" : "BasePageRiverpod";
|
|
243
|
+
}
|
|
244
|
+
getBaseViewModelParent(isListPage) {
|
|
245
|
+
return isListPage ? "BaseListNotifier" : "BaseNotifier";
|
|
246
|
+
}
|
|
247
|
+
getImports(relativeCorePath) {
|
|
248
|
+
return [
|
|
249
|
+
`import 'package:flutter_riverpod/flutter_riverpod.dart';`,
|
|
250
|
+
`import '${relativeCorePath}';`
|
|
251
|
+
];
|
|
252
|
+
}
|
|
253
|
+
getBaseViewModelTemplate(context) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
patchAppEntry(content, context) {
|
|
257
|
+
let raw = content;
|
|
258
|
+
if (raw.includes("runApp(") && !raw.includes("ProviderScope")) {
|
|
259
|
+
raw = raw.replace(/runApp\((.*?)\)/, "runApp(const ProviderScope(child: $1))");
|
|
260
|
+
if (!raw.includes("import 'package:flutter_riverpod/flutter_riverpod.dart'")) {
|
|
261
|
+
raw = `import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
262
|
+
` + raw;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return raw;
|
|
266
|
+
}
|
|
267
|
+
async onEnrich(context) {
|
|
268
|
+
const { projectPath } = context;
|
|
269
|
+
const baseDir = join(projectPath, "lib", "core", "base");
|
|
270
|
+
await fsx.ensureDir(baseDir);
|
|
271
|
+
await fsx.writeFile(join(baseDir, "view_state.dart"), "enum ViewState { idle, loading, success, error }\n");
|
|
272
|
+
await fsx.writeFile(join(baseDir, "base_state.dart"), `
|
|
273
|
+
import 'view_state.dart';
|
|
274
|
+
|
|
275
|
+
class BaseState {
|
|
276
|
+
final ViewState state;
|
|
277
|
+
final bool isRefreshing;
|
|
278
|
+
final String? error;
|
|
279
|
+
|
|
280
|
+
const BaseState({
|
|
281
|
+
this.state = ViewState.idle,
|
|
282
|
+
this.isRefreshing = false,
|
|
283
|
+
this.error,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
BaseState copyWith({ ViewState? state, bool? isRefreshing, String? error }) {
|
|
287
|
+
return BaseState(
|
|
288
|
+
state: state ?? this.state,
|
|
289
|
+
isRefreshing: isRefreshing ?? this.isRefreshing,
|
|
290
|
+
error: error ?? this.error,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
bool get isLoading => state == ViewState.loading;
|
|
295
|
+
bool get isSuccess => state == ViewState.success;
|
|
296
|
+
bool get isError => state == ViewState.error;
|
|
297
|
+
bool get isIdle => state == ViewState.idle;
|
|
298
|
+
}
|
|
299
|
+
`);
|
|
300
|
+
await fsx.writeFile(join(baseDir, "base_notifier.dart"), `
|
|
301
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
302
|
+
import 'view_state.dart';
|
|
303
|
+
import 'base_state.dart';
|
|
304
|
+
|
|
305
|
+
abstract class BaseNotifier<T extends BaseState> extends Notifier<T> {
|
|
306
|
+
T createInitialState();
|
|
307
|
+
@override
|
|
308
|
+
T build() => createInitialState();
|
|
309
|
+
|
|
310
|
+
void setLoading() { state = (state.copyWith(state: ViewState.loading, error: null)) as T; }
|
|
311
|
+
void setSuccess() { state = (state.copyWith(state: ViewState.success)) as T; }
|
|
312
|
+
void setError(String message) { state = (state.copyWith(state: ViewState.error, error: message)) as T; }
|
|
313
|
+
void setRefreshing(bool refreshing) { state = (state.copyWith(isRefreshing: refreshing)) as T; }
|
|
314
|
+
|
|
315
|
+
Future<void> run(Future<void> Function() task) async {
|
|
316
|
+
setLoading();
|
|
317
|
+
try {
|
|
318
|
+
await task();
|
|
319
|
+
setSuccess();
|
|
320
|
+
} catch (e) {
|
|
321
|
+
setError(e.toString());
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
`);
|
|
326
|
+
await fsx.writeFile(join(baseDir, "base_page_riverpod.dart"), `
|
|
327
|
+
import 'package:flutter/material.dart';
|
|
328
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
329
|
+
import 'base_state.dart';
|
|
330
|
+
|
|
331
|
+
typedef ContentBuilder<TState extends BaseState, TNotifier extends Notifier<TState>> = Widget Function(BuildContext context, TState state, TNotifier vm);
|
|
332
|
+
|
|
333
|
+
class BasePageRiverpod<TState extends BaseState, TNotifier extends Notifier<TState>> extends ConsumerWidget {
|
|
334
|
+
final ProviderListenable<TState> provider;
|
|
335
|
+
final String title;
|
|
336
|
+
final ContentBuilder<TState, TNotifier> builder;
|
|
337
|
+
|
|
338
|
+
const BasePageRiverpod({ super.key, required this.provider, required this.title, required this.builder });
|
|
339
|
+
|
|
340
|
+
@override
|
|
341
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
342
|
+
final state = ref.watch(provider);
|
|
343
|
+
final vm = ref.read(provider as dynamic).notifier as TNotifier;
|
|
344
|
+
return Scaffold(
|
|
345
|
+
appBar: AppBar(title: Text(title)),
|
|
346
|
+
body: Center(
|
|
347
|
+
child: state.isLoading
|
|
348
|
+
? const CircularProgressIndicator()
|
|
349
|
+
: state.isError
|
|
350
|
+
? Text('Error: ' + (state.error ?? ''))
|
|
351
|
+
: builder(context, state, vm),
|
|
352
|
+
),
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
`);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// src/generators/tasks/adapters/factory.ts
|
|
361
|
+
var StateManagerAdapterFactory = class {
|
|
362
|
+
static getAdapter(name) {
|
|
363
|
+
switch (name.toLowerCase()) {
|
|
364
|
+
case "getx":
|
|
365
|
+
return new GetXAdapter();
|
|
366
|
+
case "provider":
|
|
367
|
+
return new ProviderAdapter("provider");
|
|
368
|
+
case "riverpod":
|
|
369
|
+
return new RiverpodAdapter();
|
|
370
|
+
default:
|
|
371
|
+
return new ProviderAdapter("default");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
export {
|
|
377
|
+
StateManagerAdapterFactory
|
|
378
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// src/commands/upgrade_snippets.ts
|
|
2
|
+
import { existsSync, mkdirSync, copyFileSync, readFileSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
// src/utils/logger.ts
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
var ConsoleLogger = class {
|
|
9
|
+
info(message) {
|
|
10
|
+
console.log(chalk.blue("\u2139\uFE0F " + message));
|
|
11
|
+
}
|
|
12
|
+
success(message) {
|
|
13
|
+
console.log(chalk.green("\u2705 " + message));
|
|
14
|
+
}
|
|
15
|
+
warn(message) {
|
|
16
|
+
console.log(chalk.yellow("\u26A0\uFE0F " + message));
|
|
17
|
+
}
|
|
18
|
+
error(message) {
|
|
19
|
+
console.log(chalk.red("\u274C " + message));
|
|
20
|
+
}
|
|
21
|
+
title(message) {
|
|
22
|
+
console.log(chalk.bold.cyan("\n" + message + "\n"));
|
|
23
|
+
}
|
|
24
|
+
newLine() {
|
|
25
|
+
console.log("");
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var logger = new ConsoleLogger();
|
|
29
|
+
|
|
30
|
+
// src/commands/upgrade_snippets.ts
|
|
31
|
+
var __dirname;
|
|
32
|
+
try {
|
|
33
|
+
if (typeof import.meta !== "undefined" && import.meta.url) {
|
|
34
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
35
|
+
__dirname = dirname(__filename);
|
|
36
|
+
} else {
|
|
37
|
+
throw new Error("Using CJS fallback");
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
__dirname = typeof global !== "undefined" && global.__dirname ? global.__dirname : process.cwd();
|
|
41
|
+
}
|
|
42
|
+
async function upgradeSnippets(options = {}, logger2 = new ConsoleLogger()) {
|
|
43
|
+
try {
|
|
44
|
+
const projectDir = options.projectDir || process.cwd();
|
|
45
|
+
const force = options.force || false;
|
|
46
|
+
logger2.info("\u{1F504} \u6B63\u5728\u5347\u7EA7\u9879\u76EE snippets...");
|
|
47
|
+
const pubspecPath = join(projectDir, "pubspec.yaml");
|
|
48
|
+
if (!existsSync(pubspecPath)) {
|
|
49
|
+
logger2.error("\u274C \u672A\u68C0\u6D4B\u5230 Flutter \u9879\u76EE (\u7F3A\u5C11 pubspec.yaml)");
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const sourceSnippetsPath = join(
|
|
53
|
+
__dirname,
|
|
54
|
+
"..",
|
|
55
|
+
"..",
|
|
56
|
+
"templates",
|
|
57
|
+
"snippets",
|
|
58
|
+
"flu-cli.code-snippets"
|
|
59
|
+
);
|
|
60
|
+
if (!existsSync(sourceSnippetsPath)) {
|
|
61
|
+
logger2.error(`\u274C \u627E\u4E0D\u5230\u6E90 snippets \u6587\u4EF6: ${sourceSnippetsPath}`);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const targetDir = join(projectDir, ".vscode");
|
|
65
|
+
const targetPath = join(targetDir, "flu-cli.code-snippets");
|
|
66
|
+
if (existsSync(targetPath) && !force) {
|
|
67
|
+
logger2.warn("\u26A0\uFE0F \u9879\u76EE\u4E2D\u5DF2\u5B58\u5728 snippets \u6587\u4EF6");
|
|
68
|
+
logger2.info("\u63D0\u793A: \u4F7F\u7528 --force \u53C2\u6570\u5F3A\u5236\u8986\u76D6");
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (!existsSync(targetDir)) {
|
|
72
|
+
mkdirSync(targetDir, { recursive: true });
|
|
73
|
+
logger2.info("\u2713 \u521B\u5EFA .vscode \u76EE\u5F55");
|
|
74
|
+
}
|
|
75
|
+
copyFileSync(sourceSnippetsPath, targetPath);
|
|
76
|
+
logger2.success(`\u2705 Snippets \u66F4\u65B0\u6210\u529F!`);
|
|
77
|
+
logger2.info(` \u4F4D\u7F6E: ${targetPath}`);
|
|
78
|
+
logger2.newLine();
|
|
79
|
+
logger2.info("\u{1F4DD} \u66F4\u65B0\u5185\u5BB9:");
|
|
80
|
+
logger2.info(" \u2022 Core \u5BFC\u5165\u8DEF\u5F84\u6539\u4E3A\u4F7F\u7528\u53D8\u91CF ${relative_core_path}");
|
|
81
|
+
logger2.info(" \u2022 \u652F\u6301 package imports (\u81EA\u52A8\u751F\u6210)");
|
|
82
|
+
logger2.info(" \u2022 \u9002\u914D\u6240\u6709\u67B6\u6784 (Lite/Modular/Clean)");
|
|
83
|
+
return true;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
86
|
+
logger2.error(`\u274C \u5347\u7EA7\u5931\u8D25: ${msg}`);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function checkSnippetsVersion(projectDir = process.cwd()) {
|
|
91
|
+
const snippetsPath = join(projectDir, ".vscode", "flu-cli.code-snippets");
|
|
92
|
+
if (!existsSync(snippetsPath)) {
|
|
93
|
+
return "missing";
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const content = readFileSync(snippetsPath, "utf8");
|
|
97
|
+
if (content.includes("${relative_core_path}")) {
|
|
98
|
+
return "up-to-date";
|
|
99
|
+
} else {
|
|
100
|
+
return "outdated";
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
return "missing";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export {
|
|
108
|
+
ConsoleLogger,
|
|
109
|
+
logger,
|
|
110
|
+
upgradeSnippets,
|
|
111
|
+
checkSnippetsVersion
|
|
112
|
+
};
|