flu-cli 0.0.4 → 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.
- package/CLI.md +349 -0
- package/README.md +59 -155
- package/config/dev.config.js +56 -0
- package/config/templates.js +147 -0
- package/index.js +128 -81
- package/lib/commands/add.js +472 -0
- package/lib/commands/cache.js +99 -0
- package/lib/commands/completion.js +94 -0
- package/lib/commands/generate.js +26 -0
- package/lib/commands/newClack.js +396 -0
- package/lib/commands/snippets.js +39 -0
- package/lib/commands/templates.js +84 -0
- package/lib/generators/component_generator.js +93 -0
- package/lib/generators/model_generator.js +303 -0
- package/lib/generators/module_generator.js +141 -0
- package/lib/generators/page_generator.js +322 -0
- package/lib/generators/project_generator.js +96 -0
- package/lib/generators/service_generator.js +408 -0
- package/lib/generators/state_manager_generator.js +402 -0
- package/lib/generators/viewmodel_generator.js +115 -0
- package/lib/generators/widget_generator.js +104 -0
- package/lib/templates/templateCopier.js +296 -0
- package/lib/templates/templateManager.js +191 -0
- package/lib/utils/config.js +99 -0
- package/lib/utils/flutterHelper.js +85 -0
- package/lib/utils/index_updater.js +69 -0
- package/lib/utils/logger.js +57 -0
- package/lib/utils/project_detector.js +227 -0
- package/lib/utils/snippet_loader.js +32 -0
- package/lib/utils/string_helper.js +56 -0
- package/lib/utils/templateSelectorEnquirer.js +200 -0
- package/package.json +31 -6
- package/release.sh +107 -0
- package/scripts/e2e-state-tests.js +116 -0
- package/scripts/sync-base-to-templates.js +108 -0
- package/scripts/workspace-clone-all.sh +101 -0
- package/scripts/workspace-status-all.sh +112 -0
- package/templates/README.md +138 -0
- package/templates/base_files/base_list_page.dart.template +174 -0
- package/templates/base_files/base_list_viewmodel.dart.template +134 -0
- package/templates/base_files/base_page.dart.template +251 -0
- package/templates/base_files/base_viewmodel.dart.template +77 -0
- package/templates/base_files/theme/status_views_theme.dart.template +46 -0
- package/templates/snippets/dart.code-snippets +487 -0
- package/lib/createProject.js +0 -220
- package/lib/flutterProjectCreator.js +0 -80
- package/lib/libCopier.js +0 -368
- package/lib/userInteraction.js +0 -274
- package/lib/utils.js +0 -200
- package/publish.sh +0 -29
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// 统一状态管理生成器:按模板与状态管理器选择执行依赖注入、入口增强、路由增强与变体生成
|
|
2
|
+
// 原则:最小侵入、单一代码路径、可演进(便于后续扩展 riverpod 专用变体等)
|
|
3
|
+
import fsx from 'fs-extra';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import Handlebars from 'handlebars';
|
|
7
|
+
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
import { detectProjectTemplate } from '../utils/project_detector.js';
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 状态管理器生成器
|
|
15
|
+
* 功能:
|
|
16
|
+
* - 注入依赖(get/provider/flutter_riverpod)
|
|
17
|
+
* - 所有模板生成 BaseViewModel 变体(ChangeNotifier/GetX)
|
|
18
|
+
* - 所有模板按需增强入口(GetMaterialApp/ProviderScope)与路由(GetPage 列表)
|
|
19
|
+
*/
|
|
20
|
+
export class StateManagerGenerator {
|
|
21
|
+
/**
|
|
22
|
+
* 构造函数
|
|
23
|
+
* @param {string} projectPath 项目根目录
|
|
24
|
+
* @param {'default'|'provider'|'getx'|'riverpod'} stateManager 选中的状态管理器
|
|
25
|
+
* @param {{module?: string}} options 额外选项
|
|
26
|
+
*/
|
|
27
|
+
constructor(projectPath, stateManager = 'default', options = {}) {
|
|
28
|
+
this.projectPath = projectPath;
|
|
29
|
+
this.stateManager = stateManager;
|
|
30
|
+
this.module = options.module || null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 主流程:依赖 → BaseViewModel 生成 → 入口 → App/路由增强 → 完成
|
|
35
|
+
*/
|
|
36
|
+
async generate () {
|
|
37
|
+
logger.info(`🔧 配置状态管理器: ${this.stateManager}`);
|
|
38
|
+
try {
|
|
39
|
+
await this.addDependencies();
|
|
40
|
+
|
|
41
|
+
// 所有模板都生成 BaseViewModel (统一策略)
|
|
42
|
+
await this.generateBaseViewModel();
|
|
43
|
+
|
|
44
|
+
await this.configureMain();
|
|
45
|
+
await this.configureAppWidget();
|
|
46
|
+
await this.configureRoutes();
|
|
47
|
+
|
|
48
|
+
logger.success('✅ 状态管理器配置完成');
|
|
49
|
+
} catch (error) {
|
|
50
|
+
logger.error(`❌ 状态管理器配置失败: ${error.message}`);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 注入依赖到 pubspec.yaml:
|
|
57
|
+
* - 处理模板占位 {{{state_manager_dependency}}} / {{{flu_core_dependency}}}
|
|
58
|
+
* - 兼容非模板 pubspec,通过正则插入
|
|
59
|
+
*/
|
|
60
|
+
async addDependencies () {
|
|
61
|
+
const pubspecPath = join(this.projectPath, 'pubspec.yaml');
|
|
62
|
+
|
|
63
|
+
if (!(await fsx.pathExists(pubspecPath))) {
|
|
64
|
+
throw new Error(`pubspec.yaml not found at ${pubspecPath}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let content = await fsx.readFile(pubspecPath, 'utf8');
|
|
68
|
+
const dependency = this.getDependency();
|
|
69
|
+
|
|
70
|
+
// 移除 flu_core_dependency 占位符
|
|
71
|
+
if (content.includes('{{{flu_core_dependency}}}')) {
|
|
72
|
+
content = content.replace(/\s*\{{{flu_core_dependency}}}\s*\n?/, '');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (content.includes('{{{state_manager_dependency}}}')) {
|
|
76
|
+
content = content.replace('{{{state_manager_dependency}}}', dependency);
|
|
77
|
+
} else if (dependency) {
|
|
78
|
+
content = content.replace(/(dependencies:\s*\n(?:[\s\S]*?))(\n\w)/, `$1\n ${dependency}$2`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await fsx.writeFile(pubspecPath, content);
|
|
82
|
+
if (dependency) logger.info(` ✓ 添加依赖: ${dependency.split(':')[0]}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 返回选中状态管理器的依赖字符串
|
|
87
|
+
*/
|
|
88
|
+
getDependency () {
|
|
89
|
+
const map = {
|
|
90
|
+
default: '',
|
|
91
|
+
provider: 'provider: ^6.1.1',
|
|
92
|
+
getx: 'get: ^4.6.6',
|
|
93
|
+
riverpod: 'flutter_riverpod: ^2.4.9'
|
|
94
|
+
};
|
|
95
|
+
return map[this.stateManager] || '';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 为所有模板生成 BaseViewModel 变体并重导出统一入口
|
|
100
|
+
*/
|
|
101
|
+
async generateBaseViewModel () {
|
|
102
|
+
const { detectProjectTemplate } = await import('../utils/project_detector.js');
|
|
103
|
+
const tpl = detectProjectTemplate(this.projectPath);
|
|
104
|
+
|
|
105
|
+
// 根据模板类型确定目标路径
|
|
106
|
+
let baseDir;
|
|
107
|
+
if (tpl === 'lite') {
|
|
108
|
+
baseDir = join(this.projectPath, 'lib', 'base');
|
|
109
|
+
} else if (tpl === 'modular') {
|
|
110
|
+
baseDir = join(this.projectPath, 'lib', 'core', 'base');
|
|
111
|
+
} else if (tpl === 'clean') {
|
|
112
|
+
baseDir = join(this.projectPath, 'lib', 'core', 'base');
|
|
113
|
+
} else {
|
|
114
|
+
return; // 未知模板,跳过
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await fsx.ensureDir(baseDir);
|
|
118
|
+
const target = join(baseDir, 'base_viewmodel.dart');
|
|
119
|
+
|
|
120
|
+
const content = this.buildBaseViewModelContent();
|
|
121
|
+
await fsx.writeFile(target, content);
|
|
122
|
+
|
|
123
|
+
const relPath = target.replace(this.projectPath + '/', '');
|
|
124
|
+
logger.info(` ✓ 生成 BaseViewModel: ${relPath}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 构建 BaseViewModel 变体源码:
|
|
129
|
+
* - getx:GetxController + 自管理监听列表(兼容 BasePage 的 addListener/removeListener)
|
|
130
|
+
* - default/provider:ChangeNotifier 变体
|
|
131
|
+
*/
|
|
132
|
+
buildBaseViewModelContent () {
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
const commonInfo = `
|
|
136
|
+
ViewState _state = ViewState.idle;
|
|
137
|
+
String? _errorMessage;
|
|
138
|
+
bool _isRefreshing = false;
|
|
139
|
+
|
|
140
|
+
// ==================== Getters ====================
|
|
141
|
+
// 当前视图状态
|
|
142
|
+
ViewState get state => _state;
|
|
143
|
+
// 错误信息
|
|
144
|
+
String? get errorMessage => _errorMessage;
|
|
145
|
+
// 是否正在加载
|
|
146
|
+
bool get isLoading => _state == ViewState.loading;
|
|
147
|
+
// 是否加载失败
|
|
148
|
+
bool get isError => _state == ViewState.error;
|
|
149
|
+
// 是否加载成功
|
|
150
|
+
bool get isSuccess => _state == ViewState.success;
|
|
151
|
+
// 是否空闲状态
|
|
152
|
+
bool get isIdle => _state == ViewState.idle;
|
|
153
|
+
// 是否正在刷新
|
|
154
|
+
bool get isRefreshing => _isRefreshing;
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
// ==================== 状态设置 ====================
|
|
158
|
+
|
|
159
|
+
/// 设置视图状态
|
|
160
|
+
///
|
|
161
|
+
/// [state] 新的状态
|
|
162
|
+
/// [error] 错误信息(仅在 error 状态时有效)
|
|
163
|
+
void setState(ViewState state, {String? error}) {
|
|
164
|
+
_state = state;
|
|
165
|
+
_errorMessage = error;
|
|
166
|
+
notifyListeners();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// 设置刷新状态
|
|
170
|
+
void setRefreshing(bool refreshing) {
|
|
171
|
+
_isRefreshing = refreshing;
|
|
172
|
+
notifyListeners();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
`;
|
|
177
|
+
if (this.stateManager === 'getx') {
|
|
178
|
+
return `import 'package:get/get.dart';
|
|
179
|
+
enum ViewState { idle, loading, success, error }
|
|
180
|
+
class BaseViewModel extends GetxController {
|
|
181
|
+
${commonInfo}
|
|
182
|
+
|
|
183
|
+
/// 刷新数据(通知所有监听器)
|
|
184
|
+
void notifyListeners() {
|
|
185
|
+
notifyDataChange();
|
|
186
|
+
}
|
|
187
|
+
/// 通知数据变化(仅用于局部刷新)
|
|
188
|
+
void notifyDataChange() {
|
|
189
|
+
update();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ==================== 生命周期 ====================
|
|
193
|
+
/// 初始化钩子(函数级注释)
|
|
194
|
+
@override
|
|
195
|
+
Future<void> onInit() async {
|
|
196
|
+
super.onInit();
|
|
197
|
+
// 子类可以重写此方法进行初始化
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// 刷新数据(钩子)
|
|
201
|
+
Future<void> refreshData() async {
|
|
202
|
+
// 子类可以重写此方法实现刷新逻辑
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// provider 与 default 采用 ChangeNotifier 变体
|
|
209
|
+
return `import 'package:flutter/foundation.dart';
|
|
210
|
+
enum ViewState { idle, loading, success, error }
|
|
211
|
+
|
|
212
|
+
class BaseViewModel extends ChangeNotifier {
|
|
213
|
+
${commonInfo}
|
|
214
|
+
|
|
215
|
+
/// 通知数据变化(仅用于局部刷新)
|
|
216
|
+
void notifyDataChange() {
|
|
217
|
+
notifyListeners();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ==================== 生命周期 ====================
|
|
221
|
+
/// 初始化钩子(函数级注释)
|
|
222
|
+
Future<void> onInit() async {
|
|
223
|
+
// 子类可以重写此方法进行初始化
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// 刷新数据(钩子)
|
|
227
|
+
Future<void> refreshData() async {
|
|
228
|
+
// 子类可以重写此方法实现刷新逻辑
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 配置入口 main.dart:
|
|
236
|
+
* - 模板文件执行 Handlebars 渲染(只处理 riverpod 条件)
|
|
237
|
+
* - 非模板文件无需额外处理
|
|
238
|
+
*/
|
|
239
|
+
async configureMain () {
|
|
240
|
+
const mainPath = join(this.projectPath, 'lib', 'main.dart');
|
|
241
|
+
if (!(await fsx.pathExists(mainPath))) {
|
|
242
|
+
throw new Error(`main.dart not found at ${mainPath}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const raw = await fsx.readFile(mainPath, 'utf8');
|
|
246
|
+
|
|
247
|
+
// 检查是否为模板文件
|
|
248
|
+
const isTemplate = raw.includes('{{#if_riverpod}}');
|
|
249
|
+
if (isTemplate) {
|
|
250
|
+
this.registerHandlebarsHelpers();
|
|
251
|
+
const compiled = Handlebars.compile(raw);
|
|
252
|
+
const rendered = compiled({
|
|
253
|
+
if_riverpod: this.stateManager === 'riverpod'
|
|
254
|
+
});
|
|
255
|
+
await fsx.writeFile(mainPath, rendered);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 增强 App 入口组件(GetX):
|
|
261
|
+
* - lite/clean:lib/app.dart → MaterialApp 替换为 GetMaterialApp,注入 get 导入与 getPages
|
|
262
|
+
* - modular:lib/main.dart → MaterialApp 替换为 GetMaterialApp,注入 get 导入与 getPages
|
|
263
|
+
*/
|
|
264
|
+
async configureAppWidget () {
|
|
265
|
+
try {
|
|
266
|
+
const tpl = detectProjectTemplate(this.projectPath);
|
|
267
|
+
const isGetX = this.stateManager === 'getx';
|
|
268
|
+
if (!isGetX) return;
|
|
269
|
+
|
|
270
|
+
if (tpl === 'lite' || tpl === 'clean') {
|
|
271
|
+
const appPath = join(this.projectPath, 'lib', 'app.dart');
|
|
272
|
+
if (!(await fsx.pathExists(appPath))) return;
|
|
273
|
+
let raw = await fsx.readFile(appPath, 'utf8');
|
|
274
|
+
if (!raw.includes('GetMaterialApp(')) {
|
|
275
|
+
raw = raw.replace(/MaterialApp\s*\(/, 'GetMaterialApp(');
|
|
276
|
+
}
|
|
277
|
+
if (!/package:get\/get\.dart/.test(raw)) {
|
|
278
|
+
raw = `import 'package:get/get.dart';\n` + raw;
|
|
279
|
+
}
|
|
280
|
+
const pagesAccessor = tpl === 'lite' ? 'AppRoutes.pages' : 'AppRoutes.pages';
|
|
281
|
+
if (!new RegExp(`getPages:\\s*${pagesAccessor}`).test(raw)) {
|
|
282
|
+
raw = raw.replace(/initialRoute:\s*AppRoutes\.home,/, `initialRoute: AppRoutes.home,\n getPages: ${pagesAccessor},`);
|
|
283
|
+
}
|
|
284
|
+
raw = raw.replace(/\s*routes:\s*AppRoutes\.routes,\s*\n/, '\n');
|
|
285
|
+
await fsx.writeFile(appPath, raw);
|
|
286
|
+
} else if (tpl === 'modular') {
|
|
287
|
+
const mainPath = join(this.projectPath, 'lib', 'main.dart');
|
|
288
|
+
if (!(await fsx.pathExists(mainPath))) return;
|
|
289
|
+
let raw = await fsx.readFile(mainPath, 'utf8');
|
|
290
|
+
if (!raw.includes('GetMaterialApp(')) {
|
|
291
|
+
raw = raw.replace(/MaterialApp\s*\(/, 'GetMaterialApp(');
|
|
292
|
+
}
|
|
293
|
+
if (!/package:get\/get\.dart/.test(raw)) {
|
|
294
|
+
raw = raw.replace(/^(import\s+['"].+?['"];\s*\n)+/m, (m) => `${m}import 'package:get/get.dart';\n`);
|
|
295
|
+
if (!raw.includes("import 'package:get/get.dart';")) {
|
|
296
|
+
raw = `import 'package:get/get.dart';\n` + raw;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!/getPages:\s*AppRouter\.pages/.test(raw)) {
|
|
300
|
+
raw = raw.replace(/initialRoute:\s*AppRouter\.home,/, "initialRoute: AppRouter.home,\n getPages: AppRouter.pages,");
|
|
301
|
+
}
|
|
302
|
+
raw = raw.replace(/\s*routes:\s*AppRouter\.routes,\s*\n/, '\n');
|
|
303
|
+
await fsx.writeFile(mainPath, raw);
|
|
304
|
+
}
|
|
305
|
+
} catch (_) { }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* 增强路由文件(GetX):为不同模板注入 List<GetPage> pages 与 get 导入
|
|
310
|
+
* 智能转换原有路由定义,保留所有路由
|
|
311
|
+
*/
|
|
312
|
+
async configureRoutes () {
|
|
313
|
+
try {
|
|
314
|
+
const tpl = detectProjectTemplate(this.projectPath);
|
|
315
|
+
if (this.stateManager !== 'getx') return;
|
|
316
|
+
let routesPath;
|
|
317
|
+
if (tpl === 'lite') {
|
|
318
|
+
routesPath = join(this.projectPath, 'lib', 'config', 'routes.dart');
|
|
319
|
+
} else if (tpl === 'modular') {
|
|
320
|
+
routesPath = join(this.projectPath, 'lib', 'core', 'router', 'app_router.dart');
|
|
321
|
+
} else if (tpl === 'clean') {
|
|
322
|
+
routesPath = join(this.projectPath, 'lib', 'config', 'routes', 'app_routes.dart');
|
|
323
|
+
} else {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (!(await fsx.pathExists(routesPath))) return;
|
|
327
|
+
let raw = await fsx.readFile(routesPath, 'utf8');
|
|
328
|
+
|
|
329
|
+
// 替换 flutter import 为 get import
|
|
330
|
+
if (!/package:get\/get\.dart/.test(raw)) {
|
|
331
|
+
raw = raw.replace("import 'package:flutter/material.dart';\n", "import 'package:get/get.dart';\n");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 如果已经是 GetPage 格式,跳过
|
|
335
|
+
if (/static\s+List<\s*GetPage/.test(raw)) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 提取路由常量定义
|
|
340
|
+
const routeConstantsMatch = raw.match(/static\s+const\s+String\s+\w+\s*=\s*['"][^'"]+['"];/g);
|
|
341
|
+
const routeConstants = routeConstantsMatch ? routeConstantsMatch.join('\n ') : "static const String home = '/';";
|
|
342
|
+
|
|
343
|
+
// 提取原有的 routes map 并转换为 GetPage
|
|
344
|
+
let getPages = [];
|
|
345
|
+
const routesMapMatch = raw.match(/routes:\s*\(context\)\s*=>\s*const\s+(\w+)\(\)/g) ||
|
|
346
|
+
raw.match(/(\w+):\s*\(context\)\s*=>\s*const\s+(\w+)\(\)/g);
|
|
347
|
+
|
|
348
|
+
if (routesMapMatch) {
|
|
349
|
+
// 从 routes getter 中提取
|
|
350
|
+
const routesGetterMatch = raw.match(/static\s+Map<String,\s*WidgetBuilder>\s+get\s+routes\s*\{[^}]*return\s*\{([^}]+)\};/s);
|
|
351
|
+
if (routesGetterMatch) {
|
|
352
|
+
const routesContent = routesGetterMatch[1];
|
|
353
|
+
// 解析每个路由
|
|
354
|
+
const routePattern = /(\w+):\s*\(context\)\s*=>\s*const\s+(\w+)\(\)/g;
|
|
355
|
+
let match;
|
|
356
|
+
while ((match = routePattern.exec(routesContent)) !== null) {
|
|
357
|
+
const [, routeName, widgetName] = match;
|
|
358
|
+
getPages.push(` GetPage(name: ${routeName}, page: () => const ${widgetName}())`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// 如果没有提取到任何路由,使用默认的 home
|
|
364
|
+
if (getPages.length === 0) {
|
|
365
|
+
getPages.push(` GetPage(name: home, page: () => const HomePage())`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const className = raw.match(/class\s+(\w+)/)?.[1] || 'AppRoutes';
|
|
369
|
+
|
|
370
|
+
// 构建新的 class
|
|
371
|
+
const newClass = `class ${className} {
|
|
372
|
+
${className}._();
|
|
373
|
+
|
|
374
|
+
${routeConstants}
|
|
375
|
+
|
|
376
|
+
static List<GetPage<dynamic>> get pages {
|
|
377
|
+
return [
|
|
378
|
+
${getPages.join(',\n')},
|
|
379
|
+
];
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
`;
|
|
383
|
+
|
|
384
|
+
// 替换整个 class
|
|
385
|
+
raw = raw.replace(/class\s+\w+[\s\S]*?^}/m, newClass);
|
|
386
|
+
|
|
387
|
+
await fsx.writeFile(routesPath, raw);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
logger.warn(` ⚠️ 路由配置可能需要手动调整: ${error.message}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* 注册 Handlebars 条件助手:控制模板片段的渲染/隐藏
|
|
395
|
+
*/
|
|
396
|
+
registerHandlebarsHelpers () {
|
|
397
|
+
Handlebars.registerHelper('if_riverpod', function (options) {
|
|
398
|
+
return options.data.root.if_riverpod ? options.fn(this) : options.inverse(this);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ViewModel 生成器
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
import { toPascalCase, toSnakeCase } from '../utils/string_helper.js';
|
|
9
|
+
import { updateIndexFile } from '../utils/index_updater.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 生成 ViewModel 文件
|
|
13
|
+
*/
|
|
14
|
+
export function generateViewModel (name, options = {}) {
|
|
15
|
+
try {
|
|
16
|
+
const {
|
|
17
|
+
feature = null,
|
|
18
|
+
outputDir = process.cwd()
|
|
19
|
+
} = options;
|
|
20
|
+
|
|
21
|
+
// 转换命名
|
|
22
|
+
const namePascal = toPascalCase(name);
|
|
23
|
+
const nameSnake = toSnakeCase(name);
|
|
24
|
+
|
|
25
|
+
// 确定输出路径
|
|
26
|
+
let vmDir;
|
|
27
|
+
if (feature) {
|
|
28
|
+
// Modular/Clean 架构
|
|
29
|
+
vmDir = join(outputDir, 'lib', 'features', feature, 'viewmodels');
|
|
30
|
+
} else {
|
|
31
|
+
// Lite 架构
|
|
32
|
+
vmDir = join(outputDir, 'lib', 'viewmodels');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 创建目录
|
|
36
|
+
if (!existsSync(vmDir)) {
|
|
37
|
+
mkdirSync(vmDir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 生成文件内容
|
|
41
|
+
const content = generateViewModelContent(namePascal);
|
|
42
|
+
|
|
43
|
+
// 写入文件
|
|
44
|
+
const filePath = join(vmDir, `${nameSnake}_viewmodel.dart`);
|
|
45
|
+
if (existsSync(filePath)) {
|
|
46
|
+
logger.error(`文件已存在: ${filePath}`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
writeFileSync(filePath, content, 'utf8');
|
|
51
|
+
logger.success(`ViewModel 创建成功: ${filePath}`);
|
|
52
|
+
|
|
53
|
+
// 更新 index.dart
|
|
54
|
+
updateIndexFile(vmDir, `${nameSnake}_viewmodel.dart`);
|
|
55
|
+
|
|
56
|
+
return true;
|
|
57
|
+
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logger.error(`生成 ViewModel 失败: ${error.message}`);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 生成 ViewModel 内容
|
|
66
|
+
*/
|
|
67
|
+
function generateViewModelContent (namePascal) {
|
|
68
|
+
return `import 'package:flutter/foundation.dart';
|
|
69
|
+
|
|
70
|
+
class ${namePascal}ViewModel extends ChangeNotifier {
|
|
71
|
+
// 状态
|
|
72
|
+
bool _isLoading = false;
|
|
73
|
+
bool get isLoading => _isLoading;
|
|
74
|
+
|
|
75
|
+
String? _error;
|
|
76
|
+
String? get error => _error;
|
|
77
|
+
|
|
78
|
+
// 初始化
|
|
79
|
+
void init() {
|
|
80
|
+
// 初始化逻辑
|
|
81
|
+
loadData();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 加载数据
|
|
85
|
+
Future<void> loadData() async {
|
|
86
|
+
_isLoading = true;
|
|
87
|
+
_error = null;
|
|
88
|
+
notifyListeners();
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// TODO: 实现数据加载逻辑
|
|
92
|
+
await Future.delayed(const Duration(seconds: 1));
|
|
93
|
+
|
|
94
|
+
_isLoading = false;
|
|
95
|
+
notifyListeners();
|
|
96
|
+
} catch (e) {
|
|
97
|
+
_error = e.toString();
|
|
98
|
+
_isLoading = false;
|
|
99
|
+
notifyListeners();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 刷新
|
|
104
|
+
Future<void> refresh() async {
|
|
105
|
+
await loadData();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@override
|
|
109
|
+
void dispose() {
|
|
110
|
+
// 清理资源
|
|
111
|
+
super.dispose();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Widget 生成器
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
import { toPascalCase, toSnakeCase } from '../utils/string_helper.js';
|
|
9
|
+
import { updateIndexFile } from '../utils/index_updater.js';
|
|
10
|
+
import { detectProjectTemplate, getWidgetPath } from '../utils/project_detector.js';
|
|
11
|
+
import { getSnippetContent } from '../utils/snippet_loader.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 生成 Widget 文件
|
|
15
|
+
*/
|
|
16
|
+
export function generateWidget (name, options = {}) {
|
|
17
|
+
try {
|
|
18
|
+
const {
|
|
19
|
+
feature = null,
|
|
20
|
+
stateful = false,
|
|
21
|
+
outputDir = process.cwd()
|
|
22
|
+
} = options;
|
|
23
|
+
|
|
24
|
+
// 检测项目模板类型
|
|
25
|
+
const template = detectProjectTemplate(outputDir);
|
|
26
|
+
logger.info(`检测到项目类型: ${template || '未知'}`);
|
|
27
|
+
|
|
28
|
+
// 转换命名
|
|
29
|
+
const namePascal = toPascalCase(name);
|
|
30
|
+
const nameSnake = toSnakeCase(name);
|
|
31
|
+
|
|
32
|
+
// 根据模板类型确定输出路径
|
|
33
|
+
const widgetsDir = getWidgetPath(outputDir, feature);
|
|
34
|
+
|
|
35
|
+
// 创建目录
|
|
36
|
+
if (!existsSync(widgetsDir)) {
|
|
37
|
+
mkdirSync(widgetsDir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 生成文件内容(优先片段)
|
|
41
|
+
const key = stateful ? 'flu.stWidget' : 'flu.lessWidget';
|
|
42
|
+
const contentFromSnippet = getSnippetContent(outputDir, key, { Name: namePascal });
|
|
43
|
+
const content = contentFromSnippet || generateWidgetContent(namePascal, stateful);
|
|
44
|
+
|
|
45
|
+
// 写入文件
|
|
46
|
+
const filePath = join(widgetsDir, `${nameSnake}_widget.dart`);
|
|
47
|
+
if (existsSync(filePath)) {
|
|
48
|
+
logger.error(`文件已存在: ${filePath}`);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
writeFileSync(filePath, content, 'utf8');
|
|
53
|
+
logger.success(`Widget 创建成功: ${filePath}`);
|
|
54
|
+
|
|
55
|
+
// 更新 index.dart
|
|
56
|
+
updateIndexFile(widgetsDir, `${nameSnake}_widget.dart`);
|
|
57
|
+
|
|
58
|
+
return true;
|
|
59
|
+
|
|
60
|
+
} catch (error) {
|
|
61
|
+
logger.error(`生成 Widget 失败: ${error.message}`);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 生成 Widget 内容
|
|
68
|
+
*/
|
|
69
|
+
function generateWidgetContent (namePascal, stateful) {
|
|
70
|
+
if (stateful) {
|
|
71
|
+
return `import 'package:flutter/material.dart';
|
|
72
|
+
|
|
73
|
+
class ${namePascal}Widget extends StatefulWidget {
|
|
74
|
+
const ${namePascal}Widget({super.key});
|
|
75
|
+
|
|
76
|
+
@override
|
|
77
|
+
State<${namePascal}Widget> createState() => _${namePascal}WidgetState();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class _${namePascal}WidgetState extends State<${namePascal}Widget> {
|
|
81
|
+
@override
|
|
82
|
+
Widget build(BuildContext context) {
|
|
83
|
+
return Container(
|
|
84
|
+
child: const Text('${namePascal}Widget'),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
`;
|
|
89
|
+
} else {
|
|
90
|
+
return `import 'package:flutter/material.dart';
|
|
91
|
+
|
|
92
|
+
class ${namePascal}Widget extends StatelessWidget {
|
|
93
|
+
const ${namePascal}Widget({super.key});
|
|
94
|
+
|
|
95
|
+
@override
|
|
96
|
+
Widget build(BuildContext context) {
|
|
97
|
+
return Container(
|
|
98
|
+
child: const Text('${namePascal}Widget'),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
}
|