flu-cli 2.0.5 → 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.
- package/CHANGELOG.md +23 -0
- package/README.md +17 -4
- package/config/dev.config.js +11 -11
- package/config/templates.js +10 -10
- package/index.js +554 -102
- package/lib/commands/add.js +365 -266
- package/lib/commands/assets.js +77 -78
- package/lib/commands/cache.js +29 -52
- package/lib/commands/completion.js +13 -11
- package/lib/commands/config.js +150 -44
- package/lib/commands/init-ai-base.js +89 -0
- package/lib/commands/newClack.js +269 -178
- package/lib/commands/snippets.js +58 -43
- package/lib/commands/template.js +98 -58
- package/lib/commands/templates.js +101 -57
- package/lib/commands/upload.js +313 -0
- package/lib/commands/vnext-options.js +206 -0
- package/lib/generators/model_generator.js +91 -88
- package/lib/generators/page_generator.js +100 -93
- package/lib/generators/service_generator.js +44 -39
- package/lib/generators/viewmodel_generator.js +25 -29
- package/lib/generators/widget_generator.js +30 -35
- package/lib/templates/templateCopier.js +14 -15
- package/lib/templates/templateManager.js +22 -21
- package/lib/utils/config.js +37 -20
- package/lib/utils/flutterHelper.js +2 -2
- package/lib/utils/i18n.js +3 -3
- package/lib/utils/index_updater.js +22 -23
- package/lib/utils/json-output.js +59 -0
- package/lib/utils/logger.js +17 -17
- package/lib/utils/project_detector.js +66 -66
- package/lib/utils/snippet_loader.js +21 -19
- package/lib/utils/string_helper.js +13 -13
- package/lib/utils/templateSelectorEnquirer.js +94 -108
- package/locales/en-US.json +1 -1
- package/locales/zh-CN.json +2 -2
- package/package.json +60 -57
- package/scripts/smoke-vnext-generate.mjs +1934 -0
- package/scripts/smoke-vnext-params.mjs +92 -0
- package/CLI.md +0 -513
- package/release.sh +0 -529
- package/scripts/e2e-state-tests.js +0 -116
- package/scripts/sync-base-to-templates.js +0 -108
- package/scripts/workspace-clone-all.sh +0 -101
- 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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
248
|
-
const {
|
|
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 =
|
|
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 =
|
|
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
|
}
|