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,327 @@
1
+ function renderAdminDetailPage(context, helpers) {
2
+ if (context.childTables.length > 0) {
3
+ return renderChildTableAdminDetailPage(context, helpers);
4
+ }
5
+
6
+ const headerIcon = helpers.getAdminHeaderIcon(context);
7
+ return `import { useCallback, useEffect, useMemo, useState } from "react";
8
+ import { useNavigate, useParams } from "react-router";
9
+ import { Pencil, ${headerIcon} } from "lucide-react";
10
+ import type { ${context.objectPascal}Vo } from "@scaffold/api/client";
11
+ import { ObjectPage } from "@/components/admin-ui/object-page";
12
+ import { Button } from "@/components/ui/button";
13
+ import {
14
+ create${context.objectPascal}Api,
15
+ formatDateTime,
16
+ getOptionLabel,
17
+ ${context.fields.some((field) => field.name === 'status') ? `get${context.objectPascal}StatusMeta,\n ` : ''}${context.fields.filter((field) => field.type === 'enum').map((field) => `${field.name.toUpperCase()}_OPTIONS`).join(',\n ')}
18
+ } from "./${context.objectKebab}-ui";
19
+
20
+ function LoadingState() {
21
+ return (
22
+ <div className="rounded-xl border border-border bg-card p-6 shadow-sm">
23
+ <p className="text-sm text-muted-foreground">正在加载${context.label}详情...</p>
24
+ </div>
25
+ );
26
+ }
27
+
28
+ interface ErrorStateProps {
29
+ message: string;
30
+ onBack: () => void;
31
+ }
32
+
33
+ function ErrorState({ message, onBack }: ErrorStateProps) {
34
+ return (
35
+ <div className="rounded-xl border border-destructive/20 bg-card p-6 shadow-sm">
36
+ <h2 className="text-base font-semibold text-destructive">${context.label}详情加载失败</h2>
37
+ <p className="mt-2 text-sm text-muted-foreground">{message}</p>
38
+ <Button type="button" variant="outline" className="mt-4" onClick={onBack}>
39
+ 返回列表
40
+ </Button>
41
+ </div>
42
+ );
43
+ }
44
+
45
+ export function DetailPage() {
46
+ const navigate = useNavigate();
47
+ const params = useParams<{ id: string }>();
48
+ const ${context.objectCamel}Api = useMemo(() => create${context.objectPascal}Api(), []);
49
+ const ${context.objectCamel}Id = params.id;
50
+ const [${context.objectCamel}, set${context.objectPascal}] = useState<${context.objectPascal}Vo | null>(null);
51
+ const [loading, setLoading] = useState(true);
52
+ const [error, setError] = useState<string | null>(null);
53
+
54
+ const load${context.objectPascal} = useCallback(async () => {
55
+ if (!${context.objectCamel}Id) {
56
+ setError("缺少${context.label} ID");
57
+ setLoading(false);
58
+ return;
59
+ }
60
+
61
+ setLoading(true);
62
+ setError(null);
63
+ try {
64
+ const response = await ${context.objectCamel}Api.getById(${context.objectCamel}Id) as ${context.objectPascal}Vo;
65
+ set${context.objectPascal}(response);
66
+ } catch (loadError) {
67
+ const message = loadError instanceof Error ? loadError.message : "加载${context.label}详情失败";
68
+ setError(message);
69
+ } finally {
70
+ setLoading(false);
71
+ }
72
+ }, [${context.objectCamel}Api, ${context.objectCamel}Id]);
73
+
74
+ useEffect(() => {
75
+ void load${context.objectPascal}();
76
+ }, [load${context.objectPascal}]);
77
+
78
+ if (loading) {
79
+ return <LoadingState />;
80
+ }
81
+
82
+ if (error || !${context.objectCamel}) {
83
+ return <ErrorState message={error ?? "${context.label}不存在"} onBack={() => navigate("/${context.routePath}")} />;
84
+ }
85
+
86
+ ${context.fields.some((field) => field.name === 'status') ? ` const statusMeta = get${context.objectPascal}StatusMeta(${context.objectCamel}.status);\n` : ''} return (
87
+ <ObjectPage
88
+ mode="display"
89
+ backPath="/${context.routePath}"
90
+ breadcrumb="${context.label}管理"
91
+ title={${context.objectCamel}.${helpers.getPrimaryTitleField(context)} || "${context.label}详情"}
92
+ subtitle={${context.objectCamel}.${context.fields[0].name}}
93
+ ${context.fields.some((field) => field.name === 'status') ? 'status={{ label: statusMeta.label, color: statusMeta.objectPageColor }}' : ''}
94
+ headerIcon={<${headerIcon} className="h-6 w-6" />}
95
+ headerFields={[
96
+ ${helpers.indent(helpers.renderDetailHeaderFields(context), 8)}
97
+ ]}
98
+ showSectionNav={true}
99
+ actions={[
100
+ {
101
+ key: "edit",
102
+ label: "编辑",
103
+ icon: <Pencil className="h-4 w-4" />,
104
+ variant: "secondary",
105
+ onClick: () => navigate("/${context.routePath}/" + ${context.objectCamel}.id + "/edit"),
106
+ },
107
+ ]}
108
+ sections={[
109
+ {
110
+ id: "basicInfo",
111
+ title: "基本信息",
112
+ subtitle: "${helpers.getPatternDetailSubtitle(context)}",
113
+ content: (
114
+ <div className="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
115
+ ${helpers.indent(helpers.renderDetailFields(context), 14)}
116
+ </div>
117
+ ),
118
+ },
119
+ ]}
120
+ />
121
+ );
122
+ }
123
+
124
+ export default DetailPage;
125
+ `;
126
+ }
127
+
128
+ function renderChildTableAdminDetailPage(context, helpers) {
129
+ const headerIcon = helpers.getAdminHeaderIcon(context);
130
+ const childSections = context.childTables.map((child) => renderChildDetailSection(context, child)).join(',\n');
131
+ const childColumns = context.childTables.map((child) => renderChildDetailColumns(context, child, helpers)).join('\n\n');
132
+
133
+ return `import { useCallback, useEffect, useMemo, useState } from "react";
134
+ import { useNavigate, useParams } from "react-router";
135
+ import { Pencil, ${headerIcon} } from "lucide-react";
136
+ import type { ${context.objectPascal}Vo } from "@scaffold/api/client";
137
+ import { ObjectPage } from "@/components/admin-ui/object-page";
138
+ import {
139
+ EditableTable,
140
+ TableText,
141
+ type EditableTableColumn,
142
+ } from "@/components/admin-ui/editable-table";
143
+ import { Button } from "@/components/ui/button";
144
+ import {
145
+ create${context.objectPascal}Api,
146
+ formatDateTime,
147
+ getOptionLabel,
148
+ ${context.fields.some((field) => field.name === 'status') ? `get${context.objectPascal}StatusMeta,\n ` : ''}${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 ')}
149
+ } from "./${context.objectKebab}-ui";
150
+
151
+ function LoadingState() {
152
+ return (
153
+ <div className="rounded-xl border border-border bg-card p-6 shadow-sm">
154
+ <p className="text-sm text-muted-foreground">正在加载${context.label}详情...</p>
155
+ </div>
156
+ );
157
+ }
158
+
159
+ interface ErrorStateProps {
160
+ message: string;
161
+ onBack: () => void;
162
+ }
163
+
164
+ function ErrorState({ message, onBack }: ErrorStateProps) {
165
+ return (
166
+ <div className="rounded-xl border border-destructive/20 bg-card p-6 shadow-sm">
167
+ <h2 className="text-base font-semibold text-destructive">${context.label}详情加载失败</h2>
168
+ <p className="mt-2 text-sm text-muted-foreground">{message}</p>
169
+ <Button type="button" variant="outline" className="mt-4" onClick={onBack}>
170
+ 返回列表
171
+ </Button>
172
+ </div>
173
+ );
174
+ }
175
+
176
+ export function DetailPage() {
177
+ const navigate = useNavigate();
178
+ const params = useParams<{ id: string }>();
179
+ const ${context.objectCamel}Api = useMemo(() => create${context.objectPascal}Api(), []);
180
+ const ${context.objectCamel}Id = params.id;
181
+ const [${context.objectCamel}, set${context.objectPascal}] = useState<${context.objectPascal}Vo | null>(null);
182
+ const [loading, setLoading] = useState(true);
183
+ const [error, setError] = useState<string | null>(null);
184
+
185
+ const load${context.objectPascal} = useCallback(async () => {
186
+ if (!${context.objectCamel}Id) {
187
+ setError("缺少${context.label} ID");
188
+ setLoading(false);
189
+ return;
190
+ }
191
+
192
+ setLoading(true);
193
+ setError(null);
194
+ try {
195
+ const response = await ${context.objectCamel}Api.getById(${context.objectCamel}Id) as ${context.objectPascal}Vo;
196
+ set${context.objectPascal}(response);
197
+ } catch (loadError) {
198
+ const message = loadError instanceof Error ? loadError.message : "加载${context.label}详情失败";
199
+ setError(message);
200
+ } finally {
201
+ setLoading(false);
202
+ }
203
+ }, [${context.objectCamel}Api, ${context.objectCamel}Id]);
204
+
205
+ useEffect(() => {
206
+ void load${context.objectPascal}();
207
+ }, [load${context.objectPascal}]);
208
+
209
+ if (loading) {
210
+ return <LoadingState />;
211
+ }
212
+
213
+ if (error || !${context.objectCamel}) {
214
+ return <ErrorState message={error ?? "${context.label}不存在"} onBack={() => navigate("/${context.routePath}")} />;
215
+ }
216
+
217
+ ${context.fields.some((field) => field.name === 'status') ? ` const statusMeta = get${context.objectPascal}StatusMeta(${context.objectCamel}.status);\n` : ''}${helpers.indent(childColumns, 2)}
218
+
219
+ return (
220
+ <ObjectPage
221
+ mode="display"
222
+ backPath="/${context.routePath}"
223
+ breadcrumb="${context.label}管理"
224
+ title={${context.objectCamel}.${helpers.getPrimaryTitleField(context)} || "${context.label}详情"}
225
+ subtitle={${context.objectCamel}.${context.fields[0].name}}
226
+ ${context.fields.some((field) => field.name === 'status') ? 'status={{ label: statusMeta.label, color: statusMeta.objectPageColor }}' : ''}
227
+ headerIcon={<${headerIcon} className="h-6 w-6" />}
228
+ headerFields={[
229
+ ${helpers.indent(helpers.renderDetailHeaderFields(context), 8)}
230
+ ]}
231
+ showSectionNav={true}
232
+ actions={[
233
+ {
234
+ key: "edit",
235
+ label: "编辑",
236
+ icon: <Pencil className="h-4 w-4" />,
237
+ variant: "secondary",
238
+ onClick: () => navigate("/${context.routePath}/" + ${context.objectCamel}.id + "/edit"),
239
+ },
240
+ ]}
241
+ sections={[
242
+ {
243
+ id: "basicInfo",
244
+ title: "基本信息",
245
+ subtitle: "${helpers.getPatternDetailSubtitle(context)}",
246
+ content: (
247
+ <div className="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
248
+ ${helpers.indent(helpers.renderDetailFields(context), 14)}
249
+ </div>
250
+ ),
251
+ }${childSections ? `,\n${helpers.indent(childSections, 8)}` : ''}
252
+ ]}
253
+ />
254
+ );
255
+ }
256
+
257
+ export default DetailPage;
258
+ `;
259
+ }
260
+
261
+ function renderChildDetailColumns(context, child, helpers) {
262
+ return `const ${child.variableName}Columns: EditableTableColumn<NonNullable<${context.objectPascal}Vo["${child.variableName}"]>[number]>[] = [
263
+ ${helpers.indent(child.fields.map((field) => renderChildDetailColumn(child, field, helpers)).join(',\n'), 2)}
264
+ ];`;
265
+ }
266
+
267
+ function renderChildDetailColumn(child, field, helpers) {
268
+ if (field.tsType === 'boolean') {
269
+ return `{
270
+ key: "${field.name}",
271
+ title: "${field.label || field.name}",
272
+ width: 100,
273
+ align: "center",
274
+ render: (record) => (
275
+ <TableText variant={record.${field.name} ? "primary" : "muted"}>
276
+ {record.${field.name} ? "是" : "否"}
277
+ </TableText>
278
+ ),
279
+ }`;
280
+ }
281
+ if (field.type === 'enum') {
282
+ return `{
283
+ key: "${field.name}",
284
+ title: "${field.label || field.name}",
285
+ width: 180,
286
+ render: (record) => <TableText>{getOptionLabel(${helpers.getChildEnumOptionConst(child, field)}, record.${field.name})}</TableText>,
287
+ }`;
288
+ }
289
+ if (field.tsType === 'number') {
290
+ return `{
291
+ key: "${field.name}",
292
+ title: "${field.label || field.name}",
293
+ width: 180,
294
+ render: (record) => <TableText>{record.${field.name} === undefined || record.${field.name} === null ? "-" : String(record.${field.name})}</TableText>,
295
+ }`;
296
+ }
297
+ return `{
298
+ key: "${field.name}",
299
+ title: "${field.label || field.name}",
300
+ width: 180,
301
+ render: (record) => <TableText>{record.${field.name} || "-"}</TableText>,
302
+ }`;
303
+ }
304
+
305
+ function renderChildDetailSection(context, child) {
306
+ const sectionId = (context.ui.detail && context.ui.detail.sections || []).includes(child.name) ? child.name : child.variableName;
307
+ return `{
308
+ id: "${sectionId}",
309
+ title: "${child.label || child.name}",
310
+ subtitle: "${context.label}${child.label || child.name}子表",
311
+ content: (
312
+ <EditableTable
313
+ embedded={true}
314
+ showIndex={false}
315
+ rowKey={(record) => JSON.stringify(record)}
316
+ dataSource={${context.objectCamel}.${child.variableName} ?? []}
317
+ columns={${child.variableName}Columns}
318
+ minWidth={700}
319
+ emptyText="暂无${child.label || child.name}"
320
+ />
321
+ ),
322
+ }`;
323
+ }
324
+
325
+ module.exports = {
326
+ renderAdminDetailPage,
327
+ };