flu-cli 0.0.5 → 2.0.1
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 +512 -0
- package/README.md +70 -260
- package/config/dev.config.js +56 -0
- package/config/templates.js +147 -0
- package/index.js +177 -81
- package/lib/commands/add.js +595 -0
- package/lib/commands/cache.js +99 -0
- package/lib/commands/completion.js +92 -0
- package/lib/commands/config.js +70 -0
- package/lib/commands/newClack.js +399 -0
- package/lib/commands/snippets.js +39 -0
- package/lib/commands/template.js +113 -0
- package/lib/commands/templates.js +84 -0
- package/lib/generators/model_generator.js +303 -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 +203 -0
- package/package.json +34 -7
- 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 +392 -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,113 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { ConfigManager } from '@flu-cli/core';
|
|
3
|
+
import { getAllTemplates } from '../../config/templates.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
import { getTemplateCachePath } from '../templates/templateManager.js';
|
|
7
|
+
import { format } from 'date-fns'; // 需要检查是否已安装 date-fns,如果没有可以用原生 Date
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 格式化日期
|
|
11
|
+
*/
|
|
12
|
+
function formatDate (timestamp) {
|
|
13
|
+
if (!timestamp) return '从未';
|
|
14
|
+
return new Date(timestamp).toLocaleString('zh-CN');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 列出所有模板(内置 + 自定义)
|
|
19
|
+
*/
|
|
20
|
+
export function listAllTemplates () {
|
|
21
|
+
logger.title('📦 可用的项目模板');
|
|
22
|
+
|
|
23
|
+
// 1. 内置模板
|
|
24
|
+
console.log(chalk.bold.cyan('📌 内置模板'));
|
|
25
|
+
const builtinTemplates = getAllTemplates();
|
|
26
|
+
builtinTemplates.forEach((template) => {
|
|
27
|
+
const cachePath = getTemplateCachePath(template.name);
|
|
28
|
+
const isCached = existsSync(cachePath);
|
|
29
|
+
const status = isCached ? chalk.green('已缓存') : chalk.gray('未缓存');
|
|
30
|
+
|
|
31
|
+
console.log(chalk.bold(` • ${template.name} (${template.displayName})`) + ` [${status}]`);
|
|
32
|
+
console.log(chalk.gray(` ${template.description}`));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// 2. 自定义模板
|
|
36
|
+
const configManager = ConfigManager.getInstance();
|
|
37
|
+
const customTemplates = configManager.getTemplates();
|
|
38
|
+
|
|
39
|
+
if (customTemplates.length > 0) {
|
|
40
|
+
logger.newLine();
|
|
41
|
+
console.log(chalk.bold.cyan('🔧 自定义模板'));
|
|
42
|
+
customTemplates.forEach((template) => {
|
|
43
|
+
const source = template.type === 'git' ? template.url : template.path;
|
|
44
|
+
let status = '';
|
|
45
|
+
let updateInfo = '';
|
|
46
|
+
|
|
47
|
+
if (template.type === 'git') {
|
|
48
|
+
const cachePath = getTemplateCachePath(template.id);
|
|
49
|
+
const isCached = existsSync(cachePath);
|
|
50
|
+
status = isCached ? chalk.green('已缓存') : chalk.gray('未缓存');
|
|
51
|
+
if (template.lastUsedAt) {
|
|
52
|
+
updateInfo = chalk.gray(` (上次使用: ${formatDate(template.lastUsedAt)})`);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
status = chalk.blue('本地链接');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(chalk.bold(` • ${template.id} (${template.name})`) + ` [${status}]` + updateInfo);
|
|
59
|
+
console.log(chalk.gray(` ${template.description || '无描述'}`));
|
|
60
|
+
console.log(chalk.gray(` 来源: [${template.type}] ${source}`));
|
|
61
|
+
if (template.branch) {
|
|
62
|
+
console.log(chalk.gray(` 分支: ${template.branch}`));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
} else {
|
|
66
|
+
logger.newLine();
|
|
67
|
+
console.log(chalk.gray(' (暂无自定义模板)'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
logger.newLine();
|
|
71
|
+
logger.info('使用 "flu template add" 添加自定义模板');
|
|
72
|
+
logger.info('使用 "flu new <name> -t <template>" 创建项目');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 添加自定义模板
|
|
77
|
+
*/
|
|
78
|
+
export function addTemplate (name, source, options) {
|
|
79
|
+
const configManager = ConfigManager.getInstance();
|
|
80
|
+
const existing = configManager.getTemplate(name);
|
|
81
|
+
|
|
82
|
+
if (existing && !options.force) {
|
|
83
|
+
logger.error(`模板 "${name}" 已存在`);
|
|
84
|
+
logger.info('使用 --force 覆盖,或使用 "flu template remove" 删除');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const isLocal = options.local;
|
|
89
|
+
const template = {
|
|
90
|
+
id: name,
|
|
91
|
+
name: options.name || name,
|
|
92
|
+
type: isLocal ? 'local' : 'git',
|
|
93
|
+
description: options.description || '自定义模板',
|
|
94
|
+
...(isLocal ? { path: source } : { url: source, branch: options.branch || 'main' })
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
configManager.addTemplate(template);
|
|
98
|
+
logger.success(`✅ 模板 "${name}" 添加成功`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 删除自定义模板
|
|
103
|
+
*/
|
|
104
|
+
export function removeTemplate (name) {
|
|
105
|
+
const configManager = ConfigManager.getInstance();
|
|
106
|
+
const success = configManager.removeTemplate(name);
|
|
107
|
+
|
|
108
|
+
if (success) {
|
|
109
|
+
logger.success(`🗑️ 模板 "${name}" 已删除`);
|
|
110
|
+
} else {
|
|
111
|
+
logger.error(`模板 "${name}" 不存在`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模板查看命令
|
|
3
|
+
* 显示所有模板或指定模板的详细信息
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { getAllTemplates, getTemplate, isValidTemplate } from '../../config/templates.js';
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 列出所有模板
|
|
12
|
+
*/
|
|
13
|
+
export function listTemplates () {
|
|
14
|
+
logger.title('📦 可用的项目模板');
|
|
15
|
+
|
|
16
|
+
const templates = getAllTemplates();
|
|
17
|
+
|
|
18
|
+
templates.forEach((template, index) => {
|
|
19
|
+
console.log(chalk.bold(`${index + 1}. ${template.displayName}`));
|
|
20
|
+
console.log(chalk.gray(` ${template.description}`));
|
|
21
|
+
console.log(chalk.gray(` 复杂度: ${'⭐'.repeat(template.complexity)}${'☆'.repeat(5 - template.complexity)}`));
|
|
22
|
+
console.log(chalk.gray(` 团队规模: ${template.teamSize}`));
|
|
23
|
+
console.log(chalk.gray(` 代码量: ${template.codeSize}`));
|
|
24
|
+
logger.newLine();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
logger.info('使用 "flu-cli templates <name>" 查看详细信息');
|
|
28
|
+
logger.info('使用 "flu-cli new <project-name> -t <template>" 创建项目');
|
|
29
|
+
|
|
30
|
+
logger.newLine();
|
|
31
|
+
console.log(chalk.gray('示例:'));
|
|
32
|
+
console.log(chalk.yellow(' flu-cli templates lite'));
|
|
33
|
+
console.log(chalk.yellow(' flu-cli new my_app -t modular'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 显示模板详情
|
|
38
|
+
*/
|
|
39
|
+
export function showTemplateDetail (templateName) {
|
|
40
|
+
if (!isValidTemplate(templateName)) {
|
|
41
|
+
logger.error(`模板 "${templateName}" 不存在`);
|
|
42
|
+
logger.info('使用 "flu-cli templates" 查看所有可用模板');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const template = getTemplate(templateName);
|
|
47
|
+
|
|
48
|
+
// 显示模板信息框
|
|
49
|
+
console.log(chalk.bold.cyan('\n┌─────────────────────────────────────────────────────────────┐'));
|
|
50
|
+
console.log(chalk.bold.cyan(`│ 📦 ${template.displayName.padEnd(56)} │`));
|
|
51
|
+
console.log(chalk.bold.cyan('├─────────────────────────────────────────────────────────────┤'));
|
|
52
|
+
|
|
53
|
+
// 基本信息
|
|
54
|
+
console.log(chalk.cyan('│ ') + chalk.gray('描述: ') + template.description.padEnd(50) + chalk.cyan(' │'));
|
|
55
|
+
console.log(chalk.cyan('│ ') + chalk.gray('复杂度: ') + ('⭐'.repeat(template.complexity) + '☆'.repeat(5 - template.complexity)).padEnd(50) + chalk.cyan(' │'));
|
|
56
|
+
console.log(chalk.cyan('│ ') + chalk.gray('团队规模: ') + template.teamSize.padEnd(48) + chalk.cyan(' │'));
|
|
57
|
+
console.log(chalk.cyan('│ ') + chalk.gray('代码量: ') + template.codeSize.padEnd(50) + chalk.cyan(' │'));
|
|
58
|
+
|
|
59
|
+
console.log(chalk.bold.cyan('├─────────────────────────────────────────────────────────────┤'));
|
|
60
|
+
console.log(chalk.bold.cyan('│ 特性: │'));
|
|
61
|
+
console.log(chalk.cyan('│ │'));
|
|
62
|
+
|
|
63
|
+
template.features.forEach(feature => {
|
|
64
|
+
console.log(chalk.cyan('│ ') + feature.padEnd(60) + chalk.cyan(' │'));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
console.log(chalk.bold.cyan('├─────────────────────────────────────────────────────────────┤'));
|
|
68
|
+
console.log(chalk.bold.cyan('│ 项目结构: │'));
|
|
69
|
+
console.log(chalk.cyan('│ │'));
|
|
70
|
+
|
|
71
|
+
// 显示项目结构
|
|
72
|
+
const structureLines = template.structure.trim().split('\n');
|
|
73
|
+
structureLines.forEach(line => {
|
|
74
|
+
const paddedLine = line.padEnd(60);
|
|
75
|
+
console.log(chalk.cyan('│ ') + paddedLine + chalk.cyan(' │'));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
console.log(chalk.bold.cyan('└─────────────────────────────────────────────────────────────┘'));
|
|
79
|
+
|
|
80
|
+
logger.newLine();
|
|
81
|
+
logger.info('创建项目:');
|
|
82
|
+
console.log(chalk.yellow(` flu-cli new my_app -t ${templateName}`));
|
|
83
|
+
logger.newLine();
|
|
84
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model 生成器
|
|
3
|
+
*/
|
|
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';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 生成 Model 文件
|
|
14
|
+
*/
|
|
15
|
+
export function generateModel (name, options = {}) {
|
|
16
|
+
try {
|
|
17
|
+
const {
|
|
18
|
+
feature = null,
|
|
19
|
+
jsonFile = null,
|
|
20
|
+
outputDir = process.cwd()
|
|
21
|
+
} = options;
|
|
22
|
+
|
|
23
|
+
// 转换命名
|
|
24
|
+
const namePascal = toPascalCase(name);
|
|
25
|
+
const nameSnake = toSnakeCase(name);
|
|
26
|
+
|
|
27
|
+
// 确定输出路径
|
|
28
|
+
let modelsDir;
|
|
29
|
+
if (feature) {
|
|
30
|
+
// Modular/Clean 架构
|
|
31
|
+
modelsDir = join(outputDir, 'lib', 'features', feature, 'models');
|
|
32
|
+
} else {
|
|
33
|
+
// Lite 架构
|
|
34
|
+
modelsDir = join(outputDir, 'lib', 'models');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 创建目录
|
|
38
|
+
if (!existsSync(modelsDir)) {
|
|
39
|
+
mkdirSync(modelsDir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 生成文件内容
|
|
43
|
+
let content;
|
|
44
|
+
if (jsonFile && existsSync(jsonFile)) {
|
|
45
|
+
// 从 JSON 文件生成
|
|
46
|
+
const jsonContent = readFileSync(jsonFile, 'utf8');
|
|
47
|
+
const jsonData = JSON.parse(jsonContent);
|
|
48
|
+
content = generateModelFromJson(namePascal, jsonData);
|
|
49
|
+
} else {
|
|
50
|
+
// 优先片段,其次基础模板
|
|
51
|
+
content = getSnippetContent(outputDir, 'flu.model', { Name: namePascal }) || generateBasicModel(namePascal);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 写入文件
|
|
55
|
+
const filePath = join(modelsDir, `${nameSnake}_model.dart`);
|
|
56
|
+
if (existsSync(filePath)) {
|
|
57
|
+
logger.error(`文件已存在: ${filePath}`);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
writeFileSync(filePath, content, 'utf8');
|
|
62
|
+
logger.success(`Model 创建成功: ${filePath}`);
|
|
63
|
+
|
|
64
|
+
// 更新 index.dart
|
|
65
|
+
updateIndexFile(modelsDir, `${nameSnake}_model.dart`);
|
|
66
|
+
|
|
67
|
+
return true;
|
|
68
|
+
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logger.error(`生成 Model 失败: ${error.message}`);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 生成基础 Model
|
|
77
|
+
*/
|
|
78
|
+
function generateBasicModel (namePascal) {
|
|
79
|
+
return `class ${namePascal}Model {
|
|
80
|
+
final String id;
|
|
81
|
+
final String name;
|
|
82
|
+
final DateTime createdAt;
|
|
83
|
+
|
|
84
|
+
${namePascal}Model({
|
|
85
|
+
required this.id,
|
|
86
|
+
required this.name,
|
|
87
|
+
required this.createdAt,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// 从 JSON 创建
|
|
91
|
+
factory ${namePascal}Model.fromJson(Map<String, dynamic> json) {
|
|
92
|
+
return ${namePascal}Model(
|
|
93
|
+
id: json['id'] as String,
|
|
94
|
+
name: json['name'] as String,
|
|
95
|
+
createdAt: DateTime.parse(json['created_at'] as String),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 转换为 JSON
|
|
100
|
+
Map<String, dynamic> toJson() {
|
|
101
|
+
return {
|
|
102
|
+
'id': id,
|
|
103
|
+
'name': name,
|
|
104
|
+
'created_at': createdAt.toIso8601String(),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 复制并修改
|
|
109
|
+
${namePascal}Model copyWith({
|
|
110
|
+
String? id,
|
|
111
|
+
String? name,
|
|
112
|
+
DateTime? createdAt,
|
|
113
|
+
}) {
|
|
114
|
+
return ${namePascal}Model(
|
|
115
|
+
id: id ?? this.id,
|
|
116
|
+
name: name ?? this.name,
|
|
117
|
+
createdAt: createdAt ?? this.createdAt,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@override
|
|
122
|
+
String toString() {
|
|
123
|
+
return '${namePascal}Model(id: $id, name: $name, createdAt: $createdAt)';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@override
|
|
127
|
+
bool operator ==(Object other) {
|
|
128
|
+
if (identical(this, other)) return true;
|
|
129
|
+
return other is ${namePascal}Model &&
|
|
130
|
+
other.id == id &&
|
|
131
|
+
other.name == name &&
|
|
132
|
+
other.createdAt == createdAt;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@override
|
|
136
|
+
int get hashCode {
|
|
137
|
+
return id.hashCode ^ name.hashCode ^ createdAt.hashCode;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 从 JSON 生成 Model
|
|
145
|
+
*/
|
|
146
|
+
function generateModelFromJson (namePascal, jsonData) {
|
|
147
|
+
const fields = analyzeJsonStructure(jsonData);
|
|
148
|
+
|
|
149
|
+
// 生成字段声明
|
|
150
|
+
const fieldDeclarations = fields.map(f => ` final ${f.type} ${f.name};`).join('\n');
|
|
151
|
+
|
|
152
|
+
// 生成构造函数参数
|
|
153
|
+
const constructorParams = fields.map(f => ` required this.${f.name},`).join('\n');
|
|
154
|
+
|
|
155
|
+
// 生成 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');
|
|
173
|
+
|
|
174
|
+
// 生成 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');
|
|
182
|
+
|
|
183
|
+
// 生成 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');
|
|
186
|
+
|
|
187
|
+
// 生成 toString
|
|
188
|
+
const toStringFields = fields.map(f => `${f.name}: $${f.name}`).join(', ');
|
|
189
|
+
|
|
190
|
+
// 生成 == 比较
|
|
191
|
+
const equalityChecks = fields.map(f => ` other.${f.name} == ${f.name}`).join(' &&\n');
|
|
192
|
+
|
|
193
|
+
// 生成 hashCode
|
|
194
|
+
const hashCodeFields = fields.map(f => `${f.name}.hashCode`).join(' ^ ');
|
|
195
|
+
|
|
196
|
+
return `class ${namePascal}Model {
|
|
197
|
+
${fieldDeclarations}
|
|
198
|
+
|
|
199
|
+
${namePascal}Model({
|
|
200
|
+
${constructorParams}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// 从 JSON 创建
|
|
204
|
+
factory ${namePascal}Model.fromJson(Map<String, dynamic> json) {
|
|
205
|
+
return ${namePascal}Model(
|
|
206
|
+
${fromJsonFields}
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 转换为 JSON
|
|
211
|
+
Map<String, dynamic> toJson() {
|
|
212
|
+
return {
|
|
213
|
+
${toJsonFields}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 复制并修改
|
|
218
|
+
${namePascal}Model copyWith({
|
|
219
|
+
${copyWithParams}
|
|
220
|
+
}) {
|
|
221
|
+
return ${namePascal}Model(
|
|
222
|
+
${copyWithFields}
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@override
|
|
227
|
+
String toString() {
|
|
228
|
+
return '${namePascal}Model(${toStringFields})';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@override
|
|
232
|
+
bool operator ==(Object other) {
|
|
233
|
+
if (identical(this, other)) return true;
|
|
234
|
+
return other is ${namePascal}Model &&
|
|
235
|
+
${equalityChecks};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@override
|
|
239
|
+
int get hashCode {
|
|
240
|
+
return ${hashCodeFields};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 分析 JSON 结构
|
|
248
|
+
*/
|
|
249
|
+
function analyzeJsonStructure (jsonData) {
|
|
250
|
+
const fields = [];
|
|
251
|
+
|
|
252
|
+
for (const [key, value] of Object.entries(jsonData)) {
|
|
253
|
+
const camelKey = toCamelCase(key);
|
|
254
|
+
const type = inferDartType(value);
|
|
255
|
+
|
|
256
|
+
fields.push({
|
|
257
|
+
name: camelKey,
|
|
258
|
+
jsonKey: key,
|
|
259
|
+
type: type
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return fields;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 推断 Dart 类型
|
|
268
|
+
*/
|
|
269
|
+
function inferDartType (value) {
|
|
270
|
+
if (value === null) return 'dynamic';
|
|
271
|
+
|
|
272
|
+
const type = typeof value;
|
|
273
|
+
|
|
274
|
+
if (type === 'string') {
|
|
275
|
+
// 尝试判断是否是日期
|
|
276
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
|
|
277
|
+
return 'DateTime';
|
|
278
|
+
}
|
|
279
|
+
return 'String';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (type === 'number') {
|
|
283
|
+
return Number.isInteger(value) ? 'int' : 'double';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (type === 'boolean') {
|
|
287
|
+
return 'bool';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (Array.isArray(value)) {
|
|
291
|
+
if (value.length === 0) {
|
|
292
|
+
return 'List<dynamic>';
|
|
293
|
+
}
|
|
294
|
+
const itemType = inferDartType(value[0]);
|
|
295
|
+
return `List<${itemType}>`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (type === 'object') {
|
|
299
|
+
return 'Map<String, dynamic>';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return 'dynamic';
|
|
303
|
+
}
|