openyida 1.0.0-beta.2 → 1.0.0-beta.5

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/README.md CHANGED
@@ -156,9 +156,13 @@ npx clawhub@latest install nicky1108/yida-app
156
156
  感谢所有为 OpenYida 做出贡献的开发者!欢迎阅读 [贡献指南](./CONTRIBUTING.md) 参与共建。
157
157
 
158
158
  <p align="left">
159
- <a href="https://github.com/yize"><img src="https://avatars.githubusercontent.com/u/1578814?v=4&s=48" width="48" height="48" alt="yize" title="yize"/></a>
160
- <a href="https://github.com/alex-mm"><img src="https://avatars.githubusercontent.com/u/3302053?v=4&s=48" width="48" height="48" alt="alex-mm" title="alex-mm"/></a>
159
+ <a href="https://github.com/yize"><img src="https://avatars.githubusercontent.com/u/1578814?v=4&s=48" width="48" height="48" alt="九神" title="九神"/></a>
160
+ <a href="https://github.com/alex-mm"><img src="https://avatars.githubusercontent.com/u/3302053?v=4&s=48" width="48" height="48" alt="天晟" title="天晟"/></a>
161
161
  <a href="https://github.com/nicky1108"><img src="https://avatars.githubusercontent.com/u/4279283?v=4&s=48" width="48" height="48" alt="nicky1108" title="nicky1108"/></a>
162
+ <a href="https://github.com/angelinheys"><img src="https://avatars.githubusercontent.com/u/49426983?v=4&s=48" width="48" height="48" alt="angelinheys" title="angelinheys"/></a>
163
+ <a href="https://github.com/yipengmu"><img src="https://avatars.githubusercontent.com/u/3232735?v=4&s=48" width="48" height="48" alt="yipengmu" title="yipengmu"/></a>
164
+ <a href="https://github.com/Waawww"><img src="https://avatars.githubusercontent.com/u/31886449?v=4&s=48" width="48" height="48" alt="Waawww" title="Waawww"/></a>
165
+ <a href="https://github.com/kangjiano"><img src="https://avatars.githubusercontent.com/u/54129385?v=4&s=48" width="48" height="48" alt="kangjiano" title="kangjiano"/></a>
162
166
  </p>
163
167
 
164
168
  ---
package/bin/yida.js CHANGED
@@ -18,7 +18,7 @@
18
18
  * openyida org switch --corp-id <corpId> 切换组织(无需重新登录)
19
19
  * openyida create-app "<名称>" [desc] [icon] [color] 创建应用
20
20
  * openyida create-page <appType> "<页面名>" 创建自定义页面
21
- * openyida create-form create <appType> "<表单名>" <字段JSON> 创建表单页面
21
+ * openyida create-form create <appType> "<表单名>" <字段JSON> [--layout <布局>] [--theme <主题>] [--label-align <对齐>] 创建表单页面
22
22
  * openyida create-form update <appType> <formUuid> <修改JSON> 更新表单页面
23
23
  * openyida get-schema <appType> <formUuid> 获取表单 Schema
24
24
  * openyida publish <源文件路径> <appType> <formUuid> 编译并发布自定义页面
@@ -26,6 +26,7 @@
26
26
  * openyida save-share-config <appType> <formUuid> <url> <isOpen> [openAuth] 保存公开访问/分享配置
27
27
  * openyida get-page-config <appType> <formUuid> 查询页面公开访问/分享配置
28
28
  * openyida update-form-config <appType> <formUuid> <isRenderNav> <title> 更新表单配置
29
+ * openyida doctor [选项] 检查环境依赖,诊断应用问题
29
30
  * openyida export <appType> [output] 导出应用所有表单 Schema(生成迁移包)
30
31
  * openyida import <file> [name] 导入迁移包,在目标环境重建应用
31
32
  */
@@ -43,6 +44,58 @@ const command = process.argv[2];
43
44
  const args = process.argv.slice(3);
44
45
 
45
46
  function printHelp() {
47
+ console.log(`
48
+ openyida - 宜搭命令行工具
49
+
50
+ 用法:
51
+ openyida <命令> [参数...](别名:yida)
52
+
53
+ 命令:
54
+ env 检测当前 AI 工具环境和登录态
55
+ copy [--force] 复制 project 工作目录到当前 AI 工具环境
56
+ login 登录态管理(优先缓存,否则扫码)
57
+ logout 退出登录 / 切换账号
58
+ create-app "<名称>" [描述] [图标] [颜色] 创建应用,输出 appType
59
+ create-page <appType> "<页面名>" 创建自定义页面,输出 pageId
60
+ create-form create <appType> "<表单名>" <字段JSON> [--layout <布局>] [--theme <主题>] [--label-align <对齐>] 创建表单页面
61
+ create-form update <appType> <formUuid> <修改JSON> 更新表单页面
62
+ get-schema <appType> <formUuid> 获取表单 Schema
63
+ publish <源文件路径> <appType> <formUuid> 编译并发布自定义页面
64
+ verify-short-url <appType> <formUuid> <url> 验证短链接 URL 是否可用
65
+ save-share-config <appType> <formUuid> <url> <isOpen> [auth] 保存公开访问/分享配置
66
+ get-page-config <appType> <formUuid> 查询页面公开访问/分享配置
67
+ update-form-config <appType> <formUuid> <isRenderNav> <title> 更新表单配置
68
+ doctor [选项] 检查环境依赖,诊断应用问题
69
+ --fix / --repair 诊断并自动修复
70
+ --production --app <appId> 线上应用诊断
71
+ --monitor 启动实时健康度监控
72
+ --report <format> 生成诊断报告(json | markdown | html)
73
+ --create-ticket 根据诊断结果创建工单
74
+ --create-voc 创建 VOC(需求反馈)
75
+ --auto-submit 自动判断并提交工单或 VOC
76
+
77
+ 示例:
78
+ openyida login
79
+ openyida logout
80
+ openyida create-app "考勤管理"
81
+ openyida create-page APP_XXX "游戏主页"
82
+ openyida create-form create APP_XXX "员工信息" fields.json
83
+ openyida create-form update APP_XXX FORM-XXX '[{"action":"add","field":{"type":"TextField","label":"备注"}}]'
84
+ openyida get-schema APP_XXX FORM-XXX
85
+ openyida publish pages/src/home.jsx APP_XXX FORM-XXX
86
+ openyida verify-short-url APP_XXX FORM-XXX /o/myapp
87
+ openyida save-share-config APP_XXX FORM-XXX /o/myapp y n
88
+ openyida get-page-config APP_XXX FORM-XXX
89
+ openyida update-form-config APP_XXX FORM-XXX false "页面标题"
90
+ openyida doctor 完整诊断
91
+ openyida doctor --fix 诊断并自动修复
92
+ openyida doctor --production --app APP_XXX 线上应用诊断
93
+ openyida doctor --monitor 实时监控
94
+ openyida doctor --report markdown 生成 Markdown 报告
95
+ openyida doctor --create-ticket 创建工单
96
+ openyida doctor --create-voc 创建 VOC
97
+ openyida doctor --auto-submit 自动判断并提交
98
+ `);
46
99
  console.log(t('cli.help'));
47
100
  }
48
101
 
@@ -304,6 +357,12 @@ async function main() {
304
357
  break;
305
358
  }
306
359
 
360
+ case 'doctor': {
361
+ const { run } = require('../lib/doctor');
362
+ await run(args);
363
+ break;
364
+ }
365
+
307
366
  case 'export': {
308
367
  if (args.length < 1) {
309
368
  console.error(t('cli.export_usage'));
package/lib/copy.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * openyida copy → 复制 project/ 目录模板(默认,合并模式)
6
6
  * openyida copy --force → 复制 project/ 目录模板(强制覆盖,先清空目标目录)
7
7
  * openyida copy -skills → 创建 yida-skills/ 软链接(如果存在实际目录则先删除)
8
+ * 悟空环境下:删除已有软链(悟空通过手动上传技能,不需要软链)
8
9
  * openyida copy -project → 复制 project/ 目录模板(与默认行为相同,显式指定)
9
10
  * openyida copy -project --force → 复制 project/ 目录模板(强制覆盖)
10
11
  *
@@ -16,7 +17,12 @@
16
17
  *
17
18
  * project/ 合并模式(默认):已存在的文件强制覆盖,目标目录中多余的文件保留不动
18
19
  * project/ 强制模式(--force):先清空目标目录,再完整复制
19
- * yida-skills/:始终创建软链接,如目标存在实际目录则先删除
20
+ * yida-skills/(非悟空):始终创建软链接,如目标存在实际目录则先删除
21
+ * yida-skills/(悟空):删除已有软链或目录(悟空通过手动上传技能,不需要软链)
22
+ *
23
+ * Windows 兼容说明:
24
+ * - 软链接在 Windows 上需要管理员权限或开发者模式,失败时自动降级为目录复制
25
+ * - 路径分隔符统一使用 path.join 处理
20
26
  */
21
27
 
22
28
  "use strict";
@@ -94,15 +100,22 @@ function forceCopyDir(sourceDir, destDir) {
94
100
  }
95
101
 
96
102
  /**
97
- * 创建软链接:如果目标存在实际目录则先删除,再创建软链接。
98
- * @returns {boolean} 是否成功创建
103
+ * 删除已有的 yida-skills 软链接或目录(悟空环境专用)。
104
+ * 悟空通过手动上传技能,不需要软链,执行 -skills 时只做清理。
105
+ * 使用 lstatSync 而非 existsSync,可以检测到悬空软链(目标不存在但链接本身存在)。
106
+ * @returns {boolean} 是否执行了删除操作
99
107
  */
100
- function createSymlink(sourceDir, destLink) {
101
- if (!fs.existsSync(sourceDir)) return false;
108
+ function removeSkillsLink(destLink) {
109
+ let stats;
110
+ try {
111
+ stats = fs.lstatSync(destLink);
112
+ } catch {
113
+ // 路径不存在(包括悬空软链也不存在的情况)
114
+ console.log(t("copy.wukong_skills_not_found", destLink));
115
+ return false;
116
+ }
102
117
 
103
- // 如果目标已存在,判断是目录还是软链接
104
- if (fs.existsSync(destLink)) {
105
- const stats = fs.lstatSync(destLink);
118
+ try {
106
119
  if (stats.isSymbolicLink()) {
107
120
  fs.unlinkSync(destLink);
108
121
  console.log(t("copy.symlink_removed", destLink));
@@ -113,20 +126,69 @@ function createSymlink(sourceDir, destLink) {
113
126
  fs.unlinkSync(destLink);
114
127
  console.log(t("copy.removed", destLink));
115
128
  }
129
+ return true;
130
+ } catch (error) {
131
+ console.error(t("copy.remove_failed", destLink, error.message));
132
+ return false;
116
133
  }
134
+ }
117
135
 
118
- fs.symlinkSync(sourceDir, destLink, "junction");
119
- console.log(t("copy.symlink_created", destLink, sourceDir));
120
- return true;
136
+ /**
137
+ * 创建软链接:如果目标存在实际目录则先删除,再创建软链接。
138
+ * Windows 上软链需要管理员权限或开发者模式,失败时自动降级为目录复制。
139
+ * @returns {boolean} 是否成功创建
140
+ */
141
+ function createSymlink(sourceDir, destLink) {
142
+ if (!fs.existsSync(sourceDir)) return false;
143
+
144
+ // 如果目标已存在,判断是目录还是软链接
145
+ if (fs.existsSync(destLink)) {
146
+ try {
147
+ const stats = fs.lstatSync(destLink);
148
+ if (stats.isSymbolicLink()) {
149
+ fs.unlinkSync(destLink);
150
+ console.log(t("copy.symlink_removed", destLink));
151
+ } else if (stats.isDirectory()) {
152
+ fs.rmSync(destLink, { recursive: true, force: true });
153
+ console.log(t("copy.dir_deleted", destLink));
154
+ } else {
155
+ fs.unlinkSync(destLink);
156
+ console.log(t("copy.removed", destLink));
157
+ }
158
+ } catch (error) {
159
+ console.error(t("copy.remove_failed", destLink, error.message));
160
+ return false;
161
+ }
162
+ }
163
+
164
+ // Windows 上 junction 只支持目录,且需要管理员权限或开发者模式
165
+ // 失败时降级为目录复制
166
+ const symlinkType = process.platform === "win32" ? "junction" : "dir";
167
+ try {
168
+ fs.symlinkSync(sourceDir, destLink, symlinkType);
169
+ console.log(t("copy.symlink_created", destLink, sourceDir));
170
+ return true;
171
+ } catch (error) {
172
+ if (process.platform === "win32" && error.code === "EPERM") {
173
+ console.log(t("copy.symlink_fallback_copy", destLink));
174
+ const count = mergeCopyDir(sourceDir, destLink);
175
+ console.log(t("copy.files_copied", count));
176
+ return true;
177
+ }
178
+ console.error(t("copy.symlink_failed", destLink, error.message));
179
+ return false;
180
+ }
121
181
  }
122
182
 
123
183
  /**
124
- * 检测 AI 工具环境,返回目标根目录。
184
+ * 根据已检测的环境信息返回目标根目录,避免重复调用 detectEnvironment()。
185
+ * @param {string|null} activeToolName
186
+ * @param {string|null} activeProjectRoot
187
+ * @param {Array} envResults
125
188
  * @returns {string} 目标根目录路径
126
189
  */
127
- function resolveDestBase() {
128
- const { activeToolName, activeProjectRoot, results } = detectEnvironment();
129
- const activeResult = results.find((r) => r.displayName === activeToolName);
190
+ function resolveDestBaseFromEnv(activeToolName, activeProjectRoot, envResults) {
191
+ const activeResult = envResults.find((r) => r.displayName === activeToolName);
130
192
  const isWukong = activeResult && activeResult.dirName === ".real";
131
193
 
132
194
  if (isWukong) {
@@ -141,7 +203,7 @@ function resolveDestBase() {
141
203
 
142
204
  // 未检测到活跃工具
143
205
  console.error(t("copy.no_ai_tool"));
144
- results.forEach((r) => {
206
+ envResults.forEach((r) => {
145
207
  console.error(` ${r.isActive ? "✅" : "⬜"} ${r.displayName}`);
146
208
  });
147
209
  console.error(t("copy.force_hint"));
@@ -188,17 +250,24 @@ function run() {
188
250
  console.log(t("copy.package_root", packageRoot));
189
251
 
190
252
  // 2. 确定目标根目录(检测 AI 工具环境)
191
- const destBase = resolveDestBase();
253
+ // 同时获取 isWukong 标志,避免后续重复调用 detectEnvironment()
254
+ const { activeToolName, activeProjectRoot, results: envResults } = detectEnvironment();
255
+ const activeEnvResult = envResults.find((r) => r.isActive);
256
+ const isWukong = activeEnvResult && activeEnvResult.dirName === ".real";
257
+ const destBase = resolveDestBaseFromEnv(activeToolName, activeProjectRoot, envResults);
192
258
  console.log(t("copy.dest_base", destBase));
193
259
  if (isForce) {
194
260
  console.log(t("copy.force_mode"));
195
261
  }
196
262
 
197
263
  // 3. 确定要复制/链接的内容
198
- // - 指定了 -skills:只创建 yida-skills/ 软链接
264
+ // - 指定了 -skills
265
+ // 悟空环境:删除已有的 yida-skills/ 软链(悟空手动上传技能,不需要软链)
266
+ // 其他环境:创建 yida-skills/ 软链接(如果存在实际目录则先删除)
199
267
  // - 指定了 -project:只复制 project/
200
268
  // - 两者都没指定(默认):只复制 project/
201
269
  // - 两者都指定:同时处理两项
270
+
202
271
  const shouldCopyProject = wantsProject || (!wantsSkills);
203
272
  const shouldLinkSkills = wantsSkills;
204
273
 
@@ -215,17 +284,28 @@ function run() {
215
284
  }
216
285
 
217
286
  if (shouldLinkSkills) {
218
- console.log(t("copy.creating_symlink"));
219
- const success = createSymlink(
220
- packageYidaSkillsDir,
221
- path.join(destBase, "yida-skills")
222
- );
223
- results.push({
224
- label: "yida-skills/",
225
- dest: path.join(destBase, "yida-skills"),
226
- count: success ? 1 : 0,
227
- type: "symlink"
228
- });
287
+ const destSkillsLink = path.join(destBase, "yida-skills");
288
+ if (isWukong) {
289
+ // 悟空环境:删除已有软链,不创建新软链
290
+ console.log(t("copy.wukong_skills_cleanup"));
291
+ const removed = removeSkillsLink(destSkillsLink);
292
+ results.push({
293
+ label: "yida-skills/",
294
+ dest: destSkillsLink,
295
+ count: removed ? 1 : 0,
296
+ type: "wukong-cleanup"
297
+ });
298
+ } else {
299
+ // 其他环境:创建软链接
300
+ console.log(t("copy.creating_symlink"));
301
+ const success = createSymlink(packageYidaSkillsDir, destSkillsLink);
302
+ results.push({
303
+ label: "yida-skills/",
304
+ dest: destSkillsLink,
305
+ count: success ? 1 : 0,
306
+ type: "symlink"
307
+ });
308
+ }
229
309
  }
230
310
 
231
311
  // 4. 打印汇总
@@ -242,6 +322,9 @@ function run() {
242
322
  results.forEach((r) => {
243
323
  if (r.type === "symlink") {
244
324
  console.log(` ${r.label.padEnd(14)} → ${r.dest} (${t("copy.symlink_label")})`);
325
+ } else if (r.type === "wukong-cleanup") {
326
+ const statusText = r.count > 0 ? t("copy.wukong_skills_cleaned") : t("copy.wukong_skills_not_found", r.dest);
327
+ console.log(` ${r.label.padEnd(14)} → ${r.dest} (${statusText})`);
245
328
  } else {
246
329
  console.log(` ${r.label.padEnd(14)} → ${r.dest} (${t("copy.files_count", r.count)})`);
247
330
  }
@@ -95,7 +95,35 @@ function buildApiPath(appType, apiName, options = {}) {
95
95
  // ── 参数解析 ─────────────────────────────────────────
96
96
 
97
97
  function parseArgs() {
98
- const args = process.argv.slice(2);
98
+ const rawArgs = process.argv.slice(2);
99
+
100
+ // 解析可选参数
101
+ const options = {
102
+ layout: "single", // 布局:single/double/card/section
103
+ theme: "default", // 主题:default/compact/comfortable
104
+ labelAlign: "top", // 标签对齐:top/left/right
105
+ };
106
+
107
+ // 复制一份 args 用于解析(避免修改原始数组影响后续处理)
108
+ const args = [...rawArgs];
109
+
110
+ // 解析 --layout, --theme, --label-align 参数
111
+ for (let i = 0; i < args.length; i++) {
112
+ if (args[i] === "--layout" && i + 1 < args.length) {
113
+ options.layout = args[i + 1];
114
+ args.splice(i, 2);
115
+ i--;
116
+ } else if (args[i] === "--theme" && i + 1 < args.length) {
117
+ options.theme = args[i + 1];
118
+ args.splice(i, 2);
119
+ i--;
120
+ } else if (args[i] === "--label-align" && i + 1 < args.length) {
121
+ options.labelAlign = args[i + 1];
122
+ args.splice(i, 2);
123
+ i--;
124
+ }
125
+ }
126
+
99
127
  const mode = args[0];
100
128
 
101
129
  if (mode === "create") {
@@ -104,7 +132,13 @@ function parseArgs() {
104
132
  console.error(t("create_form.example_create"));
105
133
  process.exit(1);
106
134
  }
107
- return { mode: "create", appType: args[1], formTitle: args[2], fieldsJsonOrFile: args[3] };
135
+ return {
136
+ mode: "create",
137
+ appType: args[1],
138
+ formTitle: args[2],
139
+ fieldsJsonOrFile: args[3],
140
+ ...options
141
+ };
108
142
  }
109
143
 
110
144
  if (mode === "update") {
@@ -113,12 +147,24 @@ function parseArgs() {
113
147
  console.error(t("create_form.example_update"));
114
148
  process.exit(1);
115
149
  }
116
- return { mode: "update", appType: args[1], formUuid: args[2], changesJsonOrFile: args[3] };
150
+ return {
151
+ mode: "update",
152
+ appType: args[1],
153
+ formUuid: args[2],
154
+ changesJsonOrFile: args[3],
155
+ ...options
156
+ };
117
157
  }
118
158
 
119
159
  // 兼容旧用法(无 mode 参数,默认 create 模式)
120
160
  if (args.length >= 3 && mode !== "create" && mode !== "update") {
121
- return { mode: "create", appType: args[0], formTitle: args[1], fieldsJsonOrFile: args[2] };
161
+ return {
162
+ mode: "create",
163
+ appType: args[0],
164
+ formTitle: args[1],
165
+ fieldsJsonOrFile: args[2],
166
+ ...options
167
+ };
122
168
  }
123
169
 
124
170
  console.error(t("create_form.usage_label"));
@@ -1007,10 +1053,160 @@ function resolveFieldIdReferences(fieldComponents) {
1007
1053
  });
1008
1054
  }
1009
1055
 
1056
+ // ── 布局配置映射 ─────────────────────────────────────
1057
+
1058
+ /**
1059
+ * 获取布局配置
1060
+ * @param {string} layout - 布局类型:single/double/card/section
1061
+ * @returns {object} 布局配置对象 { columns, formLayout, groupFields }
1062
+ */
1063
+ function getLayoutConfig(layout) {
1064
+ const layoutMap = {
1065
+ single: { columns: 1, formLayout: "default", groupFields: false },
1066
+ "1": { columns: 1, formLayout: "default", groupFields: false },
1067
+ double: { columns: 2, formLayout: "default", groupFields: false },
1068
+ "2": { columns: 2, formLayout: "default", groupFields: false },
1069
+ card: { columns: 1, formLayout: "card", groupFields: true },
1070
+ section: { columns: 1, formLayout: "section", groupFields: true },
1071
+ };
1072
+ return layoutMap[layout] || layoutMap.single;
1073
+ }
1074
+
1075
+ /**
1076
+ * 获取主题样式配置
1077
+ * @param {string} theme - 主题类型:default/compact/comfortable
1078
+ * @param {string} labelAlign - 标签对齐:top/left/right
1079
+ * @returns {object} 样式配置对象
1080
+ */
1081
+ function getThemeConfig(theme, labelAlign) {
1082
+ const baseConfig = {
1083
+ labelAlignPc: labelAlign || "top",
1084
+ labelWidthPc: labelAlign === "left" || labelAlign === "right" ? "130px" : "auto",
1085
+ labelWeightPc: "normal",
1086
+ contentMargin: "20",
1087
+ contentPadding: "20",
1088
+ fieldSpacing: "medium",
1089
+ };
1090
+
1091
+ const themeMap = {
1092
+ default: {
1093
+ ...baseConfig,
1094
+ contentMargin: "20",
1095
+ contentPadding: "20",
1096
+ },
1097
+ compact: {
1098
+ ...baseConfig,
1099
+ contentMargin: "12",
1100
+ contentPadding: "12",
1101
+ fieldSpacing: "small",
1102
+ },
1103
+ comfortable: {
1104
+ ...baseConfig,
1105
+ contentMargin: "32",
1106
+ contentPadding: "32",
1107
+ fieldSpacing: "large",
1108
+ },
1109
+ };
1110
+
1111
+ return themeMap[theme] || themeMap.default;
1112
+ }
1113
+
1114
+ // ── 按 group 分组字段 ─────────────────────────────────
1115
+
1116
+ /**
1117
+ * 将字段按 group 属性分组
1118
+ * @param {Array} fields - 字段定义数组
1119
+ * @returns {Array} 分组后的字段数组,每个元素是 { groupName, fields }
1120
+ */
1121
+ function groupFieldsByGroup(fields) {
1122
+ const groups = [];
1123
+ const groupMap = new Map();
1124
+
1125
+ fields.forEach((field) => {
1126
+ const groupName = field.group || "基本信息";
1127
+ if (!groupMap.has(groupName)) {
1128
+ groupMap.set(groupName, []);
1129
+ }
1130
+ groupMap.get(groupName).push(field);
1131
+ });
1132
+
1133
+ groupMap.forEach((groupFields, groupName) => {
1134
+ groups.push({ groupName, fields: groupFields });
1135
+ });
1136
+
1137
+ return groups;
1138
+ }
1139
+
1140
+ // ── 构建分组字段组件 ─────────────────────────────────
1141
+
1142
+ /**
1143
+ * 构建分组/卡片布局的字段组件
1144
+ * @param {Array} fields - 字段定义数组
1145
+ * @param {string} formLayout - 布局类型:card/section
1146
+ * @returns {Array} 分组后的组件数组
1147
+ */
1148
+ function buildGroupedFieldComponents(fields, formLayout) {
1149
+ const groups = groupFieldsByGroup(fields);
1150
+
1151
+ return groups.map((group, groupIndex) => {
1152
+ // 构建分组内的字段组件
1153
+ const groupFieldComponents = group.fields.map((field) => buildFieldComponent(field));
1154
+
1155
+ if (formLayout === "card") {
1156
+ // 卡片式布局:每个分组是一个卡片容器
1157
+ return {
1158
+ componentName: "CardContainer",
1159
+ id: nextNodeId(),
1160
+ props: {
1161
+ title: i18n(group.groupName, group.groupName),
1162
+ collapsible: true,
1163
+ defaultCollapsed: false,
1164
+ showTitle: true,
1165
+ cardStyle: "default",
1166
+ headerStyle: "default",
1167
+ __gridSpan: 1,
1168
+ },
1169
+ condition: true,
1170
+ hidden: false,
1171
+ title: "",
1172
+ isLocked: false,
1173
+ conditionGroup: "",
1174
+ children: groupFieldComponents,
1175
+ };
1176
+ } else {
1177
+ // 分组式布局:每个分组是一个区块
1178
+ return {
1179
+ componentName: "SectionContainer",
1180
+ id: nextNodeId(),
1181
+ props: {
1182
+ title: i18n(group.groupName, group.groupName),
1183
+ collapsible: true,
1184
+ defaultCollapsed: false,
1185
+ showTitle: true,
1186
+ sectionStyle: "default",
1187
+ divider: groupIndex > 0, // 第一个分组不显示分隔线
1188
+ __gridSpan: 1,
1189
+ },
1190
+ condition: true,
1191
+ hidden: false,
1192
+ title: "",
1193
+ isLocked: false,
1194
+ conditionGroup: "",
1195
+ children: groupFieldComponents,
1196
+ };
1197
+ }
1198
+ });
1199
+ }
1200
+
1010
1201
  // ── 生成表单 Schema ──────────────────────────────────
1011
1202
 
1012
- function buildFormSchema(formTitle, fields, formUuid, corpId, appType, columns) {
1013
- columns = columns || 1;
1203
+ function buildFormSchema(formTitle, fields, formUuid, corpId, appType, layout, theme, labelAlign) {
1204
+ // 解析布局配置
1205
+ const layoutConfig = getLayoutConfig(layout || "single");
1206
+ const columns = layoutConfig.columns;
1207
+
1208
+ // 解析主题配置
1209
+ const themeConfig = getThemeConfig(theme || "default", labelAlign || "top");
1014
1210
  const fieldComponents = fields.map(function (field) {
1015
1211
  return buildFieldComponent(field);
1016
1212
  });
@@ -1070,12 +1266,12 @@ function buildFormSchema(formTitle, fields, formUuid, corpId, appType, columns)
1070
1266
  titleColor: "light",
1071
1267
  titleBg: "https://img.alicdn.com/imgextra/i2/O1CN0143ATPP1wIa9TrVvzN_!!6000000006285-2-tps-3360-400.png_.webp",
1072
1268
  backgroundColorCustom: "#f1f2f3",
1073
- sizePc: "medium",
1074
- labelAlignPc: "top",
1075
- labelWidthPc: "130px",
1076
- labelWeightPc: "normal",
1077
- labelAlignMobile: "top",
1078
- labelWidthMobile: "80px",
1269
+ sizePc: themeConfig.fieldSpacing === "small" ? "small" : themeConfig.fieldSpacing === "large" ? "large" : "medium",
1270
+ labelAlignPc: themeConfig.labelAlignPc,
1271
+ labelWidthPc: themeConfig.labelWidthPc,
1272
+ labelWeightPc: themeConfig.labelWeightPc,
1273
+ labelAlignMobile: labelAlign || "top",
1274
+ labelWidthMobile: labelAlign === "left" || labelAlign === "right" ? "80px" : "auto",
1079
1275
  labelWeightMobile: "normal",
1080
1276
  },
1081
1277
  condition: true,
@@ -1150,15 +1346,36 @@ function buildFormSchema(formTitle, fields, formUuid, corpId, appType, columns)
1150
1346
  afterSubmit: false,
1151
1347
  onProcessActionValidate: false,
1152
1348
  afterFormDataInit: false,
1153
- },
1154
- condition: true,
1155
- hidden: false,
1156
- title: "",
1157
- isLocked: false,
1158
- conditionGroup: "",
1159
- // ★ 核心:FormContainer 内层的字段组件
1160
- children: fieldComponents,
1161
- },
1349
+ }, children: [
1350
+ {
1351
+ componentName: "FormContainer",
1352
+ id: nextNodeId(),
1353
+ props: {
1354
+ formLabel: i18n(formTitle, formTitle),
1355
+ formLabelVisible: true,
1356
+ columns: columns,
1357
+ labelAlign: labelAlign || "top",
1358
+ submitText: i18n("提交", "Submit"),
1359
+ stageText: i18n("暂存", "Stage"),
1360
+ submitAndNewText: i18n("提交并继续", "Submit and New"),
1361
+ fieldId: "formContainer_" + Date.now().toString(36),
1362
+ aiFormConfig: { systemPrompt: "", model: "qwen" },
1363
+ beforeSubmit: false,
1364
+ afterSubmit: false,
1365
+ onProcessActionValidate: false,
1366
+ afterFormDataInit: false,
1367
+ },
1368
+ condition: true,
1369
+ hidden: false,
1370
+ title: "",
1371
+ isLocked: false,
1372
+ conditionGroup: "",
1373
+ // ★ 核心:FormContainer 内层的字段组件(支持分组布局)
1374
+ children: layoutConfig.groupFields
1375
+ ? buildGroupedFieldComponents(fields, layoutConfig.formLayout)
1376
+ : fieldComponents,
1377
+ },
1378
+ ], },
1162
1379
  ],
1163
1380
  },
1164
1381
  {
@@ -1974,7 +2191,7 @@ async function saveSchemaAndUpdateConfig(authRef, appType, formUuid, schema, ver
1974
2191
  // ── create 模式主流程 ─────────────────────────────────
1975
2192
 
1976
2193
  async function mainCreate(parsedArgs, csrfToken, cookies, baseUrl, cookieData) {
1977
- const { appType, formTitle, fieldsJsonOrFile } = parsedArgs;
2194
+ const { appType, formTitle, fieldsJsonOrFile, layout, theme, labelAlign } = parsedArgs;
1978
2195
 
1979
2196
  const SEP = "=".repeat(50);
1980
2197
  console.error(SEP);
@@ -2025,7 +2242,7 @@ async function mainCreate(parsedArgs, csrfToken, cookies, baseUrl, cookieData) {
2025
2242
  console.error(t("create_form.corp_id_ok", corpId));
2026
2243
  }
2027
2244
 
2028
- const schema = buildFormSchema(formTitle, fields, formUuid, corpId, appType, columns);
2245
+ const schema = buildFormSchema(formTitle, fields, formUuid, corpId, appType, layout, theme, labelAlign);
2029
2246
  var { configResult } = await saveSchemaAndUpdateConfig(authRef, appType, formUuid, schema, 1, 4);
2030
2247
 
2031
2248
  // 输出结果