@zat-design/sisyphus-scene 1.0.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 +44 -0
- package/bin/sisyphus-scene.mjs +81 -0
- package/mcp/sisyphus-react.json +8 -0
- package/package.json +36 -0
- package/scripts/install-mcp.mjs +158 -0
- package/scripts/sync-platforms.mjs +87 -0
- package/skills/sisyphus-scene/SKILL.md +149 -0
- package/skills/sisyphus-scene/agents/openai.yaml +8 -0
- package/skills/sisyphus-scene/assets/templates/scene-registry.json +75 -0
- package/skills/sisyphus-scene/evals/fixtures/invalid-antd-fallback.tsx +5 -0
- package/skills/sisyphus-scene/evals/fixtures/invalid-decisions.json +11 -0
- package/skills/sisyphus-scene/evals/fixtures/valid-decisions.json +21 -0
- package/skills/sisyphus-scene/evals/fixtures/valid-page.tsx +25 -0
- package/skills/sisyphus-scene/evals/generation-cases.yaml +36 -0
- package/skills/sisyphus-scene/evals/trigger-cases.yaml +13 -0
- package/skills/sisyphus-scene/references/approved-examples.yaml +16 -0
- package/skills/sisyphus-scene/references/integration-contracts.md +112 -0
- package/skills/sisyphus-scene/references/platform-claude.md +10 -0
- package/skills/sisyphus-scene/references/platform-codex.md +10 -0
- package/skills/sisyphus-scene/references/platform-cursor.md +10 -0
- package/skills/sisyphus-scene/references/scene-model.md +51 -0
- package/skills/sisyphus-scene/references/validation-rules.md +45 -0
- package/skills/sisyphus-scene/references/workflow.md +94 -0
- package/skills/sisyphus-scene/scripts/inspect-project.mjs +229 -0
- package/skills/sisyphus-scene/scripts/install.mjs +100 -0
- package/skills/sisyphus-scene/scripts/resolve-versions.mjs +204 -0
- package/skills/sisyphus-scene/scripts/validate-scaffold.mjs +158 -0
- package/skills/sisyphus-scene/scripts/validate-skill.mjs +48 -0
- package/skills/sisyphus-scene-legacy/SKILL.md +614 -0
- package/skills/sisyphus-scene-legacy/agents/openai.yaml +7 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sisyphus-scene-legacy
|
|
3
|
+
description: 基于旧版 @zat-design/sisyphus-react v3 与 antd4 规则生成查询列表和新增/编辑抽屉页面。仅当目标项目明确使用 v3 或 antd4,或用户明确要求兼容旧规则时触发;不得用于 Sisyphus 4 或 antd 6 项目。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# sisyphus-query-page
|
|
7
|
+
|
|
8
|
+
生成基于 `@zat-design/sisyphus-react` 的标准查询列表页面,包含完整四层文件结构。
|
|
9
|
+
|
|
10
|
+
## 触发条件
|
|
11
|
+
|
|
12
|
+
用户提供以下任意组合时触发:
|
|
13
|
+
- 查询/搜索字段描述
|
|
14
|
+
- 表格列描述
|
|
15
|
+
- 抽屉/弹框表单字段描述
|
|
16
|
+
- 增删改查接口 URL
|
|
17
|
+
|
|
18
|
+
## 输入收集
|
|
19
|
+
|
|
20
|
+
触发后,首先从用户描述中提取以下信息。缺少必要项时主动追问,可选项缺失则使用默认值:
|
|
21
|
+
|
|
22
|
+
### 必要项
|
|
23
|
+
1. **页面名称**(中文 + 英文,用于文件命名和注释)
|
|
24
|
+
2. **查询字段**:字段名、label、类型
|
|
25
|
+
3. **表格列**:字段名、label、渲染方式
|
|
26
|
+
4. **操作列按钮**:按钮文案、对应 handler 名、动作描述
|
|
27
|
+
5. **主接口**:list / add / update 的 URL 和请求方法
|
|
28
|
+
|
|
29
|
+
### 可选项
|
|
30
|
+
6. **抽屉表单字段**:字段名、label、类型、必填、联动规则(缺省时沿用查询字段生成基础表单)
|
|
31
|
+
7. **是否分组**(ProCollapse):默认不分组;用户说"分组"或"分 XX 区域"时启用
|
|
32
|
+
8. **字典映射字段**:需要用接口映射显示名的字段(如 teamId → teamName)
|
|
33
|
+
9. **数据转换**:提交/回显时需要特殊处理的字段(如数组↔字符串、日期格式)
|
|
34
|
+
10. **额外操作**:除修改外的其他操作(如重置密码、删除、查看详情)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 生成前必须做
|
|
39
|
+
|
|
40
|
+
**检查 package.json 确认版本:**
|
|
41
|
+
|
|
42
|
+
| 特征 | v3 | v4 |
|
|
43
|
+
|------|----|----|
|
|
44
|
+
| `@zat-design/sisyphus-react` | `^3.x` | `^4.x` |
|
|
45
|
+
| antd | `antd4` | `antd` |
|
|
46
|
+
| 列类型 | `ProColumnProps` / `ProTableColumn` | `ProFormColumnType` / `ProTableColumnType` |
|
|
47
|
+
| 日期 | `moment` | `dayjs` |
|
|
48
|
+
|
|
49
|
+
找不到 package.json 则询问用户版本,默认给 v3(当前项目主要版本)。
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 文件结构
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
PageName/
|
|
57
|
+
├── index.tsx # 页面容器
|
|
58
|
+
├── index.less # 样式
|
|
59
|
+
├── propsType.ts # 类型定义
|
|
60
|
+
├── config/
|
|
61
|
+
│ └── index.tsx # 纯配置:查询列、表格列、表单列
|
|
62
|
+
├── services/
|
|
63
|
+
│ └── index.ts # API 调用
|
|
64
|
+
└── utils/
|
|
65
|
+
└── index.ts # 数据转换(仅有特殊转换时生成)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## index.tsx 规范
|
|
71
|
+
|
|
72
|
+
### 区块顺序(严格按此顺序,用注释分隔)
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
1. 导入语句
|
|
76
|
+
2. Props 和基础 Hooks(form 实例等)
|
|
77
|
+
3. 状态和 Refs
|
|
78
|
+
4. 配置和计算值(useMemo,非必要不用)
|
|
79
|
+
5. 事件处理函数
|
|
80
|
+
6. API 和 WebSocket(useAntdTable、useRequest)
|
|
81
|
+
7. 所有 useEffect 集中放置
|
|
82
|
+
8. 返回 JSX
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 状态定义规范
|
|
86
|
+
|
|
87
|
+
**多个状态必须合并为一个 `useSetState`,并用 interface 声明类型:**
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
interface PageState {
|
|
91
|
+
modalVisible: boolean;
|
|
92
|
+
modalMode: 'add' | 'edit';
|
|
93
|
+
currentRecord: XxxRecord | null;
|
|
94
|
+
// 有字典映射时加:
|
|
95
|
+
// dictMaps: { teamIdMap: Record<string, string> };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const [{ modalVisible, modalMode, currentRecord }, setState] = useSetState<PageState>({
|
|
99
|
+
modalVisible: false,
|
|
100
|
+
modalMode: 'add',
|
|
101
|
+
currentRecord: null,
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### useMemo / useCallback 使用原则
|
|
106
|
+
|
|
107
|
+
- **非必要不用**。不要为了"看起来规范"而滥用
|
|
108
|
+
- `getFormColumns()` 无依赖,可用 `useMemo(() => getFormColumns(), [])` 避免每次重算
|
|
109
|
+
- `getTableColumns(handler1, handler2)` 依赖 handler 函数,**不要加 useMemo**(handler 每次渲染都是新引用,useMemo 无效且误导)
|
|
110
|
+
- `getModalFormColumns(currentRecord, form)` 依赖运行时数据,**不加 useMemo**,直接调用
|
|
111
|
+
|
|
112
|
+
### useEffect 依赖项规范
|
|
113
|
+
|
|
114
|
+
- **禁止把函数、form 实例放入依赖数组**
|
|
115
|
+
- 正确写法:`useEffect(() => { ... }, [modalVisible, modalMode, currentRecord])`
|
|
116
|
+
- 错误写法:`useEffect(() => { ... }, [modalVisible, modalForm, refresh])`
|
|
117
|
+
|
|
118
|
+
### 完整示例
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import React, { useEffect } from 'react';
|
|
122
|
+
import { Button, message } from 'antd4';
|
|
123
|
+
import { useRequest, useSetState } from 'ahooks';
|
|
124
|
+
import { ProForm, ProTable, ProDrawerForm } from '@zat-design/sisyphus-react';
|
|
125
|
+
import { getFormColumns, getTableColumns, getModalFormColumns } from './config';
|
|
126
|
+
import * as services from './services';
|
|
127
|
+
import type { XxxRecord } from './propsType';
|
|
128
|
+
|
|
129
|
+
const { useAntdTable } = ProTable;
|
|
130
|
+
|
|
131
|
+
interface PageState {
|
|
132
|
+
modalVisible: boolean;
|
|
133
|
+
modalMode: 'add' | 'edit';
|
|
134
|
+
currentRecord: XxxRecord | null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const XxxPage: React.FC = () => {
|
|
138
|
+
// ========== Props 和基础 Hooks ==========
|
|
139
|
+
const [searchForm] = ProForm.useForm();
|
|
140
|
+
const [modalForm] = ProForm.useForm();
|
|
141
|
+
|
|
142
|
+
// ========== 状态和 Refs ==========
|
|
143
|
+
const [{ modalVisible, modalMode, currentRecord }, setState] = useSetState<PageState>({
|
|
144
|
+
modalVisible: false,
|
|
145
|
+
modalMode: 'add',
|
|
146
|
+
currentRecord: null,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ========== 配置和计算值 ==========
|
|
150
|
+
const formColumns = getFormColumns();
|
|
151
|
+
// getTableColumns 依赖 handler,不加 useMemo
|
|
152
|
+
const tableColumns = getTableColumns(handleEdit, handleXxx);
|
|
153
|
+
const modalFormColumns = getModalFormColumns(currentRecord, modalForm);
|
|
154
|
+
|
|
155
|
+
// ========== 事件处理函数 ==========
|
|
156
|
+
const handleAdd = () => setState({ currentRecord: null, modalMode: 'add', modalVisible: true });
|
|
157
|
+
|
|
158
|
+
const handleEdit = (record: XxxRecord) =>
|
|
159
|
+
setState({ currentRecord: record, modalMode: 'edit', modalVisible: true });
|
|
160
|
+
|
|
161
|
+
const handleModalCancel = () => {
|
|
162
|
+
setState({ modalVisible: false });
|
|
163
|
+
modalForm.resetFields();
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const handleModalOk = async () => {
|
|
167
|
+
try {
|
|
168
|
+
const values = await modalForm.validateFields();
|
|
169
|
+
if (modalMode === 'add') {
|
|
170
|
+
const res = await services.addXxx(values);
|
|
171
|
+
if (res.data.status === 200) {
|
|
172
|
+
message.success('新增成功');
|
|
173
|
+
setState({ modalVisible: false });
|
|
174
|
+
modalForm.resetFields();
|
|
175
|
+
refresh();
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
const res = await services.updateXxx({ ...values, id: currentRecord!.id });
|
|
179
|
+
if (res.data.status === 200) {
|
|
180
|
+
message.success('修改成功');
|
|
181
|
+
setState({ modalVisible: false });
|
|
182
|
+
modalForm.resetFields();
|
|
183
|
+
refresh();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch (error: any) {
|
|
187
|
+
if (error?.errorFields) return; // 表单校验错误
|
|
188
|
+
console.error('提交失败:', error);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// ========== API 和 WebSocket ==========
|
|
193
|
+
const { loading, tableProps, refresh } = useAntdTable(
|
|
194
|
+
async ({ pageNum, pageSize }) => {
|
|
195
|
+
const formValues = searchForm.getFieldsValue();
|
|
196
|
+
const res = await services.getXxxList({ ...formValues, pageNum, pageSize });
|
|
197
|
+
return {
|
|
198
|
+
data: {
|
|
199
|
+
list: res.data.data.list || [],
|
|
200
|
+
total: res.data.data.total || 0,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
{ form: searchForm },
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// 字典加载(有字典映射时)
|
|
208
|
+
// useRequest(services.getXxxOptions, {
|
|
209
|
+
// onSuccess: (res) => {
|
|
210
|
+
// if (res?.data?.status === 200) {
|
|
211
|
+
// const map: Record<string, string> = {};
|
|
212
|
+
// res.data.data.forEach((v: any) => { map[v.id] = v.xxxName; });
|
|
213
|
+
// setState((prev) => ({ dictMaps: { ...prev.dictMaps, xxxMap: map } }));
|
|
214
|
+
// }
|
|
215
|
+
// },
|
|
216
|
+
// });
|
|
217
|
+
|
|
218
|
+
// ========== 所有 useEffect 集中放置 ==========
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (modalVisible && modalMode === 'edit' && currentRecord) {
|
|
221
|
+
modalForm.setFieldsValue(currentRecord);
|
|
222
|
+
} else if (modalVisible && modalMode === 'add') {
|
|
223
|
+
modalForm.resetFields();
|
|
224
|
+
}
|
|
225
|
+
}, [modalVisible, modalMode, currentRecord]); // ⚠️ 不加 modalForm(函数引用)
|
|
226
|
+
|
|
227
|
+
// ========== 返回 JSX ==========
|
|
228
|
+
return (
|
|
229
|
+
<div className="pro-search-container">
|
|
230
|
+
<ProForm
|
|
231
|
+
form={searchForm}
|
|
232
|
+
columns={formColumns}
|
|
233
|
+
onFinish={() => refresh()}
|
|
234
|
+
onCancel={() => { searchForm.resetFields(); refresh(); }}
|
|
235
|
+
/>
|
|
236
|
+
<div style={{ margin: '0 0 12px' }}>
|
|
237
|
+
<Button type="primary" onClick={handleAdd}>新增</Button>
|
|
238
|
+
</div>
|
|
239
|
+
<ProTable
|
|
240
|
+
{...tableProps}
|
|
241
|
+
rowKey="id"
|
|
242
|
+
columns={tableColumns}
|
|
243
|
+
loading={loading}
|
|
244
|
+
scroll={{ x: 'max-content' }}
|
|
245
|
+
/>
|
|
246
|
+
<ProDrawerForm
|
|
247
|
+
title={modalMode === 'add' ? '新增XXX' : '修改XXX'}
|
|
248
|
+
open={modalVisible}
|
|
249
|
+
onOk={handleModalOk}
|
|
250
|
+
onCancel={handleModalCancel}
|
|
251
|
+
width="1200"
|
|
252
|
+
>
|
|
253
|
+
<ProForm form={modalForm} columns={modalFormColumns} footer={false} />
|
|
254
|
+
</ProDrawerForm>
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export default XxxPage;
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## config/index.tsx 规范
|
|
265
|
+
|
|
266
|
+
**原则:config 是纯配置,不含业务状态,动态数据通过参数注入。**
|
|
267
|
+
|
|
268
|
+
### 查询表单 `getFormColumns()`
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
export const getFormColumns = (): ProFormColumnType[] => [
|
|
272
|
+
{ type: 'Input', label: '工号', name: 'query_no' },
|
|
273
|
+
|
|
274
|
+
// ProEnum:枚举下拉,allowClear 必加(查询表单允许清空)
|
|
275
|
+
{ type: 'ProEnum', label: '状态', name: 'onDuty', fieldProps: { code: 'onDuty', allowClear: true } },
|
|
276
|
+
|
|
277
|
+
// Select/Radio 静态选项:用 dataSource(不是 options)
|
|
278
|
+
{
|
|
279
|
+
type: 'Select',
|
|
280
|
+
label: '类型',
|
|
281
|
+
name: 'type',
|
|
282
|
+
fieldProps: { dataSource: [{ label: '启用', value: 'Y' }, { label: '停用', value: 'N' }], allowClear: true },
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
// ProSelect:接口数据下拉
|
|
286
|
+
{
|
|
287
|
+
type: 'ProSelect',
|
|
288
|
+
label: '工作组',
|
|
289
|
+
name: 'teamId',
|
|
290
|
+
fieldProps: {
|
|
291
|
+
allowClear: true,
|
|
292
|
+
fieldNames: { label: 'teamName', value: 'id' },
|
|
293
|
+
useRequest: { service: getTeamOptions },
|
|
294
|
+
transformResponse: (res: any) => res?.data?.status === 200 ? res.data.data || [] : [],
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
];
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### 表格列 `getTableColumns(...handlers)`
|
|
301
|
+
|
|
302
|
+
**操作列绝对不能放在 config 里。** config 只导出 `baseColumns`(数据列),操作列在 `index.tsx` 里组装:
|
|
303
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
// config/index.tsx — 只有数据列,无操作列
|
|
306
|
+
export const getBaseColumns = (): ProTableColumnType[] => [
|
|
307
|
+
{ title: '名称', dataIndex: 'name' },
|
|
308
|
+
{ title: '状态', dataIndex: 'status', valueType: 'enumName', code: 'status' },
|
|
309
|
+
{ title: '更新时间', dataIndex: 'gmtModified', valueType: 'dateTime' },
|
|
310
|
+
];
|
|
311
|
+
|
|
312
|
+
// index.tsx — 在离 ProTable 最近的地方组装操作列
|
|
313
|
+
const getTableColumns = (): ProTableColumnType[] => {
|
|
314
|
+
const baseColumns = getBaseColumns();
|
|
315
|
+
return [
|
|
316
|
+
...baseColumns,
|
|
317
|
+
{
|
|
318
|
+
title: '操作',
|
|
319
|
+
key: 'action',
|
|
320
|
+
fixed: 'right',
|
|
321
|
+
width: 150,
|
|
322
|
+
render: (_: any, record: XxxRecord) => (
|
|
323
|
+
<Space>
|
|
324
|
+
<Button type="link" onClick={() => handleEdit(record)}>编辑</Button>
|
|
325
|
+
{record.enable === 'N' && (
|
|
326
|
+
<Button type="link" danger onClick={() => handleDelete(record)}>删除</Button>
|
|
327
|
+
)}
|
|
328
|
+
</Space>
|
|
329
|
+
),
|
|
330
|
+
},
|
|
331
|
+
];
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// JSX 调用
|
|
335
|
+
<ProTable {...tableProps} rowKey="id" columns={getTableColumns()} scroll={{ x: 'max-content' }} />
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
操作列按钮如果逻辑复杂,可以提取为 `render` 辅助函数放在 `getTableColumns` 上方:
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
const renderStatusAction = (record: XxxRecord) => {
|
|
342
|
+
if (record.status === 'active') return <Button type="link">停用</Button>;
|
|
343
|
+
return <Button type="link">启用</Button>;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const getTableColumns = () => [
|
|
347
|
+
...getBaseColumns(),
|
|
348
|
+
{
|
|
349
|
+
title: '操作',
|
|
350
|
+
key: 'action',
|
|
351
|
+
fixed: 'right',
|
|
352
|
+
width: 150,
|
|
353
|
+
render: (_: any, record: XxxRecord) => (
|
|
354
|
+
<Space>
|
|
355
|
+
<Button type="link" onClick={() => handleEdit(record)}>编辑</Button>
|
|
356
|
+
{renderStatusAction(record)}
|
|
357
|
+
</Space>
|
|
358
|
+
),
|
|
359
|
+
},
|
|
360
|
+
];
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### 抽屉表单 `getModalFormColumns(currentRecord?, form?)`
|
|
364
|
+
|
|
365
|
+
#### 默认模式(平铺,字段少时使用)
|
|
366
|
+
|
|
367
|
+
```tsx
|
|
368
|
+
export const getModalFormColumns = (currentRecord?: XxxRecord | null, form?: any): ProFormColumnType[] => [
|
|
369
|
+
{ type: 'Input', label: '名称', name: 'name', required: true },
|
|
370
|
+
|
|
371
|
+
// ProSelect 多选
|
|
372
|
+
{
|
|
373
|
+
type: 'ProSelect',
|
|
374
|
+
label: '角色',
|
|
375
|
+
name: 'roleId',
|
|
376
|
+
required: true,
|
|
377
|
+
fieldProps: {
|
|
378
|
+
mode: 'multiple',
|
|
379
|
+
fieldNames: { label: 'roleName', value: 'id' },
|
|
380
|
+
useRequest: { service: getRoleOptions },
|
|
381
|
+
transformResponse: (res: any) => res?.data?.status === 200 ? res.data.data || [] : [],
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
// 条件显示
|
|
386
|
+
{
|
|
387
|
+
type: 'DatePicker',
|
|
388
|
+
label: '离职日期',
|
|
389
|
+
name: 'leaveDate',
|
|
390
|
+
valueType: 'date',
|
|
391
|
+
show: (values: any) => values?.onDuty === 2,
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
// 动态必填
|
|
395
|
+
{
|
|
396
|
+
type: 'Input',
|
|
397
|
+
label: '某字段',
|
|
398
|
+
name: 'xxx',
|
|
399
|
+
required: (values: any) => !!values.yyy,
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
// 联动 fieldProps(选项依赖另一字段)
|
|
403
|
+
{
|
|
404
|
+
type: 'ProSelect',
|
|
405
|
+
label: '上级',
|
|
406
|
+
name: 'leaderId',
|
|
407
|
+
show: (values: any) => !!values.dutieId,
|
|
408
|
+
fieldProps: (values: any) => ({
|
|
409
|
+
fieldNames: { label: 'name', value: 'id' },
|
|
410
|
+
useRequest: {
|
|
411
|
+
service: () => getLeaderList(values.dutieId),
|
|
412
|
+
options: { ready: !!values.dutieId, refreshDeps: [values.dutieId] },
|
|
413
|
+
},
|
|
414
|
+
transformResponse: (res: any) => res?.data?.status === 200 ? res.data.data || [] : [],
|
|
415
|
+
}),
|
|
416
|
+
},
|
|
417
|
+
];
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
#### 分组模式(用户明确要求时)
|
|
421
|
+
|
|
422
|
+
```tsx
|
|
423
|
+
export const getModalFormColumns = (currentRecord?: XxxRecord | null, form?: any): ProFormColumnType[] => [
|
|
424
|
+
{
|
|
425
|
+
type: 'ProCollapse',
|
|
426
|
+
fieldProps: {
|
|
427
|
+
title: '基本信息',
|
|
428
|
+
level: '2',
|
|
429
|
+
children: [
|
|
430
|
+
{ type: 'Input', label: '姓名', name: 'name', required: true },
|
|
431
|
+
// ...
|
|
432
|
+
],
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
type: 'ProCollapse',
|
|
437
|
+
fieldProps: {
|
|
438
|
+
title: '服务范围',
|
|
439
|
+
level: '2',
|
|
440
|
+
children: [
|
|
441
|
+
{ type: 'Input', label: '软电账户', name: 'callNo', required: true },
|
|
442
|
+
],
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
];
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## services/index.ts 规范
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
import axios from '../../../../utils/intercept'; // 路径按项目层级调整
|
|
454
|
+
|
|
455
|
+
export const getXxxList = (params: any) =>
|
|
456
|
+
axios({ method: 'post', url: `/xxx/list/${params.pageSize}/${params.pageNum}`, data: params });
|
|
457
|
+
|
|
458
|
+
export const addXxx = (data: any) =>
|
|
459
|
+
axios({ method: 'post', url: '/xxx/add', data });
|
|
460
|
+
|
|
461
|
+
export const updateXxx = (data: any) =>
|
|
462
|
+
axios({ method: 'post', url: '/xxx/update', data });
|
|
463
|
+
|
|
464
|
+
// 字典接口(有字典映射时)
|
|
465
|
+
// export const getXxxOptions = () =>
|
|
466
|
+
// axios({ method: 'get', url: '/xxx/options' });
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## propsType.ts 规范
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
export interface XxxRecord {
|
|
475
|
+
id: number;
|
|
476
|
+
name: string;
|
|
477
|
+
status?: number | string;
|
|
478
|
+
gmtModified?: string;
|
|
479
|
+
[key: string]: any;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export interface XxxFormValues {
|
|
483
|
+
name: string;
|
|
484
|
+
status: number | string;
|
|
485
|
+
// ... 按实际字段补充
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## utils/index.ts 规范
|
|
492
|
+
|
|
493
|
+
**仅在有特殊转换时生成。无转换需求则不创建此文件。**
|
|
494
|
+
|
|
495
|
+
常见需要转换的场景:
|
|
496
|
+
- 多选数组 → 逗号分隔字符串(提交前)/ 逗号字符串 → 数组(回显)
|
|
497
|
+
- `depId` 数字 → `[depId]` 数组(ProTree/ProCascader 回显)
|
|
498
|
+
- 日期 moment/dayjs → 格式化字符串(提交前)
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
// 提交前转换(按实际字段取消注释)
|
|
502
|
+
export const transformFormData = (values: any): any => {
|
|
503
|
+
const data = { ...values };
|
|
504
|
+
// if (Array.isArray(data.roleId)) data.roleId = data.roleId.join(',');
|
|
505
|
+
// if (data.entryDate) data.entryDate = data.entryDate.format('YYYY-MM-DD');
|
|
506
|
+
return data;
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// 回显时转换(按实际字段取消注释)
|
|
510
|
+
export const transformRecordToFormValues = (record: any): any => {
|
|
511
|
+
const values = { ...record };
|
|
512
|
+
// if (typeof values.roleIds === 'string') {
|
|
513
|
+
// values.roleId = values.roleIds.split(',').map(Number).filter(Boolean);
|
|
514
|
+
// }
|
|
515
|
+
// if (values.depId && typeof values.depId === 'number') {
|
|
516
|
+
// values.depId = [values.depId];
|
|
517
|
+
// }
|
|
518
|
+
return values;
|
|
519
|
+
};
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## 强制规范检查清单
|
|
525
|
+
|
|
526
|
+
生成代码时逐条核对:
|
|
527
|
+
|
|
528
|
+
### 状态
|
|
529
|
+
- [ ] 多个状态用一个 `useSetState<PageState>`,不拆成多个 `useState`
|
|
530
|
+
- [ ] PageState 用 interface 声明,不用内联类型
|
|
531
|
+
|
|
532
|
+
### Hooks 使用
|
|
533
|
+
- [ ] `useMemo` 仅用于无外部依赖的纯配置(如 `getFormColumns()`)
|
|
534
|
+
- [ ] `getTableColumns`(依赖 handler)不加 `useMemo`
|
|
535
|
+
- [ ] `getModalFormColumns`(依赖运行时数据)不加 `useMemo`
|
|
536
|
+
- [ ] `useEffect` 依赖数组不含函数、form 实例
|
|
537
|
+
|
|
538
|
+
### Placeholder
|
|
539
|
+
- [ ] **禁止** `fieldProps: { placeholder: '请输入' }` 或 `placeholder: '请选择'`(组件自动添加)
|
|
540
|
+
- [ ] 仅特殊 placeholder 才手动指定(如 `placeholder: '自动获取'`)
|
|
541
|
+
- [ ] 如 fieldProps 只有 placeholder 且为默认值,移除整个 fieldProps
|
|
542
|
+
|
|
543
|
+
### 区块顺序
|
|
544
|
+
- [ ] 导入 → Props/Hooks → 状态 → 配置 → 事件处理 → API → useEffect → JSX
|
|
545
|
+
- [ ] 所有 `useEffect` 集中在 return 之前,不分散
|
|
546
|
+
|
|
547
|
+
### Config 纯净性
|
|
548
|
+
- [ ] config 文件不含 `useState`/`useRequest`/`useEffect`
|
|
549
|
+
- [ ] 运行时数据(dictMaps、form、currentRecord)通过参数传入
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## 字段类型速查
|
|
554
|
+
|
|
555
|
+
| 场景 | sisyphus type | 关键 fieldProps |
|
|
556
|
+
|------|--------------|----------------|
|
|
557
|
+
| 普通文本输入 | `Input` | `maxLength` |
|
|
558
|
+
| 数字输入 | `InputNumber` | `min`, `max`, `precision` |
|
|
559
|
+
| 静态选项下拉 | `Select` / `Radio` | `dataSource: [{label, value}]`(**不是 options**) |
|
|
560
|
+
| 枚举/字典下拉 | `ProEnum` | `code`, `allowClear`, `mode: 'multiple'` |
|
|
561
|
+
| 接口数据下拉 | `ProSelect` | `useRequest.service`, `transformResponse`, `fieldNames`, `mode` |
|
|
562
|
+
| 树形选择 | `ProTree` | `mode: 'treeSelect'`, `useRequest`, `transformResponse`, `onFieldChange` |
|
|
563
|
+
| 日期 | `DatePicker` | `valueType: 'date'` 或 `'dateTime'` |
|
|
564
|
+
| 日期范围 | `RangePicker` | `valueType: 'date'` |
|
|
565
|
+
| 分组折叠 | `ProCollapse` | `title`, `level: '2'`, `children: [...]` |
|
|
566
|
+
| 文件上传 | `ProUpload` | 见 sisyphus-assistant |
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
## 联动规则速查
|
|
571
|
+
|
|
572
|
+
| 需求 | 写法 |
|
|
573
|
+
|------|------|
|
|
574
|
+
| 条件显示字段 | `show: (values) => !!values.xxx` |
|
|
575
|
+
| 动态必填 | `required: (values) => values.xxx === 'Y'` |
|
|
576
|
+
| 选项依赖另一字段 | `fieldProps: (values) => ({ useRequest: { service: () => getXxx(values.dep), options: { ready: !!values.dep, refreshDeps: [values.dep] } } })` |
|
|
577
|
+
| 字段变化时联动 | `fieldProps: { onFieldChange: (value, _, { form }) => form.setFieldValue('xxx', undefined) }` |
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## 生成流程
|
|
582
|
+
|
|
583
|
+
1. 检查 `package.json` 确认 sisyphus 版本
|
|
584
|
+
2. 解析用户输入,提取字段/接口/分组需求
|
|
585
|
+
3. 追问缺失的必要项
|
|
586
|
+
4. 按顺序生成:
|
|
587
|
+
- `services/index.ts`(config 需要 import,先生成)
|
|
588
|
+
- `propsType.ts`
|
|
589
|
+
- `config/index.tsx`
|
|
590
|
+
- `index.tsx`
|
|
591
|
+
- `utils/index.ts`(仅有转换时)
|
|
592
|
+
- `index.less`(仅含 `.pro-search-container {}`)
|
|
593
|
+
5. 生成后说明需要用户调整的地方(axios 相对路径、枚举 code 值、字典接口返回结构)
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## 示例
|
|
598
|
+
|
|
599
|
+
**用户输入:**
|
|
600
|
+
```
|
|
601
|
+
页面:角色管理(RolePage)
|
|
602
|
+
查询:角色名(roleName:Input), 状态(status:ProEnum/code=status)
|
|
603
|
+
表格:角色名(roleName), 状态(status:enumName/code=status), 更新时间(gmtModified:dateTime), 操作[修改]
|
|
604
|
+
抽屉表单:角色名(roleName:Input*), 状态(status:ProEnum/code=status*)
|
|
605
|
+
接口:list=POST /sys/role/list, add=POST /sys/role/add, update=POST /sys/role/update
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
**生成结果:**
|
|
609
|
+
- 无 ProCollapse(字段少,默认平铺)
|
|
610
|
+
- 无 utils(无特殊转换)
|
|
611
|
+
- 无字典映射(状态用 enumName 直接渲染)
|
|
612
|
+
- PageState 含 `modalVisible`、`modalMode`、`currentRecord`
|
|
613
|
+
- `getFormColumns()` 不加 useMemo(直接调用即可)
|
|
614
|
+
- `getTableColumns(handleEdit)` 不加 useMemo
|