@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 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(`'bunker': 'bunker boot'`)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yorha2b-lab/autodev",
3
- "version": "7.8.0",
3
+ "version": "7.9.0",
4
4
  "description": "基于 AI 视觉与语义协议的前端 (React+Antd) 代码构筑与旧系统重构引擎",
5
5
  "bin": {
6
6
  "bunker": "bin/bunker.js"
@@ -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()
@@ -10,6 +10,7 @@ ${moduleCodeView}
10
10
  ## 3. 极速审计准则
11
11
  1. 从需求中仅聚焦提取:【条件分支】、【联动】、【条件禁用/必填】、【二次确认/弹窗】、【特殊数值/边界限制】。
12
12
  2. ⚠️【禁止废话】:已实现的功能只用一行短语概括;未实现的问题只挑最致命的缺口
13
+ 3. 【输出】:仅输出未实现的功能,不输出已实现的功能
13
14
 
14
15
  ## 4. 输出紧凑 JSON:
15
16
  {
@@ -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
- const refinedApis = Object.entries(apiData.paths).flatMap(([apiUrl, methods]) => {
21
- return Object.entries(methods).map(([method, info]) => ({
22
- path: apiUrl,
23
- method: method?.toUpperCase() || '',
24
- desc: info.summary || info.description || 'N/A',
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
- // 辅助函数:根据 $ref 名字解析出实体
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
- // 2. 解析外层包装类 (如 TableResult)
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
- // 5. 组装成高纯度的伪样本数据!
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
- const pages = path.join(process.cwd(), pagesDir)
63
- const files = fs.readdirSync(pages)
64
- const enumParamsMap = {}
65
-
66
- for (const fileName of files) {
67
- const pageDir = path.join(pages, fileName)
68
- if (!fs.existsSync(pageDir) || !fs.statSync(pageDir).isDirectory()) continue
69
-
70
- const subFiles = fs.readdirSync(pageDir).filter(f => f.endsWith('.js') || f.endsWith('.jsx'))
71
- const fileContents = {}
72
- const allSemantics = []
73
- const allAnchors = new Set()
74
-
75
- for (const file of subFiles) {
76
- const filePath = path.join(pageDir, file)
77
- const code = fs.readFileSync(filePath, 'utf-8')
78
- fileContents[filePath] = code
79
-
80
- const anchors = code.match(/BUNKER_API_ANCHOR_\w+/g) || []
81
- anchors.forEach(a => allAnchors.add(a))
82
-
83
- const textMatches = code.match(/(?:['"`]([^'"`\n]{2,})['"`]|>([^<>{}\n]{2,})<)/g) || []
84
- const uiSemantics = textMatches
85
- .map(str => str.replace(/['"`><]/g, '').trim())
86
- // 过滤掉纯代码路径(/api/xxx)、锚点占位符、纯数字和空字符
87
- .filter(text => (
88
- text.length >= 2 &&
89
- !text.startsWith('/') &&
90
- !text.startsWith('http') &&
91
- !text.includes('BUNKER_API_ANCHOR') &&
92
- !/^[0-9]+$/.test(text)
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
- if (allAnchors.size > 0) {
99
- const bunkerAnchors = Array.from(allAnchors).join('\n')
100
- const pageKeywords = getSemanticKeywords(allSemantics)
101
-
102
- const candidates = refinedApis.map(api => ({ ...api, score: getLocalScore(api, pageKeywords, fileName) }))
103
- .filter(item => item.score > 0)
104
- .sort((a, b) => b.score - a.score)
105
- .slice(0, 20)
106
-
107
- const finalCandidates = candidates.map(item => `${item.method} ${item.path} ${item.desc}`).join('\n')
108
- const spinner = pod153.start(dialog.pod153.inactiveModule(fileName), 'yellow')
109
- const result = await apiParser({ bunkerAnchors, realApis: finalCandidates })
110
-
111
- if (result) {
112
- for (const [filePath, rawCode] of Object.entries(fileContents)) {
113
- let code = rawCode
114
- let isModified = false
115
- let hasQsImport = code.includes(`import qs from 'qs'`)
116
-
117
- Object.entries(result).forEach(([anchor, { uri, method }]) => {
118
- if (code.includes(anchor)) {
119
- isModified = true
120
- const cleanPath = uri.startsWith('/') ? uri : `/${uri}`
121
- let finalUri = cleanPath.startsWith('/api') ? cleanPath : `/api${cleanPath}`
122
- finalUri = finalUri.replace(/\{(\w+)\}/g, (_, key) => '${params.' + key + '}')
123
- const quotationMark = finalUri.includes('${params.') ? '`' : "'"
124
- if (method?.toUpperCase() === 'GET') {
125
- code = code.replaceAll(anchor, `request(\`${finalUri}?\${qs.stringify(params)}\`)`)
126
- if (!hasQsImport) {
127
- code = `import qs from 'qs'\n${code}`
128
- hasQsImport = true
129
- }
130
- } else {
131
- code = code.replaceAll(anchor, `request(${quotationMark}${finalUri}${quotationMark}, { method: '${method?.toUpperCase()}', body: params })`)
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
- if (isModified) {
136
- fs.writeFileSync(filePath, code)
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
- const pagesConfig = result?.['BUNKER_API_ANCHOR_pages']
141
- if (pagesConfig) {
142
- const { uri: pagesApi, method: pagesMethod } = pagesConfig
143
- const schemas = apiData.definitions || apiData.components?.schemas || {}
144
- const methodObj = apiData.paths[pagesApi]?.[pagesMethod] ?? apiData.paths[pagesApi]?.[pagesMethod?.toLowerCase()]
145
- let schemaName = methodObj?.parameters?.flatMap(item => Object.values(item.schema ?? {})?.flatMap(def => def?.split('/')?.at(-1)))?.[0]
146
- if (!schemaName && methodObj?.requestBody) {
147
- const bodySchema = methodObj.requestBody.content?.['application/json']?.schema
148
- schemaName = (bodySchema?.$ref || bodySchema?.items?.$ref)?.split('/')?.at(-1)
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
- pod153.success(spinner, dialog.pod153.signalSynchronized(Object.keys(result).length, fileName))
158
- commander.report(dialog.bunker.disclaimer, 'yellow')
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 enumParamsMap = await resurrection()
169
+ const core = { falseTruth, resurrection }
169
170
 
170
- return {
171
- falseTruth,
172
- resurrection,
173
- ...enumParamsMap,
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 => server.listen(relayServerPort, () => resolve({ server })))
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 } = logistics.supporter.getConfig()
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 mod = moduleName.toLowerCase()
75
- if (normalize(path).includes(normalize(mod))) score += 50
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
- if (desc.includes(word.toLowerCase())) score += 10
78
- if (path.includes(word.toLowerCase())) score += 5
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
  }
@@ -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
- if (fs.existsSync(mission.input)) {
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) {
@@ -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} search={search} loading={loading} formItems={ {{formItems}} } initialParams={initialParams} initialValues={query} setSearch={setSearch} />
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
- if (fetching) return
82
- fetching = true
81
+ fetchingRef.current = true
83
82
  const response = await requestMap[modal.type]?.()
84
- fetching = false
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
  })
@@ -2,6 +2,8 @@
2
2
  const { } = useContext(GlobalContext)
3
3
 
4
4
  const rowKey = 'id'
5
+ const fetchingRef = useRef(false)
6
+
5
7
  {{#if hasFormItems}}
6
8
  const [form] = Form.useForm()
7
9
  {{/if}}
@@ -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: needMock ? `request('/api/${fileName}', { method: 'POST', body: params })` : 'BUNKER_API_ANCHOR_pages',
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
- export const useTableQuery = ({ api, cols = [], initialParams = {}, formatResponse, isLocalPaging = false }) => {
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, isLocalPaging])
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 = ({ props, customSave, setDataSource, dataSourceRef }) => {
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 modeCol = columns.find(col => col.dataIndex in record && col.mode)
16
- const extra = modeCol ? { [modeCol.dataIndex]: record[modeCol.dataIndex]?.toString()?.split(',') || [] } : {}
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].join(',')
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
- setSelectedTableRows([])
38
- setModalTable({ visible: true, title: `选择${item.label}`, ...tableConfig[item.name] })
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.split(config.separator ?? ',')
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, modalTable, selectedTableRows])
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({ ...search, ...values, pageNo: 1 })
14
+ const handleSearch = values => setSearch(prev => ({ ...prev, ...values, pageNo: 1 }))
15
15
 
16
- const handleTableChange = (pagination, filters, sorter) => setSearch({ ...search, pageNo: pagination.current, pageSize: pagination.pageSize, orderBy: sorter.column ? sorter.field : undefined })
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) {
@@ -11,6 +11,7 @@ export const MyPopoverTable = ({ api, columns, children, formatter }) => {
11
11
  if (loading) return
12
12
  setLoading(true)
13
13
  const response = await api()
14
+ setLoading(false)
14
15
  const datas = formatter(response)
15
16
  setDataSource(datas)
16
17
  }
@@ -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 MySearchForm = ({ form, extra, search, options, loading, labelCol, setSearch, formItems, showLimit = 7, initialValues = {}, initialParams = {}, customReset, customFinish, renderAction, onValuesChange, syncUrlParams = true, defaultPageSize = 10 }) => {
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: search.pageSize ?? defaultPageSize, ...initialParams })
48
+ setSearch(prev => ({ pageNo: 1, pageSize: prev.pageSize ?? defaultPageSize, ...initialParams }))
30
49
  }
31
50
  }
32
51
 
33
52
  const handleFinish = values => {
34
- const formattedValues = { ...values }
35
- Object.keys(formattedValues).forEach(key => {
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({ ...search, ...formattedValues, pageNo: 1 })
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 = props => <DraggableContainer props={props} customSave={customSave} setDataSource={setDataSource} dataSourceRef={dataSourceRef} />
38
+ c.body.wrapper = DraggableContainer
37
39
  }
38
40
  return c
39
- }, [isEditable, draggable, customSave, setDataSource])
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.remote ? undefined : col.customFilter({ value, record, dataIndex: col.dataIndex }),
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, handleSave, editRender, renderAction])
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
- <Table
130
- {...restProps}
131
- rowKey={rowKey}
132
- scroll={scroll}
133
- loading={loading}
134
- onChange={onChange}
135
- columns={mergedColumns}
136
- dataSource={dataSource}
137
- size={size ?? 'middle'}
138
- components={components}
139
- rowSelection={rowSelection}
140
- pagination={paginationConfig}
141
- onRow={(record, index) => ({ record, index, draggable, columns: mergedColumns, ...(typeof lineFormChange === 'function' ? { onValuesChange: lineFormChange } : {}) })}
142
- rowClassName={(record, index) => {
143
- const externalClass = typeof rowClassName === 'function' ? rowClassName(record, index) : rowClassName
144
- const classes = [externalClass]
145
- if (isEditable) classes.push('editable-row')
146
- if (autoScroll && query?.rowIndex === index) classes.push('search-highlight-row')
147
- return classes.filter(Boolean).join(' ')
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, Form, Radio, Input, Upload, Select, Cascader, Checkbox, DatePicker, InputNumber, TreeSelect, AutoComplete } from 'antd'
4
+ import { Tree, Radio, Input, Upload, Select, Cascader, Checkbox, DatePicker, InputNumber, TreeSelect, AutoComplete } from 'antd'
4
5
 
5
- export const formNode = ({ item }) => {
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
- <Form.Item noStyle key={subItem.name}>
12
- <div style={{ flex: 1, minWidth: 0, marginRight: 8 }}><MyBaseForm item={subItem} key={subItem.name} /></div>
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
- </Form.Item>
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 componentDict = {
28
- date: <DatePicker {...commonProps} />,
29
- number: <InputNumber {...commonProps} />,
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 componentDict[item.type] ?? componentDict.default
81
+ return (
82
+ <Component {...commonProps} {...specialProps} {...item.props}>
83
+ {item.type === 'upload' && (item.content ?? '上传文件')}
84
+ </Component>
85
+ )
45
86
  }