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.
Files changed (50) hide show
  1. package/CLI.md +349 -0
  2. package/README.md +59 -155
  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,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
+ }