@mingfuyun/star-allinone 0.3.2 → 0.4.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.
- package/README.md +35 -75
- package/dist/components/CustomDrawer/index.d.ts +2 -0
- package/dist/components/CustomDrawer/index.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.js +1 -1
- package/dist/tracking/index.d.ts +60 -0
- package/dist/tracking/index.d.ts.map +1 -0
- package/examples/AGENTS.md +14 -2
- package/package.json +4 -2
- package/scripts/cli.js +28 -1
- package/scripts/gen/doctor.js +293 -0
- package/scripts/gen/fields.js +118 -0
- package/scripts/gen/fixtures/run.js +78 -10
- package/scripts/gen/fixtures/widgets.openapi.json +2 -1
- package/scripts/gen/page.js +194 -43
- package/skills/README.md +16 -3
- package/skills/shared/design-to-page/SKILL.md +3 -3
- package/skills/shared/design-to-page/references/list-page-template.md +13 -3
- package/skills/shared/design-to-page/references/overlays.md +6 -0
- package/skills/shared/design-to-page/references/page-conventions.md +2 -1
- package/skills/shared/project-conventions/references/architecture.md +5 -2
- package/skills/shared/project-conventions/references/component-contract.md +38 -0
- package/skills/shared/project-conventions/references/mfprotable.md +59 -0
- package/skills/shared/project-conventions/references/quality-checklist.md +14 -0
- package/skills/shared/project-conventions/references/review-checklist.md +2 -2
- package/skills/shared/publish-test/SKILL.md +10 -0
- package/skills/shared/review-and-submit/references/ai-review.md +5 -1
- package/skills/shared/review-and-submit/references/verify.md +2 -0
package/scripts/gen/page.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const { toPascal
|
|
3
|
+
const { toPascal } = require('./names');
|
|
4
4
|
const { generateService } = require('./service');
|
|
5
5
|
const { loadProject, writeFile } = require('./project');
|
|
6
|
+
const {
|
|
7
|
+
schemaFields,
|
|
8
|
+
formComponent,
|
|
9
|
+
columnValueType,
|
|
10
|
+
fallbackFields,
|
|
11
|
+
SKIP_FORM,
|
|
12
|
+
querySearchNames,
|
|
13
|
+
} = require('./fields');
|
|
6
14
|
|
|
7
15
|
function detectLayout(root) {
|
|
8
16
|
const hasPageContainer = fs.existsSync(path.join(root, 'src/components/PageContainer/index.tsx'));
|
|
@@ -28,30 +36,39 @@ function featureNames(options) {
|
|
|
28
36
|
};
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
function searchFieldNames(fields) {
|
|
40
|
+
return fields
|
|
41
|
+
.filter((field) => field.name !== 'id' && (field.type === 'string' || !field.type))
|
|
42
|
+
.slice(0, 2)
|
|
43
|
+
.map((field) => field.name);
|
|
35
44
|
}
|
|
36
45
|
|
|
37
|
-
function emitColumns(names, itemType, fields, permissions) {
|
|
38
|
-
const
|
|
39
|
-
|
|
46
|
+
function emitColumns(names, itemType, fields, permissions, queryNames) {
|
|
47
|
+
const columnFields = fields.length ? fields : [{ name: 'id', title: 'ID', type: 'string' }];
|
|
48
|
+
const searchable = new Set(
|
|
49
|
+
queryNames?.length
|
|
50
|
+
? columnFields.filter((field) => queryNames.includes(field.name)).map((field) => field.name)
|
|
51
|
+
: searchFieldNames(columnFields),
|
|
52
|
+
);
|
|
53
|
+
const columns = columnFields.map((field) => {
|
|
54
|
+
const search = searchable.has(field.name);
|
|
40
55
|
return ` {
|
|
41
|
-
title:
|
|
42
|
-
dataIndex: '${field}',
|
|
43
|
-
key: '${field}',
|
|
56
|
+
title: ${JSON.stringify(field.title)},
|
|
57
|
+
dataIndex: '${field.name}',
|
|
58
|
+
key: '${field.name}',
|
|
44
59
|
hideInSearch: ${search ? 'false' : 'true'},
|
|
45
|
-
ellipsis: true
|
|
46
|
-
render: (_, record) => record.${field} ?? '-',
|
|
60
|
+
ellipsis: true,${valueTypeBlock(field)}${enumBlock(field)}
|
|
61
|
+
render: (_, record) => record.${field.name} ?? '-',
|
|
47
62
|
}`;
|
|
48
63
|
});
|
|
49
|
-
|
|
64
|
+
for (const name of queryNames || []) {
|
|
65
|
+
if (columnFields.some((field) => field.name === name)) continue;
|
|
50
66
|
columns.push(` {
|
|
51
|
-
title:
|
|
52
|
-
dataIndex: '
|
|
53
|
-
key: '
|
|
54
|
-
|
|
67
|
+
title: ${JSON.stringify(name)},
|
|
68
|
+
dataIndex: '${name}',
|
|
69
|
+
key: '${name}',
|
|
70
|
+
hideInTable: true,
|
|
71
|
+
hideInSearch: false,
|
|
55
72
|
}`);
|
|
56
73
|
}
|
|
57
74
|
const permCreate = permissions.create ? ` permission: '${permissions.create}',\n` : '';
|
|
@@ -105,14 +122,47 @@ ${permCreate} onClick: handleCreate,
|
|
|
105
122
|
`;
|
|
106
123
|
}
|
|
107
124
|
|
|
108
|
-
function
|
|
109
|
-
const
|
|
125
|
+
function valueTypeBlock(field) {
|
|
126
|
+
const valueType = columnValueType(field);
|
|
127
|
+
return valueType ? `\n valueType: '${valueType}',` : '';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function enumBlock(field) {
|
|
131
|
+
if (field.enum?.length) {
|
|
132
|
+
const entries = field.enum
|
|
133
|
+
.map((value) => ` ${JSON.stringify(String(value))}: { text: ${JSON.stringify(String(value))} }`)
|
|
134
|
+
.join(',\n');
|
|
135
|
+
return `\n valueEnum: {\n${entries},\n },`;
|
|
136
|
+
}
|
|
137
|
+
if (field.type === 'boolean') {
|
|
138
|
+
return `\n valueEnum: { true: { text: '是' }, false: { text: '否' } },`;
|
|
139
|
+
}
|
|
140
|
+
return '';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function emitContainerIndex() {
|
|
144
|
+
return `import Container from '@/components/Page/Container';
|
|
145
|
+
|
|
146
|
+
export default () => {
|
|
147
|
+
return <Container />;
|
|
148
|
+
};
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function emitIndex(names, layout, types, fns, pagination, variant = 'page') {
|
|
153
|
+
const isList = variant === 'list';
|
|
154
|
+
const usePageContainer = layout.hasPageContainer && !layout.hasContainerConfig && !isList;
|
|
155
|
+
const pageImport = usePageContainer
|
|
110
156
|
? `import PageContainer from '@/components/PageContainer';\n`
|
|
111
157
|
: '';
|
|
112
|
-
const
|
|
158
|
+
const overlayDir = isList ? './' : './components/';
|
|
159
|
+
const constImport = isList ? '../const' : './const';
|
|
160
|
+
const stylesImport = isList ? '../index.less' : './index.less';
|
|
161
|
+
const exportName = isList ? 'List' : names.leaf;
|
|
162
|
+
const open = usePageContainer
|
|
113
163
|
? ` <PageContainer header={{ title: undefined }}>`
|
|
114
164
|
: ' <>';
|
|
115
|
-
const close =
|
|
165
|
+
const close = usePageContainer ? ` </PageContainer>` : ' </>';
|
|
116
166
|
const listAccess = pagination === 'results-count'
|
|
117
167
|
? 'list: data?.results || [],\n total: data?.count || 0,'
|
|
118
168
|
: 'list: data?.list || [],\n total: data?.total || 0,';
|
|
@@ -130,14 +180,14 @@ import { message } from 'antd';
|
|
|
130
180
|
import { useCallback, useRef } from 'react';
|
|
131
181
|
import ${names.confirm}, {
|
|
132
182
|
type ${names.confirm}Ref,
|
|
133
|
-
} from '
|
|
183
|
+
} from '${overlayDir}${names.confirm}';
|
|
134
184
|
import ${names.drawer}, {
|
|
135
185
|
type ${names.drawer}Ref,
|
|
136
|
-
} from '
|
|
137
|
-
import { columns, getActionColumn, getToolBarRender } from '
|
|
138
|
-
import styles from '
|
|
186
|
+
} from '${overlayDir}${names.drawer}';
|
|
187
|
+
import { columns, getActionColumn, getToolBarRender } from '${constImport}';
|
|
188
|
+
import styles from '${stylesImport}';
|
|
139
189
|
|
|
140
|
-
export default function ${
|
|
190
|
+
export default function ${exportName}() {
|
|
141
191
|
const tableRef = useRef<MFTableRef>(null);
|
|
142
192
|
const drawerRef = useRef<${names.drawer}Ref>(null);
|
|
143
193
|
const confirmRef = useRef<${names.confirm}Ref>(null);
|
|
@@ -224,8 +274,62 @@ function emitLess(layout) {
|
|
|
224
274
|
`;
|
|
225
275
|
}
|
|
226
276
|
|
|
227
|
-
function
|
|
277
|
+
function emitFormItem(field) {
|
|
278
|
+
const component = formComponent(field);
|
|
279
|
+
const label = JSON.stringify(field.title);
|
|
280
|
+
const requiredVerb = component === 'ProFormSelect' ? '请选择' : '请输入';
|
|
281
|
+
const required = field.required
|
|
282
|
+
? `\n rules={[{ required: true, message: ${JSON.stringify(`${requiredVerb}${field.title}`)} }]}`
|
|
283
|
+
: '';
|
|
284
|
+
if (component === 'ProFormSelect') {
|
|
285
|
+
const options = (field.enum || [])
|
|
286
|
+
.map((value) => `{ label: ${JSON.stringify(String(value))}, value: ${JSON.stringify(value)} }`)
|
|
287
|
+
.join(', ');
|
|
288
|
+
return ` <ProFormSelect
|
|
289
|
+
name="${field.name}"
|
|
290
|
+
label={${label}}
|
|
291
|
+
options={[${options}]}${required}
|
|
292
|
+
/>`;
|
|
293
|
+
}
|
|
294
|
+
if (component === 'ProFormSwitch') {
|
|
295
|
+
return ` <ProFormSwitch
|
|
296
|
+
name="${field.name}"
|
|
297
|
+
label={${label}}
|
|
298
|
+
/>`;
|
|
299
|
+
}
|
|
300
|
+
if (component === 'ProFormDigit') {
|
|
301
|
+
return ` <ProFormDigit
|
|
302
|
+
name="${field.name}"
|
|
303
|
+
label={${label}}
|
|
304
|
+
placeholder={${JSON.stringify(`请输入${field.title}`)}}${required}
|
|
305
|
+
/>`;
|
|
306
|
+
}
|
|
307
|
+
if (component === 'ProFormDateTimePicker' || component === 'ProFormDatePicker') {
|
|
308
|
+
return ` <${component}
|
|
309
|
+
name="${field.name}"
|
|
310
|
+
label={${label}}${required}
|
|
311
|
+
/>`;
|
|
312
|
+
}
|
|
313
|
+
if (component === 'ProFormTextArea') {
|
|
314
|
+
return ` <ProFormTextArea
|
|
315
|
+
name="${field.name}"
|
|
316
|
+
label={${label}}
|
|
317
|
+
placeholder={${JSON.stringify(`请输入${field.title}`)}}${required}
|
|
318
|
+
/>`;
|
|
319
|
+
}
|
|
320
|
+
return ` <ProFormText
|
|
321
|
+
name="${field.name}"
|
|
322
|
+
label={${label}}
|
|
323
|
+
placeholder={${JSON.stringify(`请输入${field.title}`)}}${required}
|
|
324
|
+
/>`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function emitDrawer(names, types, fns, formFields) {
|
|
228
328
|
const itemType = types.itemType || 'Record<string, unknown>';
|
|
329
|
+
const fields = formFields.length
|
|
330
|
+
? formFields
|
|
331
|
+
: [{ name: 'name', title: '名称', required: true, type: 'string' }];
|
|
332
|
+
const components = ['ProForm', ...new Set(fields.map(formComponent))];
|
|
229
333
|
const createCall = fns.create
|
|
230
334
|
? ` await Service.${fns.create}(values);`
|
|
231
335
|
: ' // TODO: call create service';
|
|
@@ -233,7 +337,7 @@ function emitDrawer(names, types, fns) {
|
|
|
233
337
|
? ` await Service.${fns.update}(record.id, values);`
|
|
234
338
|
: ' // TODO: call update service';
|
|
235
339
|
return `import Service from '@/services';
|
|
236
|
-
import {
|
|
340
|
+
import { ${components.join(', ')} } from '@ant-design/pro-components';
|
|
237
341
|
import { CustomDrawer, HocModal } from '@mingfuyun/star-allinone';
|
|
238
342
|
import { Form, message } from 'antd';
|
|
239
343
|
import { useEffect, useState } from 'react';
|
|
@@ -303,12 +407,7 @@ ${createCall}
|
|
|
303
407
|
layout="horizontal"
|
|
304
408
|
submitter={false}
|
|
305
409
|
>
|
|
306
|
-
|
|
307
|
-
name="name"
|
|
308
|
-
label="名称"
|
|
309
|
-
placeholder="请输入名称"
|
|
310
|
-
rules={[{ required: true, message: '请输入名称' }]}
|
|
311
|
-
/>
|
|
410
|
+
${fields.map(emitFormItem).join('\n')}
|
|
312
411
|
</ProForm>
|
|
313
412
|
</CustomDrawer>
|
|
314
413
|
);
|
|
@@ -407,6 +506,41 @@ function patchRoute(root, names, dryRun) {
|
|
|
407
506
|
writeFile(file, next, dryRun);
|
|
408
507
|
}
|
|
409
508
|
|
|
509
|
+
function patchContainerConfig(root, names, dryRun) {
|
|
510
|
+
const file = path.join(root, 'src/components/Page/Container/config.tsx');
|
|
511
|
+
if (!fs.existsSync(file)) return false;
|
|
512
|
+
let current = fs.readFileSync(file, 'utf8');
|
|
513
|
+
const importPath = `@/pages/${names.featurePath}/components/List`;
|
|
514
|
+
const hasRoute = current.includes(`'${names.route}'`) || current.includes(`"${names.route}"`);
|
|
515
|
+
const hasLoader =
|
|
516
|
+
current.includes(`import('${importPath}')`) || current.includes(`import("${importPath}")`);
|
|
517
|
+
if (hasLoader && hasRoute) {
|
|
518
|
+
console.log(`tab container already has ${names.route}`);
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
if (!hasRoute) {
|
|
522
|
+
const entry = ` '${names.route}': [
|
|
523
|
+
{
|
|
524
|
+
key: 'tab-${names.leaf}',
|
|
525
|
+
label: ${JSON.stringify(names.title)},
|
|
526
|
+
loader: () => import('${importPath}'),
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
`;
|
|
530
|
+
if (/export default \{\r?\n/.test(current)) {
|
|
531
|
+
current = current.replace(/export default \{\r?\n/, (match) => `${match}${entry}`);
|
|
532
|
+
} else if (current.includes('export default {')) {
|
|
533
|
+
current = current.replace('export default {', `export default {\n${entry}`);
|
|
534
|
+
} else {
|
|
535
|
+
console.log('Could not patch src/components/Page/Container/config.tsx; register the tab manually.');
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
writeFile(file, current, dryRun);
|
|
540
|
+
console.log(`${dryRun ? '[dry-run] ' : ''}registered tab ${names.route} in src/components/Page/Container/config.tsx`);
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
|
|
410
544
|
function generatePage(cwd, options) {
|
|
411
545
|
const operations = [
|
|
412
546
|
['list', options.listOperation],
|
|
@@ -429,6 +563,9 @@ function generatePage(cwd, options) {
|
|
|
429
563
|
path.join(pageDir, 'components', `${names.drawer}.tsx`),
|
|
430
564
|
path.join(pageDir, 'components', `${names.confirm}.tsx`),
|
|
431
565
|
];
|
|
566
|
+
if (layout.hasContainerConfig) {
|
|
567
|
+
files.push(path.join(pageDir, 'components', 'List.tsx'));
|
|
568
|
+
}
|
|
432
569
|
const exists = files.filter((file) => fs.existsSync(file));
|
|
433
570
|
if (exists.length) {
|
|
434
571
|
throw new Error(`Refusing to overwrite existing page files:\n${exists.map((file) => path.relative(project.root, file)).join('\n')}`);
|
|
@@ -438,6 +575,7 @@ function generatePage(cwd, options) {
|
|
|
438
575
|
let types = { itemType: `${toPascal(names.leaf)}_API.${names.leaf}Item`, queryType: undefined };
|
|
439
576
|
let pagination = 'list-total';
|
|
440
577
|
let itemSource = '';
|
|
578
|
+
const operationByKey = {};
|
|
441
579
|
|
|
442
580
|
for (const [key, selector] of operations) {
|
|
443
581
|
const result = generateService(cwd, {
|
|
@@ -446,6 +584,7 @@ function generatePage(cwd, options) {
|
|
|
446
584
|
mockFile: false,
|
|
447
585
|
});
|
|
448
586
|
fns[key] = result.fn;
|
|
587
|
+
operationByKey[key] = result.operation;
|
|
449
588
|
if (key === 'list') {
|
|
450
589
|
types = {
|
|
451
590
|
itemType: result.itemType || types.itemType,
|
|
@@ -459,25 +598,37 @@ function generatePage(cwd, options) {
|
|
|
459
598
|
}
|
|
460
599
|
}
|
|
461
600
|
|
|
462
|
-
const
|
|
601
|
+
const spec = project.spec;
|
|
602
|
+
let fields = schemaFields(spec, operationByKey.list?.itemSchema, { limit: 8 });
|
|
603
|
+
if (!fields.length) fields = fallbackFields(itemSource);
|
|
604
|
+
const formFields = schemaFields(
|
|
605
|
+
spec,
|
|
606
|
+
operationByKey.create?.bodySchema || operationByKey.update?.bodySchema || operationByKey.list?.itemSchema,
|
|
607
|
+
{ skip: SKIP_FORM, forForm: true },
|
|
608
|
+
);
|
|
463
609
|
const permissions = {
|
|
464
610
|
create: options.permissionCreate,
|
|
465
611
|
edit: options.permissionEdit,
|
|
466
612
|
delete: options.permissionDelete,
|
|
467
613
|
};
|
|
468
614
|
|
|
469
|
-
|
|
615
|
+
const queryNames = querySearchNames(operationByKey.list?.queryParams);
|
|
470
616
|
writeFile(files[1], emitLess(layout), project.dryRun);
|
|
471
|
-
writeFile(files[2], emitColumns(names, types.itemType, fields, permissions), project.dryRun);
|
|
472
|
-
writeFile(files[3], emitDrawer(names, types, fns), project.dryRun);
|
|
617
|
+
writeFile(files[2], emitColumns(names, types.itemType, fields, permissions, queryNames), project.dryRun);
|
|
618
|
+
writeFile(files[3], emitDrawer(names, types, fns, formFields), project.dryRun);
|
|
473
619
|
writeFile(files[4], emitConfirm(names), project.dryRun);
|
|
474
|
-
patchRoute(project.root, names, project.dryRun);
|
|
475
|
-
|
|
476
620
|
if (layout.hasContainerConfig) {
|
|
477
|
-
|
|
621
|
+
writeFile(files[0], emitContainerIndex(), project.dryRun);
|
|
622
|
+
writeFile(files[5], emitIndex(names, layout, types, fns, pagination, 'list'), project.dryRun);
|
|
623
|
+
patchRoute(project.root, names, project.dryRun);
|
|
624
|
+
patchContainerConfig(project.root, names, project.dryRun);
|
|
625
|
+
} else {
|
|
626
|
+
writeFile(files[0], emitIndex(names, layout, types, fns, pagination), project.dryRun);
|
|
627
|
+
patchRoute(project.root, names, project.dryRun);
|
|
478
628
|
}
|
|
629
|
+
|
|
479
630
|
console.log(`${project.dryRun ? '[dry-run] ' : ''}generated page ${names.route} -> src/pages/${names.featurePath}`);
|
|
480
631
|
return names;
|
|
481
632
|
}
|
|
482
633
|
|
|
483
|
-
module.exports = { generatePage, featureNames };
|
|
634
|
+
module.exports = { generatePage, featureNames, patchContainerConfig };
|
package/skills/README.md
CHANGED
|
@@ -46,11 +46,13 @@ Cursor / Codex 的常驻说明只放仓库根 `AGENTS.md`(模板见 `examples/
|
|
|
46
46
|
├── design-to-page/ # 页面;标准列表内部跑 gen page
|
|
47
47
|
├── review-and-submit/ # 审查并提交;步骤在 references/
|
|
48
48
|
├── publish-test/ # 测发
|
|
49
|
-
├── project-conventions/ # architecture / review-checklist / quality-checklist
|
|
49
|
+
├── project-conventions/ # architecture / component-contract / mfprotable / review-checklist / quality-checklist
|
|
50
50
|
└── i-have-adhd/ # 用户输出格式
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
公共入口写死两边已对齐的硬规则,例如:页面走 `MFRequest`、DTO 放 namespace typings
|
|
53
|
+
公共入口写死两边已对齐的硬规则,例如:页面走 `MFRequest`、DTO 放 namespace typings、表格用业务仓 `@/components/CommonTable/MFProTable`(不要从包里找 `MFTablePro`)、`ProForm`、弹层从 `@mingfuyun/star-allinone` 导入 `BaseModal` / `CustomDrawer` / `HocModal`、`await` 必须 try/catch、禁止无说明的 `any`、React 18。
|
|
54
|
+
|
|
55
|
+
接入或升级后跑 `pnpm exec star-allinone doctor`。
|
|
54
56
|
|
|
55
57
|
业务项目的 `.agents/project/` 只写差异:路径、权限体系、OpenAPI 位置、领域特例。项目文件可以补充,但不能削弱公共硬规则。
|
|
56
58
|
|
|
@@ -70,6 +72,7 @@ npm 包中的 skills/shared
|
|
|
70
72
|
|
|
71
73
|
```bash
|
|
72
74
|
pnpm exec star-allinone skills sync
|
|
75
|
+
pnpm exec star-allinone doctor
|
|
73
76
|
```
|
|
74
77
|
|
|
75
78
|
同步会整体替换 `shared-*` 目录,并删除包中已不存在的过期 `shared-*`,但不会改动 `custom-*`。
|
|
@@ -132,7 +135,7 @@ git commit -m "chore(skill): 更新公共 skill"
|
|
|
132
135
|
| `shared-design-to-page` | SKILL + `page-conventions.md`、`overlays.md`、`list-page-template.md` | 页面分层、标准列表 CLI、弹层合同 |
|
|
133
136
|
| `shared-review-and-submit` | SKILL + `impact-analysis.md`、`verify.md`、`ai-review.md`、`commit.md` | 影响分析 → 验证 → 确认后提交 |
|
|
134
137
|
| `shared-publish-test` | SKILL + `scripts/publish-current-branch.sh` | 云效测发;只读 `publishPipelineId` |
|
|
135
|
-
| (非入口)`shared-project-conventions` | `architecture.md`、`review-checklist.md`、`quality-checklist.md` |
|
|
138
|
+
| (非入口)`shared-project-conventions` | `architecture.md`、`mfprotable.md`、`component-contract.md`、`review-checklist.md`、`quality-checklist.md` | 架构、导入合同、质量清单 |
|
|
136
139
|
| (非入口)`shared-i-have-adhd` | `i-have-adhd.md` | 用户输出格式 |
|
|
137
140
|
|
|
138
141
|
以下内容**不**写进公共 skill 源码:
|
|
@@ -141,6 +144,16 @@ git commit -m "chore(skill): 更新公共 skill"
|
|
|
141
144
|
- OpenAPI 快照:业务仓库数据文件
|
|
142
145
|
- 真正的业务特例:权限常量名、IP 校验、设备/节点领域规则
|
|
143
146
|
|
|
147
|
+
## 云效门禁
|
|
148
|
+
|
|
149
|
+
业务仓通常没有 pipeline YAML。在云效构建控制台显式增加:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pnpm typecheck && pnpm lint
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
再跑现有 `pnpm build`。`pnpm test` 若实际是 `max dev`(测网开发服务),改名为 `start:test` / `dev:test`,避免 Agent 当成单测。
|
|
156
|
+
|
|
144
157
|
## 最终推荐流程
|
|
145
158
|
|
|
146
159
|
```text
|
|
@@ -48,8 +48,8 @@ pnpm exec star-allinone gen page \
|
|
|
48
48
|
Add `--permission-create`, `--permission-edit`, `--permission-delete` when codes are known.
|
|
49
49
|
|
|
50
50
|
2. If only services are missing, run `pnpm exec star-allinone gen service --operation "METHOD /path/"` first. Follow `.agents/skills/shared-api-to-service/SKILL.md`.
|
|
51
|
-
3.
|
|
52
|
-
4. If `src/components/Page/Container/config.tsx` exists,
|
|
51
|
+
3. The CLI fills columns and drawer fields from the OpenAPI schema (required, enum → `ProFormSelect`, dates/numbers). Review labels and permission codes; do not recreate the file tree. See [references/list-page-template.md](references/list-page-template.md).
|
|
52
|
+
4. If `src/components/Page/Container/config.tsx` exists, `gen page` writes a Container `index.tsx`, puts the table in `components/List.tsx`, and registers a Tab with `loader: () => import(...)`. Do not invent a second page shell.
|
|
53
53
|
5. Do not overwrite existing page files. Tell the user the CLI refused, then edit in place.
|
|
54
54
|
|
|
55
55
|
## Design path
|
|
@@ -67,7 +67,7 @@ For non-standard layouts:
|
|
|
67
67
|
## Hard rules
|
|
68
68
|
|
|
69
69
|
- React function components, TypeScript, Ant Design 5, Pro Components, Less.
|
|
70
|
-
- `ProForm` for business forms. `MFProTable`
|
|
70
|
+
- `ProForm` for business forms. `MFProTable` from `@/components/CommonTable/MFProTable` (not from the npm package).
|
|
71
71
|
- Overlays from `@mingfuyun/star-allinone`: `CustomDrawer`, `BaseModal`, `HocModal`.
|
|
72
72
|
- Pages consume `import Service from '@/services'` only.
|
|
73
73
|
- Keep columns and toolbar builders in `const.tsx`. Empty cells render as `-`.
|
|
@@ -13,9 +13,11 @@ src/pages/<Feature>/
|
|
|
13
13
|
└── <Feature>ConfirmModal.tsx
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
When `src/components/Page/Container/config.tsx` exists, `index.tsx` is a Container shell and the table lives in `components/List.tsx`. The CLI also registers a tab for the new route.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
## index.tsx / List.tsx
|
|
19
|
+
|
|
20
|
+
- Wrap with `PageContainer` when `src/components/PageContainer` exists and the page is not a Container tab.
|
|
19
21
|
- `fetchData` is a `useCallback` that calls `Service.<listFn>`, maps pagination, and returns `{ code, data: { list, total } }`.
|
|
20
22
|
- Open the drawer with `drawerRef.current?.show({ record })`.
|
|
21
23
|
- Destructive actions go through the confirm modal ref, then `tableRef.current?.reload()`.
|
|
@@ -23,6 +25,7 @@ src/pages/<Feature>/
|
|
|
23
25
|
## const.tsx
|
|
24
26
|
|
|
25
27
|
- Export `columns`, `getActionColumn`, `getToolBarRender`.
|
|
28
|
+
- Columns come from the list item schema: types, enums, and the first string fields as search.
|
|
26
29
|
- Put permission strings on actions and toolbar items when the project uses them.
|
|
27
30
|
- Render empty cells as `'-'`.
|
|
28
31
|
|
|
@@ -30,6 +33,7 @@ src/pages/<Feature>/
|
|
|
30
33
|
|
|
31
34
|
- `CustomDrawer` + `HocModal`.
|
|
32
35
|
- `ProForm` with `submitter={false}` and `layout="horizontal"`.
|
|
36
|
+
- Fields come from the create/update body schema: required rules, `enum` → `ProFormSelect`, numbers → `ProFormDigit`, dates → date pickers.
|
|
33
37
|
- `validateFields()` in `try`. Create vs update from `record`.
|
|
34
38
|
- Reset fields when `visible` becomes true.
|
|
35
39
|
|
|
@@ -41,4 +45,10 @@ src/pages/<Feature>/
|
|
|
41
45
|
|
|
42
46
|
## Container tabs
|
|
43
47
|
|
|
44
|
-
If the project uses `src/components/Page/Container/config.tsx`, the
|
|
48
|
+
If the project uses `src/components/Page/Container/config.tsx`, the CLI:
|
|
49
|
+
|
|
50
|
+
1. Writes `index.tsx` as `import Container from '@/components/Page/Container'`.
|
|
51
|
+
2. Writes the table in `components/List.tsx`.
|
|
52
|
+
3. Registers `'<route>': [{ key, label, loader: () => import('.../List') }]` in `config.tsx`. Do not add a static page import or `children: <List />`.
|
|
53
|
+
|
|
54
|
+
Do not duplicate fetch logic inside Container. Do not wrap the List with a second `PageContainer`.
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Use this reference for every modal, confirmation dialog, and drawer. Import `@mingfuyun/star-allinone/dist/styles.css` once from the application entry, not from feature components.
|
|
4
4
|
|
|
5
|
+
Overlays belong to the npm package. Do not add `src/components/CustomDrawer`, `src/components/BaseModal`, or `src/components/HocModal`.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { CustomDrawer, BaseModal, HocModal } from '@mingfuyun/star-allinone';
|
|
9
|
+
```
|
|
10
|
+
|
|
5
11
|
## Required components
|
|
6
12
|
|
|
7
13
|
- `CustomDrawer` for create, edit, detail, or configuration drawers.
|
|
@@ -31,7 +31,8 @@ Styles live in the current page directory. Reuse shared mixins such as `@/styles
|
|
|
31
31
|
|
|
32
32
|
## Tables
|
|
33
33
|
|
|
34
|
-
- Prefer `MFProTable` for management lists. Use Ant Design `Table` only for a deliberately simple static shell.
|
|
34
|
+
- Prefer `MFProTable` for management lists. Import it from `@/components/CommonTable/MFProTable`. See [mfprotable.md](../../project-conventions/references/mfprotable.md). Use Ant Design `Table` only for a deliberately simple static shell.
|
|
35
|
+
- Do not import `MFTablePro` or any table from `@mingfuyun/star-allinone`.
|
|
35
36
|
- Extract `fetchData` as an `async` method. Do not inline request logic in JSX props.
|
|
36
37
|
- Wrap `await` in `try-catch` and return a table-safe empty shape on failure.
|
|
37
38
|
- Adapt backend pagination to the table contract. Common backend shapes are `{ results, count }` or `{ list, total }`; the table adapter is typically `{ code, data: { list, total } }`. Follow the nearest page.
|
|
@@ -8,10 +8,11 @@ Apply these rules to every non-trivial frontend change. Shared review, commit, m
|
|
|
8
8
|
- Use React function components and TypeScript.
|
|
9
9
|
- Put transport code in `src/services`. Pages must not call axios or the request helper directly.
|
|
10
10
|
- Keep API DTOs in the domain `typings.d.ts`. Use named types and adapters instead of `any`.
|
|
11
|
-
- Reuse `MFProTable
|
|
11
|
+
- Reuse `MFProTable` from `@/components/CommonTable/MFProTable` (see [component-contract.md](./component-contract.md) and [mfprotable.md](./mfprotable.md)). It is not exported by `@mingfuyun/star-allinone`.
|
|
12
|
+
- Overlays (`CustomDrawer`, `BaseModal`, `HocModal`) and `ErrorBoundary` / `Cron` come from `@mingfuyun/star-allinone`. Do not keep local copies under `src/components`.
|
|
12
13
|
- Use `@/` imports for application modules and keep feature-local imports relative.
|
|
13
14
|
- Do not add a UI/state library or duplicate an existing component without explicit approval.
|
|
14
|
-
- Prefer feature-level code splitting for Excel, editors, charts, drag-and-drop, and other heavy modules.
|
|
15
|
+
- Prefer feature-level code splitting for Excel, editors, charts, drag-and-drop, and other heavy modules. `xlsx`, `@uiw/react-codemirror` / `@codemirror` / monaco, and chart libraries must use `import()`. Do not static-import them from `src/app.ts` or generated list pages.
|
|
15
16
|
|
|
16
17
|
## Business behavior
|
|
17
18
|
|
|
@@ -43,4 +44,6 @@ Prefer `pnpm typecheck` when that script exists.
|
|
|
43
44
|
|
|
44
45
|
Run `pnpm lint` for broad or shared changes. Run `pnpm run build` when routes, entry behavior, shared runtime, or multiple page modules change. If `.star-allinone.json` documents a TypeScript `baseUrl` or build script, use that command.
|
|
45
46
|
|
|
47
|
+
Yunxiao (and any CI without in-repo pipeline YAML) must run `pnpm typecheck && pnpm lint` as an explicit build step. `pnpm test` / `pnpm start:test` in business apps starts the test-env dev server; it is not a unit-test suite.
|
|
48
|
+
|
|
46
49
|
Distinguish failures introduced by the current change from pre-existing or environment failures.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Component contract
|
|
2
|
+
|
|
3
|
+
Use this file as the import source of truth. Do not invent package exports.
|
|
4
|
+
|
|
5
|
+
## From `@mingfuyun/star-allinone`
|
|
6
|
+
|
|
7
|
+
The npm package ships overlays, Cron, and ErrorBoundary. It does **not** export a table.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
BaseModal,
|
|
12
|
+
Cron,
|
|
13
|
+
CustomDrawer,
|
|
14
|
+
ErrorBoundary,
|
|
15
|
+
HocModal,
|
|
16
|
+
} from '@mingfuyun/star-allinone';
|
|
17
|
+
import '@mingfuyun/star-allinone/dist/styles.css';
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Do not write any of the following:
|
|
21
|
+
|
|
22
|
+
- `import { MFTablePro } from '@mingfuyun/star-allinone'`
|
|
23
|
+
- `import { MFProTable } from '@mingfuyun/star-allinone'`
|
|
24
|
+
- `@mingfuyun/star-allinone/components/BaseModal` (use the main export)
|
|
25
|
+
|
|
26
|
+
## From the business app
|
|
27
|
+
|
|
28
|
+
Management lists use the local wrapper. Props and usage are in [mfprotable.md](./mfprotable.md).
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import MFProTable from '@/components/CommonTable/MFProTable';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Pages still consume APIs with `import Service from '@/services'`. Overlay shells come from the package above, not from `@/components/CustomDrawer` or `@/components/BaseModal`.
|
|
35
|
+
|
|
36
|
+
## Runtime
|
|
37
|
+
|
|
38
|
+
Peer React is **18.3.x**. Do not assume React 19 APIs. Ant Design 5 and Pro Components live in the business app.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# MFProTable contract
|
|
2
|
+
|
|
3
|
+
`MFProTable` lives in the **business app**, not in `@mingfuyun/star-allinone`. Do not invent `MFTablePro` and do not import a table from the npm package.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import MFProTable from '@/components/CommonTable/MFProTable';
|
|
7
|
+
import type { MFTableRef } from '@/components/CommonTable/type';
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The capability package will not absorb this component until peer/externals/Umi versions are stable across apps.
|
|
11
|
+
|
|
12
|
+
## Required usage
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
<MFProTable<Item, Query>
|
|
16
|
+
ref={tableRef}
|
|
17
|
+
actionColumn={{
|
|
18
|
+
actions: getActionColumn({ handleEdit, handleDelete }),
|
|
19
|
+
}}
|
|
20
|
+
bordered
|
|
21
|
+
columns={columns()}
|
|
22
|
+
fetchData={fetchData}
|
|
23
|
+
queryParams={{}}
|
|
24
|
+
rowKey="id"
|
|
25
|
+
scroll={{ x: 'max-content' }}
|
|
26
|
+
topTitle={false}
|
|
27
|
+
toolBarRender={getToolBarRender({ handleCreate })}
|
|
28
|
+
/>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- `fetchData` is an `async` callback. On failure return `{ code: 0, data: { list: [], total: 0 } }`.
|
|
32
|
+
- Adapt backend `{ results, count }` or `{ list, total }` to `{ code, data: { list, total } }`.
|
|
33
|
+
- Put columns, action builders, and toolbar items in `const.tsx`.
|
|
34
|
+
- Empty display values render as `'-'`.
|
|
35
|
+
- Open overlays through refs. Do not store drawer visibility on the page when `HocModal` owns it.
|
|
36
|
+
|
|
37
|
+
## Common props
|
|
38
|
+
|
|
39
|
+
These names match `src/components/CommonTable/type.ts` (`ITableProps` / `MFTableRef`):
|
|
40
|
+
|
|
41
|
+
| Prop | Role |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `columns` | `ProColumns<T>[]` from `const.tsx` |
|
|
44
|
+
| `fetchData` | `(params) => Promise<{ code, data: { list, total } }>` |
|
|
45
|
+
| `queryParams` | Extra query merged into requests |
|
|
46
|
+
| `rowKey` | Stable id field, usually `id` |
|
|
47
|
+
| `actionColumn.actions` | Row actions; `permission` when the project uses it |
|
|
48
|
+
| `toolBarRender` | Toolbar buttons (`TableSearchType[]`) |
|
|
49
|
+
| `scroll` | Wide tables: `{ x: 'max-content' }` |
|
|
50
|
+
| `columnsStateKey` | Persist column state via the project hook |
|
|
51
|
+
| `ref.reload` / `ref.reloadAndReset` | After mutations |
|
|
52
|
+
|
|
53
|
+
Permission filtering is implemented **in the app** (`useAuthFilter`, `withAuth`, `TableActions`). The package overlays do not wrap table permissions.
|
|
54
|
+
|
|
55
|
+
## Do not
|
|
56
|
+
|
|
57
|
+
- `import { MFTablePro } from '@mingfuyun/star-allinone'`
|
|
58
|
+
- `import { MFProTable } from '@mingfuyun/star-allinone'`
|
|
59
|
+
- Copy `MFProTable` into a second path and import both.
|
|
@@ -17,3 +17,17 @@ Check unsafe HTML, URL construction, upload/download names, sensitive logs/stora
|
|
|
17
17
|
## Testing
|
|
18
18
|
|
|
19
19
|
Request tests for concrete regression boundaries such as transformations, races, conditional validation, reusable hooks, complex transitions, or likely recurring bugs. Run focused Prettier, TypeScript, lint, `git diff --check`, and a production build in proportion to risk.
|
|
20
|
+
|
|
21
|
+
## CI / Yunxiao
|
|
22
|
+
|
|
23
|
+
These apps do not keep pipeline YAML in the repo. In the Yunxiao build step run:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pnpm typecheck && pnpm lint
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`pnpm test` is not a unit-test command in the business apps (use `start:test` / `dev:test` for the test-env dev server). `@mingfuyun/star-allinone` `npm test` only covers codegen fixtures.
|
|
30
|
+
|
|
31
|
+
## Performance
|
|
32
|
+
|
|
33
|
+
Flag new top-level imports of `xlsx`, `@uiw/react-codemirror` / CodeMirror, chart libraries, and similar heavy modules. They must be `import()` inside the feature that needs them. Generated pages must not pull them into `src/app.ts`.
|
|
@@ -19,8 +19,8 @@ This checklist is complete for shared review and commit. If `.agents/project/rev
|
|
|
19
19
|
|
|
20
20
|
## Pages and forms
|
|
21
21
|
|
|
22
|
-
- Management lists prefer `MFProTable`. Column and toolbar configuration belong outside the render function.
|
|
23
|
-
- Business forms use `ProForm`. Drawers use `CustomDrawer`. Dialogs use `BaseModal` / `HocModal`.
|
|
22
|
+
- Management lists prefer `MFProTable` from `@/components/CommonTable/MFProTable`. Column and toolbar configuration belong outside the render function.
|
|
23
|
+
- Business forms use `ProForm`. Drawers use `CustomDrawer`. Dialogs use `BaseModal` / `HocModal` from `@mingfuyun/star-allinone`.
|
|
24
24
|
- Successful mutations refresh the parent list. Closing an overlay clears form, loading, selected record, and other async state.
|
|
25
25
|
- Every `await` has error handling. Loading restores in `finally`.
|
|
26
26
|
- Check loading, empty, error, disabled, long content, and narrow-screen layout.
|
|
@@ -41,6 +41,16 @@ Read `.star-allinone.json` before running. `publishPipelineId` is required:
|
|
|
41
41
|
|
|
42
42
|
Never ask the user to repeat the pipeline ID. Never copy another project's pipeline ID.
|
|
43
43
|
|
|
44
|
+
## Yunxiao build gates
|
|
45
|
+
|
|
46
|
+
The pipeline lives in the Yunxiao console (there is no in-repo YAML). The build step must run:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pnpm typecheck && pnpm lint
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
before or as part of `max build`. Do not treat `pnpm test` as unit tests; in business apps that script was the test-env dev server and is now `start:test` / `dev:test`. `ANALYZE=1` is a manual pre-release chunk check, not a pipeline substitute.
|
|
53
|
+
|
|
44
54
|
## Preconditions
|
|
45
55
|
|
|
46
56
|
- Current HEAD is a named branch, not detached.
|