@yorha2b-lab/autodev 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/.env.example +12 -0
- package/CONTRIBUTING.md +118 -0
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/bin/autodev.js +40 -0
- package/config.js +28 -0
- package/package.json +50 -0
- package/src/commands/watch-api.js +41 -0
- package/src/commands/watch-page.js +94 -0
- package/src/commands/watch-part.js +46 -0
- package/src/core/react-compiler.js +53 -0
- package/src/core/task-queue.js +38 -0
- package/src/prompts/mock.js +5 -0
- package/src/prompts/system.js +9 -0
- package/src/prompts/watch-api.js +11 -0
- package/src/prompts/watch-page.js +39 -0
- package/src/prompts/watch-part.js +26 -0
- package/src/services/llm.js +66 -0
- package/src/utils/utils.js +69 -0
- package/templates/react/components/EditableCell.js +76 -0
- package/templates/react/components/MyBaseForm.js +47 -0
- package/templates/react/components/MyModalForm.js +51 -0
- package/templates/react/components/MyModalTable.js +40 -0
- package/templates/react/components/MySearchForm.js +46 -0
- package/templates/react/components/MyTable.js +48 -0
- package/templates/react/components/index.js +40 -0
- package/templates/react/hooks/useTableQuery.js +40 -0
- package/templates/react/index.hbs +110 -0
- package/templates/react/resource.hbs +18 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const createTaskQueue = (concurrency = 1) => {
|
|
2
|
+
|
|
3
|
+
let running = 0
|
|
4
|
+
let queue = []
|
|
5
|
+
let onIdleCallback = null
|
|
6
|
+
|
|
7
|
+
const next = async () => {
|
|
8
|
+
|
|
9
|
+
if (running >= concurrency || queue.length === 0) return
|
|
10
|
+
|
|
11
|
+
const task = queue.shift()
|
|
12
|
+
running++
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
await task()
|
|
16
|
+
} catch (err) {
|
|
17
|
+
console.error('任务执行异常:', err)
|
|
18
|
+
} finally {
|
|
19
|
+
running--
|
|
20
|
+
next()
|
|
21
|
+
if (running === 0 && queue.length === 0 && onIdleCallback) {
|
|
22
|
+
onIdleCallback()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
add: (task) => {
|
|
29
|
+
queue.push(task)
|
|
30
|
+
next()
|
|
31
|
+
},
|
|
32
|
+
onIdle: (callback) => {
|
|
33
|
+
onIdleCallback = callback
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { createTaskQueue }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const commonPrompt = '你必须严格按照用户的JSON结构输出,严禁输出任何思考过程(</think>内容),严禁解释、输出Markdown标签。必须输出合法的、带双引号的标准JSON。'
|
|
2
|
+
|
|
3
|
+
const UI_DESIGNER = `你是一个专业的UI设计师。${commonPrompt}`
|
|
4
|
+
|
|
5
|
+
const MOCK_DESIGNER = `你是一个专业的Mock数据生成器。${commonPrompt}`
|
|
6
|
+
|
|
7
|
+
const API_DESIGNER = `你是一个资深前端与后端接口对接专家。${commonPrompt}`
|
|
8
|
+
|
|
9
|
+
module.exports = { UI_DESIGNER, API_DESIGNER, MOCK_DESIGNER }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module.exports = (swaggerStr, resourceStr) => `
|
|
2
|
+
swaggerStr: ${swaggerStr}
|
|
3
|
+
resourceStr: ${resourceStr}
|
|
4
|
+
resourceStr是前端目前猜测的字段名列表(请从resourceStr中提取dataIndex和name),
|
|
5
|
+
swaggerStr是后端的真实响应。
|
|
6
|
+
请比对两者,找出现有前端字段名应该被替换为哪个真实的后端字段名。
|
|
7
|
+
匹配规则: 1.完全相同 2.下划线/驼峰转换 3.语义相似。
|
|
8
|
+
请只输出一个JSON对象, Key为前端猜测的旧名字, Value为Swagger里的真实新名字。
|
|
9
|
+
例如:{"key_1": "key1"}
|
|
10
|
+
如果没有找到对应的,请不要包含在结果中。
|
|
11
|
+
`.trim()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module.exports = `
|
|
2
|
+
识别图片内容(搜索项,表格列,是否有勾选框,查询下拉选项,统计条等页面结构)
|
|
3
|
+
⚠️⚠️⚠️注意:
|
|
4
|
+
1.变量使用驼峰命名法
|
|
5
|
+
【严禁中文】绝对不允许使用中文作为变量名或函数名
|
|
6
|
+
【严禁js关键字】绝对不允许使用js关键字作为变量名或函数名(export/delete/const...)
|
|
7
|
+
2.标签页:
|
|
8
|
+
tabs: [{tab:'tab1',key:'tab1'}]
|
|
9
|
+
表格上方的Button请务必归类为functionButton,只有当一组水平排列的项下方有明显的长横线(Ink Bar),或者呈现明显的'卡片包裹感'(Card style)时才识别为tabs
|
|
10
|
+
3.搜索项:
|
|
11
|
+
formItems: [{ label: '文本', name: '对应的英文名词' }],
|
|
12
|
+
⚠️⚠️⚠️注意:
|
|
13
|
+
如果是下拉框,请加入type:'select',options:_CODE_对应的英文名词Options_CODE_
|
|
14
|
+
如果是时间范围查询,请加入type:'daterange',name:'对应的英文名词start,对应的英文名词end'
|
|
15
|
+
4.表格:
|
|
16
|
+
{
|
|
17
|
+
pagination: true/false,
|
|
18
|
+
rowSelection: true/false,
|
|
19
|
+
staticInfo:{has:true/false,text:''},
|
|
20
|
+
operation: [{label:'操作',action:'操作动词+ByRecord'}],
|
|
21
|
+
columns: [{ title: '普通列', dataIndex:'对应的英文名词' }],
|
|
22
|
+
}
|
|
23
|
+
⚠️⚠️⚠️注意:
|
|
24
|
+
请不要在columns里加上操作列
|
|
25
|
+
如果列需要排序请加上sorter:true
|
|
26
|
+
如果是时间列,请加入render:_CODE_text=>timeRender({time:text})_CODE_
|
|
27
|
+
如果是序号列直接渲染为{ title: '序号',render: _CODE_(_, record, index) => index + 1_CODE_ }
|
|
28
|
+
如果是下拉框列(所对应的查询项明确是下拉框),请加入render:_CODE_text=>对应的options.find(item=>item.value===text)?.label_CODE_
|
|
29
|
+
如果列需要过滤请加上filters:[],onFilter: _CODE_(value, record) => record[对应的英文名词].includes(value)_CODE_
|
|
30
|
+
5.功能按钮
|
|
31
|
+
functionButtion:[{btn:'btn',action:'操作动词'}]
|
|
32
|
+
如果和表格行操作重复请在action加上BySelected前缀
|
|
33
|
+
如果是导出功能,对应的动词为export+对应的英文名词,没有名词则为exportData
|
|
34
|
+
6.下拉选项字典
|
|
35
|
+
optionDict:{_CODE_对应的英文名词Options_CODE:[]]}
|
|
36
|
+
数组值为对应列展示的内容组成的类似{label:'',value:''}的数组
|
|
37
|
+
最后输出一个JSON对象,不要包含任何Markdown标签,格式如下
|
|
38
|
+
{ tabs: [], table: {}, formItems: [], optionDict: {}, functionButton: []}
|
|
39
|
+
`
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module.exports = `
|
|
2
|
+
识别图片页面结构输出一个JSON对象,不要包含任何Markdown标签
|
|
3
|
+
1.表格
|
|
4
|
+
{ columns:[{ title: '普通列', dataIndex:'对应的英文名词' }]}
|
|
5
|
+
⚠️⚠️⚠️注意:
|
|
6
|
+
请不要在columns里加上操作列
|
|
7
|
+
如果列需要排序请加上sorter:true
|
|
8
|
+
如果是时间列,请加入render:_CODE_text=>timeRender({time:text})_CODE_
|
|
9
|
+
如果是序号列直接渲染为{ title: '序号',render: _CODE_(_, record, index) => index + 1_CODE_ }
|
|
10
|
+
如果是下拉框列(所对应的查询项明确是下拉框),请加入render:_CODE_text=>对应的options.find(item=>item.value===text)?.label_CODE_
|
|
11
|
+
如果列需要过滤请加上filters:[],onFilter: _CODE_(value, record) => record[对应的英文名词].includes(value)_CODE_
|
|
12
|
+
2.查询表单
|
|
13
|
+
formItems: [{ label: '文本', name: '对应的英文名词' }],
|
|
14
|
+
⚠️⚠️⚠️注意:
|
|
15
|
+
如果是下拉框,请加入type:'select',options:_CODE_对应的英文名词Options_CODE_
|
|
16
|
+
如果是时间范围查询,请加入type:'daterange',name:'对应的英文名词start,对应的英文名词end'
|
|
17
|
+
3.下拉选项字典
|
|
18
|
+
optionDict:{_CODE_对应的英文名词Options_CODE:[]]}
|
|
19
|
+
数组值为对应列展示的内容组成的类似{label:'',value:''}的数组
|
|
20
|
+
4.弹窗表单
|
|
21
|
+
modalItems:[{label:'基础项',name:'对应的英文名词',type:'text',rules:[{required:true,message:'字段不能为空'}]}]
|
|
22
|
+
⚠️⚠️⚠️注意:
|
|
23
|
+
type:text/date/daterange/radio/checkbox/select/auto/textarea,如果是text可以省略
|
|
24
|
+
如果type是auto/radio/select/checkbox,请加入options:_CODE_对应的英文名词Options_CODE_
|
|
25
|
+
** 只输出modalItems就可以,不需要对应的formItems和columns **
|
|
26
|
+
`
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const JSON5 = require('json5')
|
|
2
|
+
const sharp = require('sharp')
|
|
3
|
+
const OpenAI = require('openai')
|
|
4
|
+
const config = require('../../config.js')
|
|
5
|
+
const mockPrompt = require('../prompts/mock.js')
|
|
6
|
+
const apiPrompt = require('../prompts/watch-api.js')
|
|
7
|
+
const { UI_DESIGNER, API_DESIGNER, MOCK_DESIGNER } = require('../prompts/system.js')
|
|
8
|
+
|
|
9
|
+
const openai = new OpenAI({ apiKey: process.env.API_KEY, baseURL: process.env.BASE_URL })
|
|
10
|
+
|
|
11
|
+
const askAI = async (model, messages, retryCount = 0) => {
|
|
12
|
+
|
|
13
|
+
if (retryCount > 3) throw new Error('AI 重试次数耗尽,请检查网络或图片')
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const response = await openai.chat.completions.create({ model, messages, response_format: { type: 'json_object' } })
|
|
17
|
+
let raw = response.choices[0].message.content.trim()
|
|
18
|
+
raw = raw.replace(/^```json\n?/, '').replace(/\n?```$/, '')
|
|
19
|
+
const match = raw.match(/[\{\[][\s\S]*[\}\]]/)
|
|
20
|
+
return JSON5.parse(match ? match[0] : raw)
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.warn(`[解析失败,第 ${retryCount + 1} 次重试...]`, err.message)
|
|
23
|
+
return askAI(model, messages, retryCount + 1)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
generateMock: async (columns, fileName) => {
|
|
29
|
+
return askAI(
|
|
30
|
+
config.textModel,
|
|
31
|
+
[
|
|
32
|
+
{ role: 'system', content: MOCK_DESIGNER },
|
|
33
|
+
{ role: 'user', content: mockPrompt(columns, fileName) }
|
|
34
|
+
]
|
|
35
|
+
)
|
|
36
|
+
},
|
|
37
|
+
recognizePage: async (prompt, filePath) => {
|
|
38
|
+
const compressedBuffer = await sharp(filePath)
|
|
39
|
+
.resize(1280, null, { withoutEnlargement: true })
|
|
40
|
+
.jpeg({ quality: 80 })
|
|
41
|
+
.toBuffer()
|
|
42
|
+
const base64Image = compressedBuffer.toString('base64')
|
|
43
|
+
return askAI(
|
|
44
|
+
config.visionModel,
|
|
45
|
+
[
|
|
46
|
+
{ role: 'system', content: UI_DESIGNER },
|
|
47
|
+
{
|
|
48
|
+
role: 'user',
|
|
49
|
+
content: [
|
|
50
|
+
{ type: 'text', text: prompt },
|
|
51
|
+
{ type: 'image_url', image_url: { url: `data:image/png;base64,${base64Image}` } }
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
)
|
|
56
|
+
},
|
|
57
|
+
alignSwaggerFields: async (swaggerStr, resourceStr) => {
|
|
58
|
+
return askAI(
|
|
59
|
+
config.textModel,
|
|
60
|
+
[
|
|
61
|
+
{ role: 'system', content: API_DESIGNER },
|
|
62
|
+
{ role: 'user', content: apiPrompt(swaggerStr, resourceStr) }
|
|
63
|
+
]
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const config = require('../../config.js')
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 大模型输出的标准 JSON 无法携带 render: (text) => <Tag> 等箭头函数。
|
|
8
|
+
* 我们在 Prompt 中要求大模型用 _CODE_ 占位符包裹函数字符串,
|
|
9
|
+
* 在此处用正则剥离外层的双引号和占位符,将其还原为真正的可执行 JS 代码。
|
|
10
|
+
*/
|
|
11
|
+
const cleanCode = str => {
|
|
12
|
+
return str
|
|
13
|
+
.replace(/"(\w+)":/g, '$1:') // 去掉 key 的双引号
|
|
14
|
+
.replace(/"/g, "'") // 双引号全部转单引号
|
|
15
|
+
.replace(/['"]_CODE_([\s\S]*?)_CODE_['"]/g, '$1') // 去掉 _CODE_ 包裹的代码
|
|
16
|
+
.replace(/_CODE_/g, '') // 兜底清理
|
|
17
|
+
.replace(/[ \t]+$/gm, '') // 去除每一行行尾的多余空格
|
|
18
|
+
.replace(/\n{3,}/g, '\n\n') // 将3个或以上的换行符压缩成2个换行符
|
|
19
|
+
.replace(/^\s+/, '') // 去掉文件头部的空行
|
|
20
|
+
.trim() + '\n'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const copyHooks = options => {
|
|
24
|
+
const targetDir = path.join(process.cwd(), config.hooksDir)
|
|
25
|
+
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true })
|
|
26
|
+
const hookDir = path.join(__dirname, `../../templates/${options.template}/hooks`)
|
|
27
|
+
fs.readdirSync(hookDir).forEach(file => fs.copyFileSync(path.join(hookDir, file), path.join(targetDir, file)))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const copyComponents = options => {
|
|
31
|
+
const targetDir = path.join(process.cwd(), config.componentsDir)
|
|
32
|
+
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true })
|
|
33
|
+
const componentsDir = path.join(__dirname, `../../templates/${options.template}/components`)
|
|
34
|
+
fs.readdirSync(componentsDir).forEach(file => fs.copyFileSync(path.join(componentsDir, file), path.join(targetDir, file)))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const getExistingMenus = (dir = config.pagesDir) => {
|
|
38
|
+
const pagesDir = path.join(process.cwd(), dir)
|
|
39
|
+
if (!fs.existsSync(pagesDir)) return []
|
|
40
|
+
return fs.readdirSync(pagesDir)
|
|
41
|
+
.filter(file => fs.statSync(path.join(pagesDir, file)).isDirectory())
|
|
42
|
+
.map(file => ({ label: file, key: file }))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const generateSmartImports = (codeStr, hasTabs) => {
|
|
46
|
+
const hooksLib = ['useTableQuery']
|
|
47
|
+
const reactLib = ['useState', 'useEffect', 'useRef', 'useMemo']
|
|
48
|
+
const componentsLib = ['MyTable', 'MyModalForm', 'MySearchForm']
|
|
49
|
+
const antdLib = ['Card', 'Space', 'Modal', 'Button', 'Alert', 'Table', 'Input', 'Select']
|
|
50
|
+
|
|
51
|
+
const usedAntd = antdLib.filter(name => new RegExp(`\\b${name}\\b`).test(codeStr))
|
|
52
|
+
const usedHooks = hooksLib.filter(name => new RegExp(`\\b${name}\\b`).test(codeStr))
|
|
53
|
+
const usedReact = reactLib.filter(name => new RegExp(`\\b${name}\\b`).test(codeStr))
|
|
54
|
+
const usedComps = componentsLib.filter(name => new RegExp(`\\b${name}\\b`).test(codeStr))
|
|
55
|
+
|
|
56
|
+
const imports = [
|
|
57
|
+
usedReact.length && `import { ${usedReact.join(', ')} } from 'react'`,
|
|
58
|
+
`import { Link, history } from 'umi'`,
|
|
59
|
+
`import { request } from '../../utils/request'`,
|
|
60
|
+
`import { ${hasTabs ? 'tabs, ' : ''}columns, formItems, modalItems } from './resource'`,
|
|
61
|
+
...usedHooks.map(hook => `import { ${hook} } from '../../hooks/${hook}'`),
|
|
62
|
+
...usedComps.map(comp => `import { ${comp} } from '../../components/${comp}'`),
|
|
63
|
+
usedAntd.length && `import { Form,${usedAntd.join(', ')} } from 'antd'`
|
|
64
|
+
].sort((a, b) => a.length - b.length)
|
|
65
|
+
|
|
66
|
+
return imports.filter(Boolean).join('\n')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { copyHooks, cleanCode, copyComponents, getExistingMenus, generateSmartImports }
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Form, Input, InputNumber, Select, Checkbox } from 'antd'
|
|
2
|
+
import React, { useRef, useState, useEffect, useContext } from 'react'
|
|
3
|
+
|
|
4
|
+
const EditableContext = React.createContext(null)
|
|
5
|
+
|
|
6
|
+
export const EditableRow = ({ index, ...props }) => {
|
|
7
|
+
const [form] = Form.useForm()
|
|
8
|
+
return (
|
|
9
|
+
<Form form={form} component={false}>
|
|
10
|
+
<EditableContext.Provider value={form}>
|
|
11
|
+
<tr {...props} />
|
|
12
|
+
</EditableContext.Provider>
|
|
13
|
+
</Form>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const EditableCell = ({ title, record, editable, children, dataIndex, handleSave, rules = [], options = [], editType = 'text', ...restProps }) => {
|
|
18
|
+
|
|
19
|
+
const inputRef = useRef(null)
|
|
20
|
+
const form = useContext(EditableContext)
|
|
21
|
+
|
|
22
|
+
const [editing, setEditing] = useState(false)
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (editing) {
|
|
26
|
+
inputRef.current?.focus()
|
|
27
|
+
}
|
|
28
|
+
}, [editing])
|
|
29
|
+
|
|
30
|
+
const toggleEdit = () => {
|
|
31
|
+
setEditing(!editing)
|
|
32
|
+
form.setFieldsValue({ [dataIndex]: record[dataIndex] })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const save = async () => {
|
|
36
|
+
try {
|
|
37
|
+
const values = await form.validateFields()
|
|
38
|
+
toggleEdit()
|
|
39
|
+
handleSave({ ...record, ...values })
|
|
40
|
+
} catch (errInfo) {
|
|
41
|
+
console.log('保存失败:', errInfo)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let inputNode
|
|
46
|
+
|
|
47
|
+
switch (editType) {
|
|
48
|
+
case 'number':
|
|
49
|
+
inputNode = <InputNumber ref={inputRef} onPressEnter={save} onBlur={save} />
|
|
50
|
+
break
|
|
51
|
+
case 'select':
|
|
52
|
+
inputNode = <Select ref={inputRef} options={options} onBlur={save} open={true} />
|
|
53
|
+
break;
|
|
54
|
+
case 'checkbox':
|
|
55
|
+
inputNode = <Checkbox.Group options={options} onChange={save} />
|
|
56
|
+
break;
|
|
57
|
+
default:
|
|
58
|
+
inputNode = <Input ref={inputRef} onPressEnter={save} onBlur={save} />
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<td {...restProps}>
|
|
63
|
+
{editable ? (
|
|
64
|
+
editing ? (
|
|
65
|
+
<Form.Item rules={rules} name={dataIndex} style={{ margin: 0 }}>
|
|
66
|
+
{inputNode}
|
|
67
|
+
</Form.Item>
|
|
68
|
+
) : (
|
|
69
|
+
<div onClick={toggleEdit} className='editable-cell-value-wrap' style={{ paddingRight: 24, minHeight: 32, cursor: 'pointer' }}>
|
|
70
|
+
{children}
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
) : (children)}
|
|
74
|
+
</td>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Form } from 'antd'
|
|
2
|
+
import { formNode } from './index'
|
|
3
|
+
|
|
4
|
+
export const MyBaseForm = ({ item, form }) => {
|
|
5
|
+
|
|
6
|
+
const renderFormContent = item => {
|
|
7
|
+
|
|
8
|
+
if (!item.name) {
|
|
9
|
+
return typeof item.render === 'function' ? item.render(item) : (item.value ?? '-')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const inputNode = (
|
|
13
|
+
<Form.Item noStyle name={item.name} rules={item.rules} initialValue={item.value}>
|
|
14
|
+
{formNode({ item })}
|
|
15
|
+
</Form.Item>
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if (item.unit) {
|
|
19
|
+
return (
|
|
20
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
21
|
+
<div style={{ flex: 1, minWidth: 0 }}>{inputNode}</div>
|
|
22
|
+
<span style={{ color: '#888', whiteSpace: 'nowrap' }}>{item.unit}</span>
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return inputNode
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Form.Item
|
|
32
|
+
name={item.name}
|
|
33
|
+
label={item.label}
|
|
34
|
+
rules={item.rules}
|
|
35
|
+
extra={item.extra}
|
|
36
|
+
tooltip={item.tooltip}
|
|
37
|
+
labelCol={item.labelCol}
|
|
38
|
+
initialValue={item.value}
|
|
39
|
+
wrapperCol={item.wrapperCol}
|
|
40
|
+
valuePropName={item.valuePropName}
|
|
41
|
+
required={item.required ?? !!item.rules}
|
|
42
|
+
style={{ marginBottom: !item.name ? 0 : undefined, ...item.style }}
|
|
43
|
+
>
|
|
44
|
+
{item.render ? (typeof item.render === 'function' ? item.render(item, form) : item.render) : renderFormContent({ item })}
|
|
45
|
+
</Form.Item>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { formNode } from './index'
|
|
2
|
+
import { useState, useEffect } from 'react'
|
|
3
|
+
import { Row, Col, Form, Modal, } from 'antd'
|
|
4
|
+
import { MyBaseForm } from './MyBaseForm'
|
|
5
|
+
|
|
6
|
+
export const MyModalForm = ({ width, title, submit, record, visible, setModal, labelCol, formItems, wrapperCol, onValuesChange }) => {
|
|
7
|
+
|
|
8
|
+
const [form] = Form.useForm()
|
|
9
|
+
const [pending, setPending] = useState(false)
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (visible) {
|
|
13
|
+
form.resetFields()
|
|
14
|
+
if (record && Object.keys(record).length > 0) {
|
|
15
|
+
form.setFieldsValue(record)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}, [visible, record, form])
|
|
19
|
+
|
|
20
|
+
const handleOk = async () => {
|
|
21
|
+
try {
|
|
22
|
+
const values = await form.validateFields()
|
|
23
|
+
if (submit) {
|
|
24
|
+
setPending(true)
|
|
25
|
+
await submit({ ...record, ...values })
|
|
26
|
+
setPending(false)
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.log('表单校验失败:', error)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const handleCancel = () => {
|
|
34
|
+
setModal({ visible: false })
|
|
35
|
+
form.resetFields()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Modal centered title={title} width={width} open={visible} onOk={handleOk} destroyOnHidden onCancel={handleCancel} confirmLoading={pending}>
|
|
40
|
+
<Form form={form} preserve={false} labelCol={labelCol} wrapperCol={wrapperCol} onValuesChange={(changed, all) => onValuesChange?.({ changed, all, form, record })}>
|
|
41
|
+
<Row gutter={[24, 0]}>
|
|
42
|
+
{formItems.map((item, index) =>
|
|
43
|
+
<Col span={item.span ?? 24} key={item.name || index}>
|
|
44
|
+
<MyBaseForm item={item} form={form} />
|
|
45
|
+
</Col>
|
|
46
|
+
)}
|
|
47
|
+
</Row>
|
|
48
|
+
</Form>
|
|
49
|
+
</Modal>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Modal } from 'antd'
|
|
2
|
+
import { MyTable } from './MyTable'
|
|
3
|
+
import { MySearchForm } from './MySearchForm'
|
|
4
|
+
import { useTableQuery } from '../hooks/useTableQuery'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export const MyModalTable = ({ onOk, title, width, footer, visible, request, columns, setModal, formItems, rowSelection, extraParams = {} }) => {
|
|
8
|
+
|
|
9
|
+
const { total, loading, dataSource, search, setSearch } = useTableQuery(async params => await request({ ...params, ...extraParams }), {})
|
|
10
|
+
|
|
11
|
+
const handleSearch = values => setSearch({ ...search, ...values, pageNo: 1 })
|
|
12
|
+
|
|
13
|
+
const handleTableChange = (pagination, filters, sorter) => setSearch({ ...search, pageNo: pagination.current, pageSize: pagination.pageSize, orderBy: sorter.column ? sorter.field : undefined })
|
|
14
|
+
|
|
15
|
+
const handleOk = () => {
|
|
16
|
+
if (onOk) {
|
|
17
|
+
onOk({ selectedRows: rowSelection?.selectedRows, dataSource })
|
|
18
|
+
}
|
|
19
|
+
setModal({ visible: false })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Modal centered destroyOnHidden title={title} width={width} open={visible} onOk={handleOk} footer={footer} onCancel={() => setModal({ visible: false })}>
|
|
24
|
+
{formItems?.length > 0 && <MySearchForm search={search} formItems={formItems} setSearch={handleSearch} />}
|
|
25
|
+
<MyTable
|
|
26
|
+
loading={loading}
|
|
27
|
+
columns={columns}
|
|
28
|
+
scroll={{ y: 400 }}
|
|
29
|
+
dataSource={dataSource}
|
|
30
|
+
rowSelection={rowSelection}
|
|
31
|
+
onChange={handleTableChange}
|
|
32
|
+
pagination={{
|
|
33
|
+
total: total,
|
|
34
|
+
showSizeChanger: true,
|
|
35
|
+
current: search.pageNo,
|
|
36
|
+
pageSize: search.pageSize,
|
|
37
|
+
}} />
|
|
38
|
+
</Modal>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { MyBaseForm } from './MyBaseForm'
|
|
3
|
+
import { Row, Col, Form, Button, Space } from 'antd'
|
|
4
|
+
|
|
5
|
+
export const MySearchForm = ({ form, search, labelCol, setSearch, formItems, showLimit = 7, customReset, extraOperate, customFinish, onValuesChange, defaultPageSize = 10 }) => {
|
|
6
|
+
|
|
7
|
+
const [limit, setLimit] = useState(showLimit)
|
|
8
|
+
|
|
9
|
+
const handleReset = () => {
|
|
10
|
+
if (customReset) {
|
|
11
|
+
customReset()
|
|
12
|
+
} else {
|
|
13
|
+
setSearch({ pageNo: 1, pageSize: search.pageSize ?? defaultPageSize })
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const handleFinish = values => {
|
|
18
|
+
if (customFinish) {
|
|
19
|
+
customFinish(values)
|
|
20
|
+
} else {
|
|
21
|
+
setSearch({ ...search, ...values, pageNo: 1 })
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Form onFinish={handleFinish} labelCol={labelCol} onValuesChange={onValuesChange}>
|
|
27
|
+
<Row gutter={24}>
|
|
28
|
+
{formItems.slice(0, limit)?.map((item, ind) => (
|
|
29
|
+
<Col key={ind} span={item.span ?? 6}>
|
|
30
|
+
<MyBaseForm item={item} form={form} />
|
|
31
|
+
</Col>
|
|
32
|
+
))}
|
|
33
|
+
<Col flex='auto' style={{ textAlign: 'right' }}>
|
|
34
|
+
<Form.Item>
|
|
35
|
+
<Space>
|
|
36
|
+
<Button type='primary' htmlType='submit'>查询</Button>
|
|
37
|
+
<Button onClick={handleReset} htmlType='reset'>重置</Button>
|
|
38
|
+
{extraOperate}
|
|
39
|
+
{formItems.length > showLimit && (<a onClick={() => setLimit(formItems.length > limit ? formItems.length : 7)}>{formItems.length > limit ? '展开' : '收起'}</a>)}
|
|
40
|
+
</Space>
|
|
41
|
+
</Form.Item>
|
|
42
|
+
</Col>
|
|
43
|
+
</Row>
|
|
44
|
+
</Form>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Table } from 'antd'
|
|
2
|
+
import { EditableRow, EditableCell } from './EditableCell'
|
|
3
|
+
|
|
4
|
+
const components = {
|
|
5
|
+
body: {
|
|
6
|
+
row: EditableRow,
|
|
7
|
+
cell: EditableCell,
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const MyTable = ({ size, onSave, onChange, pagination, columns = [], rowSelection, rowKey = 'id', loading = false, dataSource = [], editable = false, scroll = { x: 'max-content' }, ...restProps }) => {
|
|
12
|
+
|
|
13
|
+
const mergedColumns = columns.map(col => {
|
|
14
|
+
if (!col.editable) {
|
|
15
|
+
return col
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
...col,
|
|
19
|
+
onCell: (record) => ({
|
|
20
|
+
record,
|
|
21
|
+
title: col.title,
|
|
22
|
+
rules: col.rules,
|
|
23
|
+
handleSave: onSave,
|
|
24
|
+
options: col.options,
|
|
25
|
+
editType: col.editType,
|
|
26
|
+
editable: !!col.editType,
|
|
27
|
+
dataIndex: col.dataIndex,
|
|
28
|
+
}),
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Table
|
|
34
|
+
{...restProps}
|
|
35
|
+
rowKey={rowKey}
|
|
36
|
+
scroll={scroll}
|
|
37
|
+
loading={loading}
|
|
38
|
+
onChange={onChange}
|
|
39
|
+
columns={mergedColumns}
|
|
40
|
+
dataSource={dataSource}
|
|
41
|
+
pagination={pagination}
|
|
42
|
+
size={size ?? 'middle'}
|
|
43
|
+
rowSelection={rowSelection}
|
|
44
|
+
components={editable ? components : undefined}
|
|
45
|
+
rowClassName={editable ? () => 'editable-row' : undefined}
|
|
46
|
+
/>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Radio, Input, Upload, Select, Cascader, Checkbox, DatePicker, InputNumber, AutoComplete } from 'antd'
|
|
2
|
+
|
|
3
|
+
export const formNode = ({ item }) => {
|
|
4
|
+
|
|
5
|
+
if (typeof item.render === 'function') {
|
|
6
|
+
return item.render(item)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const commonProps = {
|
|
10
|
+
disabled: item.readOnly,
|
|
11
|
+
style: { width: item.width ?? '100%' },
|
|
12
|
+
placeholder: item.placeholder ?? `请${['select', 'cascader', 'date', 'daterange'].includes(item.type) ? '选择' : '输入'}`,
|
|
13
|
+
...item.props
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
switch (item.type) {
|
|
17
|
+
case 'date':
|
|
18
|
+
return <DatePicker {...commonProps} />
|
|
19
|
+
case 'number':
|
|
20
|
+
return <InputNumber {...commonProps} />
|
|
21
|
+
case 'daterange':
|
|
22
|
+
return <DatePicker.RangePicker {...commonProps} />
|
|
23
|
+
case 'radio':
|
|
24
|
+
return <Radio.Group options={item.options} {...commonProps} />
|
|
25
|
+
case 'auto':
|
|
26
|
+
return <AutoComplete options={item.options} {...commonProps} />
|
|
27
|
+
case 'checkbox':
|
|
28
|
+
return <Checkbox.Group options={item.options} {...commonProps} />
|
|
29
|
+
case 'textarea':
|
|
30
|
+
return <Input.TextArea autoSize={{ minRows: 4 }} {...commonProps} />
|
|
31
|
+
case 'cascader':
|
|
32
|
+
return <Cascader options={item.options} allowClear {...commonProps} />
|
|
33
|
+
case 'select':
|
|
34
|
+
return <Select allowClear mode={item.mode} options={item.options} {...commonProps} />
|
|
35
|
+
case 'upload':
|
|
36
|
+
return <Upload {...item.uploadProps} {...commonProps}>{item.content ?? '上传文件'}</Upload>
|
|
37
|
+
default:
|
|
38
|
+
return <Input allowClear {...commonProps} />
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useRef, useState, useEffect, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
export const useTableQuery = (api, formatResponse, initialParams = {}) => {
|
|
4
|
+
|
|
5
|
+
const [total, setTotal] = useState(0)
|
|
6
|
+
const [loading, setLoading] = useState(false)
|
|
7
|
+
const [dataSource, setDataSource] = useState([])
|
|
8
|
+
const [search, setSearch] = useState({ pageNo: 1, pageSize: 10, ...initialParams })
|
|
9
|
+
|
|
10
|
+
const apiRef = useRef(api)
|
|
11
|
+
const fetchIdRef = useRef(0)
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
apiRef.current = api
|
|
15
|
+
}, [api])
|
|
16
|
+
|
|
17
|
+
const fetchData = useCallback(async () => {
|
|
18
|
+
if (!apiRef.current) return
|
|
19
|
+
setLoading(true)
|
|
20
|
+
const currentFetchId = ++fetchIdRef.current
|
|
21
|
+
try {
|
|
22
|
+
const response = await apiRef.current(search)
|
|
23
|
+
if (currentFetchId !== fetchIdRef.current) return
|
|
24
|
+
setDataSource(formatResponse(response))
|
|
25
|
+
setTotal(response?.total || 0)
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error('查询失败', error)
|
|
28
|
+
} finally {
|
|
29
|
+
if (currentFetchId === fetchIdRef.current) {
|
|
30
|
+
setLoading(false)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}, [search])
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
fetchData()
|
|
37
|
+
}, [fetchData])
|
|
38
|
+
|
|
39
|
+
return { total, loading, dataSource, search, setSearch, refresh: fetchData }
|
|
40
|
+
}
|