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,482 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function requireOptionalDependency(name) {
5
+ try {
6
+ return require(name);
7
+ } catch (error) {
8
+ throw new Error(`缺少依赖 "${name}"。请先在仓库中执行 pnpm install 后再运行 scaffold 工具链。`);
9
+ }
10
+ }
11
+
12
+ const YAML = requireOptionalDependency('yaml');
13
+ const Ajv = requireOptionalDependency('ajv');
14
+ const { SCAFFOLD_ROOT, loadRuntimeConfig, resolveRulePath } = require('./runtime-config.cjs');
15
+
16
+ function resolveScaffoldPath(...segments) {
17
+ return path.join(SCAFFOLD_ROOT, ...segments);
18
+ }
19
+
20
+ function readText(filePath) {
21
+ return fs.readFileSync(filePath, 'utf8');
22
+ }
23
+
24
+ function readJson(filePath) {
25
+ return JSON.parse(readText(filePath));
26
+ }
27
+
28
+ function loadSpec(filePath) {
29
+ const absolutePath = path.resolve(filePath);
30
+ const content = readText(absolutePath);
31
+ const ext = path.extname(absolutePath).toLowerCase();
32
+
33
+ if (ext === '.yaml' || ext === '.yml') {
34
+ return { absolutePath, model: YAML.parse(content) };
35
+ }
36
+
37
+ if (ext === '.json') {
38
+ return { absolutePath, model: JSON.parse(content) };
39
+ }
40
+
41
+ throw new Error(`不支持的模型文件格式: ${absolutePath}`);
42
+ }
43
+
44
+ function validateSchema(model, schemaPath) {
45
+ const schema = readJson(schemaPath);
46
+ delete schema.$schema;
47
+ const ajv = new Ajv({ allErrors: true, strict: false });
48
+ const validate = ajv.compile(schema);
49
+ const valid = validate(model);
50
+ return {
51
+ valid,
52
+ errors: valid
53
+ ? []
54
+ : (validate.errors || []).map((item) => {
55
+ const pathText = item.instancePath || '/';
56
+ return `[schema] ${pathText} ${item.message || 'invalid'}`;
57
+ }),
58
+ };
59
+ }
60
+
61
+ function validateBusinessRules(model, boundaries) {
62
+ const errors = [];
63
+ const warnings = [];
64
+
65
+ const systemFields = new Set(boundaries.systemFields || []);
66
+ const reservedTables = new Set(boundaries.reservedTables || []);
67
+ const reservedPrefixes = boundaries.reservedTablePrefixes || [];
68
+ const protectedDomains = new Set((boundaries.protectedDomains || []).map((item) => String(item).toLowerCase()));
69
+ const protectedObjects = new Set((boundaries.protectedMetaObjects || []).map((item) => String(item).toLowerCase()));
70
+
71
+ const meta = model.meta || {};
72
+ const data = model.data || {};
73
+ const app = model.app || {};
74
+ const ui = model.ui || {};
75
+ const fields = Array.isArray(data.fields) ? data.fields : [];
76
+ const children = Array.isArray(data.children) ? data.children : [];
77
+
78
+ if (protectedDomains.has(String(meta.domain || '').toLowerCase())) {
79
+ errors.push(`[boundary] meta.domain=${meta.domain} 属于受保护域,不能作为业务扩展对象`);
80
+ }
81
+
82
+ if (protectedObjects.has(String(meta.object || '').toLowerCase())) {
83
+ errors.push(`[boundary] meta.object=${meta.object} 属于受保护对象,不能作为业务扩展对象`);
84
+ }
85
+
86
+ validateTableName(data.table, 'data.table', reservedTables, reservedPrefixes, errors);
87
+ for (const child of children) {
88
+ validateTableName(child.table, `data.children.${child.name}.table`, reservedTables, reservedPrefixes, errors);
89
+ }
90
+
91
+ validateUniqueNames(fields.map((item) => item.name), 'data.fields', errors);
92
+ validateUniqueNames(children.map((item) => item.name), 'data.children', errors);
93
+
94
+ for (const child of children) {
95
+ validateUniqueNames((child.fields || []).map((item) => item.name), `data.children.${child.name}.fields`, errors);
96
+ }
97
+
98
+ const rootFieldNames = new Set(fields.map((item) => item.name));
99
+ const childNames = new Set(children.map((item) => item.name));
100
+ const allowedRootRefs = new Set([...rootFieldNames, ...systemFields]);
101
+
102
+ const listColumns = (ui.list && Array.isArray(ui.list.columns)) ? ui.list.columns : [];
103
+ const listFilters = (ui.list && Array.isArray(ui.list.filters)) ? ui.list.filters : [];
104
+ const formGroups = (ui.form && Array.isArray(ui.form.groups)) ? ui.form.groups : [];
105
+
106
+ for (const item of listColumns) {
107
+ if (!allowedRootRefs.has(item)) {
108
+ errors.push(`[ui] list.columns 引用了不存在的字段: ${item}`);
109
+ }
110
+ }
111
+
112
+ for (const item of listFilters) {
113
+ if (!allowedRootRefs.has(item)) {
114
+ errors.push(`[ui] list.filters 引用了不存在的字段: ${item}`);
115
+ }
116
+ }
117
+
118
+ for (const group of formGroups) {
119
+ if (Array.isArray(group.fields)) {
120
+ for (const fieldName of group.fields) {
121
+ if (!rootFieldNames.has(fieldName)) {
122
+ errors.push(`[ui] form.groups(${group.title}) 引用了不存在的字段: ${fieldName}`);
123
+ }
124
+ }
125
+ }
126
+ if (group.childTable && !childNames.has(group.childTable)) {
127
+ errors.push(`[ui] form.groups(${group.title}) 引用了不存在的子表: ${group.childTable}`);
128
+ }
129
+ }
130
+
131
+ const actions = Array.isArray(app.actions) ? app.actions : [];
132
+ const statusFlow = app.statusFlow || null;
133
+ const responseContract = app.responseContract || null;
134
+ const permissionProfile = app.permissionProfile || null;
135
+ const validationRules = Array.isArray(app.validationRules) ? app.validationRules : [];
136
+ const errorCodes = Array.isArray(app.errorCodes) ? app.errorCodes : [];
137
+ if ((actions.includes('activate') || actions.includes('deactivate')) && !rootFieldNames.has('status')) {
138
+ errors.push('[app] 启用/停用动作依赖 status 字段,但当前模型未定义 status');
139
+ }
140
+ if (statusFlow) {
141
+ if (!rootFieldNames.has('status')) {
142
+ errors.push('[app] statusFlow 依赖 status 字段,但当前模型未定义 status');
143
+ }
144
+ const states = new Set(Array.isArray(statusFlow.states) ? statusFlow.states : []);
145
+ if (states.size === 0) {
146
+ errors.push('[app] statusFlow.states 不能为空');
147
+ }
148
+ if (app.defaultStatus && !states.has(app.defaultStatus)) {
149
+ errors.push(`[app] defaultStatus=${app.defaultStatus} 未在 statusFlow.states 中定义`);
150
+ }
151
+ for (const transition of Array.isArray(statusFlow.transitions) ? statusFlow.transitions : []) {
152
+ if (!actions.includes(transition.action)) {
153
+ errors.push(`[app] statusFlow.transitions.action=${transition.action} 未在 app.actions 中声明`);
154
+ }
155
+ for (const fromState of transition.from || []) {
156
+ if (!states.has(fromState)) {
157
+ errors.push(`[app] statusFlow.transitions.from 包含未定义状态: ${fromState}`);
158
+ }
159
+ }
160
+ if (transition.to && !states.has(transition.to)) {
161
+ errors.push(`[app] statusFlow.transitions.to=${transition.to} 未在 statusFlow.states 中定义`);
162
+ }
163
+ }
164
+ }
165
+ if (errorCodes.length > 0) {
166
+ validateUniqueNames(errorCodes.map((item) => item.code), 'app.errorCodes', errors);
167
+ }
168
+ if (responseContract) {
169
+ if (actions.includes('list') && !responseContract.listSchema) {
170
+ errors.push('[app] list 动作要求声明 responseContract.listSchema');
171
+ }
172
+ if (actions.includes('detail') && !responseContract.detailSchema) {
173
+ errors.push('[app] detail 动作要求声明 responseContract.detailSchema');
174
+ }
175
+ }
176
+ if (permissionProfile) {
177
+ for (const item of permissionProfile.actionPolicies || []) {
178
+ if (!actions.includes(item.action)) {
179
+ errors.push(`[app] permissionProfile.actionPolicies.action=${item.action} 未在 app.actions 中声明`);
180
+ }
181
+ }
182
+ }
183
+ if (validationRules.length > 0) {
184
+ validateUniqueNames(validationRules.map((item) => item.name), 'app.validationRules', errors);
185
+ for (const rule of validationRules) {
186
+ for (const fieldName of rule.fields || []) {
187
+ if (!rootFieldNames.has(fieldName) && !systemFields.has(fieldName)) {
188
+ errors.push(`[app] validationRules(${rule.name}) 引用了不存在的字段: ${fieldName}`);
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ for (const field of fields) {
195
+ if (field.type === 'enum' && (!Array.isArray(field.options) || field.options.length === 0)) {
196
+ errors.push(`[data] 枚举字段 ${field.name} 缺少 options`);
197
+ }
198
+ }
199
+
200
+ for (const child of children) {
201
+ for (const field of child.fields || []) {
202
+ if (field.type === 'enum' && (!Array.isArray(field.options) || field.options.length === 0)) {
203
+ errors.push(`[data] 子表 ${child.name} 中的枚举字段 ${field.name} 缺少 options`);
204
+ }
205
+ }
206
+ if (!child.foreignKey) {
207
+ errors.push(`[data] 子表 ${child.name} 缺少 foreignKey`);
208
+ }
209
+ }
210
+
211
+ if (fields.length < 3) {
212
+ warnings.push('[data] 当前主表字段较少,作为业务试点对象可能过于简单');
213
+ }
214
+ if (!responseContract) {
215
+ warnings.push('[app] 当前模型未声明 responseContract,后续接 IDE 入口时建议补齐接口返回约定');
216
+ }
217
+ if (!permissionProfile) {
218
+ warnings.push('[app] 当前模型未声明 permissionProfile,后续接 IDE 入口时建议补齐权限占位');
219
+ }
220
+
221
+ return { errors, warnings };
222
+ }
223
+
224
+ function validateTableName(tableName, label, reservedTables, reservedPrefixes, errors) {
225
+ if (!tableName) {
226
+ errors.push(`[boundary] ${label} 不能为空`);
227
+ return;
228
+ }
229
+
230
+ if (reservedTables.has(tableName)) {
231
+ errors.push(`[boundary] ${label}=${tableName} 属于系统保留表`);
232
+ return;
233
+ }
234
+
235
+ for (const prefix of reservedPrefixes) {
236
+ if (tableName.startsWith(prefix)) {
237
+ errors.push(`[boundary] ${label}=${tableName} 使用了系统保留前缀 ${prefix}`);
238
+ }
239
+ }
240
+ }
241
+
242
+ function validateUniqueNames(names, label, errors) {
243
+ const seen = new Set();
244
+ for (const name of names) {
245
+ if (seen.has(name)) {
246
+ errors.push(`[data] ${label} 存在重复名称: ${name}`);
247
+ continue;
248
+ }
249
+ seen.add(name);
250
+ }
251
+ }
252
+
253
+ function validateModelFile(specPath, runtimeConfigOrOptions = {}) {
254
+ const runtimeConfig = runtimeConfigOrOptions?.rules
255
+ ? runtimeConfigOrOptions
256
+ : loadRuntimeConfig(runtimeConfigOrOptions);
257
+ const schemaPath = resolveRulePath(runtimeConfig, 'businessModelSchema');
258
+ const boundariesPath = resolveRulePath(runtimeConfig, 'extensionBoundaries');
259
+ const governancePath = resolveRulePath(runtimeConfig, 'specGovernance');
260
+ const { absolutePath, model } = loadSpec(specPath);
261
+ const schemaResult = validateSchema(model, schemaPath);
262
+ const boundaryResult = validateBusinessRules(model, readJson(boundariesPath));
263
+ const locationResult = validateSpecLocation(absolutePath, model, readJson(governancePath), runtimeConfig);
264
+ return {
265
+ absolutePath,
266
+ model,
267
+ schemaErrors: schemaResult.errors,
268
+ boundaryErrors: boundaryResult.errors,
269
+ governanceErrors: locationResult.errors,
270
+ warnings: [...boundaryResult.warnings, ...locationResult.warnings],
271
+ location: locationResult.location,
272
+ valid: schemaResult.errors.length === 0
273
+ && boundaryResult.errors.length === 0
274
+ && locationResult.errors.length === 0,
275
+ };
276
+ }
277
+
278
+ function validateSpecLocation(absolutePath, model, governance, runtimeConfig) {
279
+ const location = classifySpecLocation(absolutePath, governance, runtimeConfig);
280
+ const errors = [];
281
+ const warnings = [];
282
+ const meta = model.meta || {};
283
+ const domain = String(meta.domain || '');
284
+ const object = String(meta.object || '');
285
+ const description = String(meta.description || '').trim();
286
+
287
+ if (location.scope === 'unknown') {
288
+ warnings.push('[governance] 当前模型文件不在受管 spec 目录下,后续不建议作为模板或项目资产沉淀');
289
+ return { location, errors, warnings };
290
+ }
291
+
292
+ if (!location.policy.allowCustomDomain && domain === 'custom') {
293
+ errors.push(`[governance] ${location.scope} 范围不允许使用 custom domain`);
294
+ }
295
+
296
+ if (location.policy.requireDescription && !description) {
297
+ warnings.push(`[governance] ${location.scope} 范围建议补充 meta.description,便于后续模板治理`);
298
+ }
299
+
300
+ if (location.policy.forbidSampleObjectSuffix && /sample$/i.test(object)) {
301
+ errors.push(`[governance] ${location.scope} 范围不应保留 *Sample 这类临时对象名`);
302
+ }
303
+
304
+ if (location.policy.requirePattern && !meta.pattern) {
305
+ errors.push(`[governance] ${location.scope} 范围必须声明 meta.pattern`);
306
+ }
307
+
308
+ return { location, errors, warnings };
309
+ }
310
+
311
+ function classifySpecLocation(absolutePath, governance, runtimeConfig) {
312
+ const root = runtimeConfig?.scaffoldRoot || SCAFFOLD_ROOT;
313
+ const relativePath = path.relative(root, absolutePath).replace(/\\/g, '/');
314
+ const scopes = governance.scopes || {};
315
+ const orderedScopes = Object.entries(scopes).sort((a, b) => {
316
+ const aLen = Math.max(...(a[1].roots || ['']).map((item) => item.length));
317
+ const bLen = Math.max(...(b[1].roots || ['']).map((item) => item.length));
318
+ return bLen - aLen;
319
+ });
320
+
321
+ for (const [scope, policy] of orderedScopes) {
322
+ const roots = Array.isArray(policy.roots) ? policy.roots : [];
323
+ if (roots.some((root) => relativePath.startsWith(root))) {
324
+ return { scope, relativePath, policy };
325
+ }
326
+ }
327
+
328
+ return { scope: 'unknown', relativePath, policy: {} };
329
+ }
330
+
331
+ function splitWords(value) {
332
+ return String(value)
333
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
334
+ .replace(/[^a-zA-Z0-9]+/g, ' ')
335
+ .trim()
336
+ .split(/\s+/)
337
+ .filter(Boolean);
338
+ }
339
+
340
+ function pascalCase(value) {
341
+ return splitWords(value)
342
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
343
+ .join('');
344
+ }
345
+
346
+ function camelCase(value) {
347
+ const text = pascalCase(value);
348
+ return text ? text.charAt(0).toLowerCase() + text.slice(1) : text;
349
+ }
350
+
351
+ function kebabCase(value) {
352
+ return splitWords(value).map((part) => part.toLowerCase()).join('-');
353
+ }
354
+
355
+ function snakeCase(value) {
356
+ return splitWords(value).map((part) => part.toLowerCase()).join('_');
357
+ }
358
+
359
+ function ensureDir(dirPath) {
360
+ fs.mkdirSync(dirPath, { recursive: true });
361
+ }
362
+
363
+ function writeGeneratedFile(filePath, content, options) {
364
+ const { force = false, dryRun = false } = options || {};
365
+ if (fs.existsSync(filePath) && !force) {
366
+ return { status: 'skipped', filePath };
367
+ }
368
+
369
+ if (!dryRun) {
370
+ ensureDir(path.dirname(filePath));
371
+ fs.writeFileSync(filePath, content, 'utf8');
372
+ }
373
+
374
+ return { status: dryRun ? 'planned' : 'written', filePath };
375
+ }
376
+
377
+ function buildContext(model) {
378
+ const meta = model.meta;
379
+ const data = model.data;
380
+ const ui = model.ui;
381
+ const app = model.app;
382
+ const objectPascal = pascalCase(meta.object);
383
+ const objectCamel = camelCase(meta.object);
384
+ const objectKebab = kebabCase(meta.object);
385
+ const domainKebab = kebabCase(meta.domain);
386
+ const domainUpper = String(meta.domain).toUpperCase();
387
+ const moduleSlug = `${domainKebab}-${objectKebab}`;
388
+ const routePath = `${domainKebab}-${objectKebab}`;
389
+ const apiBasePath = `/${domainKebab}/${objectKebab}`;
390
+ const pattern = meta.pattern || '';
391
+
392
+ const rootFields = normalizeFields(data.fields || []);
393
+ const childTables = (data.children || []).map((item) => ({
394
+ ...item,
395
+ className: `${objectPascal}${pascalCase(item.name)}`,
396
+ variableName: camelCase(item.name),
397
+ fileName: `${objectKebab}-${kebabCase(item.name)}`,
398
+ fields: normalizeFields(item.fields || []),
399
+ }));
400
+
401
+ return {
402
+ meta,
403
+ data,
404
+ app,
405
+ ui,
406
+ moduleSlug,
407
+ routePath,
408
+ apiBasePath,
409
+ objectPascal,
410
+ objectCamel,
411
+ objectKebab,
412
+ domainKebab,
413
+ domainUpper,
414
+ pattern,
415
+ label: meta.label,
416
+ fields: rootFields,
417
+ childTables,
418
+ hasAction(action) {
419
+ return Array.isArray(app.actions) && app.actions.includes(action);
420
+ },
421
+ };
422
+ }
423
+
424
+ function normalizeFields(fields) {
425
+ return fields.map((field) => ({
426
+ ...field,
427
+ column: field.column || snakeCase(field.name),
428
+ tsType: mapToTsType(field.type),
429
+ sqlType: mapToSqlType(field.type),
430
+ }));
431
+ }
432
+
433
+ function mapToTsType(type) {
434
+ switch (type) {
435
+ case 'boolean':
436
+ return 'boolean';
437
+ case 'integer':
438
+ case 'number':
439
+ case 'decimal':
440
+ return 'number';
441
+ case 'date':
442
+ case 'datetime':
443
+ return 'Date';
444
+ default:
445
+ return 'string';
446
+ }
447
+ }
448
+
449
+ function mapToSqlType(type) {
450
+ switch (type) {
451
+ case 'text':
452
+ return 'text';
453
+ case 'boolean':
454
+ return 'integer';
455
+ case 'integer':
456
+ return 'integer';
457
+ case 'number':
458
+ case 'decimal':
459
+ return 'decimal(18,2)';
460
+ case 'date':
461
+ case 'datetime':
462
+ return 'datetime';
463
+ default:
464
+ return 'varchar(255)';
465
+ }
466
+ }
467
+
468
+ module.exports = {
469
+ SCAFFOLD_ROOT,
470
+ buildContext,
471
+ camelCase,
472
+ ensureDir,
473
+ kebabCase,
474
+ loadRuntimeConfig,
475
+ loadSpec,
476
+ pascalCase,
477
+ readJson,
478
+ resolveScaffoldPath,
479
+ snakeCase,
480
+ validateModelFile,
481
+ writeGeneratedFile,
482
+ };
@@ -0,0 +1,166 @@
1
+ const PATTERN_CONFIG = {
2
+ singleEntity: {
3
+ icon: 'Package',
4
+ listSubtitle: '标准单实体页面,适合台账、档案、基础资料类对象。',
5
+ formSubtitle: (context) => `补齐${context.label}基础字段`,
6
+ detailSubtitle: (context) => `${context.label}基础信息详情`,
7
+ listTitle: (context) => `${context.label}台账`,
8
+ createLabel: (context) => `新建${context.label}`,
9
+ searchPlaceholder: (context, fieldLabel) => `搜索${fieldLabel}或编码...`,
10
+ fieldPriority: {
11
+ code: ['materialCode', 'warehouseCode', 'supplierCode', 'productCode', 'employeeCode', 'customerCode'],
12
+ title: ['materialName', 'warehouseName', 'supplierName', 'productName', 'employeeName', 'customerName'],
13
+ search: ['materialName', 'materialCode', 'customerName', 'customerCode'],
14
+ secondary: ['materialType', 'materialGroup', 'baseUnit', 'status'],
15
+ header: ['materialCode', 'materialType', 'materialGroup', 'baseUnit', 'status'],
16
+ },
17
+ },
18
+ masterDetail: {
19
+ icon: 'Building2',
20
+ listSubtitle: '标准主子表页面,适合主表带一张子表的对象。',
21
+ formSubtitle: (context) => `补齐${context.label}主数据与子表信息`,
22
+ detailSubtitle: (context) => `${context.label}主数据与联系人详情`,
23
+ listTitle: (context) => `${context.label}管理`,
24
+ createLabel: (context) => `新建${context.label}`,
25
+ searchPlaceholder: (context, fieldLabel) => `搜索${fieldLabel}或联系人相关信息...`,
26
+ fieldPriority: {
27
+ code: ['customerCode', 'supplierCode', 'partnerCode'],
28
+ title: ['customerName', 'supplierName', 'partnerName'],
29
+ search: ['customerName', 'customerCode', 'supplierName', 'supplierCode'],
30
+ secondary: ['ownerName', 'source', 'status'],
31
+ header: ['customerCode', 'ownerName', 'source', 'status'],
32
+ },
33
+ },
34
+ statusEntity: {
35
+ icon: (context) => (context.objectKebab.includes('lead') ? 'Target' : 'BriefcaseBusiness'),
36
+ listSubtitle: '标准状态对象页面,聚焦阶段、状态与负责人协同。',
37
+ formSubtitle: (context) => `补齐${context.label}过程字段与状态信息`,
38
+ detailSubtitle: (context) => `${context.label}过程对象详情`,
39
+ listTitle: (context) => `${context.label}管理`,
40
+ createLabel: (context) => `新建${context.label}`,
41
+ searchPlaceholder: (context, fieldLabel) => `搜索${fieldLabel}、阶段或客户...`,
42
+ fieldPriority: {
43
+ code: ['leadCode', 'opportunityCode', 'quoteCode', 'contractCode'],
44
+ title: ['leadName', 'opportunityName', 'quoteName', 'contractName'],
45
+ search: ['leadName', 'opportunityName', 'quoteName', 'leadCode', 'opportunityCode', 'quoteCode'],
46
+ secondary: ['ownerName', 'customerName', 'stage', 'source', 'status'],
47
+ header: ['leadCode', 'opportunityCode', 'quoteCode', 'customerName', 'ownerName', 'stage', 'status'],
48
+ },
49
+ },
50
+ documentLines: {
51
+ icon: 'FileSpreadsheet',
52
+ listSubtitle: '标准单据对象页面,适合头表加行项目明细的对象。',
53
+ formSubtitle: (context) => `补齐${context.label}头信息与明细行`,
54
+ detailSubtitle: (context) => `${context.label}单据与明细详情`,
55
+ listTitle: (context) => `${context.label}单据`,
56
+ createLabel: (context) => `新建${context.label}`,
57
+ searchPlaceholder: (context, fieldLabel) => `搜索${fieldLabel}、业务日期或状态...`,
58
+ fieldPriority: {
59
+ code: ['documentNo', 'orderNo', 'requestNo', 'billNo'],
60
+ title: ['documentTitle', 'orderTitle', 'requestTitle', 'billTitle'],
61
+ search: ['documentNo', 'documentTitle', 'orderNo', 'orderTitle'],
62
+ secondary: ['bizDate', 'status', 'ownerName'],
63
+ header: ['documentNo', 'bizDate', 'ownerName', 'status'],
64
+ },
65
+ },
66
+ treeEntity: {
67
+ icon: 'Folders',
68
+ listSubtitle: '标准树形对象页面,适合组织、类目、科目等层级结构。',
69
+ formSubtitle: (context) => `补齐${context.label}层级字段`,
70
+ detailSubtitle: (context) => `${context.label}层级详情`,
71
+ listTitle: (context) => `${context.label}树`,
72
+ createLabel: (context) => `新建${context.label}`,
73
+ searchPlaceholder: (context, fieldLabel) => `搜索${fieldLabel}或层级节点...`,
74
+ fieldPriority: {
75
+ code: ['categoryCode', 'deptCode', 'subjectCode', 'code'],
76
+ title: ['categoryName', 'deptName', 'subjectName', 'name'],
77
+ search: ['categoryName', 'deptName', 'subjectName', 'name', 'categoryCode', 'deptCode', 'subjectCode', 'code'],
78
+ secondary: ['parentName', 'status'],
79
+ header: ['code', 'parentName', 'status'],
80
+ },
81
+ },
82
+ };
83
+
84
+ function getPatternId(context) {
85
+ return String(context.pattern || '');
86
+ }
87
+
88
+ function getPatternConfig(context) {
89
+ return PATTERN_CONFIG[getPatternId(context)] || null;
90
+ }
91
+
92
+ function isPattern(context, pattern) {
93
+ return getPatternId(context) === pattern;
94
+ }
95
+
96
+ function resolvePatternValue(context, key, fallback) {
97
+ const config = getPatternConfig(context);
98
+ const value = config?.[key];
99
+ if (typeof value === 'function') {
100
+ return value(context);
101
+ }
102
+ return value || fallback;
103
+ }
104
+
105
+ function getAdminHeaderIcon(context) {
106
+ const icon = resolvePatternValue(context, 'icon', '');
107
+ if (icon) {
108
+ return icon;
109
+ }
110
+ if (context.objectKebab.includes('lead')) {
111
+ return 'Target';
112
+ }
113
+ if (context.objectKebab.includes('customer')) {
114
+ return 'Building2';
115
+ }
116
+ if (context.objectKebab.includes('opportunity')) {
117
+ return 'BriefcaseBusiness';
118
+ }
119
+ return 'Building2';
120
+ }
121
+
122
+ function getPatternListSubtitle(context) {
123
+ return resolvePatternValue(context, 'listSubtitle', '由业务模型生成的标准对象页面,已接入真实列表与基础操作。');
124
+ }
125
+
126
+ function getPatternFormSubtitle(context) {
127
+ return resolvePatternValue(context, 'formSubtitle', `补齐${context.label}主数据`);
128
+ }
129
+
130
+ function getPatternDetailSubtitle(context) {
131
+ return resolvePatternValue(context, 'detailSubtitle', `${context.label}主数据详情`);
132
+ }
133
+
134
+ function getPatternListTitle(context) {
135
+ return resolvePatternValue(context, 'listTitle', `${context.label}管理`);
136
+ }
137
+
138
+ function getPatternCreateLabel(context) {
139
+ return resolvePatternValue(context, 'createLabel', `新建${context.label}`);
140
+ }
141
+
142
+ function getPatternSearchPlaceholder(context, fieldLabel) {
143
+ const config = getPatternConfig(context);
144
+ const value = config?.searchPlaceholder;
145
+ if (typeof value === 'function') {
146
+ return value(context, fieldLabel);
147
+ }
148
+ return value || `搜索${fieldLabel}...`;
149
+ }
150
+
151
+ function getPatternFieldPriority(context, slot) {
152
+ return getPatternConfig(context)?.fieldPriority?.[slot] || [];
153
+ }
154
+
155
+ module.exports = {
156
+ getAdminHeaderIcon,
157
+ getPatternCreateLabel,
158
+ getPatternDetailSubtitle,
159
+ getPatternFieldPriority,
160
+ getPatternFormSubtitle,
161
+ getPatternId,
162
+ getPatternListSubtitle,
163
+ getPatternListTitle,
164
+ getPatternSearchPlaceholder,
165
+ isPattern,
166
+ };