@mingfuyun/star-allinone 0.3.1 → 0.3.3
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/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 +75 -10
- package/scripts/gen/fixtures/widgets.openapi.json +2 -1
- package/scripts/gen/page.js +202 -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
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const { deref } = require('./spec');
|
|
2
|
+
|
|
3
|
+
const SKIP_COLUMNS = new Set([
|
|
4
|
+
'created_at',
|
|
5
|
+
'updated_at',
|
|
6
|
+
'createTime',
|
|
7
|
+
'updateTime',
|
|
8
|
+
'deleted_at',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const SKIP_FORM = new Set([
|
|
12
|
+
'id',
|
|
13
|
+
'pk',
|
|
14
|
+
'created_at',
|
|
15
|
+
'updated_at',
|
|
16
|
+
'createTime',
|
|
17
|
+
'updateTime',
|
|
18
|
+
'deleted_at',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const SKIP_SEARCH = new Set([
|
|
22
|
+
'page',
|
|
23
|
+
'page_size',
|
|
24
|
+
'pageSize',
|
|
25
|
+
'current',
|
|
26
|
+
'limit',
|
|
27
|
+
'offset',
|
|
28
|
+
'ordering',
|
|
29
|
+
'sort',
|
|
30
|
+
'sort_by',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function schemaType(schema) {
|
|
34
|
+
if (!schema) return undefined;
|
|
35
|
+
if (schema.enum?.length) return 'string';
|
|
36
|
+
const types = Array.isArray(schema.type)
|
|
37
|
+
? schema.type.filter((item) => item !== 'null')
|
|
38
|
+
: [schema.type];
|
|
39
|
+
return types[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fieldFromSchema(spec, name, schema, required) {
|
|
43
|
+
const resolved = deref(spec, schema) || {};
|
|
44
|
+
return {
|
|
45
|
+
name,
|
|
46
|
+
title: resolved.title || name,
|
|
47
|
+
required: Boolean(required),
|
|
48
|
+
type: schemaType(resolved),
|
|
49
|
+
format: resolved.format,
|
|
50
|
+
enum: Array.isArray(resolved.enum) ? resolved.enum : undefined,
|
|
51
|
+
readOnly: Boolean(resolved.readOnly),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function schemaFields(spec, schema, options = {}) {
|
|
56
|
+
if (!spec || !schema) return [];
|
|
57
|
+
const skip = options.skip || SKIP_COLUMNS;
|
|
58
|
+
const resolved = deref(spec, schema);
|
|
59
|
+
const properties = resolved?.properties || {};
|
|
60
|
+
const required = new Set(resolved?.required || []);
|
|
61
|
+
const fields = [];
|
|
62
|
+
for (const [name, prop] of Object.entries(properties)) {
|
|
63
|
+
if (skip.has(name)) continue;
|
|
64
|
+
const field = fieldFromSchema(spec, name, prop, required.has(name));
|
|
65
|
+
if (options.forForm && field.readOnly) continue;
|
|
66
|
+
fields.push(field);
|
|
67
|
+
}
|
|
68
|
+
if (typeof options.limit === 'number') return fields.slice(0, options.limit);
|
|
69
|
+
return fields;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formComponent(field) {
|
|
73
|
+
if (field.enum?.length) return 'ProFormSelect';
|
|
74
|
+
if (field.type === 'boolean') return 'ProFormSwitch';
|
|
75
|
+
if (field.type === 'integer' || field.type === 'number') return 'ProFormDigit';
|
|
76
|
+
if (field.format === 'date-time') return 'ProFormDateTimePicker';
|
|
77
|
+
if (field.format === 'date') return 'ProFormDatePicker';
|
|
78
|
+
if (/desc|note|remark|comment|content|message/i.test(field.name)) return 'ProFormTextArea';
|
|
79
|
+
return 'ProFormText';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function columnValueType(field) {
|
|
83
|
+
if (field.enum?.length) return 'select';
|
|
84
|
+
if (field.type === 'boolean') return 'select';
|
|
85
|
+
if (field.type === 'integer' || field.type === 'number') return 'digit';
|
|
86
|
+
if (field.format === 'date-time') return 'dateTime';
|
|
87
|
+
if (field.format === 'date') return 'date';
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function fallbackFields(itemTypeSource = '') {
|
|
92
|
+
const names = [...itemTypeSource.matchAll(/^\s+(\w+)\??:/gm)]
|
|
93
|
+
.map((match) => match[1])
|
|
94
|
+
.filter((name) => !SKIP_COLUMNS.has(name));
|
|
95
|
+
return names.slice(0, 6).map((name) => ({
|
|
96
|
+
name,
|
|
97
|
+
title: name,
|
|
98
|
+
required: false,
|
|
99
|
+
type: 'string',
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function querySearchNames(queryParams = []) {
|
|
104
|
+
return queryParams
|
|
105
|
+
.map((param) => param?.name)
|
|
106
|
+
.filter((name) => name && !SKIP_SEARCH.has(name));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
SKIP_COLUMNS,
|
|
111
|
+
SKIP_FORM,
|
|
112
|
+
SKIP_SEARCH,
|
|
113
|
+
schemaFields,
|
|
114
|
+
formComponent,
|
|
115
|
+
columnValueType,
|
|
116
|
+
fallbackFields,
|
|
117
|
+
querySearchNames,
|
|
118
|
+
};
|
|
@@ -3,6 +3,7 @@ const os = require('os');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { generateService } = require('../service');
|
|
5
5
|
const { generatePage } = require('../page');
|
|
6
|
+
const { runDoctor } = require('../doctor');
|
|
6
7
|
|
|
7
8
|
function assert(condition, message) {
|
|
8
9
|
if (!condition) throw new Error(message);
|
|
@@ -15,6 +16,7 @@ function makeProject() {
|
|
|
15
16
|
JSON.stringify(
|
|
16
17
|
{
|
|
17
18
|
project: 'fixture',
|
|
19
|
+
publicSkillPrefix: 'shared-',
|
|
18
20
|
serviceDir: 'src/services',
|
|
19
21
|
openapi: 'openapi.json',
|
|
20
22
|
},
|
|
@@ -45,10 +47,41 @@ function makeProject() {
|
|
|
45
47
|
`,
|
|
46
48
|
);
|
|
47
49
|
fs.mkdirSync(path.join(root, 'src/components/CommonTable'), { recursive: true });
|
|
50
|
+
fs.writeFileSync(path.join(root, 'src/components/CommonTable/MFProTable.tsx'), 'export default function MFProTable() { return null; }\n');
|
|
48
51
|
fs.mkdirSync(path.join(root, 'src/pages'), { recursive: true });
|
|
52
|
+
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({
|
|
53
|
+
dependencies: { '@mingfuyun/star-allinone': '^0.3.2' },
|
|
54
|
+
scripts: { typecheck: 'tsc --noEmit' },
|
|
55
|
+
}, null, 2));
|
|
49
56
|
return root;
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
function writeContainerConfig(root) {
|
|
60
|
+
const dir = path.join(root, 'src/components/Page/Container');
|
|
61
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
62
|
+
fs.writeFileSync(
|
|
63
|
+
path.join(dir, 'config.tsx'),
|
|
64
|
+
`export default {
|
|
65
|
+
'/existing': [
|
|
66
|
+
{ key: 'tab-existing', label: 'Existing', children: <div /> },
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function generateWidgetsPage(root, feature = 'Widgets', route = '/widgets') {
|
|
74
|
+
generatePage(root, {
|
|
75
|
+
feature,
|
|
76
|
+
route,
|
|
77
|
+
title: '组件管理',
|
|
78
|
+
listOperation: 'GET /api/v1/widgets/',
|
|
79
|
+
createOperation: 'POST /api/v1/widgets/',
|
|
80
|
+
updateOperation: 'PATCH /api/v1/widgets/{id}/',
|
|
81
|
+
deleteOperation: 'DELETE /api/v1/widgets/{id}/',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
52
85
|
function main() {
|
|
53
86
|
const root = makeProject();
|
|
54
87
|
const first = generateService(root, {
|
|
@@ -78,21 +111,53 @@ function main() {
|
|
|
78
111
|
assert(patched.includes('widgetsPartialUpdate'), patched);
|
|
79
112
|
assert(patched.includes('MFRequest.patch<WIDGETS_API.Widget>'), patched);
|
|
80
113
|
|
|
81
|
-
|
|
82
|
-
feature: 'Widgets',
|
|
83
|
-
route: '/widgets',
|
|
84
|
-
title: '组件管理',
|
|
85
|
-
listOperation: 'GET /api/v1/widgets/',
|
|
86
|
-
createOperation: 'POST /api/v1/widgets/',
|
|
87
|
-
updateOperation: 'PATCH /api/v1/widgets/{id}/',
|
|
88
|
-
deleteOperation: 'DELETE /api/v1/widgets/{id}/',
|
|
89
|
-
});
|
|
114
|
+
generateWidgetsPage(root);
|
|
90
115
|
const page = fs.readFileSync(path.join(root, 'src/pages/Widgets/index.tsx'), 'utf8');
|
|
91
116
|
assert(page.includes('Service.widgetsList'), page);
|
|
92
|
-
assert(page.includes(
|
|
117
|
+
assert(page.includes('rowKey="id"'), page);
|
|
118
|
+
const columns = fs.readFileSync(path.join(root, 'src/pages/Widgets/const.tsx'), 'utf8');
|
|
119
|
+
assert(columns.includes("dataIndex: 'name'"), columns);
|
|
120
|
+
assert(columns.includes("dataIndex: 'kind'"), columns);
|
|
121
|
+
assert(columns.includes("valueType: 'select'"), columns);
|
|
122
|
+
assert(columns.includes("valueType: 'digit'"), columns);
|
|
123
|
+
const drawer = fs.readFileSync(path.join(root, 'src/pages/Widgets/components/WidgetsDrawer.tsx'), 'utf8');
|
|
124
|
+
assert(drawer.includes('ProFormSelect'), drawer);
|
|
125
|
+
assert(drawer.includes("name=\"status\""), drawer);
|
|
126
|
+
assert(drawer.includes("name=\"name\""), drawer);
|
|
127
|
+
assert(drawer.includes('required: true'), drawer);
|
|
93
128
|
const route = fs.readFileSync(path.join(root, 'config/route.ts'), 'utf8');
|
|
94
129
|
assert(route.includes("path: '/widgets'"), route);
|
|
130
|
+
|
|
131
|
+
const tabRoot = makeProject();
|
|
132
|
+
writeContainerConfig(tabRoot);
|
|
133
|
+
generateService(tabRoot, { operation: 'GET /api/v1/widgets/' });
|
|
134
|
+
generateWidgetsPage(tabRoot);
|
|
135
|
+
const shell = fs.readFileSync(path.join(tabRoot, 'src/pages/Widgets/index.tsx'), 'utf8');
|
|
136
|
+
assert(shell.includes("import Container from '@/components/Page/Container'"), shell);
|
|
137
|
+
const list = fs.readFileSync(path.join(tabRoot, 'src/pages/Widgets/components/List.tsx'), 'utf8');
|
|
138
|
+
assert(list.includes('Service.widgetsList'), list);
|
|
139
|
+
const tabConfig = fs.readFileSync(path.join(tabRoot, 'src/components/Page/Container/config.tsx'), 'utf8');
|
|
140
|
+
assert(tabConfig.includes("'/widgets'"), tabConfig);
|
|
141
|
+
assert(tabConfig.includes("@/pages/Widgets/components/List"), tabConfig);
|
|
142
|
+
|
|
143
|
+
const healthy = runDoctor(root);
|
|
144
|
+
assert(healthy.some((item) => item.message.includes('MFProTable')), JSON.stringify(healthy));
|
|
145
|
+
assert(!healthy.some((item) => item.level === 'error' && item.message.includes('MFRequest')), JSON.stringify(healthy));
|
|
146
|
+
|
|
147
|
+
fs.mkdirSync(path.join(root, 'src/pages/Bad'), { recursive: true });
|
|
148
|
+
fs.writeFileSync(
|
|
149
|
+
path.join(root, 'src/pages/Bad/index.tsx'),
|
|
150
|
+
"import { MFTablePro } from '@mingfuyun/star-allinone';\nimport CustomDrawer from '@/components/CustomDrawer';\n",
|
|
151
|
+
);
|
|
152
|
+
fs.mkdirSync(path.join(root, 'src/components/CustomDrawer'), { recursive: true });
|
|
153
|
+
fs.writeFileSync(path.join(root, 'src/components/CustomDrawer/index.tsx'), 'export default function CustomDrawer() { return null; }\n');
|
|
154
|
+
const sick = runDoctor(root);
|
|
155
|
+
assert(sick.some((item) => item.level === 'error' && /MFTablePro/.test(item.message)), JSON.stringify(sick));
|
|
156
|
+
assert(sick.some((item) => item.level === 'error' && /Local overlay/.test(item.message)), JSON.stringify(sick));
|
|
157
|
+
|
|
95
158
|
fs.rmSync(root, { recursive: true, force: true });
|
|
159
|
+
fs.rmSync(tabRoot, { recursive: true, force: true });
|
|
160
|
+
process.exitCode = 0;
|
|
96
161
|
console.log('codegen fixtures passed');
|
|
97
162
|
}
|
|
98
163
|
|
|
@@ -165,6 +165,7 @@
|
|
|
165
165
|
"properties": {
|
|
166
166
|
"id": { "type": "string" },
|
|
167
167
|
"name": { "type": "string" },
|
|
168
|
+
"kind": { "type": "string", "enum": ["gadget", "tool"] },
|
|
168
169
|
"status": { "type": "string", "nullable": true },
|
|
169
170
|
"count": { "type": "integer" }
|
|
170
171
|
}
|
|
@@ -174,7 +175,7 @@
|
|
|
174
175
|
"required": ["name"],
|
|
175
176
|
"properties": {
|
|
176
177
|
"name": { "type": "string" },
|
|
177
|
-
"status": { "type": "string" }
|
|
178
|
+
"status": { "type": "string", "enum": ["active", "inactive"] }
|
|
178
179
|
}
|
|
179
180
|
},
|
|
180
181
|
"WidgetList": {
|
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,49 @@ function patchRoute(root, names, dryRun) {
|
|
|
407
506
|
writeFile(file, next, dryRun);
|
|
408
507
|
}
|
|
409
508
|
|
|
509
|
+
function insertImport(source, line) {
|
|
510
|
+
if (source.includes(line.trim())) return source;
|
|
511
|
+
const matches = [...source.matchAll(/^import .*;\n/gm)];
|
|
512
|
+
if (!matches.length) return `${line}${source}`;
|
|
513
|
+
const last = matches[matches.length - 1];
|
|
514
|
+
const index = last.index + last[0].length;
|
|
515
|
+
return `${source.slice(0, index)}${line}${source.slice(index)}`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function patchContainerConfig(root, names, dryRun) {
|
|
519
|
+
const file = path.join(root, 'src/components/Page/Container/config.tsx');
|
|
520
|
+
if (!fs.existsSync(file)) return false;
|
|
521
|
+
let current = fs.readFileSync(file, 'utf8');
|
|
522
|
+
const importPath = `@/pages/${names.featurePath}/components/List`;
|
|
523
|
+
const hasRoute = current.includes(`'${names.route}'`) || current.includes(`"${names.route}"`);
|
|
524
|
+
if (current.includes(importPath) && hasRoute) {
|
|
525
|
+
console.log(`tab container already has ${names.route}`);
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
current = insertImport(current, `import ${names.leaf} from '${importPath}';\n`);
|
|
529
|
+
if (!hasRoute) {
|
|
530
|
+
const entry = ` '${names.route}': [
|
|
531
|
+
{
|
|
532
|
+
key: 'tab-${names.leaf}',
|
|
533
|
+
label: ${JSON.stringify(names.title)},
|
|
534
|
+
children: <${names.leaf} />,
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
`;
|
|
538
|
+
if (/export default \{\r?\n/.test(current)) {
|
|
539
|
+
current = current.replace(/export default \{\r?\n/, (match) => `${match}${entry}`);
|
|
540
|
+
} else if (current.includes('export default {')) {
|
|
541
|
+
current = current.replace('export default {', `export default {\n${entry}`);
|
|
542
|
+
} else {
|
|
543
|
+
console.log('Could not patch src/components/Page/Container/config.tsx; register the tab manually.');
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
writeFile(file, current, dryRun);
|
|
548
|
+
console.log(`${dryRun ? '[dry-run] ' : ''}registered tab ${names.route} in src/components/Page/Container/config.tsx`);
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
|
|
410
552
|
function generatePage(cwd, options) {
|
|
411
553
|
const operations = [
|
|
412
554
|
['list', options.listOperation],
|
|
@@ -429,6 +571,9 @@ function generatePage(cwd, options) {
|
|
|
429
571
|
path.join(pageDir, 'components', `${names.drawer}.tsx`),
|
|
430
572
|
path.join(pageDir, 'components', `${names.confirm}.tsx`),
|
|
431
573
|
];
|
|
574
|
+
if (layout.hasContainerConfig) {
|
|
575
|
+
files.push(path.join(pageDir, 'components', 'List.tsx'));
|
|
576
|
+
}
|
|
432
577
|
const exists = files.filter((file) => fs.existsSync(file));
|
|
433
578
|
if (exists.length) {
|
|
434
579
|
throw new Error(`Refusing to overwrite existing page files:\n${exists.map((file) => path.relative(project.root, file)).join('\n')}`);
|
|
@@ -438,6 +583,7 @@ function generatePage(cwd, options) {
|
|
|
438
583
|
let types = { itemType: `${toPascal(names.leaf)}_API.${names.leaf}Item`, queryType: undefined };
|
|
439
584
|
let pagination = 'list-total';
|
|
440
585
|
let itemSource = '';
|
|
586
|
+
const operationByKey = {};
|
|
441
587
|
|
|
442
588
|
for (const [key, selector] of operations) {
|
|
443
589
|
const result = generateService(cwd, {
|
|
@@ -446,6 +592,7 @@ function generatePage(cwd, options) {
|
|
|
446
592
|
mockFile: false,
|
|
447
593
|
});
|
|
448
594
|
fns[key] = result.fn;
|
|
595
|
+
operationByKey[key] = result.operation;
|
|
449
596
|
if (key === 'list') {
|
|
450
597
|
types = {
|
|
451
598
|
itemType: result.itemType || types.itemType,
|
|
@@ -459,25 +606,37 @@ function generatePage(cwd, options) {
|
|
|
459
606
|
}
|
|
460
607
|
}
|
|
461
608
|
|
|
462
|
-
const
|
|
609
|
+
const spec = project.spec;
|
|
610
|
+
let fields = schemaFields(spec, operationByKey.list?.itemSchema, { limit: 8 });
|
|
611
|
+
if (!fields.length) fields = fallbackFields(itemSource);
|
|
612
|
+
const formFields = schemaFields(
|
|
613
|
+
spec,
|
|
614
|
+
operationByKey.create?.bodySchema || operationByKey.update?.bodySchema || operationByKey.list?.itemSchema,
|
|
615
|
+
{ skip: SKIP_FORM, forForm: true },
|
|
616
|
+
);
|
|
463
617
|
const permissions = {
|
|
464
618
|
create: options.permissionCreate,
|
|
465
619
|
edit: options.permissionEdit,
|
|
466
620
|
delete: options.permissionDelete,
|
|
467
621
|
};
|
|
468
622
|
|
|
469
|
-
|
|
623
|
+
const queryNames = querySearchNames(operationByKey.list?.queryParams);
|
|
470
624
|
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);
|
|
625
|
+
writeFile(files[2], emitColumns(names, types.itemType, fields, permissions, queryNames), project.dryRun);
|
|
626
|
+
writeFile(files[3], emitDrawer(names, types, fns, formFields), project.dryRun);
|
|
473
627
|
writeFile(files[4], emitConfirm(names), project.dryRun);
|
|
474
|
-
patchRoute(project.root, names, project.dryRun);
|
|
475
|
-
|
|
476
628
|
if (layout.hasContainerConfig) {
|
|
477
|
-
|
|
629
|
+
writeFile(files[0], emitContainerIndex(), project.dryRun);
|
|
630
|
+
writeFile(files[5], emitIndex(names, layout, types, fns, pagination, 'list'), project.dryRun);
|
|
631
|
+
patchRoute(project.root, names, project.dryRun);
|
|
632
|
+
patchContainerConfig(project.root, names, project.dryRun);
|
|
633
|
+
} else {
|
|
634
|
+
writeFile(files[0], emitIndex(names, layout, types, fns, pagination), project.dryRun);
|
|
635
|
+
patchRoute(project.root, names, project.dryRun);
|
|
478
636
|
}
|
|
637
|
+
|
|
479
638
|
console.log(`${project.dryRun ? '[dry-run] ' : ''}generated page ${names.route} -> src/pages/${names.featurePath}`);
|
|
480
639
|
return names;
|
|
481
640
|
}
|
|
482
641
|
|
|
483
|
-
module.exports = { generatePage, featureNames };
|
|
642
|
+
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
|