@yorha2b-lab/autodev 7.8.0 → 7.9.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/bin/bunker.js +1 -1
- package/package.json +1 -1
- package/src/constitution/council.js +12 -0
- package/src/constitution/inspector.js +1 -0
- package/src/labs/council.js +114 -112
- package/src/labs/scout.js +12 -2
- package/src/labs/tower.js +31 -2
- package/src/logistics/analyzer.js +18 -4
- package/src/logistics/supporter.js +18 -0
- package/src/units/raider.js +4 -2
- package/src/units/striker.js +5 -1
- package/src/workshop/react/blueprint/molds/MySearchForm.hbs +1 -1
- package/src/workshop/react/blueprint/molds/handleBlock.hbs +3 -4
- package/src/workshop/react/blueprint/molds/hookBlock.hbs +0 -8
- package/src/workshop/react/blueprint/molds/stateBlock.hbs +2 -0
- package/src/workshop/react/builder/index.js +1 -1
- package/src/workshop/react/extensions/useTableQuery.js +4 -14
- package/src/workshop/react/standardParts/DraggableContainer.js +5 -1
- package/src/workshop/react/standardParts/EditableCell.js +8 -4
- package/src/workshop/react/standardParts/MyBaseForm.js +3 -3
- package/src/workshop/react/standardParts/MyDescription.js +3 -3
- package/src/workshop/react/standardParts/MyForm.js +9 -5
- package/src/workshop/react/standardParts/MyModalForm.js +1 -1
- package/src/workshop/react/standardParts/MyModalTable.js +2 -2
- package/src/workshop/react/standardParts/MyPopoverTable.js +1 -0
- package/src/workshop/react/standardParts/MySearchForm.js +24 -18
- package/src/workshop/react/standardParts/MyTable.js +32 -28
- package/src/workshop/react/standardParts/index.js +64 -23
package/bin/bunker.js
CHANGED
|
@@ -53,7 +53,7 @@ program
|
|
|
53
53
|
]
|
|
54
54
|
}
|
|
55
55
|
Object.entries(outpost).forEach(([type, items]) => items.forEach(quartermaster[type]))
|
|
56
|
-
const bunkerCmd = chalk.yellow(`
|
|
56
|
+
const bunkerCmd = chalk.yellow(`"bunker": "bunker boot"`)
|
|
57
57
|
console.log(chalk.cyan(dialog.bunker.initComplete(bunkerCmd)))
|
|
58
58
|
})
|
|
59
59
|
|
package/package.json
CHANGED
|
@@ -14,5 +14,17 @@ ${realApis}
|
|
|
14
14
|
- **输出格式**:请只输出一个 JSON 映射表。
|
|
15
15
|
- **例如**:{"BUNKER_API_ANCHOR_editByRecord": {"uri": "/api/records/{recordId}", "method": "POST"}}
|
|
16
16
|
|
|
17
|
+
## 4. 后端动作黑话映射准则 (API Action Synonyms)
|
|
18
|
+
由于后端开发命名极其不规范,匹配时可以按以下业务黑话等价映射:
|
|
19
|
+
- 【删除 / 移除 / 作废】等价于: delete, del, remove, drop, invalid, cancel, void, status, updateStatus, trash
|
|
20
|
+
- 【审核 / 审批 / 确认】等价于: audit, approve, pass, check, verify, review, examine
|
|
21
|
+
- 【编辑 / 更新 / 修改】等价于: edit, update, modify, change, save, handle
|
|
22
|
+
- 【新增 / 创建 / 提交】等价于: add, create, insert, save, submit, post
|
|
23
|
+
- 【导出 / 下载】等价于: export, download, out, dump, file
|
|
24
|
+
|
|
25
|
+
⚠️ 强制注意:
|
|
26
|
+
1. 后端极度喜欢用 POST 方法配合 status / invalid / state 实现逻辑软删除,切勿死板寻找 DELETE 方法!
|
|
27
|
+
2. 单行操作通常路径带 /{id} 或接收单个 id 参数;批量操作通常接收 ids 数组。
|
|
28
|
+
|
|
17
29
|
⚠️ 严禁解释,严禁输出 JSON 之外的内容。
|
|
18
30
|
`.trim()
|
package/src/labs/council.js
CHANGED
|
@@ -14,15 +14,20 @@ module.exports = async ({ llm, yorha, dialog, logistics }) => {
|
|
|
14
14
|
|
|
15
15
|
pod153.report(dialog.pod153.autonomousAddressing, 'magenta')
|
|
16
16
|
|
|
17
|
+
const enumParamsMap = {}
|
|
17
18
|
const apiResponse = await fetch(apiDoc)
|
|
18
19
|
const text = await apiResponse.text()
|
|
19
20
|
const apiData = JSON5.parse(text)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
|
|
22
|
+
const validMethods = ['get', 'post', 'put', 'delete', 'patch']
|
|
23
|
+
const refinedApis = Object.entries(apiData.paths || {}).flatMap(([apiUrl, methods]) => {
|
|
24
|
+
return Object.entries(methods)
|
|
25
|
+
.filter(([method]) => validMethods.includes(method.toLowerCase()))
|
|
26
|
+
.map(([method, info]) => ({
|
|
27
|
+
path: apiUrl,
|
|
28
|
+
method: method?.toUpperCase() || '',
|
|
29
|
+
desc: info.summary || info.description || 'N/A',
|
|
30
|
+
}))
|
|
26
31
|
})
|
|
27
32
|
|
|
28
33
|
const falseTruth = (apiUrl, method = 'post') => {
|
|
@@ -30,146 +35,143 @@ module.exports = async ({ llm, yorha, dialog, logistics }) => {
|
|
|
30
35
|
const matchedPath = Object.keys(apiData.paths || {}).find(p => cleanUrl.endsWith(p) || p.endsWith(cleanUrl))
|
|
31
36
|
if (!matchedPath) return null
|
|
32
37
|
const definitions = apiData.definitions || apiData.components?.schemas || {}
|
|
33
|
-
// 1. 找到该接口 200 响应对应的根 Schema
|
|
34
38
|
const methodObj = apiData.paths?.[apiUrl]?.[method.toLowerCase()]
|
|
35
39
|
const rootSchema = methodObj?.responses?.['200']?.schema || methodObj?.responses?.['200']?.content?.['application/json']?.schema
|
|
36
40
|
if (!rootSchema) return null
|
|
37
|
-
|
|
41
|
+
|
|
38
42
|
const resolveRef = (refStr) => {
|
|
39
43
|
if (!refStr) return null
|
|
40
44
|
const refName = refStr.split('/').at(-1)
|
|
41
45
|
return definitions[refName]
|
|
42
46
|
}
|
|
43
|
-
|
|
47
|
+
|
|
44
48
|
let wrapperDef = resolveRef(rootSchema.$ref)
|
|
45
|
-
// 3. 在外层属性里寻找我们熟悉的列表字段 (datas, records, list, rows 等)
|
|
46
49
|
const rowRef = unwrapSchemaRef(wrapperDef)
|
|
47
|
-
// 4. 顺藤摸瓜拿到终极行数据定义 (Row DTO)
|
|
48
50
|
const rowDef = resolveRef(rowRef)
|
|
49
51
|
if (!rowDef?.properties) return null
|
|
50
|
-
|
|
51
|
-
// 格式形如:{ contractNum: "合同编号" }
|
|
52
|
+
|
|
52
53
|
return Object.fromEntries(
|
|
53
54
|
Object.entries(rowDef.properties).map(([fieldName, propMeta]) => [
|
|
54
55
|
fieldName,
|
|
55
|
-
propMeta.description || propMeta.title || fieldName
|
|
56
|
+
propMeta.description || propMeta.title || fieldName
|
|
56
57
|
])
|
|
57
58
|
)
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
const resurrection = async
|
|
61
|
+
const resurrection = async moduleName => {
|
|
61
62
|
try {
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
.
|
|
86
|
-
|
|
87
|
-
.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
))
|
|
94
|
-
|
|
95
|
-
allSemantics.push(...uiSemantics)
|
|
96
|
-
}
|
|
63
|
+
if (!moduleName) return
|
|
64
|
+
const modulePath = path.join(process.cwd(), pagesDir, moduleName)
|
|
65
|
+
if (!fs.existsSync(modulePath) || !fs.statSync(modulePath).isDirectory()) return
|
|
66
|
+
|
|
67
|
+
const subFiles = fs.readdirSync(modulePath).filter(f => f.endsWith('.js') || f.endsWith('.jsx'))
|
|
68
|
+
if (subFiles.length === 0) return
|
|
69
|
+
|
|
70
|
+
const fileContents = {}
|
|
71
|
+
const allSemantics = []
|
|
72
|
+
const allAnchors = new Set()
|
|
73
|
+
|
|
74
|
+
for (const file of subFiles) {
|
|
75
|
+
const filePath = path.join(modulePath, file)
|
|
76
|
+
const code = fs.readFileSync(filePath, 'utf-8')
|
|
77
|
+
fileContents[filePath] = code
|
|
78
|
+
|
|
79
|
+
const anchors = code.match(/BUNKER_API_ANCHOR_\w+/g) || []
|
|
80
|
+
anchors.forEach(a => allAnchors.add(a))
|
|
81
|
+
|
|
82
|
+
const textMatches = code.match(/(?:['"`]([^'"`\n]{2,})['"`]|>([^<>{}\n]{2,})<)/g) || []
|
|
83
|
+
const uiSemantics = textMatches
|
|
84
|
+
.map(str => str.replace(/['"`><]/g, '').trim())
|
|
85
|
+
.filter(text => (
|
|
86
|
+
text.length >= 2 &&
|
|
87
|
+
!text.startsWith('/') &&
|
|
88
|
+
!text.startsWith('http') &&
|
|
89
|
+
!text.includes('BUNKER_API_ANCHOR') &&
|
|
90
|
+
!/^[0-9]+$/.test(text)
|
|
91
|
+
))
|
|
92
|
+
allSemantics.push(...uiSemantics)
|
|
93
|
+
}
|
|
97
94
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
95
|
+
if (allAnchors.size > 0) {
|
|
96
|
+
const bunkerAnchors = Array.from(allAnchors).join('\n')
|
|
97
|
+
const pageKeywords = getSemanticKeywords(allSemantics)
|
|
98
|
+
|
|
99
|
+
const candidates = refinedApis
|
|
100
|
+
.map(api => ({ ...api, score: getLocalScore(api, pageKeywords, moduleName) }))
|
|
101
|
+
.filter(item => item.score > 0)
|
|
102
|
+
.sort((a, b) => b.score - a.score)
|
|
103
|
+
.slice(0, 50)
|
|
104
|
+
|
|
105
|
+
const finalCandidates = candidates.map(item => `${item.method} ${item.path} ${item.desc}`).join('\n')
|
|
106
|
+
const spinner = pod153.start(dialog.pod153.inactiveModule(moduleName), 'yellow')
|
|
107
|
+
const result = await apiParser({ bunkerAnchors, realApis: finalCandidates })
|
|
108
|
+
|
|
109
|
+
if (result) {
|
|
110
|
+
for (const [filePath, rawCode] of Object.entries(fileContents)) {
|
|
111
|
+
let code = rawCode
|
|
112
|
+
let isModified = false
|
|
113
|
+
let hasQsImport = code.includes(`import qs from 'qs'`)
|
|
114
|
+
|
|
115
|
+
Object.entries(result).forEach(([anchor, { uri, method }]) => {
|
|
116
|
+
if (code.includes(anchor)) {
|
|
117
|
+
isModified = true
|
|
118
|
+
const cleanPath = uri.startsWith('/') ? uri : `/${uri}`
|
|
119
|
+
let finalUri = cleanPath.startsWith('/api') ? cleanPath : `/api${cleanPath}`
|
|
120
|
+
finalUri = finalUri.replace(/\{(\w+)\}/g, (_, key) => '${params.' + key + '}')
|
|
121
|
+
const quotationMark = finalUri.includes('${params.') ? '`' : "'"
|
|
122
|
+
|
|
123
|
+
let newRequestCode = ''
|
|
124
|
+
if (method?.toUpperCase() === 'GET') {
|
|
125
|
+
newRequestCode = `request(\`${finalUri}?\${qs.stringify(params)}\`)`
|
|
126
|
+
if (!hasQsImport) {
|
|
127
|
+
code = `import qs from 'qs'\n${code}`
|
|
128
|
+
hasQsImport = true
|
|
132
129
|
}
|
|
130
|
+
} else {
|
|
131
|
+
newRequestCode = `request(${quotationMark}${finalUri}${quotationMark}, { method: '${method?.toUpperCase()}', body: params })`
|
|
133
132
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
|
|
134
|
+
const anchorRegex = new RegExp(`request\\(\\s*['"\`][^'"\`]*?${anchor}[^'"\`]*?['"\`][^)]*?\\)|${anchor}`, 'g')
|
|
135
|
+
code = code.replace(anchorRegex, () => newRequestCode)
|
|
137
136
|
}
|
|
137
|
+
})
|
|
138
|
+
if (isModified) {
|
|
139
|
+
fs.writeFileSync(filePath, code)
|
|
138
140
|
}
|
|
141
|
+
}
|
|
139
142
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
const parameters = schemas[schemaName]?.properties ?? {}
|
|
151
|
-
const enumParams = Object.entries(parameters)?.filter(([_, value]) => value.hasOwnProperty('enum')) || []
|
|
152
|
-
enumParamsMap[fileName] = contextStringify({
|
|
153
|
-
maxLength: 100,
|
|
154
|
-
context: Object.fromEntries(enumParams.map(([key, value]) => [`${key}Options`, value.enum?.map(opt => ({ label: opt, value: opt }))])),
|
|
155
|
-
})
|
|
143
|
+
const pagesConfig = result?.['BUNKER_API_ANCHOR_pages']
|
|
144
|
+
if (pagesConfig) {
|
|
145
|
+
const { uri: pagesApi, method: pagesMethod } = pagesConfig
|
|
146
|
+
const schemas = apiData.definitions || apiData.components?.schemas || {}
|
|
147
|
+
const methodObj = apiData.paths[pagesApi]?.[pagesMethod] ?? apiData.paths[pagesApi]?.[pagesMethod?.toLowerCase()]
|
|
148
|
+
let schemaName = methodObj?.parameters?.flatMap(item => Object.values(item.schema ?? {})?.flatMap(def => def?.split('/')?.at(-1)))?.[0]
|
|
149
|
+
if (!schemaName && methodObj?.requestBody) {
|
|
150
|
+
const bodySchema = methodObj.requestBody.content?.['application/json']?.schema
|
|
151
|
+
schemaName = (bodySchema?.$ref || bodySchema?.items?.$ref)?.split('/')?.at(-1)
|
|
156
152
|
}
|
|
157
|
-
|
|
158
|
-
|
|
153
|
+
const parameters = schemas[schemaName]?.properties ?? {}
|
|
154
|
+
const enumParams = Object.entries(parameters)?.filter(([_, value]) => value.hasOwnProperty('enum')) || []
|
|
155
|
+
enumParamsMap[moduleName] = contextStringify({
|
|
156
|
+
maxLength: 100,
|
|
157
|
+
context: Object.fromEntries(enumParams.map(([key, value]) => [`${key}Options`, value.enum?.map(opt => ({ label: opt, value: opt }))])),
|
|
158
|
+
})
|
|
159
159
|
}
|
|
160
|
+
pod153.success(spinner, dialog.pod153.signalSynchronized(Object.keys(result).length, moduleName))
|
|
161
|
+
commander.report(dialog.bunker.disclaimer, 'yellow')
|
|
160
162
|
}
|
|
161
163
|
}
|
|
162
|
-
return enumParamsMap
|
|
163
164
|
} catch (e) {
|
|
164
165
|
pod153.report(dialog.pod153.signalLinkFault(e.message), 'red')
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
const
|
|
169
|
+
const core = { falseTruth, resurrection }
|
|
169
170
|
|
|
170
|
-
return {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
171
|
+
return new Proxy(core, {
|
|
172
|
+
get(target, prop) {
|
|
173
|
+
if (prop in target) return target[prop]
|
|
174
|
+
return enumParamsMap[prop]
|
|
175
|
+
}
|
|
176
|
+
})
|
|
175
177
|
}
|
package/src/labs/scout.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
let globalBrowser = null
|
|
2
2
|
|
|
3
|
-
module.exports = ({ yorha, dialog }) => {
|
|
3
|
+
module.exports = ({ yorha, dialog, logistics }) => {
|
|
4
4
|
|
|
5
5
|
const fs = require('fs')
|
|
6
6
|
const path = require('path')
|
|
7
7
|
const http = require('http')
|
|
8
8
|
|
|
9
9
|
const { pod042 } = yorha
|
|
10
|
+
const { killZombiePort } = logistics.supporter
|
|
10
11
|
const relayServerPort = 15342
|
|
11
12
|
|
|
12
13
|
const isHttpURL = str => {
|
|
@@ -72,7 +73,16 @@ module.exports = ({ yorha, dialog }) => {
|
|
|
72
73
|
res.end('Not Found')
|
|
73
74
|
}
|
|
74
75
|
})
|
|
75
|
-
return new Promise(resolve =>
|
|
76
|
+
return new Promise(resolve => {
|
|
77
|
+
const tryListen = () => server.listen(relayServerPort, () => resolve({ server }))
|
|
78
|
+
server.once('error', err => {
|
|
79
|
+
if (err.code === 'EADDRINUSE') {
|
|
80
|
+
killZombiePort(relayServerPort)
|
|
81
|
+
setTimeout(tryListen, 150)
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
tryListen()
|
|
85
|
+
})
|
|
76
86
|
}
|
|
77
87
|
|
|
78
88
|
return {
|
package/src/labs/tower.js
CHANGED
|
@@ -5,13 +5,16 @@ module.exports = ({ yorha, dialog, logistics, headquarters }) => {
|
|
|
5
5
|
const httpProxy = require('http-proxy')
|
|
6
6
|
|
|
7
7
|
const { pod153, commander } = yorha
|
|
8
|
+
const { getConfig, killZombiePort } = logistics.supporter
|
|
8
9
|
const { unwrapSignal, isQuerySignal } = logistics.analyzer
|
|
9
|
-
const { routeMap = {}, proxyTarget } =
|
|
10
|
+
const { routeMap = {}, proxyTarget } = getConfig()
|
|
10
11
|
|
|
11
12
|
const TOWER_PORT = 42153
|
|
12
13
|
const hackedRegistry = new Map()
|
|
13
14
|
const proxy = httpProxy.createProxyServer({})
|
|
14
15
|
|
|
16
|
+
const resurrectingSet = new Set()
|
|
17
|
+
|
|
15
18
|
const getJsonFingerprint = obj => {
|
|
16
19
|
if (!obj || typeof obj !== 'object') return ''
|
|
17
20
|
return Object.keys(Array.isArray(obj) ? (obj[0] || {}) : obj).sort().join(',')
|
|
@@ -54,13 +57,39 @@ module.exports = ({ yorha, dialog, logistics, headquarters }) => {
|
|
|
54
57
|
})
|
|
55
58
|
})
|
|
56
59
|
|
|
57
|
-
const server = http.createServer((req, res) => {
|
|
60
|
+
const server = http.createServer(async (req, res) => {
|
|
58
61
|
if (!proxyTarget) {
|
|
59
62
|
return
|
|
60
63
|
}
|
|
64
|
+
if (req.url.includes('BUNKER_API_ANCHOR')) {
|
|
65
|
+
const referer = req.headers.referer || 'http://localhost'
|
|
66
|
+
const urlPath = new URL(referer, 'http://localhost').pathname
|
|
67
|
+
const moduleName = routeMap?.[urlPath] ?? urlPath.split('/').filter(Boolean).at(-1)
|
|
68
|
+
|
|
69
|
+
if (moduleName && !resurrectingSet.has(moduleName)) {
|
|
70
|
+
resurrectingSet.add(moduleName)
|
|
71
|
+
try {
|
|
72
|
+
const { labs } = require('../awakening').get()
|
|
73
|
+
const council = await labs?.council
|
|
74
|
+
await council?.resurrection?.(moduleName)
|
|
75
|
+
} finally {
|
|
76
|
+
resurrectingSet.delete(moduleName)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
80
|
+
res.end('{}')
|
|
81
|
+
return
|
|
82
|
+
}
|
|
61
83
|
proxy.web(req, res, { target: proxyTarget, changeOrigin: true })
|
|
62
84
|
})
|
|
63
85
|
|
|
86
|
+
server.on('error', err => {
|
|
87
|
+
if (err.code === 'EADDRINUSE') {
|
|
88
|
+
killZombiePort(TOWER_PORT)
|
|
89
|
+
setTimeout(() => server.listen(TOWER_PORT), 100)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
64
93
|
server.listen(TOWER_PORT, () => {
|
|
65
94
|
commander.report(dialog.bunker.towerOnline(TOWER_PORT), 'magenta')
|
|
66
95
|
commander.report(dialog.bunker.towerConnected(TOWER_PORT), 'magenta')
|
|
@@ -2,6 +2,7 @@ const { Segment, useDefault } = require('segmentit')
|
|
|
2
2
|
const segmentit = useDefault(new Segment())
|
|
3
3
|
|
|
4
4
|
const dataKeys = ['data', 'list', 'items', 'datas', 'rows', 'result', 'payload', 'results', 'dataList']
|
|
5
|
+
const ACTION_WORDS = ['删除', '作废', '审核', '审批', '导出', '编辑', '修改', '新增', '创建', '查看', '详情', '启用', '禁用', '下载', '驳回', '作废']
|
|
5
6
|
|
|
6
7
|
const unwrapSignal = json => {
|
|
7
8
|
if (!json || typeof json !== 'object') return null
|
|
@@ -71,11 +72,24 @@ const getLocalScore = (api, pageKeywords, moduleName) => {
|
|
|
71
72
|
let score = 0
|
|
72
73
|
const path = api.path.toLowerCase()
|
|
73
74
|
const desc = api.desc.toLowerCase()
|
|
74
|
-
const
|
|
75
|
-
|
|
75
|
+
const tags = (api.tags || []).join(' ').toLowerCase()
|
|
76
|
+
const mod = normalize(moduleName)
|
|
77
|
+
|
|
78
|
+
if (normalize(path).includes(mod)) score += 50
|
|
79
|
+
if (normalize(tags).includes(mod)) score += 50
|
|
80
|
+
|
|
76
81
|
pageKeywords.forEach(word => {
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
const w = word.toLowerCase()
|
|
83
|
+
const isAction = ACTION_WORDS.includes(w)
|
|
84
|
+
if (tags.includes(w)) {
|
|
85
|
+
score += 30
|
|
86
|
+
}
|
|
87
|
+
if (desc.includes(w)) {
|
|
88
|
+
score += isAction ? 35 : 10
|
|
89
|
+
}
|
|
90
|
+
if (path.includes(w)) {
|
|
91
|
+
score += 5
|
|
92
|
+
}
|
|
79
93
|
})
|
|
80
94
|
return score
|
|
81
95
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
2
|
const path = require('path')
|
|
3
|
+
const { execSync } = require('child_process')
|
|
3
4
|
|
|
4
5
|
const getConfig = () => {
|
|
5
6
|
const localConfigPath = path.join(process.cwd(), 'bunker', 'config.js') // 项目配置
|
|
@@ -20,7 +21,24 @@ const getExistingMenus = (dir = 'src/pages') => {
|
|
|
20
21
|
.map(file => ({ label: file, key: file }))
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
const killZombiePort = port => {
|
|
25
|
+
try {
|
|
26
|
+
if (process.platform === 'win32') {
|
|
27
|
+
const out = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf-8' })
|
|
28
|
+
const pids = out.split('\n')
|
|
29
|
+
.map(line => line.trim().split(/\s+/).at(-1))
|
|
30
|
+
.filter(pid => pid && pid !== '0' && pid !== String(process.pid))
|
|
31
|
+
pids.forEach(pid => execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' }))
|
|
32
|
+
} else {
|
|
33
|
+
execSync(`lsof -ti :${port} | xargs kill -9`, { stdio: 'ignore' })
|
|
34
|
+
}
|
|
35
|
+
} catch (e) {
|
|
36
|
+
// 杀完了或没占用,直接静默放行
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
23
40
|
module.exports = {
|
|
24
41
|
getConfig,
|
|
42
|
+
killZombiePort,
|
|
25
43
|
getExistingMenus,
|
|
26
44
|
}
|
package/src/units/raider.js
CHANGED
|
@@ -68,8 +68,10 @@ module.exports = {
|
|
|
68
68
|
pod042.success(spinner, dialog.pod042.partialConstruction((endTime - startTime) / 1000))
|
|
69
69
|
pod042.success(spinner, dialog.pod042.partialRecommendation)
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
fs.unlinkSync(mission.input)
|
|
71
|
+
try {
|
|
72
|
+
if (fs.existsSync(mission.input)) fs.unlinkSync(mission.input)
|
|
73
|
+
} catch (e) {
|
|
74
|
+
// 忽略文件锁定,静默放行
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
} catch (error) {
|
package/src/units/striker.js
CHANGED
|
@@ -80,10 +80,14 @@ module.exports = {
|
|
|
80
80
|
pod042.success(spinner, dialog.pod042.assemblyComplete(fileName, (endTime - startTime) / 1000))
|
|
81
81
|
|
|
82
82
|
if (fs.existsSync(mission.input)) {
|
|
83
|
-
fs.unlinkSync(mission.input)
|
|
84
83
|
const cwdUtilsDir = path.join(process.cwd(), utilsDir)
|
|
85
84
|
if (!fs.existsSync(cwdUtilsDir)) fs.mkdirSync(cwdUtilsDir, { recursive: true })
|
|
86
85
|
fs.writeFileSync(path.join(cwdUtilsDir, 'menus.js'), `export const menus = ${contextStringify({ context: logistics.supporter.getExistingMenus(pagesDir), maxLength: 50 })}`)
|
|
86
|
+
try {
|
|
87
|
+
if (fs.existsSync(mission.input)) fs.unlinkSync(mission.input)
|
|
88
|
+
} catch (e) {
|
|
89
|
+
// 忽略文件锁定,静默放行
|
|
90
|
+
}
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
} catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<MySearchForm form={form}
|
|
1
|
+
<MySearchForm form={form} loading={loading} formItems={ {{formItems}} } initialParams={initialParams} initialValues={query} setSearch={setSearch} />
|
|
@@ -71,17 +71,16 @@ const showImg = (record, dataIndex) => {
|
|
|
71
71
|
|
|
72
72
|
{{/if}}
|
|
73
73
|
|
|
74
|
-
let fetching = false
|
|
75
74
|
const modalSubmit = async params => {
|
|
75
|
+
if (fetchingRef.current) return
|
|
76
76
|
const requestMap={
|
|
77
77
|
{{#each operations}}
|
|
78
78
|
{{this.action}}:()=> {{this.request}},
|
|
79
79
|
{{/each}}
|
|
80
80
|
}
|
|
81
|
-
|
|
82
|
-
fetching = true
|
|
81
|
+
fetchingRef.current = true
|
|
83
82
|
const response = await requestMap[modal.type]?.()
|
|
84
|
-
|
|
83
|
+
fetchingRef.current = false
|
|
85
84
|
if({{{responseSuccess}}}){
|
|
86
85
|
setModal({ visible: false })
|
|
87
86
|
refresh()
|
|
@@ -2,13 +2,6 @@
|
|
|
2
2
|
const initialParams = {{initParams}}
|
|
3
3
|
const query = formatQuery(Object.fromEntries(new URLSearchParams(location.search).entries()), {{formItems}})
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* 💡 [BUNKER 战术锚点提示]
|
|
7
|
-
* 代码中带有 BUNKER_API_ANCHOR_ 前缀的为待通电接口。
|
|
8
|
-
* 1. 自动模式:配置 config.js 中的 apiDoc 后,系统将自动扫描 Swagger 并对齐真实路径;
|
|
9
|
-
* 2. 手动模式:搜索 BUNKER_API_ANCHOR 手动替换为真实接口。
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
5
|
const { total, search, columns, loading, dataSource, setLoading, setSearch, setDataSource, refresh } = useTableQuery({
|
|
13
6
|
cols:tableColumns,
|
|
14
7
|
initialParams: { ...initialParams, ...query },
|
|
@@ -17,7 +10,6 @@ const { total, search, columns, loading, dataSource, setLoading, setSearch, setD
|
|
|
17
10
|
return {
|
|
18
11
|
data: response?.data?.datas ?? [],
|
|
19
12
|
total: response?.data?.total ?? 0,
|
|
20
|
-
columns: undefined,//可选,解析接口动态列;优先级高于cols
|
|
21
13
|
}
|
|
22
14
|
},
|
|
23
15
|
})
|
|
@@ -148,7 +148,7 @@ module.exports = ({ template, logistics, headquarters }) => {
|
|
|
148
148
|
formItems: hasFormItems ? (hasTabs ? 'formItems[activeKey]' : 'formItems') : '[]',
|
|
149
149
|
operations: pageConfig.table.operation?.sort((a, b) => a.action.length - b.action.length) || [],
|
|
150
150
|
initParams: `{ ${hasTabs ? 'type: activeKey,' : ''}${hasPagination ? 'pageNo: 1 , pageSize: 10' : ''}}`,
|
|
151
|
-
uri:
|
|
151
|
+
uri: `request('/api/${needMock ? fileName : 'BUNKER_API_ANCHOR_pages'}', { method: 'POST', body: params })`,
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
const bodyCode = indexTpl(viewData)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useRef, useState, useEffect, useCallback } from 'react'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const getColumnSchema = cols => cols.map(col => `${col.title}-${col.dataIndex}-${col.sortOrder}`).join('|')
|
|
4
|
+
|
|
5
|
+
export const useTableQuery = ({ api, cols = [], initialParams = {}, formatResponse }) => {
|
|
4
6
|
|
|
5
7
|
const [total, setTotal] = useState(0)
|
|
6
8
|
const [columns, setColumns] = useState(cols)
|
|
@@ -10,7 +12,6 @@ export const useTableQuery = ({ api, cols = [], initialParams = {}, formatRespon
|
|
|
10
12
|
|
|
11
13
|
const apiRef = useRef(api)
|
|
12
14
|
const fetchIdRef = useRef(0)
|
|
13
|
-
const lastFilterRef = useRef('')
|
|
14
15
|
const formatResponseRef = useRef(formatResponse)
|
|
15
16
|
|
|
16
17
|
useEffect(() => {
|
|
@@ -21,19 +22,8 @@ export const useTableQuery = ({ api, cols = [], initialParams = {}, formatRespon
|
|
|
21
22
|
formatResponseRef.current = formatResponse
|
|
22
23
|
}, [formatResponse])
|
|
23
24
|
|
|
24
|
-
const getColumnSchema = cols => cols.map(col => `${col.title}-${col.dataIndex}-${col.sortOrder}`).join('|')
|
|
25
|
-
|
|
26
25
|
const fetchData = useCallback(async () => {
|
|
27
26
|
if (!apiRef.current) return
|
|
28
|
-
|
|
29
|
-
if (isLocalPaging) {
|
|
30
|
-
const currentFilter = JSON.stringify(Object.fromEntries(Object.entries(search).filter(([k]) => !['pageNo', 'pageSize'].includes(k))))
|
|
31
|
-
if (lastFilterRef.current === currentFilter && dataSource.length > 0) {
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
lastFilterRef.current = currentFilter
|
|
35
|
-
}
|
|
36
|
-
|
|
37
27
|
setLoading(true)
|
|
38
28
|
const currentFetchId = ++fetchIdRef.current
|
|
39
29
|
try {
|
|
@@ -57,7 +47,7 @@ export const useTableQuery = ({ api, cols = [], initialParams = {}, formatRespon
|
|
|
57
47
|
setLoading(false)
|
|
58
48
|
}
|
|
59
49
|
}
|
|
60
|
-
}, [search
|
|
50
|
+
}, [search])
|
|
61
51
|
|
|
62
52
|
useEffect(() => {
|
|
63
53
|
fetchData()
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
import { TableContext } from './MyTable'
|
|
1
3
|
import { SortableContainer } from 'react-sortable-hoc'
|
|
2
4
|
|
|
3
5
|
const SortableBody = SortableContainer(props => <tbody {...props} />)
|
|
@@ -9,7 +11,9 @@ const arrayMoveImmutable = (array, oldIndex, newIndex) => {
|
|
|
9
11
|
return newArray
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
export const DraggableContainer =
|
|
14
|
+
export const DraggableContainer = props => {
|
|
15
|
+
|
|
16
|
+
const { customSave, setDataSource, dataSourceRef } = useContext(TableContext)
|
|
13
17
|
|
|
14
18
|
const onSortEnd = ({ oldIndex, newIndex }) => {
|
|
15
19
|
if (oldIndex !== newIndex) {
|
|
@@ -12,11 +12,15 @@ export const EditableRow = ({ index, record, columns, draggable, ...props }) =>
|
|
|
12
12
|
|
|
13
13
|
useEffect(() => {
|
|
14
14
|
if (record) {
|
|
15
|
-
const
|
|
16
|
-
|
|
15
|
+
const extra = {}
|
|
16
|
+
columns.forEach(col => {
|
|
17
|
+
if (col.dataIndex in record && col.mode) {
|
|
18
|
+
extra[col.dataIndex] = record[col.dataIndex]?.toString()?.split(',') || []
|
|
19
|
+
}
|
|
20
|
+
})
|
|
17
21
|
form.setFieldsValue({ ...record, ...extra })
|
|
18
22
|
}
|
|
19
|
-
}, [record])
|
|
23
|
+
}, [record, columns, form])
|
|
20
24
|
|
|
21
25
|
return (
|
|
22
26
|
<Form form={form} component={false} onValuesChange={(changedValue, allValue) => props?.onValuesChange?.({ changedValue, allValue, form })}>
|
|
@@ -59,7 +63,7 @@ export const EditableCell = ({ mode, title, record, rowKey, editMode, editable,
|
|
|
59
63
|
try {
|
|
60
64
|
const values = await form.validateFields()
|
|
61
65
|
if (mode) {
|
|
62
|
-
values[dataIndex] = values[dataIndex]
|
|
66
|
+
values[dataIndex] = values[dataIndex]?.join(',')
|
|
63
67
|
}
|
|
64
68
|
if (!defaultEdit) {
|
|
65
69
|
toggleEdit()
|
|
@@ -10,7 +10,7 @@ const getValueFromEvent = e => {
|
|
|
10
10
|
return e?.fileList
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const renderFormContent = ({ item, prefixName = [] }) => {
|
|
13
|
+
const renderFormContent = ({ item, form, prefixName = [] }) => {
|
|
14
14
|
|
|
15
15
|
if (!item.name) {
|
|
16
16
|
return <span dangerouslySetInnerHTML={{ __html: item.value }} />
|
|
@@ -20,7 +20,7 @@ const renderFormContent = ({ item, prefixName = [] }) => {
|
|
|
20
20
|
|
|
21
21
|
const inputNode = (
|
|
22
22
|
<Form.Item noStyle name={namePath} rules={item.rules} dependencies={item.dependencies} initialValue={item.value} {...(['upload', 'ossUpload'].includes(item.type) ? { getValueFromEvent, valuePropName: 'fileList' } : { valuePropName: item.valuePropName })}>
|
|
23
|
-
{formNode({ item })}
|
|
23
|
+
{formNode({ item, form })}
|
|
24
24
|
</Form.Item>
|
|
25
25
|
)
|
|
26
26
|
|
|
@@ -60,7 +60,7 @@ export const MyBaseForm = ({ item, form }) => {
|
|
|
60
60
|
required={item.required ?? item.rules?.some(v => v.required)}
|
|
61
61
|
style={{ marginBottom: !item.name ? 0 : undefined, ...item.style }}
|
|
62
62
|
>
|
|
63
|
-
{item.render ? (typeof item.render === 'function' ? item.render(item, form) : item.render) : renderFormContent({ item })}
|
|
63
|
+
{item.render ? (typeof item.render === 'function' ? item.render(item, form) : item.render) : renderFormContent({ item, form })}
|
|
64
64
|
</Form.Item>
|
|
65
65
|
)
|
|
66
66
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { Form, Descriptions } from 'antd'
|
|
2
2
|
import { MyBaseForm } from './MyBaseForm'
|
|
3
3
|
|
|
4
|
-
export const MyDescription = ({ title, column, layout = 'vertical', descriptions, form: externalForm, onValuesChange }) => {
|
|
4
|
+
export const MyDescription = ({ title, column, layout = 'vertical', labelStyle, contentStyle, descriptions, form: externalForm, onValuesChange }) => {
|
|
5
5
|
|
|
6
6
|
const [internalForm] = Form.useForm()
|
|
7
7
|
const form = externalForm || internalForm
|
|
8
8
|
|
|
9
9
|
return (
|
|
10
10
|
<Form form={form} preserve={false} component={false} onValuesChange={onValuesChange}>
|
|
11
|
-
<Descriptions title={title} column={column} layout={layout}>
|
|
11
|
+
<Descriptions title={title} column={column} layout={layout} labelStyle={labelStyle} contentStyle={contentStyle}>
|
|
12
12
|
{descriptions.map(item => (
|
|
13
|
-
<Descriptions.Item key={item.label} label={item.name ? undefined : item.label}>
|
|
13
|
+
<Descriptions.Item key={item.label} span={item.span ?? 1} label={item.name ? undefined : item.label}>
|
|
14
14
|
{item.name ? <MyBaseForm item={item} form={form} /> : item.value}
|
|
15
15
|
</Descriptions.Item>))}
|
|
16
16
|
</Descriptions>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import dayjs from 'dayjs'
|
|
2
2
|
import { MyBaseForm } from './MyBaseForm'
|
|
3
3
|
import { flattenFormItems } from '../utils/utils'
|
|
4
|
-
import { useContext, createContext } from 'react'
|
|
5
4
|
import { Row, Col, Form, Button, Collapse } from 'antd'
|
|
5
|
+
import { useEffect, useContext, createContext } from 'react'
|
|
6
6
|
|
|
7
7
|
const FormContext = createContext({})
|
|
8
8
|
|
|
@@ -30,12 +30,16 @@ const LAYOUT_PROCESSORS = {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const ACTION_PROCESSORS = {
|
|
33
|
-
addonAfter: (item, { tableConfig, setModalTable, setSelectedTableRows }) => ({
|
|
33
|
+
addonAfter: (item, { customClick, tableConfig = {}, setModalTable, setSelectedTableRows }) => ({
|
|
34
34
|
props: {
|
|
35
35
|
addonAfter: (
|
|
36
36
|
<Button onClick={() => {
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
if (typeof customClick === 'function') {
|
|
38
|
+
customClick()
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
setSelectedTableRows?.([])
|
|
42
|
+
setModalTable?.({ visible: true, title: `选择${item.label}`, ...tableConfig[item.name] })
|
|
39
43
|
}}>
|
|
40
44
|
{item.addonAfter}
|
|
41
45
|
</Button>
|
|
@@ -109,7 +113,7 @@ export const MyForm = ({ record, formItems = [], externalForm, labelCol, wrapper
|
|
|
109
113
|
initialData[key] = dayjs(value)
|
|
110
114
|
}
|
|
111
115
|
} else if ((config?.type === 'select' && config.mode) || config?.type === 'checkbox') {
|
|
112
|
-
initialData[key] = Array.isArray(value) ? value : value
|
|
116
|
+
initialData[key] = Array.isArray(value) ? value : value?.split(config.separator ?? ',')
|
|
113
117
|
} else {
|
|
114
118
|
initialData[key] = value
|
|
115
119
|
}
|
|
@@ -48,7 +48,7 @@ export const MyModalForm = ({ extra, width, title, layout, okText, options, foot
|
|
|
48
48
|
const tableRowSelection = useMemo(() => ({
|
|
49
49
|
selectedRowKeys: selectedTableRows.map(row => row[rowKey]),
|
|
50
50
|
onChange: (selectedRowKeys, selectedRows) => setSelectedTableRows(selectedRows),
|
|
51
|
-
}), [rowKey,
|
|
51
|
+
}), [rowKey, selectedTableRows])
|
|
52
52
|
|
|
53
53
|
const ctx = { form, record, pending, handleOk, handleCancel, setModal }
|
|
54
54
|
|
|
@@ -11,9 +11,9 @@ export const MyModalTable = ({ api, onOk, title, width, footer, visible, operate
|
|
|
11
11
|
|
|
12
12
|
const { total, loading, columns, dataSource, search, refresh, setLoading, setSearch, setDataSource } = useTableQuery({ api, cols: modalColumns, initialParams, formatResponse, isLocalPaging })
|
|
13
13
|
|
|
14
|
-
const handleSearch = values => setSearch({ ...
|
|
14
|
+
const handleSearch = values => setSearch(prev => ({ ...prev, ...values, pageNo: 1 }))
|
|
15
15
|
|
|
16
|
-
const handleTableChange = (pagination, filters, sorter) => setSearch({ ...
|
|
16
|
+
const handleTableChange = (pagination, filters, sorter) => setSearch(prev => ({ ...prev, pageNo: pagination.current, pageSize: pagination.pageSize, orderBy: sorter.column ? sorter.field : undefined }))
|
|
17
17
|
|
|
18
18
|
const handleOk = async () => {
|
|
19
19
|
if (onOk) {
|
|
@@ -4,7 +4,26 @@ import { useMemo, useState, useEffect } from 'react'
|
|
|
4
4
|
import { Row, Col, Form, Button, Space } from 'antd'
|
|
5
5
|
import { SearchOutlined, RedoOutlined } from '@ant-design/icons'
|
|
6
6
|
|
|
7
|
-
export const
|
|
7
|
+
export const finishChange = ({ formItems, formattedValues }) => {
|
|
8
|
+
const result = { ...formattedValues }
|
|
9
|
+
Object.keys(result).forEach(key => {
|
|
10
|
+
const val = result[key]
|
|
11
|
+
const isDateType = formItems.find(item => item.name === key)?.type?.includes('date')
|
|
12
|
+
if (isDateType && val) {
|
|
13
|
+
if (Array.isArray(val) && val.length > 0) {
|
|
14
|
+
const [startKey, endKey] = key.split(',')
|
|
15
|
+
result[startKey] = val[0].startOf('day').valueOf()
|
|
16
|
+
result[endKey] = val[1].endOf('day').valueOf()
|
|
17
|
+
delete result[key]
|
|
18
|
+
} else {
|
|
19
|
+
result[key] = val.valueOf()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
return result
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const MySearchForm = ({ form, extra, options, loading, labelCol, setSearch, formItems, showLimit = 7, initialValues = {}, initialParams = {}, customReset, customFinish, renderAction, onValuesChange, syncUrlParams = true, defaultPageSize = 10 }) => {
|
|
8
27
|
|
|
9
28
|
const [limit, setLimit] = useState(showLimit)
|
|
10
29
|
|
|
@@ -26,26 +45,13 @@ export const MySearchForm = ({ form, extra, search, options, loading, labelCol,
|
|
|
26
45
|
if (customReset) {
|
|
27
46
|
customReset()
|
|
28
47
|
} else {
|
|
29
|
-
setSearch({ pageNo: 1, pageSize:
|
|
48
|
+
setSearch(prev => ({ pageNo: 1, pageSize: prev.pageSize ?? defaultPageSize, ...initialParams }))
|
|
30
49
|
}
|
|
31
50
|
}
|
|
32
51
|
|
|
33
52
|
const handleFinish = values => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const val = formattedValues[key]
|
|
37
|
-
const isDateType = formItems.find(item => item.name === key)?.type?.includes('date')
|
|
38
|
-
if (isDateType && val) {
|
|
39
|
-
if (Array.isArray(val) && val.length > 0) {
|
|
40
|
-
const [startKey, endKey] = key.split(',')
|
|
41
|
-
formattedValues[startKey] = val[0].startOf('day').valueOf()
|
|
42
|
-
formattedValues[endKey] = val[1].endOf('day').valueOf()
|
|
43
|
-
delete formattedValues[key]
|
|
44
|
-
} else {
|
|
45
|
-
formattedValues[key] = val.valueOf()
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
})
|
|
53
|
+
let formattedValues = { ...values }
|
|
54
|
+
formattedValues = finishChange({ formItems, formattedValues })
|
|
49
55
|
if (syncUrlParams) {
|
|
50
56
|
const currentQuery = Object.fromEntries(new URLSearchParams(window.location.search).entries())
|
|
51
57
|
const params = new URLSearchParams(Object.fromEntries(Object.entries({ ...currentQuery, ...formattedValues }).filter(([key, value]) => !['', null, undefined].includes(value))))
|
|
@@ -54,7 +60,7 @@ export const MySearchForm = ({ form, extra, search, options, loading, labelCol,
|
|
|
54
60
|
if (customFinish) {
|
|
55
61
|
customFinish(formattedValues)
|
|
56
62
|
} else {
|
|
57
|
-
setSearch({ ...
|
|
63
|
+
setSearch(prev => ({ ...prev, ...formattedValues, pageNo: 1 }))
|
|
58
64
|
}
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Table, Space } from 'antd'
|
|
2
2
|
import { DraggableContainer } from './DraggableContainer'
|
|
3
3
|
import { EditableRow, EditableCell } from './EditableCell'
|
|
4
|
-
import { useRef, useMemo, useEffect, useCallback } from 'react'
|
|
4
|
+
import { useRef, useMemo, useEffect, useCallback, createContext } from 'react'
|
|
5
|
+
|
|
6
|
+
export const TableContext = createContext(null)
|
|
5
7
|
|
|
6
8
|
export const MyTable = ({ size, query, total, search, options, autoScroll, onChange, editingKey, setSearch, pagination, editRender = {}, renderAction, rowClassName, customSave, setDataSource, lineFormChange, columns = [], rowSelection, rowKey = 'id', editMode = 'cellEdit', loading = false, dataSource = [], draggable = false, scroll = { x: 'max-content' }, isLocalPaging = false, ...restProps }) => {
|
|
7
9
|
|
|
@@ -33,10 +35,10 @@ export const MyTable = ({ size, query, total, search, options, autoScroll, onCha
|
|
|
33
35
|
c.body.cell = EditableCell
|
|
34
36
|
}
|
|
35
37
|
if (draggable) {
|
|
36
|
-
c.body.wrapper =
|
|
38
|
+
c.body.wrapper = DraggableContainer
|
|
37
39
|
}
|
|
38
40
|
return c
|
|
39
|
-
}, [isEditable, draggable
|
|
41
|
+
}, [isEditable, draggable])
|
|
40
42
|
|
|
41
43
|
const mergedColumns = useMemo(() => {
|
|
42
44
|
return columns.map(col => {
|
|
@@ -48,7 +50,7 @@ export const MyTable = ({ size, query, total, search, options, autoScroll, onCha
|
|
|
48
50
|
currentCol = {
|
|
49
51
|
...currentCol,
|
|
50
52
|
filterSearch: true,
|
|
51
|
-
onFilter: (value, record) => col.
|
|
53
|
+
...(col.remote ? {} : { onFilter: (value, record) => col.customFilter({ value, record, dataIndex: col.dataIndex }) }),
|
|
52
54
|
filterDropdown: ({ close, confirm, selectedKeys, clearFilters, setSelectedKeys }) => (
|
|
53
55
|
<Space direction='vertical' style={{ padding: 12 }}>
|
|
54
56
|
{col.customFilterDropdown({ dataIndex: col.dataIndex, selectedKeys, setSelectedKeys })}
|
|
@@ -89,7 +91,7 @@ export const MyTable = ({ size, query, total, search, options, autoScroll, onCha
|
|
|
89
91
|
}),
|
|
90
92
|
}
|
|
91
93
|
})
|
|
92
|
-
}, [columns, options,
|
|
94
|
+
}, [rowKey, columns, options, editMode, setSearch, editingKey, editRender, renderAction, handleSave])
|
|
93
95
|
|
|
94
96
|
const paginationConfig = useMemo(() => {
|
|
95
97
|
|
|
@@ -100,8 +102,8 @@ export const MyTable = ({ size, query, total, search, options, autoScroll, onCha
|
|
|
100
102
|
showSizeChanger: true,
|
|
101
103
|
showTotal: (total) => `共 ${total} 条`,
|
|
102
104
|
...(isLocalPaging ? {} : {
|
|
103
|
-
current: Number(search?.pageNo),
|
|
104
|
-
pageSize: Number(search?.pageSize),
|
|
105
|
+
current: Number(search?.pageNo ?? 1),
|
|
106
|
+
pageSize: Number(search?.pageSize ?? 10),
|
|
105
107
|
}),
|
|
106
108
|
...(typeof pagination === 'object' ? pagination : {})
|
|
107
109
|
}
|
|
@@ -126,27 +128,29 @@ export const MyTable = ({ size, query, total, search, options, autoScroll, onCha
|
|
|
126
128
|
|
|
127
129
|
return (
|
|
128
130
|
<div ref={tableRef}>
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
131
|
+
<TableContext.Provider value={{ customSave, setDataSource, dataSourceRef }}>
|
|
132
|
+
<Table
|
|
133
|
+
{...restProps}
|
|
134
|
+
rowKey={rowKey}
|
|
135
|
+
scroll={scroll}
|
|
136
|
+
loading={loading}
|
|
137
|
+
onChange={onChange}
|
|
138
|
+
columns={mergedColumns}
|
|
139
|
+
dataSource={dataSource}
|
|
140
|
+
size={size ?? 'middle'}
|
|
141
|
+
components={components}
|
|
142
|
+
rowSelection={rowSelection}
|
|
143
|
+
pagination={paginationConfig}
|
|
144
|
+
onRow={(record, index) => ({ record, index, draggable, columns: mergedColumns, ...(typeof lineFormChange === 'function' ? { onValuesChange: lineFormChange } : {}) })}
|
|
145
|
+
rowClassName={(record, index) => {
|
|
146
|
+
const externalClass = typeof rowClassName === 'function' ? rowClassName(record, index) : rowClassName
|
|
147
|
+
const classes = [externalClass]
|
|
148
|
+
if (isEditable) classes.push('editable-row')
|
|
149
|
+
if (autoScroll && query?.rowIndex === index) classes.push('search-highlight-row')
|
|
150
|
+
return classes.filter(Boolean).join(' ')
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
</TableContext.Provider>
|
|
150
154
|
</div>
|
|
151
155
|
)
|
|
152
156
|
}
|
|
@@ -1,17 +1,68 @@
|
|
|
1
|
+
import { Fragment } from 'react'
|
|
1
2
|
import { MyBaseForm } from './MyBaseForm'
|
|
2
3
|
import { AliyunOSSUpload } from './AliyunOSSUpload'
|
|
3
|
-
import { Tree,
|
|
4
|
+
import { Tree, Radio, Input, Upload, Select, Cascader, Checkbox, DatePicker, InputNumber, TreeSelect, AutoComplete } from 'antd'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
const componentDict = {
|
|
7
|
+
date: { Component: DatePicker },
|
|
8
|
+
number: { Component: InputNumber },
|
|
9
|
+
ossUpload: { Component: AliyunOSSUpload },
|
|
10
|
+
default: {
|
|
11
|
+
Component: Input,
|
|
12
|
+
props: () => ({ allowClear: true })
|
|
13
|
+
},
|
|
14
|
+
daterange: {
|
|
15
|
+
Component: DatePicker.RangePicker,
|
|
16
|
+
props: () => ({ placeholder: undefined })
|
|
17
|
+
},
|
|
18
|
+
textarea: {
|
|
19
|
+
Component: Input.TextArea,
|
|
20
|
+
props: () => ({ autoSize: { minRows: 4 } })
|
|
21
|
+
},
|
|
22
|
+
upload: {
|
|
23
|
+
Component: Upload,
|
|
24
|
+
props: ({ item }) => ({ ...item.uploadProps })
|
|
25
|
+
},
|
|
26
|
+
auto: {
|
|
27
|
+
Component: AutoComplete,
|
|
28
|
+
props: ({ item }) => ({ options: item.options })
|
|
29
|
+
},
|
|
30
|
+
checkbox: {
|
|
31
|
+
Component: Checkbox.Group,
|
|
32
|
+
props: ({ item }) => ({ options: item.options })
|
|
33
|
+
},
|
|
34
|
+
radio: {
|
|
35
|
+
Component: Radio.Group,
|
|
36
|
+
props: ({ item }) => ({ options: item.options })
|
|
37
|
+
},
|
|
38
|
+
treeSelect: {
|
|
39
|
+
Component: TreeSelect,
|
|
40
|
+
props: ({ item }) => ({ treeData: item.options })
|
|
41
|
+
},
|
|
42
|
+
tree: {
|
|
43
|
+
Component: Tree,
|
|
44
|
+
props: ({ item }) => ({ checkable: true, treeData: item.options })
|
|
45
|
+
},
|
|
46
|
+
cascader: {
|
|
47
|
+
Component: Cascader,
|
|
48
|
+
props: ({ item }) => ({ allowClear: true, showSearch: true, options: item.options })
|
|
49
|
+
},
|
|
50
|
+
select: {
|
|
51
|
+
Component: Select,
|
|
52
|
+
props: ({ item }) => ({ allowClear: true, showSearch: true, mode: item.mode, options: item.options, optionFilterProp: 'label' })
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const formNode = ({ item, form }) => {
|
|
6
57
|
|
|
7
58
|
if (item.type === 'group' && Array.isArray(item.groups)) {
|
|
8
59
|
return (
|
|
9
60
|
<Input.Group compact style={{ width: item.width ?? '100%', display: 'flex', ...item.style }}>
|
|
10
61
|
{item.groups.map((subItem, ind) => (
|
|
11
|
-
<
|
|
12
|
-
<div style={{ flex: 1, minWidth: 0, marginRight: 8 }}><MyBaseForm item={subItem}
|
|
62
|
+
<Fragment key={subItem.name}>
|
|
63
|
+
<div style={{ flex: 1, minWidth: 0, marginRight: 8 }}><MyBaseForm item={subItem} form={form} /></div>
|
|
13
64
|
{ind < item.groups.length - 1 && <span style={{ color: '#888', whiteSpace: 'nowrap', marginRight: 8 }}>{item.separator}</span>}
|
|
14
|
-
</
|
|
65
|
+
</Fragment>
|
|
15
66
|
))}
|
|
16
67
|
</Input.Group>
|
|
17
68
|
)
|
|
@@ -21,25 +72,15 @@ export const formNode = ({ item }) => {
|
|
|
21
72
|
disabled: item.readOnly,
|
|
22
73
|
style: { width: item.width ?? '100%' },
|
|
23
74
|
placeholder: item.placeholder ?? `请${['select', 'cascader', 'date'].includes(item.type) ? '选择' : '输入'}`,
|
|
24
|
-
...item.props
|
|
25
75
|
}
|
|
26
76
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
default: <Input allowClear {...commonProps} />,
|
|
31
|
-
ossUpload: <AliyunOSSUpload {...commonProps} />,
|
|
32
|
-
auto: <AutoComplete options={item.options} {...commonProps} />,
|
|
33
|
-
radio: <Radio.Group options={item.options} {...commonProps} />,
|
|
34
|
-
tree: <Tree checkable treeData={item.options} {...commonProps} />,
|
|
35
|
-
treeSelect: <TreeSelect treeData={item.options} {...commonProps} />,
|
|
36
|
-
checkbox: <Checkbox.Group options={item.options} {...commonProps} />,
|
|
37
|
-
textarea: <Input.TextArea autoSize={{ minRows: 4 }} {...commonProps} />,
|
|
38
|
-
daterange: <DatePicker.RangePicker {...commonProps} placeholder={undefined} />,
|
|
39
|
-
cascader: <Cascader showSearch allowClear options={item.options} {...commonProps} />,
|
|
40
|
-
upload: <Upload {...item.uploadProps} {...commonProps}>{item.content ?? '上传文件'}</Upload>,
|
|
41
|
-
select: <Select allowClear showSearch mode={item.mode} options={item.options} optionFilterProp='label' {...commonProps} />,
|
|
42
|
-
}
|
|
77
|
+
const config = componentDict[item.type] ?? componentDict.default
|
|
78
|
+
const Component = config.Component
|
|
79
|
+
const specialProps = config.props?.({ item, form }) ?? {}
|
|
43
80
|
|
|
44
|
-
return
|
|
81
|
+
return (
|
|
82
|
+
<Component {...commonProps} {...specialProps} {...item.props}>
|
|
83
|
+
{item.type === 'upload' && (item.content ?? '上传文件')}
|
|
84
|
+
</Component>
|
|
85
|
+
)
|
|
45
86
|
}
|