flu-cli 2.0.6 → 2.1.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 (45) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +17 -4
  3. package/config/dev.config.js +11 -11
  4. package/config/templates.js +10 -10
  5. package/index.js +554 -102
  6. package/lib/commands/add.js +365 -266
  7. package/lib/commands/assets.js +77 -78
  8. package/lib/commands/cache.js +29 -52
  9. package/lib/commands/completion.js +13 -11
  10. package/lib/commands/config.js +150 -44
  11. package/lib/commands/init-ai-base.js +89 -0
  12. package/lib/commands/newClack.js +269 -178
  13. package/lib/commands/snippets.js +58 -43
  14. package/lib/commands/template.js +98 -58
  15. package/lib/commands/templates.js +101 -57
  16. package/lib/commands/upload.js +313 -0
  17. package/lib/commands/vnext-options.js +206 -0
  18. package/lib/generators/model_generator.js +91 -88
  19. package/lib/generators/page_generator.js +100 -93
  20. package/lib/generators/service_generator.js +44 -39
  21. package/lib/generators/viewmodel_generator.js +25 -29
  22. package/lib/generators/widget_generator.js +30 -35
  23. package/lib/templates/templateCopier.js +14 -15
  24. package/lib/templates/templateManager.js +22 -21
  25. package/lib/utils/config.js +37 -20
  26. package/lib/utils/flutterHelper.js +2 -2
  27. package/lib/utils/i18n.js +3 -3
  28. package/lib/utils/index_updater.js +22 -23
  29. package/lib/utils/json-output.js +59 -0
  30. package/lib/utils/logger.js +17 -17
  31. package/lib/utils/project_detector.js +66 -66
  32. package/lib/utils/snippet_loader.js +21 -19
  33. package/lib/utils/string_helper.js +13 -13
  34. package/lib/utils/templateSelectorEnquirer.js +94 -108
  35. package/locales/en-US.json +1 -1
  36. package/locales/zh-CN.json +2 -2
  37. package/package.json +60 -57
  38. package/scripts/smoke-vnext-generate.mjs +1934 -0
  39. package/scripts/smoke-vnext-params.mjs +92 -0
  40. package/CLI.md +0 -513
  41. package/release.sh +0 -529
  42. package/scripts/e2e-state-tests.js +0 -116
  43. package/scripts/sync-base-to-templates.js +0 -108
  44. package/scripts/workspace-clone-all.sh +0 -101
  45. package/scripts/workspace-status-all.sh +0 -112
@@ -2,80 +2,77 @@
2
2
  * Model 生成器
3
3
  */
4
4
 
5
- import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
6
- import { join } from 'path';
7
- import { logger } from '../utils/logger.js';
8
- import { toPascalCase, toSnakeCase, toCamelCase } from '../utils/string_helper.js';
9
- import { updateIndexFile } from '../utils/index_updater.js';
10
- import { getSnippetContent } from '../utils/snippet_loader.js';
5
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs'
6
+ import { join } from 'path'
7
+ import { logger } from '../utils/logger.js'
8
+ import { toPascalCase, toSnakeCase, toCamelCase } from '../utils/string_helper.js'
9
+ import { updateIndexFile } from '../utils/index_updater.js'
10
+ import { getSnippetContent } from '../utils/snippet_loader.js'
11
11
 
12
12
  /**
13
13
  * 生成 Model 文件
14
14
  */
15
- export function generateModel (name, options = {}) {
15
+ export function generateModel(name, options = {}) {
16
16
  try {
17
- const {
18
- feature = null,
19
- jsonFile = null,
20
- outputDir = process.cwd()
21
- } = options;
17
+ const { feature = null, jsonFile = null, outputDir = process.cwd() } = options
22
18
 
23
19
  // 转换命名
24
- const namePascal = toPascalCase(name);
25
- const nameSnake = toSnakeCase(name);
20
+ const namePascal = toPascalCase(name)
21
+ const nameSnake = toSnakeCase(name)
26
22
 
27
23
  // 确定输出路径
28
- let modelsDir;
24
+ let modelsDir
29
25
  if (feature) {
30
26
  // Modular/Clean 架构
31
- modelsDir = join(outputDir, 'lib', 'features', feature, 'models');
27
+ modelsDir = join(outputDir, 'lib', 'features', feature, 'models')
32
28
  } else {
33
29
  // Lite 架构
34
- modelsDir = join(outputDir, 'lib', 'models');
30
+ modelsDir = join(outputDir, 'lib', 'models')
35
31
  }
36
32
 
37
33
  // 创建目录
38
34
  if (!existsSync(modelsDir)) {
39
- mkdirSync(modelsDir, { recursive: true });
35
+ mkdirSync(modelsDir, { recursive: true })
40
36
  }
41
37
 
42
38
  // 生成文件内容
43
- let content;
39
+ let content
44
40
  if (jsonFile && existsSync(jsonFile)) {
45
41
  // 从 JSON 文件生成
46
- const jsonContent = readFileSync(jsonFile, 'utf8');
47
- const jsonData = JSON.parse(jsonContent);
48
- content = generateModelFromJson(namePascal, jsonData);
42
+ const jsonContent = readFileSync(jsonFile, 'utf8')
43
+ const jsonData = JSON.parse(jsonContent)
44
+ content = generateModelFromJson(namePascal, jsonData)
49
45
  } else {
50
46
  // 优先片段,其次基础模板
51
- content = getSnippetContent(outputDir, 'flu.model', { Name: namePascal }) || generateBasicModel(namePascal);
47
+ content =
48
+ getSnippetContent(outputDir, 'flu.model', { Name: namePascal }) ||
49
+ generateBasicModel(namePascal)
52
50
  }
53
51
 
54
52
  // 写入文件
55
- const filePath = join(modelsDir, `${nameSnake}_model.dart`);
53
+ const filePath = join(modelsDir, `${nameSnake}_model.dart`)
56
54
  if (existsSync(filePath)) {
57
- logger.error(`文件已存在: ${filePath}`);
58
- return false;
55
+ logger.error(`文件已存在: ${filePath}`)
56
+ return false
59
57
  }
60
58
 
61
- writeFileSync(filePath, content, 'utf8');
62
- logger.success(`Model 创建成功: ${filePath}`);
59
+ writeFileSync(filePath, content, 'utf8')
60
+ logger.success(`Model 创建成功: ${filePath}`)
63
61
 
64
62
  // 更新 index.dart
65
- updateIndexFile(modelsDir, `${nameSnake}_model.dart`);
66
-
67
- return true;
63
+ updateIndexFile(modelsDir, `${nameSnake}_model.dart`)
68
64
 
65
+ return true
69
66
  } catch (error) {
70
- logger.error(`生成 Model 失败: ${error.message}`);
71
- return false;
67
+ logger.error(`生成 Model 失败: ${error.message}`)
68
+ return false
72
69
  }
73
70
  }
74
71
 
75
72
  /**
76
73
  * 生成基础 Model
77
74
  */
78
- function generateBasicModel (namePascal) {
75
+ function generateBasicModel(namePascal) {
79
76
  return `class ${namePascal}Model {
80
77
  final String id;
81
78
  final String name;
@@ -137,61 +134,67 @@ function generateBasicModel (namePascal) {
137
134
  return id.hashCode ^ name.hashCode ^ createdAt.hashCode;
138
135
  }
139
136
  }
140
- `;
137
+ `
141
138
  }
142
139
 
143
140
  /**
144
141
  * 从 JSON 生成 Model
145
142
  */
146
- function generateModelFromJson (namePascal, jsonData) {
147
- const fields = analyzeJsonStructure(jsonData);
143
+ function generateModelFromJson(namePascal, jsonData) {
144
+ const fields = analyzeJsonStructure(jsonData)
148
145
 
149
146
  // 生成字段声明
150
- const fieldDeclarations = fields.map(f => ` final ${f.type} ${f.name};`).join('\n');
147
+ const fieldDeclarations = fields.map((f) => ` final ${f.type} ${f.name};`).join('\n')
151
148
 
152
149
  // 生成构造函数参数
153
- const constructorParams = fields.map(f => ` required this.${f.name},`).join('\n');
150
+ const constructorParams = fields.map((f) => ` required this.${f.name},`).join('\n')
154
151
 
155
152
  // 生成 fromJson 字段
156
- const fromJsonFields = fields.map(f => {
157
- if (f.type === 'String') {
158
- return ` ${f.name}: json['${f.jsonKey}'] as String,`;
159
- } else if (f.type === 'int') {
160
- return ` ${f.name}: json['${f.jsonKey}'] as int,`;
161
- } else if (f.type === 'double') {
162
- return ` ${f.name}: (json['${f.jsonKey}'] as num).toDouble(),`;
163
- } else if (f.type === 'bool') {
164
- return ` ${f.name}: json['${f.jsonKey}'] as bool,`;
165
- } else if (f.type === 'DateTime') {
166
- return ` ${f.name}: DateTime.parse(json['${f.jsonKey}'] as String),`;
167
- } else if (f.type.startsWith('List<')) {
168
- return ` ${f.name}: (json['${f.jsonKey}'] as List).cast<${f.type.slice(5, -1)}>(),`;
169
- } else {
170
- return ` ${f.name}: json['${f.jsonKey}'],`;
171
- }
172
- }).join('\n');
153
+ const fromJsonFields = fields
154
+ .map((f) => {
155
+ if (f.type === 'String') {
156
+ return ` ${f.name}: json['${f.jsonKey}'] as String,`
157
+ } else if (f.type === 'int') {
158
+ return ` ${f.name}: json['${f.jsonKey}'] as int,`
159
+ } else if (f.type === 'double') {
160
+ return ` ${f.name}: (json['${f.jsonKey}'] as num).toDouble(),`
161
+ } else if (f.type === 'bool') {
162
+ return ` ${f.name}: json['${f.jsonKey}'] as bool,`
163
+ } else if (f.type === 'DateTime') {
164
+ return ` ${f.name}: DateTime.parse(json['${f.jsonKey}'] as String),`
165
+ } else if (f.type.startsWith('List<')) {
166
+ return ` ${f.name}: (json['${f.jsonKey}'] as List).cast<${f.type.slice(5, -1)}>(),`
167
+ } else {
168
+ return ` ${f.name}: json['${f.jsonKey}'],`
169
+ }
170
+ })
171
+ .join('\n')
173
172
 
174
173
  // 生成 toJson 字段
175
- const toJsonFields = fields.map(f => {
176
- if (f.type === 'DateTime') {
177
- return ` '${f.jsonKey}': ${f.name}.toIso8601String(),`;
178
- } else {
179
- return ` '${f.jsonKey}': ${f.name},`;
180
- }
181
- }).join('\n');
174
+ const toJsonFields = fields
175
+ .map((f) => {
176
+ if (f.type === 'DateTime') {
177
+ return ` '${f.jsonKey}': ${f.name}.toIso8601String(),`
178
+ } else {
179
+ return ` '${f.jsonKey}': ${f.name},`
180
+ }
181
+ })
182
+ .join('\n')
182
183
 
183
184
  // 生成 copyWith 参数
184
- const copyWithParams = fields.map(f => ` ${f.type}? ${f.name},`).join('\n');
185
- const copyWithFields = fields.map(f => ` ${f.name}: ${f.name} ?? this.${f.name},`).join('\n');
185
+ const copyWithParams = fields.map((f) => ` ${f.type}? ${f.name},`).join('\n')
186
+ const copyWithFields = fields
187
+ .map((f) => ` ${f.name}: ${f.name} ?? this.${f.name},`)
188
+ .join('\n')
186
189
 
187
190
  // 生成 toString
188
- const toStringFields = fields.map(f => `${f.name}: $${f.name}`).join(', ');
191
+ const toStringFields = fields.map((f) => `${f.name}: $${f.name}`).join(', ')
189
192
 
190
193
  // 生成 == 比较
191
- const equalityChecks = fields.map(f => ` other.${f.name} == ${f.name}`).join(' &&\n');
194
+ const equalityChecks = fields.map((f) => ` other.${f.name} == ${f.name}`).join(' &&\n')
192
195
 
193
196
  // 生成 hashCode
194
- const hashCodeFields = fields.map(f => `${f.name}.hashCode`).join(' ^ ');
197
+ const hashCodeFields = fields.map((f) => `${f.name}.hashCode`).join(' ^ ')
195
198
 
196
199
  return `class ${namePascal}Model {
197
200
  ${fieldDeclarations}
@@ -240,64 +243,64 @@ ${equalityChecks};
240
243
  return ${hashCodeFields};
241
244
  }
242
245
  }
243
- `;
246
+ `
244
247
  }
245
248
 
246
249
  /**
247
250
  * 分析 JSON 结构
248
251
  */
249
- function analyzeJsonStructure (jsonData) {
250
- const fields = [];
252
+ function analyzeJsonStructure(jsonData) {
253
+ const fields = []
251
254
 
252
255
  for (const [key, value] of Object.entries(jsonData)) {
253
- const camelKey = toCamelCase(key);
254
- const type = inferDartType(value);
256
+ const camelKey = toCamelCase(key)
257
+ const type = inferDartType(value)
255
258
 
256
259
  fields.push({
257
260
  name: camelKey,
258
261
  jsonKey: key,
259
- type: type
260
- });
262
+ type: type,
263
+ })
261
264
  }
262
265
 
263
- return fields;
266
+ return fields
264
267
  }
265
268
 
266
269
  /**
267
270
  * 推断 Dart 类型
268
271
  */
269
- function inferDartType (value) {
270
- if (value === null) return 'dynamic';
272
+ function inferDartType(value) {
273
+ if (value === null) return 'dynamic'
271
274
 
272
- const type = typeof value;
275
+ const type = typeof value
273
276
 
274
277
  if (type === 'string') {
275
278
  // 尝试判断是否是日期
276
279
  if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
277
- return 'DateTime';
280
+ return 'DateTime'
278
281
  }
279
- return 'String';
282
+ return 'String'
280
283
  }
281
284
 
282
285
  if (type === 'number') {
283
- return Number.isInteger(value) ? 'int' : 'double';
286
+ return Number.isInteger(value) ? 'int' : 'double'
284
287
  }
285
288
 
286
289
  if (type === 'boolean') {
287
- return 'bool';
290
+ return 'bool'
288
291
  }
289
292
 
290
293
  if (Array.isArray(value)) {
291
294
  if (value.length === 0) {
292
- return 'List<dynamic>';
295
+ return 'List<dynamic>'
293
296
  }
294
- const itemType = inferDartType(value[0]);
295
- return `List<${itemType}>`;
297
+ const itemType = inferDartType(value[0])
298
+ return `List<${itemType}>`
296
299
  }
297
300
 
298
301
  if (type === 'object') {
299
- return 'Map<String, dynamic>';
302
+ return 'Map<String, dynamic>'
300
303
  }
301
304
 
302
- return 'dynamic';
305
+ return 'dynamic'
303
306
  }
@@ -2,19 +2,19 @@
2
2
  * 页面生成器
3
3
  */
4
4
 
5
- import { existsSync, mkdirSync, writeFileSync } from 'fs';
6
- import { join, relative, dirname } from 'path';
7
- import { logger } from '../utils/logger.js';
8
- import { toPascalCase, toSnakeCase, toTitleCase } from '../utils/string_helper.js';
9
- import { updateIndexFile } from '../utils/index_updater.js';
10
- import { detectProjectTemplate, getPagePath, getViewModelPath } from '../utils/project_detector.js';
11
- import { getSnippetContent } from '../utils/snippet_loader.js';
12
- import { generateModel } from './model_generator.js';
5
+ import { existsSync, mkdirSync, writeFileSync } from 'fs'
6
+ import { join, relative, dirname } from 'path'
7
+ import { logger } from '../utils/logger.js'
8
+ import { toPascalCase, toSnakeCase, toTitleCase } from '../utils/string_helper.js'
9
+ import { updateIndexFile } from '../utils/index_updater.js'
10
+ import { detectProjectTemplate, getPagePath, getViewModelPath } from '../utils/project_detector.js'
11
+ import { getSnippetContent } from '../utils/snippet_loader.js'
12
+ import { generateModel } from './model_generator.js'
13
13
 
14
14
  /**
15
15
  * 生成页面文件
16
16
  */
17
- export function generatePage (name, options = {}) {
17
+ export function generatePage(name, options = {}) {
18
18
  try {
19
19
  const {
20
20
  feature = null,
@@ -22,36 +22,36 @@ export function generatePage (name, options = {}) {
22
22
  stateless = false,
23
23
  withViewModel = true,
24
24
  isListPage = false,
25
- outputDir = process.cwd()
26
- } = options;
25
+ outputDir = process.cwd(),
26
+ } = options
27
27
 
28
28
  // 检测项目模板类型
29
- const template = detectProjectTemplate(outputDir);
30
- logger.info(`检测到项目类型: ${template || '未知'}`);
29
+ const template = detectProjectTemplate(outputDir)
30
+ logger.info(`检测到项目类型: ${template || '未知'}`)
31
31
 
32
32
  // 转换命名
33
- const namePascal = toPascalCase(name);
34
- const nameSnake = toSnakeCase(name);
35
- const nameTitle = toTitleCase(name);
33
+ const namePascal = toPascalCase(name)
34
+ const nameSnake = toSnakeCase(name)
35
+ const nameTitle = toTitleCase(name)
36
36
 
37
37
  // 确定模块名称
38
- let moduleName = feature || name;
38
+ let moduleName = feature || name
39
39
 
40
40
  // 根据模板类型确定输出路径
41
- const pagesDir = getPagePath(outputDir, moduleName);
41
+ const pagesDir = getPagePath(outputDir, moduleName)
42
42
 
43
43
  // 创建目录
44
44
  if (!existsSync(pagesDir)) {
45
- mkdirSync(pagesDir, { recursive: true });
45
+ mkdirSync(pagesDir, { recursive: true })
46
46
  }
47
47
 
48
48
  // 获取 ViewModel 路径用于计算相对导入
49
- const vmDir = getViewModelPath(outputDir, moduleName);
50
- const pageFilePath = join(pagesDir, `${nameSnake}_page.dart`);
51
- const vmFilePath = join(vmDir, `${nameSnake}_viewmodel.dart`);
49
+ const vmDir = getViewModelPath(outputDir, moduleName)
50
+ const pageFilePath = join(pagesDir, `${nameSnake}_page.dart`)
51
+ const vmFilePath = join(vmDir, `${nameSnake}_viewmodel.dart`)
52
52
 
53
53
  // 计算相对导入路径
54
- const vmImportPath = calculateRelativeImport(pageFilePath, vmFilePath);
54
+ const vmImportPath = calculateRelativeImport(pageFilePath, vmFilePath)
55
55
 
56
56
  // 生成文件内容(优先使用项目片段)
57
57
  const variables = {
@@ -60,114 +60,112 @@ export function generatePage (name, options = {}) {
60
60
  snake_name: nameSnake,
61
61
  title: nameTitle,
62
62
  vm_import: withViewModel ? vmImportPath : '',
63
- ModelName: namePascal + 'Model' // 默认 Model 名称
64
- };
63
+ ModelName: namePascal + 'Model', // 默认 Model 名称
64
+ }
65
65
 
66
66
  // 智能选择片段:
67
- let content = null;
68
- let key;
67
+ let content = null
68
+ let key
69
69
 
70
70
  if (isListPage) {
71
71
  // 列表页
72
- key = 'flu.listPage';
73
- logger.info('生成类型: ListPage (BaseListPage) + ViewModel');
72
+ key = 'flu.listPage'
73
+ logger.info('生成类型: ListPage (BaseListPage) + ViewModel')
74
74
  } else if (stateless) {
75
75
  // 强制使用 StatelessWidget
76
- key = 'flu.lessPage';
76
+ key = 'flu.lessPage'
77
77
  if (withViewModel) {
78
- logger.warn('⚠️ 警告: 使用 StatelessWidget + ViewModel 组合,无法监听状态变化');
78
+ logger.warn('⚠️ 警告: 使用 StatelessWidget + ViewModel 组合,无法监听状态变化')
79
79
  }
80
- logger.info('生成类型: StatelessWidget (强制)');
80
+ logger.info('生成类型: StatelessWidget (强制)')
81
81
  } else if (stateful) {
82
82
  // 明确指定 StatefulWidget
83
- key = 'flu.stPage';
84
- logger.info(`生成类型: StatefulWidget${withViewModel ? ' (BasePage) + ViewModel' : ''}`);
83
+ key = 'flu.stPage'
84
+ logger.info(`生成类型: StatefulWidget${withViewModel ? ' (BasePage) + ViewModel' : ''}`)
85
85
  } else if (withViewModel) {
86
86
  // 有 ViewModel 时,默认使用 StatefulWidget (BasePage)
87
- key = 'flu.stPage';
88
- logger.info('生成类型: StatefulWidget (BasePage) + ViewModel');
87
+ key = 'flu.stPage'
88
+ logger.info('生成类型: StatefulWidget (BasePage) + ViewModel')
89
89
  } else {
90
90
  // 无 ViewModel 且未指定,默认 Stateless
91
- key = 'flu.lessPage';
92
- logger.info('生成类型: StatelessWidget (无 ViewModel)');
91
+ key = 'flu.lessPage'
92
+ logger.info('生成类型: StatelessWidget (无 ViewModel)')
93
93
  }
94
94
 
95
- content = getSnippetContent(outputDir, key, variables);
95
+ content = getSnippetContent(outputDir, key, variables)
96
96
 
97
97
  if (!content) {
98
98
  // 如果没有片段,使用默认生成逻辑 (这里简化处理,列表页必须有片段)
99
99
  if (isListPage) {
100
- logger.error('❌ 错误: 找不到列表页代码片段 (flu.listPage)');
101
- logger.info('提示: 请运行 flu-cli sync-snippets 更新代码片段');
102
- return false;
100
+ logger.error('❌ 错误: 找不到列表页代码片段 (flu.listPage)')
101
+ logger.info('提示: 请运行 flu-cli sync-snippets 更新代码片段')
102
+ return false
103
103
  }
104
104
  content = generatePageContent(namePascal, nameSnake, nameTitle, {
105
105
  stateful,
106
106
  withViewModel,
107
107
  vmImportPath,
108
- template
109
- });
108
+ template,
109
+ })
110
110
  }
111
111
 
112
112
  // 写入文件
113
113
  if (existsSync(pageFilePath)) {
114
- logger.error(`文件已存在: ${pageFilePath}`);
115
- return false;
114
+ logger.error(`文件已存在: ${pageFilePath}`)
115
+ return false
116
116
  }
117
117
 
118
- writeFileSync(pageFilePath, content, 'utf8');
119
- logger.success(`页面创建成功: ${pageFilePath}`);
118
+ writeFileSync(pageFilePath, content, 'utf8')
119
+ logger.success(`页面创建成功: ${pageFilePath}`)
120
120
 
121
121
  // 更新 index.dart
122
- updateIndexFile(pagesDir, `${nameSnake}_page.dart`);
122
+ updateIndexFile(pagesDir, `${nameSnake}_page.dart`)
123
123
 
124
124
  // 如果是列表页,先生成 Model
125
125
  if (isListPage) {
126
- logger.info('列表页需要 Model,正在自动生成...');
126
+ logger.info('列表页需要 Model,正在自动生成...')
127
127
  // 只有 modular/clean 架构才使用 feature
128
- const modelFeature = (template === 'modular' || template === 'clean') ? moduleName : null;
129
- generateModel(name, { feature: modelFeature, outputDir });
128
+ const modelFeature = template === 'modular' || template === 'clean' ? moduleName : null
129
+ generateModel(name, { feature: modelFeature, outputDir })
130
130
  }
131
131
 
132
132
  // 如果需要 ViewModel,也生成它
133
133
  if (withViewModel) {
134
- generateViewModel(name, { feature: moduleName, outputDir, template, isListPage });
134
+ generateViewModel(name, { feature: moduleName, outputDir, template, isListPage })
135
135
  }
136
136
 
137
- return true;
138
-
137
+ return true
139
138
  } catch (error) {
140
- logger.error(`生成页面失败: ${error.message}`);
141
- return false;
139
+ logger.error(`生成页面失败: ${error.message}`)
140
+ return false
142
141
  }
143
142
  }
144
143
 
145
144
  /**
146
145
  * 计算相对导入路径
147
146
  */
148
- function calculateRelativeImport (fromFile, toFile) {
149
- const fromDir = dirname(fromFile);
150
- const relPath = relative(fromDir, toFile);
151
- return relPath.replace(/\\/g, '/');
147
+ function calculateRelativeImport(fromFile, toFile) {
148
+ const fromDir = dirname(fromFile)
149
+ const relPath = relative(fromDir, toFile)
150
+ return relPath.replace(/\\/g, '/')
152
151
  }
153
152
 
154
153
  /**
155
154
  * 生成页面内容
156
155
  */
157
- function generatePageContent (namePascal, nameSnake, nameTitle, options) {
158
- const { stateful, withViewModel, vmImportPath, template } = options;
156
+ function generatePageContent(namePascal, nameSnake, nameTitle, options) {
157
+ const { stateful, withViewModel, vmImportPath, template } = options
159
158
 
160
- let imports = `import 'package:flutter/material.dart';\n`;
159
+ let imports = `import 'package:flutter/material.dart';\n`
161
160
 
162
161
  // StatefulWidget with BasePage
163
162
  if (stateful && withViewModel) {
164
163
  // 导入 BasePage
165
- const baseImport = template === 'lite'
166
- ? '../base/base_page.dart'
167
- : '../../core/base/base_page.dart';
164
+ const baseImport =
165
+ template === 'lite' ? '../base/base_page.dart' : '../../core/base/base_page.dart'
168
166
 
169
- imports += `import '${baseImport}';\n`;
170
- imports += `import '${vmImportPath}';\n`;
167
+ imports += `import '${baseImport}';\n`
168
+ imports += `import '${vmImportPath}';\n`
171
169
 
172
170
  return `${imports}
173
171
  class ${namePascal}Page extends BasePage<${namePascal}ViewModel> {
@@ -194,7 +192,7 @@ class _${namePascal}PageState extends BasePageState<${namePascal}ViewModel, ${na
194
192
  );
195
193
  }
196
194
  }
197
- `;
195
+ `
198
196
  } else if (stateful) {
199
197
  // 普通 StatefulWidget (无 ViewModel)
200
198
  return `${imports}
@@ -218,7 +216,7 @@ class _${namePascal}PageState extends State<${namePascal}Page> {
218
216
  );
219
217
  }
220
218
  }
221
- `;
219
+ `
222
220
  } else {
223
221
  // StatelessWidget
224
222
  return `${imports}
@@ -237,37 +235,44 @@ class ${namePascal}Page extends StatelessWidget {
237
235
  );
238
236
  }
239
237
  }
240
- `;
238
+ `
241
239
  }
242
240
  }
243
241
 
244
242
  /**
245
243
  * 生成 ViewModel
246
244
  */
247
- function generateViewModel (name, options = {}) {
248
- const { feature = null, outputDir = process.cwd(), template = 'lite', isListPage = false } = options;
245
+ function generateViewModel(name, options = {}) {
246
+ const {
247
+ feature = null,
248
+ outputDir = process.cwd(),
249
+ template = 'lite',
250
+ isListPage = false,
251
+ } = options
249
252
 
250
- const namePascal = toPascalCase(name);
251
- const nameSnake = toSnakeCase(name);
253
+ const namePascal = toPascalCase(name)
254
+ const nameSnake = toSnakeCase(name)
252
255
 
253
256
  // 根据模板类型确定输出路径
254
- const vmDir = getViewModelPath(outputDir, feature);
257
+ const vmDir = getViewModelPath(outputDir, feature)
255
258
 
256
259
  if (!existsSync(vmDir)) {
257
- mkdirSync(vmDir, { recursive: true });
260
+ mkdirSync(vmDir, { recursive: true })
258
261
  }
259
262
 
260
263
  // 根据片段或模板类型生成内容
261
- const snippetKey = isListPage ? 'flu.listViewModel' : 'flu.viewmodel';
264
+ const snippetKey = isListPage ? 'flu.listViewModel' : 'flu.viewmodel'
262
265
  const vmSnippet = getSnippetContent(outputDir, snippetKey, {
263
266
  Name: namePascal,
264
267
  snake_name: nameSnake,
265
- ModelName: namePascal + 'Model'
266
- });
267
- let content = vmSnippet;
268
+ ModelName: namePascal + 'Model',
269
+ })
270
+ let content = vmSnippet
268
271
  if (template === 'clean') {
269
272
  // Clean 架构的 ViewModel 需要依赖 UseCase
270
- content = content || `import 'package:flutter/foundation.dart';
273
+ content =
274
+ content ||
275
+ `import 'package:flutter/foundation.dart';
271
276
 
272
277
  class ${namePascal}ViewModel extends ChangeNotifier {
273
278
  // TODO: 注入 UseCase
@@ -287,10 +292,12 @@ class ${namePascal}ViewModel extends ChangeNotifier {
287
292
  super.dispose();
288
293
  }
289
294
  }
290
- `;
295
+ `
291
296
  } else {
292
297
  // Lite 和 Modular 使用标准的 ViewModel
293
- content = content || `import 'package:flutter/foundation.dart';
298
+ content =
299
+ content ||
300
+ `import 'package:flutter/foundation.dart';
294
301
 
295
302
  class ${namePascal}ViewModel extends ChangeNotifier {
296
303
  void init() {
@@ -303,20 +310,20 @@ class ${namePascal}ViewModel extends ChangeNotifier {
303
310
  super.dispose();
304
311
  }
305
312
  }
306
- `;
313
+ `
307
314
  }
308
315
 
309
- const filePath = join(vmDir, `${nameSnake}_viewmodel.dart`);
316
+ const filePath = join(vmDir, `${nameSnake}_viewmodel.dart`)
310
317
  if (existsSync(filePath)) {
311
- logger.warn(`ViewModel 已存在: ${filePath}`);
312
- return false;
318
+ logger.warn(`ViewModel 已存在: ${filePath}`)
319
+ return false
313
320
  }
314
321
 
315
- writeFileSync(filePath, content, 'utf8');
316
- logger.success(`ViewModel 创建成功: ${filePath}`);
322
+ writeFileSync(filePath, content, 'utf8')
323
+ logger.success(`ViewModel 创建成功: ${filePath}`)
317
324
 
318
325
  // 更新 index.dart
319
- updateIndexFile(vmDir, `${nameSnake}_viewmodel.dart`);
326
+ updateIndexFile(vmDir, `${nameSnake}_viewmodel.dart`)
320
327
 
321
- return true;
328
+ return true
322
329
  }