scaffold-engine 0.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 (52) hide show
  1. package/README.md +117 -0
  2. package/engine.project.example.json +23 -0
  3. package/package.json +49 -0
  4. package/scripts/postinstall.cjs +42 -0
  5. package/specs/catalogs/action-templates.yaml +189 -0
  6. package/specs/catalogs/child-templates.yaml +54 -0
  7. package/specs/catalogs/field-fragments.yaml +203 -0
  8. package/specs/catalogs/object-catalog.yaml +35 -0
  9. package/specs/catalogs/object-name-suggestions.yaml +30 -0
  10. package/specs/catalogs/object-templates.yaml +45 -0
  11. package/specs/catalogs/pattern-catalog.yaml +48 -0
  12. package/specs/catalogs/status-templates.yaml +16 -0
  13. package/specs/projects/crm-pilot/customer.yaml +122 -0
  14. package/specs/projects/crm-pilot/lead.from-nl.yaml +76 -0
  15. package/specs/projects/crm-pilot/lead.yaml +82 -0
  16. package/specs/projects/generated-from-nl/crm-customer.yaml +158 -0
  17. package/specs/projects/generated-from-nl/crm-lead.yaml +76 -0
  18. package/specs/projects/generated-from-nl/crm-opportunity.yaml +78 -0
  19. package/specs/projects/generated-from-nl/crm-quote.yaml +78 -0
  20. package/specs/projects/generated-from-nl/custom-documentLines.yaml +125 -0
  21. package/specs/projects/generated-from-nl/custom-treeEntity.yaml +78 -0
  22. package/specs/projects/generated-from-nl/erp-material-pattern-test.yaml +79 -0
  23. package/specs/projects/generated-from-nl/erp-material.yaml +78 -0
  24. package/specs/projects/generated-from-nl/hr-orgUnit.yaml +100 -0
  25. package/specs/projects/pattern-examples/document-lines-demo.yaml +125 -0
  26. package/specs/projects/pattern-examples/tree-entity-demo.yaml +79 -0
  27. package/specs/rules/business-model.schema.json +262 -0
  28. package/specs/rules/extension-boundaries.json +26 -0
  29. package/specs/rules/requirement-draft.schema.json +75 -0
  30. package/specs/rules/spec-governance.json +29 -0
  31. package/specs/templates/crm/customer.template.yaml +121 -0
  32. package/specs/templates/crm/lead.template.yaml +82 -0
  33. package/tools/analyze-requirement.cjs +950 -0
  34. package/tools/cli.cjs +59 -0
  35. package/tools/create-draft.cjs +18 -0
  36. package/tools/engine.cjs +47 -0
  37. package/tools/generate-draft.cjs +33 -0
  38. package/tools/generate-module.cjs +1218 -0
  39. package/tools/init-project.cjs +194 -0
  40. package/tools/lib/draft-toolkit.cjs +357 -0
  41. package/tools/lib/model-toolkit.cjs +482 -0
  42. package/tools/lib/pattern-renderers.cjs +166 -0
  43. package/tools/lib/renderers/detail-page-renderer.cjs +327 -0
  44. package/tools/lib/renderers/form-page-renderer.cjs +553 -0
  45. package/tools/lib/renderers/list-page-renderer.cjs +371 -0
  46. package/tools/lib/runtime-config.cjs +154 -0
  47. package/tools/patch-draft.cjs +57 -0
  48. package/tools/prompts/business-model-prompt.md +58 -0
  49. package/tools/run-requirement.cjs +672 -0
  50. package/tools/validate-draft.cjs +32 -0
  51. package/tools/validate-model.cjs +140 -0
  52. package/tools/verify-patterns.cjs +67 -0
@@ -0,0 +1,553 @@
1
+ function renderAdminFormPage(context, helpers) {
2
+ if (context.childTables.length > 0) {
3
+ return renderChildTableAdminFormPage(context, helpers);
4
+ }
5
+
6
+ const formFields = helpers.getFormFields(context);
7
+ const updateFieldCases = formFields.map((field) => helpers.renderFieldInput(context, field, 'form')).join('\n');
8
+ const headerIcon = helpers.getAdminHeaderIcon(context);
9
+
10
+ return `import { useCallback, useEffect, useMemo, useState } from "react";
11
+ import { useNavigate, useParams } from "react-router";
12
+ import { Save, ${headerIcon}, X } from "lucide-react";
13
+ import { toast } from "sonner";
14
+ import type { ${context.objectPascal}Vo } from "@scaffold/api/client";
15
+ import { ObjectPage } from "@/components/admin-ui/object-page";
16
+ import { Button } from "@/components/ui/button";
17
+ import { Input } from "@/components/ui/input";
18
+ import { Label } from "@/components/ui/label";
19
+ import {
20
+ Select,
21
+ SelectContent,
22
+ SelectItem,
23
+ SelectTrigger,
24
+ SelectValue,
25
+ } from "@/components/ui/select";
26
+ import {
27
+ createEmpty${context.objectPascal}Form,
28
+ create${context.objectPascal}Api,
29
+ getOptionLabel,
30
+ ${context.fields.filter((field) => field.type === 'enum').map((field) => `${field.name.toUpperCase()}_OPTIONS`).join(',\n ')},
31
+ toCreate${context.objectPascal}Payload,
32
+ to${context.objectPascal}FormState,
33
+ toUpdate${context.objectPascal}Payload,
34
+ validate${context.objectPascal}Form,
35
+ type ${context.objectPascal}FormState,
36
+ } from "./${context.objectKebab}-ui";
37
+
38
+ type ${context.objectPascal}FieldKey = keyof ${context.objectPascal}FormState;
39
+
40
+ function LoadingState() {
41
+ return (
42
+ <div className="rounded-xl border border-border bg-card p-6 shadow-sm">
43
+ <p className="text-sm text-muted-foreground">正在加载${context.label}数据...</p>
44
+ </div>
45
+ );
46
+ }
47
+
48
+ interface ErrorStateProps {
49
+ message: string;
50
+ onBack: () => void;
51
+ }
52
+
53
+ function ErrorState({ message, onBack }: ErrorStateProps) {
54
+ return (
55
+ <div className="rounded-xl border border-destructive/20 bg-card p-6 shadow-sm">
56
+ <h2 className="text-base font-semibold text-destructive">${context.label}数据加载失败</h2>
57
+ <p className="mt-2 text-sm text-muted-foreground">{message}</p>
58
+ <Button type="button" variant="outline" className="mt-4" onClick={onBack}>
59
+ 返回列表
60
+ </Button>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ export function FormPage() {
66
+ const navigate = useNavigate();
67
+ const params = useParams<{ id: string }>();
68
+ const ${context.objectCamel}Api = useMemo(() => create${context.objectPascal}Api(), []);
69
+ const ${context.objectCamel}Id = params.id;
70
+ const mode = ${context.objectCamel}Id ? "edit" : "create";
71
+ const [form, setForm] = useState<${context.objectPascal}FormState>(() => createEmpty${context.objectPascal}Form());
72
+ const [loading, setLoading] = useState(Boolean(${context.objectCamel}Id));
73
+ const [saving, setSaving] = useState(false);
74
+ const [error, setError] = useState<string | null>(null);
75
+
76
+ const load${context.objectPascal} = useCallback(async () => {
77
+ if (!${context.objectCamel}Id) {
78
+ setForm(createEmpty${context.objectPascal}Form());
79
+ setError(null);
80
+ setLoading(false);
81
+ return;
82
+ }
83
+
84
+ setLoading(true);
85
+ setError(null);
86
+ try {
87
+ const ${context.objectCamel} = await ${context.objectCamel}Api.getById(${context.objectCamel}Id) as ${context.objectPascal}Vo;
88
+ setForm(to${context.objectPascal}FormState(${context.objectCamel}));
89
+ } catch (loadError) {
90
+ const message = loadError instanceof Error ? loadError.message : "加载${context.label}失败";
91
+ setError(message);
92
+ } finally {
93
+ setLoading(false);
94
+ }
95
+ }, [${context.objectCamel}Api, ${context.objectCamel}Id]);
96
+
97
+ useEffect(() => {
98
+ void load${context.objectPascal}();
99
+ }, [load${context.objectPascal}]);
100
+
101
+ const updateField = <T extends ${context.objectPascal}FieldKey>(field: T, value: ${context.objectPascal}FormState[T]) => {
102
+ setForm((prev) => ({
103
+ ...prev,
104
+ [field]: value,
105
+ }));
106
+ };
107
+
108
+ const handleSubmit = async () => {
109
+ const validationMessage = validate${context.objectPascal}Form(form);
110
+ if (validationMessage) {
111
+ toast.error(validationMessage);
112
+ return;
113
+ }
114
+
115
+ setSaving(true);
116
+ setError(null);
117
+ try {
118
+ const ${context.objectCamel} = ${context.objectCamel}Id
119
+ ? await ${context.objectCamel}Api.update(${context.objectCamel}Id, toUpdate${context.objectPascal}Payload(form))
120
+ : await ${context.objectCamel}Api.create(toCreate${context.objectPascal}Payload(form));
121
+
122
+ const saved${context.objectPascal} = ${context.objectCamel} as ${context.objectPascal}Vo;
123
+ toast.success(mode === "create" ? "${context.label}创建成功" : "${context.label}更新成功");
124
+ navigate("/${context.routePath}/" + saved${context.objectPascal}.id);
125
+ } catch (submitError) {
126
+ const message = submitError instanceof Error ? submitError.message : "保存${context.label}失败";
127
+ setError(message);
128
+ toast.error(message);
129
+ } finally {
130
+ setSaving(false);
131
+ }
132
+ };
133
+
134
+ if (loading) {
135
+ return <LoadingState />;
136
+ }
137
+
138
+ if (error && ${context.objectCamel}Id && !form.${context.fields[0].name}) {
139
+ return <ErrorState message={error} onBack={() => navigate("/${context.routePath}")} />;
140
+ }
141
+
142
+ return (
143
+ <ObjectPage
144
+ mode={mode === "create" ? "create" : "edit"}
145
+ backPath={${context.objectCamel}Id ? "/${context.routePath}/" + ${context.objectCamel}Id : "/${context.routePath}"}
146
+ breadcrumb="${context.label}管理"
147
+ title={mode === "create" ? "新建${context.label}" : \`编辑${context.label} \${form.${context.fields[0].name} || ""}\`.trim()}
148
+ subtitle={mode === "create" ? "${helpers.getPatternFormSubtitle(context)}" : form.${context.fields[0].name} || "${context.label}编辑"}
149
+ headerIcon={<${headerIcon} className="h-6 w-6" />}
150
+ headerFields={[
151
+ ${helpers.indent(helpers.renderFormHeaderFields(context), 8)}
152
+ ]}
153
+ showSectionNav={false}
154
+ footerLeft={error ? <span className="text-destructive">{error}</span> : "保存后将直接进入${context.label}详情页"}
155
+ actions={[
156
+ {
157
+ key: "cancel",
158
+ label: "取消",
159
+ icon: <X className="h-4 w-4" />,
160
+ variant: "secondary",
161
+ onClick: () => navigate(${context.objectCamel}Id ? "/${context.routePath}/" + ${context.objectCamel}Id : "/${context.routePath}"),
162
+ },
163
+ {
164
+ key: "save",
165
+ label: mode === "create" ? "创建${context.label}" : "保存修改",
166
+ icon: <Save className="h-4 w-4" />,
167
+ variant: "primary",
168
+ loading: saving,
169
+ onClick: () => void handleSubmit(),
170
+ },
171
+ ]}
172
+ sections={[
173
+ {
174
+ id: "basicInfo",
175
+ title: "基本信息",
176
+ subtitle: "${helpers.getPatternFormSubtitle(context)}",
177
+ content: (
178
+ <div className="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
179
+ ${helpers.indent(updateFieldCases, 14)}
180
+ </div>
181
+ ),
182
+ },
183
+ ]}
184
+ />
185
+ );
186
+ }
187
+
188
+ export default FormPage;
189
+ `;
190
+ }
191
+
192
+ function renderChildTableAdminFormPage(context, helpers) {
193
+ const headerIcon = helpers.getAdminHeaderIcon(context);
194
+ const childKeys = context.childTables.map((child) => `"${child.variableName}"`).join(' | ');
195
+ const rootFieldKeyType = childKeys
196
+ ? `Exclude<keyof ${context.objectPascal}FormState, ${childKeys}>`
197
+ : `keyof ${context.objectPascal}FormState`;
198
+ const childImports = context.childTables.map((child) => ` createEmpty${child.className}Row,\n type ${child.className}FormRow,`).join('\n');
199
+ const childColumns = context.childTables.map((child) => renderChildFormColumns(context, child, helpers)).join('\n\n');
200
+ const childHandlers = context.childTables.map((child) => renderChildFormHandlers(context, child)).join('\n\n');
201
+ const childSections = context.childTables.map((child) => renderChildFormSection(context, child)).join(',\n');
202
+
203
+ return `import { useCallback, useEffect, useMemo, useState } from "react";
204
+ import { useNavigate, useParams } from "react-router";
205
+ import { ${headerIcon}, Plus, Save, X } from "lucide-react";
206
+ import { toast } from "sonner";
207
+ import type { ${context.objectPascal}Vo } from "@scaffold/api/client";
208
+ import { ObjectPage } from "@/components/admin-ui/object-page";
209
+ import {
210
+ EditableTable,
211
+ TableDeleteButton,
212
+ TableInput,
213
+ TableSelect,
214
+ type EditableTableColumn,
215
+ } from "@/components/admin-ui/editable-table";
216
+ import { Checkbox } from "@/components/ui/checkbox";
217
+ import { Button } from "@/components/ui/button";
218
+ import { Input } from "@/components/ui/input";
219
+ import { Label } from "@/components/ui/label";
220
+ import {
221
+ Select,
222
+ SelectContent,
223
+ SelectItem,
224
+ SelectTrigger,
225
+ SelectValue,
226
+ } from "@/components/ui/select";
227
+ import {
228
+ ${context.fields.filter((field) => field.type === 'enum').map((field) => ` ${field.name.toUpperCase()}_OPTIONS,`).join('\n')}${context.fields.some((field) => field.type === 'enum') ? '\n' : ''}${context.childTables.flatMap((child) => child.fields.filter((field) => field.type === 'enum').map((field) => ` ${helpers.getChildEnumOptionConst(child, field)},`)).join('\n')}${context.childTables.some((child) => child.fields.some((field) => field.type === 'enum')) ? '\n' : ''}${childImports ? `${childImports}\n` : ''} create${context.objectPascal}Api,
229
+ createEmpty${context.objectPascal}Form,
230
+ formatDateTime,
231
+ getOptionLabel,
232
+ toCreate${context.objectPascal}Payload,
233
+ to${context.objectPascal}FormState,
234
+ toUpdate${context.objectPascal}Payload,
235
+ validate${context.objectPascal}Form,
236
+ type ${context.objectPascal}FormState,
237
+ } from "./${context.objectKebab}-ui";
238
+
239
+ type RootFieldKey = ${rootFieldKeyType};
240
+
241
+ function LoadingState() {
242
+ return (
243
+ <div className="rounded-xl border border-border bg-card p-6 shadow-sm">
244
+ <p className="text-sm text-muted-foreground">正在加载${context.label}数据...</p>
245
+ </div>
246
+ );
247
+ }
248
+
249
+ interface ErrorStateProps {
250
+ message: string;
251
+ onBack: () => void;
252
+ }
253
+
254
+ function ErrorState({ message, onBack }: ErrorStateProps) {
255
+ return (
256
+ <div className="rounded-xl border border-destructive/20 bg-card p-6 shadow-sm">
257
+ <h2 className="text-base font-semibold text-destructive">${context.label}数据加载失败</h2>
258
+ <p className="mt-2 text-sm text-muted-foreground">{message}</p>
259
+ <Button type="button" variant="outline" className="mt-4" onClick={onBack}>
260
+ 返回列表
261
+ </Button>
262
+ </div>
263
+ );
264
+ }
265
+
266
+ export function FormPage() {
267
+ const navigate = useNavigate();
268
+ const params = useParams<{ id: string }>();
269
+ const ${context.objectCamel}Api = useMemo(() => create${context.objectPascal}Api(), []);
270
+ const ${context.objectCamel}Id = params.id;
271
+ const mode = ${context.objectCamel}Id ? "edit" : "create";
272
+ const [form, setForm] = useState<${context.objectPascal}FormState>(() => createEmpty${context.objectPascal}Form());
273
+ const [loading, setLoading] = useState(Boolean(${context.objectCamel}Id));
274
+ const [saving, setSaving] = useState(false);
275
+ const [error, setError] = useState<string | null>(null);
276
+
277
+ const load${context.objectPascal} = useCallback(async () => {
278
+ if (!${context.objectCamel}Id) {
279
+ setForm(createEmpty${context.objectPascal}Form());
280
+ setError(null);
281
+ setLoading(false);
282
+ return;
283
+ }
284
+
285
+ setLoading(true);
286
+ setError(null);
287
+ try {
288
+ const ${context.objectCamel} = await ${context.objectCamel}Api.getById(${context.objectCamel}Id) as ${context.objectPascal}Vo;
289
+ setForm(to${context.objectPascal}FormState(${context.objectCamel}));
290
+ } catch (loadError) {
291
+ const message = loadError instanceof Error ? loadError.message : "加载${context.label}失败";
292
+ setError(message);
293
+ } finally {
294
+ setLoading(false);
295
+ }
296
+ }, [${context.objectCamel}Api, ${context.objectCamel}Id]);
297
+
298
+ useEffect(() => {
299
+ void load${context.objectPascal}();
300
+ }, [load${context.objectPascal}]);
301
+
302
+ const updateField = <T extends RootFieldKey>(field: T, value: ${context.objectPascal}FormState[T]) => {
303
+ setForm((prev) => ({
304
+ ...prev,
305
+ [field]: value,
306
+ }));
307
+ };
308
+
309
+ ${helpers.indent(childHandlers, 2)}
310
+
311
+ ${helpers.indent(childColumns, 2)}
312
+
313
+ const handleSubmit = async () => {
314
+ const validationMessage = validate${context.objectPascal}Form(form);
315
+ if (validationMessage) {
316
+ toast.error(validationMessage);
317
+ return;
318
+ }
319
+
320
+ setSaving(true);
321
+ setError(null);
322
+ try {
323
+ const ${context.objectCamel} = ${context.objectCamel}Id
324
+ ? await ${context.objectCamel}Api.update(${context.objectCamel}Id, toUpdate${context.objectPascal}Payload(form))
325
+ : await ${context.objectCamel}Api.create(toCreate${context.objectPascal}Payload(form));
326
+
327
+ const saved${context.objectPascal} = ${context.objectCamel} as ${context.objectPascal}Vo;
328
+ toast.success(mode === "create" ? "${context.label}创建成功" : "${context.label}更新成功");
329
+ navigate("/${context.routePath}/" + saved${context.objectPascal}.id);
330
+ } catch (submitError) {
331
+ const message = submitError instanceof Error ? submitError.message : "保存${context.label}失败";
332
+ setError(message);
333
+ toast.error(message);
334
+ } finally {
335
+ setSaving(false);
336
+ }
337
+ };
338
+
339
+ if (loading) {
340
+ return <LoadingState />;
341
+ }
342
+
343
+ if (error && ${context.objectCamel}Id && !form.${context.fields[0].name} && !form.${helpers.getPrimaryTitleField(context)}) {
344
+ return <ErrorState message={error} onBack={() => navigate("/${context.routePath}")} />;
345
+ }
346
+
347
+ return (
348
+ <ObjectPage
349
+ mode={mode === "create" ? "create" : "edit"}
350
+ backPath={${context.objectCamel}Id ? "/${context.routePath}/" + ${context.objectCamel}Id : "/${context.routePath}"}
351
+ breadcrumb="${context.label}管理"
352
+ title={mode === "create" ? "新建${context.label}" : \`编辑${context.label} \${form.${helpers.getPrimaryTitleField(context)} || form.${context.fields[0].name} || ""}\`.trim()}
353
+ subtitle={mode === "create" ? "${helpers.getPatternFormSubtitle(context)}" : form.${context.fields[0].name} || "${context.label}编辑"}
354
+ headerIcon={<${headerIcon} className="h-6 w-6" />}
355
+ headerFields={[
356
+ ${helpers.indent(helpers.renderFormHeaderFields(context), 8)}
357
+ ]}
358
+ showSectionNav={false}
359
+ footerLeft={error ? <span className="text-destructive">{error}</span> : "保存后将直接进入${context.label}详情页"}
360
+ actions={[
361
+ {
362
+ key: "cancel",
363
+ label: "取消",
364
+ icon: <X className="h-4 w-4" />,
365
+ variant: "secondary",
366
+ onClick: () => navigate(${context.objectCamel}Id ? "/${context.routePath}/" + ${context.objectCamel}Id : "/${context.routePath}"),
367
+ },
368
+ {
369
+ key: "save",
370
+ label: mode === "create" ? "创建${context.label}" : "保存修改",
371
+ icon: <Save className="h-4 w-4" />,
372
+ variant: "primary",
373
+ loading: saving,
374
+ onClick: () => void handleSubmit(),
375
+ },
376
+ ]}
377
+ sections={[
378
+ {
379
+ id: "basicInfo",
380
+ title: "基本信息",
381
+ subtitle: "${helpers.getPatternFormSubtitle(context)}",
382
+ content: (
383
+ <div className="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
384
+ ${helpers.indent(helpers.getFormFields(context).map((field) => helpers.renderFieldInput(context, field, 'form')).join('\n'), 14)}
385
+ </div>
386
+ ),
387
+ }${childSections ? `,\n${helpers.indent(childSections, 8)}` : ''}
388
+ ]}
389
+ />
390
+ );
391
+ }
392
+
393
+ export default FormPage;
394
+ `;
395
+ }
396
+
397
+ function renderChildFormHandlers(context, child) {
398
+ const hasPrimaryField = child.fields.some((field) => field.name === 'isPrimary');
399
+ return `const add${child.className} = () => {
400
+ setForm((prev) => ({
401
+ ...prev,
402
+ ${child.variableName}: [
403
+ ...prev.${child.variableName},
404
+ createEmpty${child.className}Row({${hasPrimaryField ? ` isPrimary: prev.${child.variableName}.length === 0 ` : ''}})
405
+ ],
406
+ }));
407
+ };
408
+
409
+ const update${child.className} = <T extends keyof ${child.className}FormRow>(
410
+ rowId: string,
411
+ field: T,
412
+ value: ${child.className}FormRow[T]
413
+ ) => {
414
+ setForm((prev) => {
415
+ const nextRows = prev.${child.variableName}.map((row) => {
416
+ if (row.rowId !== rowId) {${hasPrimaryField ? `
417
+ if (field === "isPrimary" && value === true) {
418
+ return {
419
+ ...row,
420
+ isPrimary: false,
421
+ };
422
+ }` : ''}
423
+ return row;
424
+ }
425
+
426
+ return {
427
+ ...row,
428
+ [field]: value,
429
+ };
430
+ });
431
+
432
+ return {
433
+ ...prev,
434
+ ${child.variableName}: nextRows,
435
+ };
436
+ });
437
+ };
438
+
439
+ const remove${child.className} = (rowId: string) => {
440
+ setForm((prev) => {
441
+ const current = prev.${child.variableName};
442
+ const removed = current.find((item) => item.rowId === rowId);
443
+ const nextRows = current.filter((item) => item.rowId !== rowId);
444
+ ${hasPrimaryField ? `
445
+ if (removed?.isPrimary && nextRows.length > 0 && !nextRows.some((item) => item.isPrimary)) {
446
+ nextRows[0] = {
447
+ ...nextRows[0],
448
+ isPrimary: true,
449
+ };
450
+ }` : ''}
451
+
452
+ return {
453
+ ...prev,
454
+ ${child.variableName}: nextRows,
455
+ };
456
+ });
457
+ };`;
458
+ }
459
+
460
+ function renderChildFormColumns(context, child, helpers) {
461
+ return `const ${child.variableName}Columns: EditableTableColumn<${child.className}FormRow>[] = [
462
+ ${helpers.indent(child.fields.map((field) => renderChildFormColumn(child, field, helpers)).join(',\n'), 2)},
463
+ {
464
+ key: "actions",
465
+ title: "",
466
+ width: 56,
467
+ render: (record) => (
468
+ <TableDeleteButton onClick={() => remove${child.className}(record.rowId)} />
469
+ ),
470
+ },
471
+ ];`;
472
+ }
473
+
474
+ function renderChildFormColumn(child, field, helpers) {
475
+ if (field.tsType === 'boolean') {
476
+ return `{
477
+ key: "${field.name}",
478
+ title: "${field.label || field.name}",
479
+ width: 100,
480
+ align: "center",
481
+ render: (record) => (
482
+ <div className="flex justify-center">
483
+ <Checkbox
484
+ checked={Boolean(record.${field.name})}
485
+ onCheckedChange={(checked) => update${child.className}(record.rowId, "${field.name}", Boolean(checked))}
486
+ />
487
+ </div>
488
+ ),
489
+ }`;
490
+ }
491
+
492
+ if (field.type === 'enum') {
493
+ return `{
494
+ key: "${field.name}",
495
+ title: "${field.label || field.name}",
496
+ width: 180,
497
+ ${field.required ? 'required: true,\n ' : ''}render: (record) => (
498
+ <TableSelect
499
+ value={record.${field.name} ?? ""}
500
+ options={${helpers.getChildEnumOptionConst(child, field)}}
501
+ placeholder="请选择${field.label || field.name}"
502
+ onChange={(value) => update${child.className}(record.rowId, "${field.name}", value)}
503
+ />
504
+ ),
505
+ }`;
506
+ }
507
+
508
+ return `{
509
+ key: "${field.name}",
510
+ title: "${field.label || field.name}",
511
+ width: 180,
512
+ ${field.required ? 'required: true,\n ' : ''}render: (record) => (
513
+ <TableInput
514
+ value={record.${field.name} ?? ""}
515
+ placeholder="请输入${field.label || field.name}"
516
+ onChange={(value) => update${child.className}(record.rowId, "${field.name}", value)}
517
+ />
518
+ ),
519
+ }`;
520
+ }
521
+
522
+ function renderChildFormSection(context, child) {
523
+ const group = ((context.ui.form && context.ui.form.groups) || []).find((item) => item.childTable === child.name);
524
+ return `{
525
+ id: "${child.variableName}",
526
+ title: "${group?.title || child.label || child.name}",
527
+ subtitle: "维护${child.label || child.name}子表",
528
+ content: (
529
+ <EditableTable<${child.className}FormRow>
530
+ embedded={true}
531
+ rowKey="rowId"
532
+ dataSource={form.${child.variableName}}
533
+ columns={${child.variableName}Columns}
534
+ minWidth={760}
535
+ emptyText="暂无${child.label || child.name},请点击右上角新增"
536
+ header={{
537
+ title: "${group?.title || child.label || child.name}列表",
538
+ subtitle: "维护有效${child.label || child.name}时,会随${context.label}一起保存",
539
+ actions: (
540
+ <Button type="button" variant="outline" size="sm" onClick={add${child.className}}>
541
+ <Plus className="size-4" />
542
+ 新增${child.label || child.name}
543
+ </Button>
544
+ ),
545
+ }}
546
+ />
547
+ ),
548
+ }`;
549
+ }
550
+
551
+ module.exports = {
552
+ renderAdminFormPage,
553
+ };