@yorha2b-lab/autodev 7.7.8 → 7.8.1
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 +3 -1
- package/config.js +2 -0
- package/package.json +1 -1
- package/src/constitution/inspector.js +21 -0
- package/src/constitution/system.js +2 -8
- package/src/dialogues/EN-US.js +8 -0
- package/src/dialogues/FR-FR.js +8 -0
- package/src/dialogues/JA-JP.js +8 -0
- package/src/dialogues/RU-RU.js +8 -0
- package/src/dialogues/ZH-CN.js +8 -0
- package/src/gateway/client.js +1 -1
- package/src/gateway/index.js +17 -7
- package/src/labs/index.js +5 -1
- package/src/labs/inspector.js +183 -0
- package/src/labs/scout.js +0 -2
- package/src/logistics/formatter.js +32 -0
- package/src/units/striker.js +1 -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/stateBlock.hbs +2 -0
- package/src/workshop/react/builder/index.js +1 -1
- package/src/workshop/react/extensions/useTableQuery.js +4 -65
- package/src/workshop/react/kit/utils.js +51 -102
- package/src/workshop/react/standardParts/AliyunOSSUpload.js +0 -45
- package/src/workshop/react/standardParts/DraggableContainer.js +6 -2
- package/src/workshop/react/standardParts/EditableCell.js +36 -74
- package/src/workshop/react/standardParts/MyBaseForm.js +4 -51
- package/src/workshop/react/standardParts/MyDescription.js +3 -3
- package/src/workshop/react/standardParts/MyForm.js +48 -18
- package/src/workshop/react/standardParts/MyModalForm.js +4 -85
- package/src/workshop/react/standardParts/MyModalTable.js +2 -55
- package/src/workshop/react/standardParts/MyPopoverTable.js +24 -0
- package/src/workshop/react/standardParts/MySearchForm.js +5 -63
- package/src/workshop/react/standardParts/MyTable.js +68 -148
- package/src/workshop/react/standardParts/index.js +67 -39
|
@@ -1,35 +1,8 @@
|
|
|
1
1
|
import { useRef, useState, useEffect, useCallback } from 'react'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* 负责全自动管理表格的生命周期,包括数据抓取、状态同步、动态列指纹识别及“竞态病毒”物理免疫。
|
|
7
|
-
*
|
|
8
|
-
* @param {Object} params - 引擎配置包
|
|
9
|
-
* @param {Array} [params.cols] - 初始战备物资:前端预设的静态列配置
|
|
10
|
-
* @param {Function} params.api - 任务指令:必须返回 Promise 的数据抓取函数
|
|
11
|
-
* @param {Object} [params.initialParams={}] - 初始导航坐标:包含 pageNo, pageSize 及默认过滤参数
|
|
12
|
-
* @param {Function} params.formatResponse - 数据解码协议:将 API 原始信号转化为 { data, total, columns } 结构
|
|
13
|
-
*
|
|
14
|
-
* @returns {Object} 物理操作句柄包
|
|
15
|
-
* @returns {number} .total - 仓库物资总量
|
|
16
|
-
* @returns {boolean} .loading - 引擎负载状态
|
|
17
|
-
* @returns {Function} .setSearch - 坐标修改器
|
|
18
|
-
* @returns {Array} .dataSource - 已解压的物理物资列表
|
|
19
|
-
* @returns {Function} .setLoading - 负载状态手动干预句柄
|
|
20
|
-
* @returns {Object} .search - 当前导航参数(pageNo, pageSize 等)
|
|
21
|
-
* @returns {Function} .refresh - 强制重启协议:立即执行一次数据抓取
|
|
22
|
-
* @returns {Array} .columns - 当前生效的动态列配置(已通过指纹协议校验)
|
|
23
|
-
* @returns {Function} .setDataSource - 物资流手动修改句柄(用于行内编辑等乐观更新)
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* const { dataSource, loading, refresh } = useTableQuery({
|
|
27
|
-
* api:async params=>await fetchUsers(params),
|
|
28
|
-
* initialParams: { pageNo: 1, pageSize: 10 },
|
|
29
|
-
* formatResponse: (res) => ({ data: res.list, total: res.total })
|
|
30
|
-
* });
|
|
31
|
-
*/
|
|
32
|
-
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 }) => {
|
|
33
6
|
|
|
34
7
|
const [total, setTotal] = useState(0)
|
|
35
8
|
const [columns, setColumns] = useState(cols)
|
|
@@ -37,13 +10,8 @@ export const useTableQuery = ({ api, cols = [], initialParams = {}, formatRespon
|
|
|
37
10
|
const [dataSource, setDataSource] = useState([])
|
|
38
11
|
const [search, setSearch] = useState(initialParams)
|
|
39
12
|
|
|
40
|
-
// 💡 引用锁定协议:确保异步回调中永远能指向最新的指令地址
|
|
41
13
|
const apiRef = useRef(api)
|
|
42
|
-
// 💡 [核心防御] 信号 ID 计数器:用于物理拦截由于网络时延导致的“竞态病毒(Race Condition)”
|
|
43
14
|
const fetchIdRef = useRef(0)
|
|
44
|
-
// 💡 物理记录仪:记录上一次“真正”的过滤参数(不含分页)
|
|
45
|
-
const lastFilterRef = useRef('')
|
|
46
|
-
// 💡 [核心防御] 数据解码协议引用:确保异步回调中永远能指向最新的指令地址
|
|
47
15
|
const formatResponseRef = useRef(formatResponse)
|
|
48
16
|
|
|
49
17
|
useEffect(() => {
|
|
@@ -54,43 +22,15 @@ export const useTableQuery = ({ api, cols = [], initialParams = {}, formatRespon
|
|
|
54
22
|
formatResponseRef.current = formatResponse
|
|
55
23
|
}, [formatResponse])
|
|
56
24
|
|
|
57
|
-
/**
|
|
58
|
-
* @function getColumnSchema
|
|
59
|
-
* @description [指纹识别协议] 对列配置执行“结构化脱水”。
|
|
60
|
-
* 通过提取标题、字段和排序状态生成物理指纹,用于判定列结构是否发生“基因变异”。
|
|
61
|
-
* @param {Array} cols - 待扫描的列配置
|
|
62
|
-
* @returns {string} 物理指纹字符串
|
|
63
|
-
*/
|
|
64
|
-
const getColumnSchema = cols => cols.map(col => `${col.title}-${col.dataIndex}-${col.sortOrder}`).join('|')
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* @async
|
|
68
|
-
* @function fetchData
|
|
69
|
-
* @description [数据装配任务] 执行核心的数据抓取与物理封存逻辑。
|
|
70
|
-
* 具备信号溯源能力,确保只有最后一次发出的指令能修改地堡状态。
|
|
71
|
-
*/
|
|
72
25
|
const fetchData = useCallback(async () => {
|
|
73
26
|
if (!apiRef.current) return
|
|
74
|
-
|
|
75
|
-
if (isLocalPaging) {
|
|
76
|
-
const currentFilter = JSON.stringify(Object.fromEntries(Object.entries(search).filter(([k]) => !['pageNo', 'pageSize'].includes(k))))
|
|
77
|
-
if (lastFilterRef.current === currentFilter && dataSource.length > 0) {
|
|
78
|
-
return
|
|
79
|
-
}
|
|
80
|
-
lastFilterRef.current = currentFilter
|
|
81
|
-
}
|
|
82
|
-
|
|
83
27
|
setLoading(true)
|
|
84
|
-
// 💡 信号加标:为本次请求分配唯一的物理 ID
|
|
85
28
|
const currentFetchId = ++fetchIdRef.current
|
|
86
29
|
try {
|
|
87
30
|
const response = await apiRef.current(search)
|
|
88
|
-
// 💡 [物理拦截] 信号校准:若当前 ID 不是最新 ID,说明该信号已过期,执行丢弃协议
|
|
89
31
|
if (currentFetchId !== fetchIdRef.current) return
|
|
90
32
|
const { data, total, columns: newCols } = formatResponseRef.current?.(response ?? {}) || {}
|
|
91
|
-
// 💡 [性能装甲] 只有当列指纹发生变化时,才触发 React 的重绘逻辑
|
|
92
33
|
if (newCols) {
|
|
93
|
-
// 使用函数式状态更新,既能拿到最新的 prevCols,又无需将 columns 作为 useCallback 的依赖!
|
|
94
34
|
setColumns(prevCols => {
|
|
95
35
|
if (getColumnSchema(newCols) !== getColumnSchema(prevCols)) {
|
|
96
36
|
return newCols
|
|
@@ -103,12 +43,11 @@ export const useTableQuery = ({ api, cols = [], initialParams = {}, formatRespon
|
|
|
103
43
|
} catch (error) {
|
|
104
44
|
console.error('查询失败', error)
|
|
105
45
|
} finally {
|
|
106
|
-
// 💡 状态闭环:确保只有最新的信号能解除 Loading 锁定
|
|
107
46
|
if (currentFetchId === fetchIdRef.current) {
|
|
108
47
|
setLoading(false)
|
|
109
48
|
}
|
|
110
49
|
}
|
|
111
|
-
}, [search
|
|
50
|
+
}, [search])
|
|
112
51
|
|
|
113
52
|
useEffect(() => {
|
|
114
53
|
fetchData()
|
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
import dayjs from 'dayjs'
|
|
2
2
|
import { request } from './request'
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* @namespace BunkerUtils
|
|
6
|
-
* @description 地堡核心工具集:包含文件处理、数据脱水、视觉渲染及后勤传输协议;使用前确保已安装对应模块(如:xlsx、ali-oss、crypto-js)
|
|
7
|
-
* */
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @async
|
|
12
|
-
* @function parseExcel
|
|
13
|
-
* @description [后勤部] 本地 Excel 解析协议:将二进制表格物资转化为地堡可读的 JSON 序列。
|
|
14
|
-
* @param {File|Blob} file - 待解析的原始文件对象
|
|
15
|
-
* @returns {Promise<Array[]>} 解析后的二维数组数据
|
|
16
|
-
*/
|
|
17
4
|
export const parseExcel = async file => {
|
|
18
5
|
/* const arrayBuffer = await file.arrayBuffer()
|
|
19
6
|
const XLSX = require('xlsx')
|
|
@@ -24,14 +11,16 @@ export const parseExcel = async file => {
|
|
|
24
11
|
return excelData.filter(item => item.length > 0) */
|
|
25
12
|
}
|
|
26
13
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
14
|
+
export const flattenFormItems = items => {
|
|
15
|
+
if (!Array.isArray(items)) return []
|
|
16
|
+
return items.flatMap(item => {
|
|
17
|
+
if (item.childItems) {
|
|
18
|
+
return flattenFormItems(item.childItems)
|
|
19
|
+
}
|
|
20
|
+
return item
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
35
24
|
export const initOSS = async (url, options) => {
|
|
36
25
|
/* const OSS = require('ali-oss')
|
|
37
26
|
const crypto = require('crypto-js')
|
|
@@ -48,13 +37,6 @@ export const initOSS = async (url, options) => {
|
|
|
48
37
|
return { client, policy, signature,host: `https://${bucket}.${endpoint.split('//')[1]}`, ...response?.data } */
|
|
49
38
|
}
|
|
50
39
|
|
|
51
|
-
/**
|
|
52
|
-
* @function formatQuery
|
|
53
|
-
* @description [数据脱水] 搜索参数校准协议:根据零部件类型(如日期)自动执行类型转换(String -> Number)。
|
|
54
|
-
* @param {Array} formItems - 对应的零部件配置清单
|
|
55
|
-
* @param {Object} params - 原始表单/URL 传感器数据
|
|
56
|
-
* @returns {Object} 经过物理清洗、可直接用于 API 请求的参数包
|
|
57
|
-
*/
|
|
58
40
|
export const formatQuery = (params, formItems) => {
|
|
59
41
|
const cleanParams = {}
|
|
60
42
|
Object.keys(params).forEach(key => {
|
|
@@ -67,15 +49,6 @@ export const formatQuery = (params, formItems) => {
|
|
|
67
49
|
return cleanParams
|
|
68
50
|
}
|
|
69
51
|
|
|
70
|
-
/**
|
|
71
|
-
* @function timeRender
|
|
72
|
-
* @description [视觉渲染] 时间戳格式化:将冷冰冰的数字转化为人类可读的时间字符串。
|
|
73
|
-
* @param {Object} params - 渲染参数
|
|
74
|
-
* @param {number|string} params.time - 原始时间戳
|
|
75
|
-
* @param {boolean} [params.date] - 是否仅保留日期(YYYY-MM-DD)
|
|
76
|
-
* @param {boolean} [params.minute] - 是否精确到分钟(YYYY-MM-DD HH:mm)
|
|
77
|
-
* @returns {string} 格式化后的时间字符串
|
|
78
|
-
*/
|
|
79
52
|
export const timeRender = ({ time, date, minute }) => {
|
|
80
53
|
if (!time) {
|
|
81
54
|
return ''
|
|
@@ -90,16 +63,6 @@ export const timeRender = ({ time, date, minute }) => {
|
|
|
90
63
|
}
|
|
91
64
|
}
|
|
92
65
|
|
|
93
|
-
/**
|
|
94
|
-
* @async
|
|
95
|
-
* @function exportDataToExcel
|
|
96
|
-
* @description [物资外运] 数据表格导出协议:支持复杂的渲染逻辑回溯,将页面数据封存为 .xlsx 格式。
|
|
97
|
-
* @param {string} url - 数据源地址
|
|
98
|
-
* @param {Object} options - 请求参数
|
|
99
|
-
* @param {string} fileName - 导出的文件名
|
|
100
|
-
* @param {Function} [formatter] - 数据预处理回调
|
|
101
|
-
* @param {Array} columns - 表格列配置,支持 exportRender 优先级
|
|
102
|
-
*/
|
|
103
66
|
export const exportDataToExcel = async ({ url, options, columns, fileName, formatter }) => {
|
|
104
67
|
/* const response = await request(url, options)
|
|
105
68
|
const result = formatter ? formatter(response) : (response?.data ?? [])
|
|
@@ -125,16 +88,47 @@ export const exportDataToExcel = async ({ url, options, columns, fileName, forma
|
|
|
125
88
|
} */
|
|
126
89
|
}
|
|
127
90
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
91
|
+
export const arrayToTree = (list, levels, keys = { title: 'title', value: 'value', children: 'children' }) => {
|
|
92
|
+
const tree = []
|
|
93
|
+
const maps = levels.map(() => new Map())
|
|
94
|
+
list.forEach(item => {
|
|
95
|
+
let parent = tree
|
|
96
|
+
levels.forEach(({ code, name }, index) => {
|
|
97
|
+
const map = maps[index]
|
|
98
|
+
const id = item[code]
|
|
99
|
+
if (!map.has(id)) {
|
|
100
|
+
const node = { [keys.title]: item[name], [keys.value]: id, ...(index < levels.length - 1 ? { [keys.children]: [] } : {}) }
|
|
101
|
+
map.set(id, node)
|
|
102
|
+
parent.push(node)
|
|
103
|
+
}
|
|
104
|
+
const node = map.get(id)
|
|
105
|
+
parent = node[keys.children] ?? []
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
return tree
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const moneyRender = (value, { decimals = 2, currency = 'CNY', language = 'zh-CN', showSymbol = true } = {}) => {
|
|
112
|
+
if (value === null || value === undefined || value === '') return '-'
|
|
113
|
+
const number = Number(value)
|
|
114
|
+
if (isNaN(number)) return '-'
|
|
115
|
+
const options = showSymbol
|
|
116
|
+
? { style: 'currency', currency, minimumFractionDigits: decimals }
|
|
117
|
+
: { style: 'decimal', minimumFractionDigits: decimals, maximumFractionDigits: decimals }
|
|
118
|
+
return new Intl.NumberFormat(language, options).format(number)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const formatUnit = (value, { k = 1024, units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'], decimals = 2 } = {}) => {
|
|
122
|
+
if (value === null || value === undefined || isNaN(value) || value === 0) {
|
|
123
|
+
return `0 ${units[0] ?? ''}`
|
|
124
|
+
}
|
|
125
|
+
const absValue = Math.abs(value)
|
|
126
|
+
const i = absValue < 1 ? 0 : Math.floor(Math.log(absValue) / Math.log(k))
|
|
127
|
+
const index = Math.min(i, units.length - 1)
|
|
128
|
+
const result = (value / Math.pow(k, index)).toFixed(decimals)
|
|
129
|
+
return `${parseFloat(result)} ${units[index]}`
|
|
130
|
+
}
|
|
131
|
+
|
|
138
132
|
export const streamDownload = async ({ url, options, headers = { 'Content-Type': 'application/json' } }, fileName = '下载') => {
|
|
139
133
|
|
|
140
134
|
const response = await fetch(url, { ...options, headers })
|
|
@@ -151,7 +145,6 @@ export const streamDownload = async ({ url, options, headers = { 'Content-Type':
|
|
|
151
145
|
return
|
|
152
146
|
}
|
|
153
147
|
|
|
154
|
-
// 💡 语义侦察逻辑:检查前 10 个字节,判断是否为 JSON 报错信息
|
|
155
148
|
const slice = blob.slice(0, 10)
|
|
156
149
|
const text = (await slice.text()).trim()
|
|
157
150
|
|
|
@@ -166,7 +159,6 @@ export const streamDownload = async ({ url, options, headers = { 'Content-Type':
|
|
|
166
159
|
return
|
|
167
160
|
}
|
|
168
161
|
|
|
169
|
-
// 物理解析 Content-Disposition 头部,获取指挥官预设的文件名
|
|
170
162
|
const disposition = response.headers.get('content-disposition')
|
|
171
163
|
let name = fileName
|
|
172
164
|
|
|
@@ -188,47 +180,4 @@ export const streamDownload = async ({ url, options, headers = { 'Content-Type':
|
|
|
188
180
|
link.remove()
|
|
189
181
|
|
|
190
182
|
setTimeout(() => URL.revokeObjectURL(urlObj), 1000)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* @function moneyRender
|
|
195
|
-
* @description [视觉渲染] 金额校准渲染:支持多国语境、货币符号及精度控制。
|
|
196
|
-
* @param {number|string} value - 原始金额数值
|
|
197
|
-
* @param {Object} [options] - 配置参数
|
|
198
|
-
* @param {number} [options.decimals=2] - 小数位数
|
|
199
|
-
* @param {string} [options.currency='CNY'] - 货币代号
|
|
200
|
-
* @param {string} [options.language='zh-CN'] - 语言环境
|
|
201
|
-
* @param {boolean} [options.showSymbol=true] - 是否显示货币符号
|
|
202
|
-
* @returns {string} 格式化后的金额文本
|
|
203
|
-
*/
|
|
204
|
-
export const moneyRender = (value, { decimals = 2, currency = 'CNY', language = 'zh-CN', showSymbol = true } = {}) => {
|
|
205
|
-
if (value === null || value === undefined || value === '') return '-'
|
|
206
|
-
const number = Number(value)
|
|
207
|
-
if (isNaN(number)) return '-'
|
|
208
|
-
const options = showSymbol
|
|
209
|
-
? { style: 'currency', currency, minimumFractionDigits: decimals }
|
|
210
|
-
: { style: 'decimal', minimumFractionDigits: decimals, maximumFractionDigits: decimals }
|
|
211
|
-
return new Intl.NumberFormat(language, options).format(number)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* @function formatUnit
|
|
216
|
-
* @description [视觉渲染] 单位自动进阶协议:实现 B/KB/MB... 等数据的物理量级自动折算。
|
|
217
|
-
* @param {number} value - 原始字节数值
|
|
218
|
-
* @param {Object} [options] - 配置参数
|
|
219
|
-
* @param {Array} [options.units] - 单位阶梯
|
|
220
|
-
* @param {number} [options.k=1024] - 进位系数
|
|
221
|
-
* @param {number} [options.decimals=2] - 保留小数位
|
|
222
|
-
* @returns {string} 带单位的物理量级文本
|
|
223
|
-
*/
|
|
224
|
-
export const formatUnit = (value, { k = 1024, units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'], decimals = 2 } = {}) => {
|
|
225
|
-
if (value === null || value === undefined || isNaN(value) || value === 0) {
|
|
226
|
-
return `0 ${units[0] ?? ''}`
|
|
227
|
-
}
|
|
228
|
-
const absValue = Math.abs(value)
|
|
229
|
-
// 💡 安全屏障:小于 1 的数值直接归入初始级单位,不进行对数拆解
|
|
230
|
-
const i = absValue < 1 ? 0 : Math.floor(Math.log(absValue) / Math.log(k))
|
|
231
|
-
const index = Math.min(i, units.length - 1)
|
|
232
|
-
const result = (value / Math.pow(k, index)).toFixed(decimals)
|
|
233
|
-
return `${parseFloat(result)} ${units[index]}`
|
|
234
183
|
}
|
|
@@ -3,39 +3,12 @@ import { initOSS } from '../utils/utils'
|
|
|
3
3
|
import { useState, useEffect } from 'react'
|
|
4
4
|
import { UploadOutlined } from '@ant-design/icons'
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* @component AliyunOSSUpload
|
|
8
|
-
* @description [地堡核心传输单元] 阿里云 OSS 高级上传组件。
|
|
9
|
-
* 集成了“物理路径自动重组”、“STS 令牌自动续期”以及“ Antd 属性无损透传”三大核心协议。
|
|
10
|
-
*
|
|
11
|
-
* @param {Object} props - 构筑参数
|
|
12
|
-
* @param {string} [props.path] - 默认存储扇区:定义文件在云端的物理根路径
|
|
13
|
-
* @param {string} [props.url] - 司令部通信地址:用于申请最新的物理传输令牌 (STS)
|
|
14
|
-
* @param {Array} props.value - 传感器当前封存的文件列表 (符合 Antd fileList 规范)
|
|
15
|
-
* @param {Function} props.onChange - 状态反馈协议:当物理文件状态变更时同步至地堡中枢
|
|
16
|
-
* @param {Object} [props.oss] - 外部空投的初始凭证包。若缺失,组件将启动自主初始化流程。
|
|
17
|
-
* @param {Object} [props.options] - 通信加密/配置参数:在申请令牌时透传给 request 的额外负载
|
|
18
|
-
* @param {Object} [props.originUploadProps] - [重要] 无损透传:支持 Antd Upload 的所有原生属性(如 multiple, accept 等)
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* // 基本构筑
|
|
22
|
-
* <AliyunOSSUpload path='project/data' url='/api/oss/token' />
|
|
23
|
-
*
|
|
24
|
-
* // 高级装配(限制文件类型并开启多选)
|
|
25
|
-
* <AliyunOSSUpload accept='.pdf' multiple={true} path='docs' url='/api/oss/token' />
|
|
26
|
-
*/
|
|
27
6
|
export const AliyunOSSUpload = ({ value, onChange, ...restProps }) => {
|
|
28
7
|
|
|
29
|
-
// 💡 物理剥离:将地堡专用参数与 Antd 原生属性分离
|
|
30
8
|
const { oss, url, path, options, expireField = 'expire', ...originUploadProps } = restProps
|
|
31
9
|
|
|
32
10
|
const [OSSData, setOSSData] = useState(oss)
|
|
33
11
|
|
|
34
|
-
/**
|
|
35
|
-
* @async
|
|
36
|
-
* @function init
|
|
37
|
-
* @description [信号握手] 执行令牌获取协议,从司令部申请最新的物理上传授权
|
|
38
|
-
*/
|
|
39
12
|
const init = async () => {
|
|
40
13
|
try {
|
|
41
14
|
const result = await initOSS(url, options)
|
|
@@ -47,7 +20,6 @@ export const AliyunOSSUpload = ({ value, onChange, ...restProps }) => {
|
|
|
47
20
|
}
|
|
48
21
|
|
|
49
22
|
useEffect(() => {
|
|
50
|
-
// 💡 自愈逻辑:如果发现能源包 (OSSData) 为空,则自动触发初始化
|
|
51
23
|
if (!OSSData) {
|
|
52
24
|
init()
|
|
53
25
|
}
|
|
@@ -60,11 +32,6 @@ export const AliyunOSSUpload = ({ value, onChange, ...restProps }) => {
|
|
|
60
32
|
onChange?.(files)
|
|
61
33
|
}
|
|
62
34
|
|
|
63
|
-
/**
|
|
64
|
-
* @function getExtraData
|
|
65
|
-
* @description [数据封装] 将动态生成的物理凭证与目标文件路径压入传输负载
|
|
66
|
-
* @param {Object} file - 待传输的文件对象
|
|
67
|
-
*/
|
|
68
35
|
const getExtraData = file => ({
|
|
69
36
|
key: file.url,
|
|
70
37
|
policy: OSSData?.policy,
|
|
@@ -73,28 +40,16 @@ export const AliyunOSSUpload = ({ value, onChange, ...restProps }) => {
|
|
|
73
40
|
'x-oss-security-token': OSSData?.stsToken,
|
|
74
41
|
})
|
|
75
42
|
|
|
76
|
-
/**
|
|
77
|
-
* @async
|
|
78
|
-
* @function beforeUpload
|
|
79
|
-
* @description [生存期侦察] 在传输启动前执行 TTL 检测。
|
|
80
|
-
* 若令牌已失效(过期),则强行拦截并执行同步刷新协议,同时执行物理路径归一化。
|
|
81
|
-
*/
|
|
82
43
|
const beforeUpload = async file => {
|
|
83
44
|
let currentOSS = OSSData
|
|
84
45
|
const expire = Number(OSSData?.[expireField] ?? 0) * 1000
|
|
85
46
|
if (expire < Date.now()) {
|
|
86
47
|
currentOSS = await init()
|
|
87
48
|
}
|
|
88
|
-
// 💡 物理路径重组:通过正则清除多余的路径分隔符,确保存储节点坐标唯一
|
|
89
49
|
file.url = `${currentOSS?.dir ?? path}/${file.uid}_${file.name}`.replace(/\/\//g, '/')
|
|
90
50
|
return file
|
|
91
51
|
}
|
|
92
52
|
|
|
93
|
-
/**
|
|
94
|
-
* @constant uploadProps
|
|
95
|
-
* @description [物理合并] 最终生成的 Antd Upload 指令集:
|
|
96
|
-
* 自动优先应用 originUploadProps 中的自定义设置。
|
|
97
|
-
*/
|
|
98
53
|
const uploadProps = {
|
|
99
54
|
onRemove,
|
|
100
55
|
name: 'file',
|
|
@@ -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,14 +11,16 @@ 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) {
|
|
16
20
|
const currentData = dataSourceRef?.current || []
|
|
17
21
|
const sortedDataSource = arrayMoveImmutable(currentData.slice(), oldIndex, newIndex)
|
|
18
22
|
setDataSource(sortedDataSource)
|
|
19
|
-
customSave?.(sortedDataSource)
|
|
23
|
+
customSave?.({}, sortedDataSource)
|
|
20
24
|
}
|
|
21
25
|
}
|
|
22
26
|
|
|
@@ -1,37 +1,26 @@
|
|
|
1
1
|
import { SortableElement } from 'react-sortable-hoc'
|
|
2
|
-
import { Form, Input, InputNumber, Select, Checkbox } from 'antd'
|
|
3
|
-
import
|
|
2
|
+
import { Form, Input, InputNumber, Select, Checkbox, TreeSelect } from 'antd'
|
|
3
|
+
import { useRef, useState, useEffect, useContext, createContext } from 'react'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
* @constant EditableContext
|
|
7
|
-
* @description [地堡跨组件通信协议] 用于在行(Row)与单元格(Cell)之间共享 Form 传感器的物理句柄。
|
|
8
|
-
*/
|
|
9
|
-
const EditableContext = React.createContext(null)
|
|
5
|
+
const EditableContext = createContext(null)
|
|
10
6
|
|
|
11
7
|
const SortableItem = SortableElement((props) => <tr {...props} />)
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
* @component EditableRow
|
|
15
|
-
* @description [地堡战术行容器] 每一行都是一个独立的逻辑闭环。
|
|
16
|
-
* 负责初始化 Form 实例并执行“行级数据同步”与“变动传感监听”。
|
|
17
|
-
*
|
|
18
|
-
* @param {Object} props - 组件属性
|
|
19
|
-
* @param {number} props.index - 行物理索引
|
|
20
|
-
* @param {Object} props.record - 初始物资包(当前行数据)
|
|
21
|
-
* @param {Function} [props.onValuesChange] - 联动传感:监听行内任何零部件的值变动
|
|
22
|
-
*/
|
|
23
|
-
export const EditableRow = ({ index, record, draggable, ...props }) => {
|
|
9
|
+
export const EditableRow = ({ index, record, columns, draggable, ...props }) => {
|
|
24
10
|
|
|
25
11
|
const [form] = Form.useForm()
|
|
26
12
|
|
|
27
|
-
/**
|
|
28
|
-
* @description [数据热更协议] 当外部记录发生变化时,物理同步至内部表单传感器。
|
|
29
|
-
*/
|
|
30
13
|
useEffect(() => {
|
|
31
14
|
if (record) {
|
|
32
|
-
|
|
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
|
+
})
|
|
21
|
+
form.setFieldsValue({ ...record, ...extra })
|
|
33
22
|
}
|
|
34
|
-
}, [record])
|
|
23
|
+
}, [record, columns, form])
|
|
35
24
|
|
|
36
25
|
return (
|
|
37
26
|
<Form form={form} component={false} onValuesChange={(changedValue, allValue) => props?.onValuesChange?.({ changedValue, allValue, form })}>
|
|
@@ -42,34 +31,14 @@ export const EditableRow = ({ index, record, draggable, ...props }) => {
|
|
|
42
31
|
)
|
|
43
32
|
}
|
|
44
33
|
|
|
45
|
-
|
|
46
|
-
* @component EditableCell
|
|
47
|
-
* @description [地堡精密编辑单元] 最小物理作战单位。
|
|
48
|
-
* 支持多种装配模式(text/number/select/checkbox),并具备“自动对焦”与“失焦自愈(自动保存)”功能。
|
|
49
|
-
*
|
|
50
|
-
* @param {Object} props - 构筑参数
|
|
51
|
-
* @param {Array} [props.rules=[]] - 逻辑校验准则
|
|
52
|
-
* @param {Object} props.record - 当前行原始物资数据
|
|
53
|
-
* @param {boolean} props.disabled - 物理锁定:是否禁止修改
|
|
54
|
-
* @param {string} props.title - 零部件视觉标题,兼作占位符使用
|
|
55
|
-
* @param {React.ReactNode} props.children - 默认展示的视觉节点
|
|
56
|
-
* @param {boolean} props.editable - 权限协议:是否允许开启编辑模式
|
|
57
|
-
* @param {string} props.dataIndex - 逻辑寻址地址:对应后端数据的字段名
|
|
58
|
-
* @param {Array} [props.options=[]] - 数据字典(仅用于 select/checkbox)
|
|
59
|
-
* @param {Function} props.handleSave - 物理封存协议:数据校验通过后的保存回调
|
|
60
|
-
* @param {boolean} [props.defaultEdit] - 持久化模式:是否默认始终开启编辑状态
|
|
61
|
-
* @param {string} [props.editType='text'] - 零部件型号(text/number/select/checkbox)
|
|
62
|
-
*/
|
|
63
|
-
export const EditableCell = ({ title, record, editable, disabled, children, dataIndex, placeholder, handleSave, defaultEdit, editRender, rules = [], options = [], editType = 'text', ...restProps }) => {
|
|
34
|
+
export const EditableCell = ({ mode, title, record, rowKey, editMode, editable, disabled, children, dataIndex, editingKey, dependencies, placeholder, handleSave, defaultEdit, editRender, rules = [], options = [], editType = 'text', ...restProps }) => {
|
|
64
35
|
|
|
65
36
|
const inputRef = useRef(null)
|
|
37
|
+
const editingKeyRef = useRef(editingKey)
|
|
66
38
|
const form = useContext(EditableContext)
|
|
67
39
|
|
|
68
40
|
const [editing, setEditing] = useState(false)
|
|
69
41
|
|
|
70
|
-
/**
|
|
71
|
-
* @description [自动对焦协议] 当单元格进入“骇入模式”时,光标自动锁定,提升作战效率。
|
|
72
|
-
*/
|
|
73
42
|
useEffect(() => {
|
|
74
43
|
if (editing) {
|
|
75
44
|
if (inputRef.current && typeof inputRef.current.focus === 'function') {
|
|
@@ -78,23 +47,24 @@ export const EditableCell = ({ title, record, editable, disabled, children, data
|
|
|
78
47
|
}
|
|
79
48
|
}, [editing])
|
|
80
49
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (editingKeyRef.current !== editingKey) {
|
|
52
|
+
editingKeyRef.current = editingKey
|
|
53
|
+
inputRef.current?.blur()
|
|
54
|
+
}
|
|
55
|
+
}, [editingKey])
|
|
56
|
+
|
|
85
57
|
const toggleEdit = () => {
|
|
86
|
-
setEditing(!
|
|
87
|
-
form.setFieldsValue({ [dataIndex]: record[dataIndex] })
|
|
58
|
+
setEditing(prev => !prev)
|
|
59
|
+
form.setFieldsValue({ [dataIndex]: mode ? record[dataIndex]?.toString()?.split(',') || [] : record[dataIndex] })
|
|
88
60
|
}
|
|
89
61
|
|
|
90
|
-
/**
|
|
91
|
-
* @async
|
|
92
|
-
* @function save
|
|
93
|
-
* @description 执行“物理封存协议”:触发表单校验,通过后自动关闭编辑状态并向中枢发送保存请求。
|
|
94
|
-
*/
|
|
95
62
|
const save = async () => {
|
|
96
63
|
try {
|
|
97
64
|
const values = await form.validateFields()
|
|
65
|
+
if (mode) {
|
|
66
|
+
values[dataIndex] = values[dataIndex]?.join(',')
|
|
67
|
+
}
|
|
98
68
|
if (!defaultEdit) {
|
|
99
69
|
toggleEdit()
|
|
100
70
|
}
|
|
@@ -108,6 +78,7 @@ export const EditableCell = ({ title, record, editable, disabled, children, data
|
|
|
108
78
|
|
|
109
79
|
const formProps = {
|
|
110
80
|
rules,
|
|
81
|
+
dependencies,
|
|
111
82
|
name: dataIndex,
|
|
112
83
|
style: { margin: 0 },
|
|
113
84
|
}
|
|
@@ -119,22 +90,21 @@ export const EditableCell = ({ title, record, editable, disabled, children, data
|
|
|
119
90
|
}
|
|
120
91
|
|
|
121
92
|
switch (editType) {
|
|
122
|
-
case 'select':
|
|
123
|
-
inputNode = <Select ref={inputRef} options={options} {...commonProps} />
|
|
124
|
-
break;
|
|
125
93
|
case 'number':
|
|
126
94
|
inputNode = <InputNumber ref={inputRef} onPressEnter={save} {...commonProps} />
|
|
127
95
|
break
|
|
96
|
+
case 'select':
|
|
97
|
+
inputNode = <Select ref={inputRef} mode={mode} options={options} {...commonProps} />
|
|
98
|
+
break;
|
|
99
|
+
case 'treeSelect':
|
|
100
|
+
inputNode = <TreeSelect ref={inputRef} treeData={options} {...commonProps} />
|
|
101
|
+
break;
|
|
128
102
|
case 'custom':
|
|
129
103
|
inputNode = editRender({ form, record, save, dataIndex, disabled, toggleEdit, commonProps })
|
|
130
104
|
break
|
|
131
105
|
case 'checkbox':
|
|
132
106
|
inputNode = (
|
|
133
107
|
<div tabIndex={-1} onBlur={e => {
|
|
134
|
-
/**
|
|
135
|
-
* 💡 [地堡特有防御逻辑]:解决 Checkbox.Group 内部焦点切换导致误触发 save 的问题。
|
|
136
|
-
* 通过 contains 判定焦点是否真正离开了当前作战区域。
|
|
137
|
-
*/
|
|
138
108
|
if (!e.currentTarget.contains(e.relatedTarget)) {
|
|
139
109
|
save()
|
|
140
110
|
}
|
|
@@ -152,17 +122,9 @@ export const EditableCell = ({ title, record, editable, disabled, children, data
|
|
|
152
122
|
return (
|
|
153
123
|
<td {...restProps}>
|
|
154
124
|
{editable ? (
|
|
155
|
-
editing || defaultEdit ?
|
|
156
|
-
|
|
157
|
-
<
|
|
158
|
-
{inputNode}
|
|
159
|
-
</Form.Item>
|
|
160
|
-
) : (
|
|
161
|
-
// 观察模式:渲染静态内容,点击开启骇入
|
|
162
|
-
<div onClick={toggleEdit} className='editable-cell-value-wrap' style={{ paddingRight: 24, minHeight: 32, cursor: 'pointer' }}>
|
|
163
|
-
{children}
|
|
164
|
-
</div>
|
|
165
|
-
)
|
|
125
|
+
editing || defaultEdit || record[rowKey] === editingKey ?
|
|
126
|
+
<Form.Item {...(['custom', 'checkbox'].includes(editType) ? { style: formProps.style } : formProps)}>{inputNode}</Form.Item> :
|
|
127
|
+
<div {...(editMode === 'cellEdit' ? { onClick: toggleEdit, className: 'editable-cell-value-wrap' } : {})} style={{ paddingRight: 24, minHeight: 32, cursor: 'pointer' }}>{children}</div>
|
|
166
128
|
) : (children)}
|
|
167
129
|
</td>
|
|
168
130
|
)
|