@yorha2b-lab/autodev 2.1.22 → 2.2.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/package.json +1 -1
- package/templates/react/components/EditableCell.js +53 -1
- package/templates/react/components/MyBaseForm.js +47 -1
- package/templates/react/components/MyModalForm.js +57 -1
- package/templates/react/components/MyModalTable.js +59 -3
- package/templates/react/components/MySearchForm.js +58 -2
- package/templates/react/components/MyTable.js +65 -0
- package/templates/react/components/index.js +60 -0
- package/templates/react/handlebars/MyTable.hbs +3 -0
- package/templates/react/handlebars/handleBlock.hbs +4 -0
- package/templates/react/handlebars/hookBlock.hbs +8 -2
- package/templates/react/handlebars/stateBlock.hbs +2 -1
- package/templates/react/hooks/useTableQuery.js +48 -0
- package/templates/react/utils/utils.js +80 -2
package/package.json
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import { Form, Input, InputNumber, Select, Checkbox } from 'antd'
|
|
2
2
|
import React, { useRef, useState, useEffect, useContext } from 'react'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @constant EditableContext
|
|
6
|
+
* @description [地堡跨组件通信协议] 用于在行(Row)与单元格(Cell)之间共享 Form 传感器的物理句柄。
|
|
7
|
+
*/
|
|
4
8
|
const EditableContext = React.createContext(null)
|
|
5
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @component EditableRow
|
|
12
|
+
* @description [地堡战术行容器] 每一行都是一个独立的逻辑闭环。
|
|
13
|
+
* 负责初始化 Form 实例并执行“行级数据同步”与“变动传感监听”。
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} props - 组件属性
|
|
16
|
+
* @param {number} props.index - 行物理索引
|
|
17
|
+
* @param {Object} props.record - 初始物资包(当前行数据)
|
|
18
|
+
* @param {Function} [props.onValuesChange] - 联动传感:监听行内任何零部件的值变动
|
|
19
|
+
*/
|
|
6
20
|
export const EditableRow = ({ index, record, ...props }) => {
|
|
7
21
|
|
|
8
22
|
const [form] = Form.useForm()
|
|
9
23
|
|
|
24
|
+
/**
|
|
25
|
+
* @description [数据热更协议] 当外部记录发生变化时,物理同步至内部表单传感器。
|
|
26
|
+
*/
|
|
10
27
|
useEffect(() => {
|
|
11
28
|
if (record) {
|
|
12
29
|
form.setFieldsValue(record)
|
|
@@ -22,6 +39,24 @@ export const EditableRow = ({ index, record, ...props }) => {
|
|
|
22
39
|
)
|
|
23
40
|
}
|
|
24
41
|
|
|
42
|
+
/**
|
|
43
|
+
* @component EditableCell
|
|
44
|
+
* @description [地堡精密编辑单元] 最小物理作战单位。
|
|
45
|
+
* 支持多种装配模式(text/number/select/checkbox),并具备“自动对焦”与“失焦自愈(自动保存)”功能。
|
|
46
|
+
*
|
|
47
|
+
* @param {Object} props - 构筑参数
|
|
48
|
+
* @param {string} props.title - 零部件视觉标题,兼作占位符使用
|
|
49
|
+
* @param {Object} props.record - 当前行原始物资数据
|
|
50
|
+
* @param {boolean} props.editable - 权限协议:是否允许开启编辑模式
|
|
51
|
+
* @param {boolean} props.disabled - 物理锁定:是否禁止修改
|
|
52
|
+
* @param {string} props.dataIndex - 逻辑寻址地址:对应后端数据的字段名
|
|
53
|
+
* @param {Function} props.handleSave - 物理封存协议:数据校验通过后的保存回调
|
|
54
|
+
* @param {boolean} [props.defaultEdit] - 持久化模式:是否默认始终开启编辑状态
|
|
55
|
+
* @param {Array} [props.rules=[]] - 逻辑校验准则
|
|
56
|
+
* @param {Array} [props.options=[]] - 数据字典(仅用于 select/checkbox)
|
|
57
|
+
* @param {string} [props.editType='text'] - 零部件型号(text/number/select/checkbox)
|
|
58
|
+
* @param {React.ReactNode} props.children - 默认展示的视觉节点
|
|
59
|
+
*/
|
|
25
60
|
export const EditableCell = ({ title, record, editable, disabled, children, dataIndex, placeholder, handleSave, defaultEdit, rules = [], options = [], editType = 'text', ...restProps }) => {
|
|
26
61
|
|
|
27
62
|
const inputRef = useRef(null)
|
|
@@ -29,6 +64,9 @@ export const EditableCell = ({ title, record, editable, disabled, children, data
|
|
|
29
64
|
|
|
30
65
|
const [editing, setEditing] = useState(false)
|
|
31
66
|
|
|
67
|
+
/**
|
|
68
|
+
* @description [自动对焦协议] 当单元格进入“骇入模式”时,光标自动锁定,提升作战效率。
|
|
69
|
+
*/
|
|
32
70
|
useEffect(() => {
|
|
33
71
|
if (editing) {
|
|
34
72
|
if (inputRef.current && typeof inputRef.current.focus === 'function') {
|
|
@@ -37,11 +75,20 @@ export const EditableCell = ({ title, record, editable, disabled, children, data
|
|
|
37
75
|
}
|
|
38
76
|
}, [editing])
|
|
39
77
|
|
|
78
|
+
/**
|
|
79
|
+
* @function toggleEdit
|
|
80
|
+
* @description 切换“观察/骇入”模式,并执行初始信号同步。
|
|
81
|
+
*/
|
|
40
82
|
const toggleEdit = () => {
|
|
41
83
|
setEditing(!editing)
|
|
42
84
|
form.setFieldsValue({ [dataIndex]: record[dataIndex] })
|
|
43
85
|
}
|
|
44
86
|
|
|
87
|
+
/**
|
|
88
|
+
* @async
|
|
89
|
+
* @function save
|
|
90
|
+
* @description 执行“物理封存协议”:触发表单校验,通过后自动关闭编辑状态并向中枢发送保存请求。
|
|
91
|
+
*/
|
|
45
92
|
const save = async () => {
|
|
46
93
|
try {
|
|
47
94
|
const values = await form.validateFields()
|
|
@@ -78,7 +125,10 @@ export const EditableCell = ({ title, record, editable, disabled, children, data
|
|
|
78
125
|
case 'checkbox':
|
|
79
126
|
inputNode = (
|
|
80
127
|
<div tabIndex={-1} onBlur={e => {
|
|
81
|
-
|
|
128
|
+
/**
|
|
129
|
+
* 💡 [地堡特有防御逻辑]:解决 Checkbox.Group 内部焦点切换导致误触发 save 的问题。
|
|
130
|
+
* 通过 contains 判定焦点是否真正离开了当前作战区域。
|
|
131
|
+
*/
|
|
82
132
|
if (!e.currentTarget.contains(e.relatedTarget)) {
|
|
83
133
|
save()
|
|
84
134
|
}
|
|
@@ -97,10 +147,12 @@ export const EditableCell = ({ title, record, editable, disabled, children, data
|
|
|
97
147
|
<td {...restProps}>
|
|
98
148
|
{editable ? (
|
|
99
149
|
editing || defaultEdit ? (
|
|
150
|
+
// 骇入模式:渲染输入零件
|
|
100
151
|
<Form.Item {...(['checkbox'].includes(editType) ? { style: formProps.style } : formProps)}>
|
|
101
152
|
{inputNode}
|
|
102
153
|
</Form.Item>
|
|
103
154
|
) : (
|
|
155
|
+
// 观察模式:渲染静态内容,点击开启骇入
|
|
104
156
|
<div onClick={toggleEdit} className='editable-cell-value-wrap' style={{ paddingRight: 24, minHeight: 32, cursor: 'pointer' }}>
|
|
105
157
|
{children}
|
|
106
158
|
</div>
|
|
@@ -3,6 +3,13 @@ import { formNode } from './index'
|
|
|
3
3
|
import { Form, Button } from 'antd'
|
|
4
4
|
import { PlusOutlined } from '@ant-design/icons'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @function getValueFromEvent
|
|
8
|
+
* @description [数据拦截协议] 专门针对上传类组件的物理补丁。
|
|
9
|
+
* 修正 Antd 默认将 Event 对象存入 Form 的行为,强制提取物理文件列表 (fileList)。
|
|
10
|
+
* @param {Object|Array} e - 原始触发事件或文件数组
|
|
11
|
+
* @returns {Array} 归一化后的文件列表
|
|
12
|
+
*/
|
|
6
13
|
const getValueFromEvent = e => {
|
|
7
14
|
if (Array.isArray(e)) {
|
|
8
15
|
return e
|
|
@@ -10,12 +17,24 @@ const getValueFromEvent = e => {
|
|
|
10
17
|
return e?.fileList
|
|
11
18
|
}
|
|
12
19
|
|
|
20
|
+
/**
|
|
21
|
+
* @function renderFormContent
|
|
22
|
+
* @description [中枢渲染逻辑] 负责装配 Form.Item 的内核。
|
|
23
|
+
* 具备“路径递归”与“单位挂件”两大核心功能。
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} params - 渲染参数
|
|
26
|
+
* @param {Object} params.item - 零部件配置对象
|
|
27
|
+
* @param {Array} [params.prefixName=[]] - 物理路径前缀,用于支持 Form.List 的深层嵌套
|
|
28
|
+
* @returns {React.ReactNode} 装配完成的输入单元
|
|
29
|
+
*/
|
|
13
30
|
const renderFormContent = ({ item, prefixName = [] }) => {
|
|
14
31
|
|
|
32
|
+
// 💡 物理占位逻辑:若无 name 属性,则直接输出原始 value 文本
|
|
15
33
|
if (!item.name) {
|
|
16
34
|
return item.value
|
|
17
35
|
}
|
|
18
36
|
|
|
37
|
+
// 💡 路径自动对齐:支持字符串或数组格式的 name,自动合并前缀实现“逻辑寻址”
|
|
19
38
|
const namePath = Array.isArray(prefixName) ? [...prefixName, ...(Array.isArray(item.name) ? item.name : [item.name])] : item.name
|
|
20
39
|
|
|
21
40
|
const inputNode = (
|
|
@@ -24,6 +43,7 @@ const renderFormContent = ({ item, prefixName = [] }) => {
|
|
|
24
43
|
</Form.Item>
|
|
25
44
|
)
|
|
26
45
|
|
|
46
|
+
// 💡 视觉单位挂件逻辑:实现类似“金额(元)”的自动横向排版
|
|
27
47
|
if (item.unit) {
|
|
28
48
|
return (
|
|
29
49
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
@@ -36,22 +56,43 @@ const renderFormContent = ({ item, prefixName = [] }) => {
|
|
|
36
56
|
return inputNode
|
|
37
57
|
}
|
|
38
58
|
|
|
39
|
-
|
|
59
|
+
/**
|
|
60
|
+
* @component MyBaseForm
|
|
61
|
+
* @description [地堡通用零部件单元] 表单系统的物理外壳。
|
|
62
|
+
* 具备“单兵作战(Item)”与“集群协同(List)”两种模式切换能力。
|
|
63
|
+
*
|
|
64
|
+
* @param {Object} props - 组件属性
|
|
65
|
+
* @param {Object} props.item - 构筑协议对象
|
|
66
|
+
* @param {boolean} [props.item.isList] - 模式切换:是否开启动态增减列表模式
|
|
67
|
+
* @param {Function} [props.item.render] - 逻辑劫持:支持自定义渲染函数,可完全改写渲染行为
|
|
68
|
+
* @param {Object} props.form - Antd Form 实例,用于实现复杂的跨组件联动
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* // 模式 A:标准表单项
|
|
72
|
+
* <MyBaseForm item={{ label: '姓名', name: 'name' }} />
|
|
73
|
+
*
|
|
74
|
+
* // 模式 B:动态列表项
|
|
75
|
+
* <MyBaseForm item={{ isList: true, name: 'users', render: (field) => <Input {...field} /> }} />
|
|
76
|
+
*/
|
|
40
77
|
export const MyBaseForm = ({ item, form }) => {
|
|
41
78
|
return (
|
|
42
79
|
item.isList ?
|
|
80
|
+
// 💡 集群模式:处理动态数组类型的表单构筑
|
|
43
81
|
<Form.List name={item.name} initialValue={item.value}>
|
|
44
82
|
{(fields, { add, remove }) => (
|
|
45
83
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
46
84
|
{fields.map((field, index) => (
|
|
47
85
|
<Fragment key={field.key}>
|
|
86
|
+
{/* 物理映射:将 field 控制器回传给指挥官自定义的 render 逻辑 */}
|
|
48
87
|
{item.render(field, index, { add, remove }, form, renderFormContent)}
|
|
49
88
|
</Fragment>
|
|
50
89
|
))}
|
|
90
|
+
{/* 零信号兜底:若列表为空,显示添加按钮 */}
|
|
51
91
|
{fields.length === 0 && <Button type='dashed' onClick={() => add()} block icon={<PlusOutlined />}>添加{item.label || '项'}</Button>}
|
|
52
92
|
</div>
|
|
53
93
|
)}
|
|
54
94
|
</Form.List> :
|
|
95
|
+
// 💡 单兵模式:标准 Form.Item 构筑
|
|
55
96
|
<Form.Item
|
|
56
97
|
label={item.label}
|
|
57
98
|
extra={item.extra}
|
|
@@ -61,6 +102,11 @@ export const MyBaseForm = ({ item, form }) => {
|
|
|
61
102
|
required={item.required ?? !!item.rules}
|
|
62
103
|
style={{ marginBottom: !item.name ? 0 : undefined, ...item.style }}
|
|
63
104
|
>
|
|
105
|
+
{/*
|
|
106
|
+
逻辑优先级:
|
|
107
|
+
1. 优先使用 item.render 执行语义劫持
|
|
108
|
+
2. 否则使用 renderFormContent 进行标准构筑
|
|
109
|
+
*/}
|
|
64
110
|
{item.render ? (typeof item.render === 'function' ? item.render(item, form) : item.render) : renderFormContent({ item })}
|
|
65
111
|
</Form.Item>
|
|
66
112
|
)
|
|
@@ -3,11 +3,40 @@ import { MyBaseForm } from './MyBaseForm'
|
|
|
3
3
|
import { useState, useEffect } from 'react'
|
|
4
4
|
import { Row, Col, Form, Modal, } from 'antd'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @component MyModalForm
|
|
8
|
+
* @description [地堡核心构筑舱] 通用弹窗表单组件。
|
|
9
|
+
* 集成了“语义回显自愈”、“自动时间戳转换”及“原子级提交锁定”三大核心协议。
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} props - 构筑参数
|
|
12
|
+
* @param {string|number} [props.width] - 舱体物理宽度
|
|
13
|
+
* @param {string} [props.title] - 舱体视觉标题
|
|
14
|
+
* @param {Function} props.submit - 数据发射协议:提交表单后的核心回调函数
|
|
15
|
+
* @param {Object} [props.record] - 初始物资包:用于编辑模式的数据回显
|
|
16
|
+
* @param {boolean} props.visible - 激活信号:控制弹窗的物理显示状态
|
|
17
|
+
* @param {Function} props.setModal - 指令中心:用于更新弹窗状态(开启/关闭)
|
|
18
|
+
* @param {Array} props.formItems - 零部件清单:定义表单内部的输入单元
|
|
19
|
+
* @param {Object} [props.labelCol] - 标签布局校准
|
|
20
|
+
* @param {Object} [props.wrapperCol] - 控件布局校准
|
|
21
|
+
* @param {Function} [props.onValuesChange] - 联动传感:捕获表单内部的信号波动
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* <MyModalForm
|
|
25
|
+
* title="构筑新模块"
|
|
26
|
+
* visible={visible}
|
|
27
|
+
* formItems={[{ label: '截止日期', name: 'deadline', type: 'date' }]}
|
|
28
|
+
* submit={async (vals) => await save(vals)}
|
|
29
|
+
* />
|
|
30
|
+
*/
|
|
6
31
|
export const MyModalForm = ({ width, title, submit, record, visible, setModal, labelCol, formItems, wrapperCol, onValuesChange }) => {
|
|
7
32
|
|
|
8
33
|
const [form] = Form.useForm()
|
|
9
34
|
const [pending, setPending] = useState(false)
|
|
10
35
|
|
|
36
|
+
/**
|
|
37
|
+
* @description [数据回显协议] 当构筑舱开启时,自动对初始物资进行“语义格式化”。
|
|
38
|
+
* 将后端传输的字符串/数字时间戳重新转化为地堡可读的 `dayjs` 对象。
|
|
39
|
+
*/
|
|
11
40
|
useEffect(() => {
|
|
12
41
|
if (visible) {
|
|
13
42
|
form.resetFields()
|
|
@@ -16,12 +45,16 @@ export const MyModalForm = ({ width, title, submit, record, visible, setModal, l
|
|
|
16
45
|
const initialData = {}
|
|
17
46
|
Object.entries(record).forEach(([key, value]) => {
|
|
18
47
|
const config = itemMap.get(key)
|
|
48
|
+
// 💡 语义识别:若零部件类型为日期且数值存在,执行物理转化
|
|
19
49
|
if (config?.type?.includes('date') && value) {
|
|
20
50
|
if (Array.isArray(value)) {
|
|
51
|
+
// 处理范围日期
|
|
21
52
|
initialData[key] = value.map(v => dayjs(v))
|
|
22
53
|
} else if (typeof value === 'string' && value.includes(',')) {
|
|
54
|
+
// 处理逗号分隔的字符串日期
|
|
23
55
|
initialData[key] = value.split(',').map(v => dayjs(v))
|
|
24
56
|
} else {
|
|
57
|
+
// 处理单日期
|
|
25
58
|
initialData[key] = dayjs(value)
|
|
26
59
|
}
|
|
27
60
|
} else {
|
|
@@ -33,12 +66,31 @@ export const MyModalForm = ({ width, title, submit, record, visible, setModal, l
|
|
|
33
66
|
}
|
|
34
67
|
}, [visible, record, form, formItems])
|
|
35
68
|
|
|
69
|
+
/**
|
|
70
|
+
* @async
|
|
71
|
+
* @function handleOk
|
|
72
|
+
* @description [物理加压提交] 执行表单校验,并自动启动“时间戳转换协议”。
|
|
73
|
+
* 确保发射至后端的数据包符合标准 Unix 时间戳(毫秒)规范。
|
|
74
|
+
*/
|
|
36
75
|
const handleOk = async () => {
|
|
37
76
|
try {
|
|
38
77
|
const values = await form.validateFields()
|
|
78
|
+
const formattedValues = { ...values }
|
|
79
|
+
formItems.forEach(item => {
|
|
80
|
+
const val = formattedValues[item.name]
|
|
81
|
+
if (item.type?.includes('date') && val) {
|
|
82
|
+
if (Array.isArray(val)) {
|
|
83
|
+
// 物理压实:将范围日期转化为逗号分隔的时间戳字符串
|
|
84
|
+
formattedValues[item.name] = val.map(v => v.valueOf()).join(',')
|
|
85
|
+
} else {
|
|
86
|
+
// 物理转化:将 dayjs 对象还原为原始数值(毫秒)
|
|
87
|
+
formattedValues[item.name] = val.valueOf()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
})
|
|
39
91
|
if (submit) {
|
|
40
92
|
setPending(true)
|
|
41
|
-
await submit({ ...record, ...
|
|
93
|
+
await submit({ ...record, ...formattedValues })
|
|
42
94
|
setPending(false)
|
|
43
95
|
}
|
|
44
96
|
} catch (error) {
|
|
@@ -46,6 +98,10 @@ export const MyModalForm = ({ width, title, submit, record, visible, setModal, l
|
|
|
46
98
|
}
|
|
47
99
|
}
|
|
48
100
|
|
|
101
|
+
/**
|
|
102
|
+
* @function handleCancel
|
|
103
|
+
* @description 执行“撤退协议”:关闭构筑舱并清除当前逻辑残留。
|
|
104
|
+
*/
|
|
49
105
|
const handleCancel = () => {
|
|
50
106
|
setModal({ visible: false })
|
|
51
107
|
form.resetFields()
|
|
@@ -1,20 +1,60 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
1
|
import { MyTable } from './MyTable'
|
|
2
|
+
import { useMemo, useState } from 'react'
|
|
3
3
|
import { Modal, Space, Button } from 'antd'
|
|
4
4
|
import { MySearchForm } from './MySearchForm'
|
|
5
5
|
import { useTableQuery } from '../hooks/useTableQuery'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* @component MyModalTable
|
|
9
|
+
* @description [地堡支援型作战单元] 弹窗数据选择平台。
|
|
10
|
+
* 专门用于执行“关联选择”、“子数据审计”或“二级逻辑骇入”任务。
|
|
11
|
+
* 具备独立的数据获取链路、可选的搜索过滤系统以及与主构筑舱(setModalForm)的跨维协同能力。
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} props - 支援参数
|
|
14
|
+
* @param {Function} props.api - 弹窗内独立的数据源指令
|
|
15
|
+
* @param {Function} [props.onOk] - 战果物理确认:按下确认按钮后的异步回调协议
|
|
16
|
+
* @param {string} [props.title] - 舱体视觉标题
|
|
17
|
+
* @param {string|number} [props.width] - 舱体物理宽度
|
|
18
|
+
* @param {React.ReactNode} [props.footer] - 底座指令区:支持完全自定义操作按钮
|
|
19
|
+
* @param {boolean} props.visible - 激活信号:控制支援单元的显示状态
|
|
20
|
+
* @param {Function} props.operate - 动态火力配置:生成操作列的函数,接收 { refresh, setModalForm } 句柄
|
|
21
|
+
* @param {Function} props.setModal - 单元注销中心:用于关闭当前弹窗
|
|
22
|
+
* @param {Array} [props.formItems] - 局部搜索零部件:用于在弹窗内执行精准情报过滤
|
|
23
|
+
* @param {Function} [props.setModalForm] - [重要] 跨维调用:由主页面注入的表单控制句柄,实现“弹窗开弹窗”的非耦合交互
|
|
24
|
+
* @param {Object} [props.rowSelection] - 战术勾选:用于多选物资
|
|
25
|
+
* @param {Function} props.formatResponse - 数据清洗协议:对 API 吐出的原始物资进行格式化
|
|
26
|
+
* @param {Array} [props.functionButtons] - 功能挂件组:位于表格上方的操作按钮,可直接操控表格状态
|
|
27
|
+
* @param {Array} props.columns - 零部件初始清单 (别名: modalColumns)
|
|
28
|
+
* @param {Object} [props.initialParams={}] - 初始战备物资:定义的初始搜索参数
|
|
29
|
+
* @param {boolean|Object} [props.modalPagination=true] - 局部后勤协议:弹窗内是否开启分页
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* <MyModalTable
|
|
33
|
+
* title="关联用户片段"
|
|
34
|
+
* api={async params=>await fetchUsers(params)}
|
|
35
|
+
* setModalForm={setModal} // 💡 借用主页面的表单构筑舱
|
|
36
|
+
* operate={({ refresh, setModalForm }) => ({ label: '详情', onClick: (rec) => setModalForm(...) })}
|
|
37
|
+
* />
|
|
38
|
+
*/
|
|
8
39
|
export const MyModalTable = ({ api, onOk, title, width, footer, visible, operate, setModal, formItems, setModalForm, rowSelection, formatResponse, functionButtons, columns: modalColumns, initialParams = {}, modalPagination = true }) => {
|
|
9
40
|
|
|
10
41
|
const [pending, setPending] = useState(false)
|
|
11
42
|
|
|
43
|
+
/**
|
|
44
|
+
* @description [独立信号链路]
|
|
45
|
+
* 在弹窗内部启动专属的 useTableQuery,实现了与主页面逻辑的“物理隔绝”。
|
|
46
|
+
*/
|
|
12
47
|
const { total, loading, columns, dataSource, search, refresh, setLoading, setSearch, setDataSource } = useTableQuery({ api, cols: modalColumns, initialParams, formatResponse })
|
|
13
48
|
|
|
14
49
|
const handleSearch = values => setSearch({ ...search, ...values, pageNo: 1 })
|
|
15
50
|
|
|
16
51
|
const handleTableChange = (pagination, filters, sorter) => setSearch({ ...search, pageNo: pagination.current, pageSize: pagination.pageSize, orderBy: sorter.column ? sorter.field : undefined })
|
|
17
52
|
|
|
53
|
+
/**
|
|
54
|
+
* @async
|
|
55
|
+
* @function handleOk
|
|
56
|
+
* @description [确认指令闭环] 执行确认操作并启动原子级锁定(Pending),防止重复指令干扰。
|
|
57
|
+
*/
|
|
18
58
|
const handleOk = async () => {
|
|
19
59
|
if (onOk) {
|
|
20
60
|
try {
|
|
@@ -28,21 +68,37 @@ export const MyModalTable = ({ api, onOk, title, width, footer, visible, operate
|
|
|
28
68
|
}
|
|
29
69
|
}
|
|
30
70
|
|
|
71
|
+
/**
|
|
72
|
+
* @constant finalColumns
|
|
73
|
+
* @description [火力全开] 将 Hook 生成的动态列与指挥官注入的 operate 操作列进行物理合并。
|
|
74
|
+
*/
|
|
75
|
+
const finalColumns = useMemo(() => {
|
|
76
|
+
return operate ? columns.concat(operate({ refresh, setModalForm })) : columns
|
|
77
|
+
}, [columns, operate])
|
|
78
|
+
|
|
31
79
|
return (
|
|
32
80
|
<Modal centered destroyOnClose title={title} width={width} open={visible} onOk={handleOk} footer={footer} onCancel={() => setModal({ visible: false })} confirmLoading={pending}>
|
|
81
|
+
{/*
|
|
82
|
+
💡 [地堡战术细节]
|
|
83
|
+
syncUrlParams={false}:确保弹窗内的搜索行为不会干扰全局 URL 信号。
|
|
84
|
+
*/}
|
|
33
85
|
{formItems?.length > 0 && <MySearchForm search={search} formItems={formItems} setSearch={handleSearch} syncUrlParams={false} />}
|
|
86
|
+
{/*
|
|
87
|
+
💡 [高阶赋能逻辑]
|
|
88
|
+
functionButtons 能够获得对当前表格状态 (refresh, setLoading...) 的完全控制权。
|
|
89
|
+
*/}
|
|
34
90
|
{functionButtons?.length > 0 && <Space>{functionButtons.map(item => <Button key={item.name} type={item.type} onClick={() => item.onClick({ refresh, setLoading, setSearch, setDataSource })}>{item.name}</Button>)}</Space>}
|
|
35
91
|
<MyTable
|
|
36
92
|
total={total}
|
|
37
93
|
search={search}
|
|
38
94
|
loading={loading}
|
|
39
95
|
scroll={{ y: 400 }}
|
|
96
|
+
columns={finalColumns}
|
|
40
97
|
dataSource={dataSource}
|
|
41
98
|
rowSelection={rowSelection}
|
|
42
99
|
pagination={modalPagination}
|
|
43
100
|
onChange={handleTableChange}
|
|
44
101
|
setDataSource={setDataSource}
|
|
45
|
-
columns={operate ? columns.concat(operate({ refresh, setModalForm })) : columns}
|
|
46
102
|
/>
|
|
47
103
|
</Modal>
|
|
48
104
|
)
|
|
@@ -3,38 +3,81 @@ import { MyBaseForm } from './MyBaseForm'
|
|
|
3
3
|
import { useState, useEffect } from 'react'
|
|
4
4
|
import { Row, Col, Form, Button, Space } from 'antd'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @component MySearchForm
|
|
8
|
+
* @description [地堡战术搜索中心] 高级搜索表单组件。
|
|
9
|
+
* 集成了“URL 状态同步(持久化)”、“日期范围物理拆解”以及“动态伸缩布局”三大核心能力。
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} props - 指挥官指令包
|
|
12
|
+
* @param {Object} props.form - Antd Form 实例,表单的主控舵盘
|
|
13
|
+
* @param {Object} props.search - 当前的全局搜索状态(包含 pageNo, pageSize 及各过滤项)
|
|
14
|
+
* @param {boolean} props.loading - 战场负载状态:请求中时锁定按钮,防止由于重复指令导致的逻辑冲突
|
|
15
|
+
* @param {Function} props.setSearch - 状态分发器:用于更新全局搜索参数并触发重新构筑
|
|
16
|
+
* @param {Array} props.formItems - 零部件清单:定义所有的搜索输入单元
|
|
17
|
+
* @param {number} [props.showLimit=7] - 初始可见能级:默认展示的零部件数量
|
|
18
|
+
* @param {Object} props.initialValues - 初始信号:从 URL 或缓存中解析出的初始参数
|
|
19
|
+
* @param {Function} [props.customReset] - 逻辑劫持:自定义重置协议
|
|
20
|
+
* @param {Function} [props.customFinish] - 逻辑劫持:自定义提交协议
|
|
21
|
+
* @param {React.ReactNode} [props.extraOperate] - 额外挂件:在按钮区域注入自定义战术单元
|
|
22
|
+
* @param {Function} [props.onValuesChange] - 联动传感:当表单值变动时的即时反馈
|
|
23
|
+
* @param {boolean} [props.syncUrlParams=true] - 物理同步开关:是否将搜索状态实时镜像至 URL 链接中
|
|
24
|
+
* @param {number} [props.defaultPageSize=10] - 默认吞吐量:重置时的初始每页条数
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* <MySearchForm
|
|
28
|
+
* form={form}
|
|
29
|
+
* search={search}
|
|
30
|
+
* formItems={[{ label: '状态', name: 'status', type: 'select', options: [...] }]}
|
|
31
|
+
* setSearch={setSearch}
|
|
32
|
+
* />
|
|
33
|
+
*/
|
|
6
34
|
export const MySearchForm = ({ form, search, loading, labelCol, setSearch, formItems, showLimit = 7, initialValues, customReset, extraOperate, customFinish, onValuesChange, syncUrlParams = true, defaultPageSize = 10 }) => {
|
|
7
35
|
|
|
8
36
|
const [limit, setLimit] = useState(showLimit)
|
|
9
37
|
|
|
38
|
+
/**
|
|
39
|
+
* @function handleReset
|
|
40
|
+
* @description 执行“归零协议”:清除当前所有搜索信号,并可选择性擦除 URL 中的物理记录。
|
|
41
|
+
*/
|
|
10
42
|
const handleReset = () => {
|
|
11
43
|
if (syncUrlParams) {
|
|
44
|
+
// 物理清除:重置浏览器历史记录,保持 URL 纯净
|
|
12
45
|
window.history.replaceState(null, '', window.location.pathname)
|
|
13
46
|
}
|
|
14
47
|
if (customReset) {
|
|
15
48
|
customReset()
|
|
16
49
|
} else {
|
|
50
|
+
// 逻辑重置:强制返回第一页,并恢复默认吞吐量
|
|
17
51
|
setSearch({ pageNo: 1, pageSize: search.pageSize ?? defaultPageSize })
|
|
18
52
|
}
|
|
19
53
|
}
|
|
20
54
|
|
|
55
|
+
/**
|
|
56
|
+
* @function handleFinish
|
|
57
|
+
* @description 执行“数据发射协议”:对原始表单数据进行脱水和转换,尤其处理复杂的日期范围协议。
|
|
58
|
+
* @param {Object} values - 原始表单数据包
|
|
59
|
+
*/
|
|
21
60
|
const handleFinish = values => {
|
|
22
61
|
const formattedValues = { ...values }
|
|
23
62
|
if (syncUrlParams) {
|
|
24
63
|
Object.keys(formattedValues).forEach(key => {
|
|
25
64
|
const val = formattedValues[key]
|
|
26
65
|
const isDateType = formItems.find(item => item.name === key)?.type?.includes('date')
|
|
66
|
+
// 💡 [黑科技 1] 日期自动转换与拆解
|
|
27
67
|
if (isDateType && val) {
|
|
28
68
|
if (Array.isArray(val) && val.length > 0) {
|
|
69
|
+
// 物理拆分:当 name 为 'start,end' 格式时,自动拆解为两个独立的后端参数
|
|
29
70
|
const [startKey, endKey] = key.split(',')
|
|
30
|
-
formattedValues[startKey] = val[0].startOf('day').valueOf()
|
|
31
|
-
formattedValues[endKey] = val[1].endOf('day').valueOf()
|
|
71
|
+
formattedValues[startKey] = val[0].startOf('day').valueOf()// 修正为当日 00:00:00
|
|
72
|
+
formattedValues[endKey] = val[1].endOf('day').valueOf()// 修正为当日 23:59:59
|
|
32
73
|
delete formattedValues[key]
|
|
33
74
|
} else {
|
|
75
|
+
// 单日转换:转化为毫秒级数字
|
|
34
76
|
formattedValues[key] = val.valueOf()
|
|
35
77
|
}
|
|
36
78
|
}
|
|
37
79
|
})
|
|
80
|
+
// 💡 [黑科技 2] 镜像至 URL
|
|
38
81
|
const params = new URLSearchParams(Object.fromEntries(Object.entries(formattedValues).filter(([key, value]) => !['', null, undefined].includes(value))))
|
|
39
82
|
window.history.replaceState(null, '', `${window.location.pathname}?${params.toString()}`)
|
|
40
83
|
}
|
|
@@ -45,14 +88,20 @@ export const MySearchForm = ({ form, search, loading, labelCol, setSearch, formI
|
|
|
45
88
|
}
|
|
46
89
|
}
|
|
47
90
|
|
|
91
|
+
/**
|
|
92
|
+
* @description [信号自愈] 初始化逻辑:
|
|
93
|
+
* 将来自 URL 的字符串信号重新转化为地堡可读的 `dayjs` 对象。
|
|
94
|
+
*/
|
|
48
95
|
useEffect(() => {
|
|
49
96
|
if (Object.values(initialValues).length > 0 && formItems.length > 0) {
|
|
50
97
|
const newValues = {}
|
|
51
98
|
formItems.forEach(item => {
|
|
99
|
+
// 处理单字段回显
|
|
52
100
|
if (initialValues[item.name] !== undefined) {
|
|
53
101
|
const value = initialValues[item.name]
|
|
54
102
|
newValues[item.name] = item.type?.includes('date') ? dayjs(Number(value)) : value
|
|
55
103
|
}
|
|
104
|
+
// 处理双字段(日期范围)回显:执行物理重组
|
|
56
105
|
if (item.name.includes(',') && item.type?.includes('date')) {
|
|
57
106
|
const [startKey, endKey] = item.name.split(',')
|
|
58
107
|
if (initialValues[startKey] && initialValues[endKey]) {
|
|
@@ -69,15 +118,22 @@ export const MySearchForm = ({ form, search, loading, labelCol, setSearch, formI
|
|
|
69
118
|
<Row gutter={24}>
|
|
70
119
|
{formItems.slice(0, limit)?.map((item, ind) => (
|
|
71
120
|
<Col key={ind} span={item.span ?? 6}>
|
|
121
|
+
{/* 物理装配:利用底层 MyBaseForm 单元进行渲染 */}
|
|
72
122
|
<MyBaseForm item={item} form={form} />
|
|
73
123
|
</Col>
|
|
74
124
|
))}
|
|
125
|
+
{/*
|
|
126
|
+
战术控制区:
|
|
127
|
+
1. 'flex=auto' 确保按钮组始终靠右对齐。
|
|
128
|
+
2. 'showLimit' 控制页面纵向熵值,防止过多搜索项挤占战场视野。
|
|
129
|
+
*/}
|
|
75
130
|
<Col flex='auto' style={{ textAlign: 'right' }}>
|
|
76
131
|
<Form.Item>
|
|
77
132
|
<Space>
|
|
78
133
|
<Button type='primary' loading={loading} htmlType='submit'>查询</Button>
|
|
79
134
|
<Button onClick={handleReset} loading={loading} htmlType='reset'>重置</Button>
|
|
80
135
|
{extraOperate}
|
|
136
|
+
{/* 动态伸缩协议:根据当前零部件数量决定是否显示展开/收起按钮 */}
|
|
81
137
|
{formItems.length > showLimit && (<a onClick={() => setLimit(formItems.length > limit ? formItems.length : showLimit)}>{formItems.length > limit ? '展开' : '收起'}</a>)}
|
|
82
138
|
</Space>
|
|
83
139
|
</Form.Item>
|
|
@@ -2,13 +2,56 @@ import { Table } from 'antd'
|
|
|
2
2
|
import { EditableRow, EditableCell } from './EditableCell'
|
|
3
3
|
import { useRef, useMemo, useEffect, useCallback } from 'react'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @component MyTable
|
|
7
|
+
* @description [地堡核心战术平台] 高级数据表格组件。
|
|
8
|
+
* 它是地堡系统的“火力中心”,集成了行内编辑、自动滚动定位、动态分页协议及 Optimistic UI (乐观更新) 等工业级特性。
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} props - 平台配置参数
|
|
11
|
+
* @param {string} [props.size='middle'] - 视觉能级:表格的物理尺寸 (small/middle/large)
|
|
12
|
+
* @param {Object} [props.query] - 侦察参数:包含 rowIndex 用于执行物理追踪定位
|
|
13
|
+
* @param {number} props.total - 物资总量:用于构筑分页器的刻度
|
|
14
|
+
* @param {Object} props.search - 当前物流状态:包含 pageNo 和 pageSize
|
|
15
|
+
* @param {boolean} [props.autoScroll] - 视觉引导开关:是否开启自动滚动至目标行逻辑
|
|
16
|
+
* @param {Function} props.onChange - 战位协同回调:分页、排序、筛选变动时的通信协议
|
|
17
|
+
* @param {boolean|Object} [props.pagination] - 分页配置:支持布尔开关或物理对象覆盖
|
|
18
|
+
* @param {string|Function} [props.rowClassName] - 行视觉涂装:支持静态字符串或基于逻辑的动态类名
|
|
19
|
+
* @param {Function} [props.customSave] - 物理封存协议:行内编辑保存后的外部持久化回调
|
|
20
|
+
* @param {Function} props.setDataSource - 状态设置器:直接操作地堡的原始数据流(支持函数式更新)
|
|
21
|
+
* @param {Function} [props.lineFormChange] - 行级信号监听:捕获行内表单的实时变动
|
|
22
|
+
* @param {Array} props.columns - 零部件清单:定义列的构筑协议,支持 editType 等扩展属性
|
|
23
|
+
* @param {Object} [props.rowSelection] - 战术勾选:配置行选择逻辑
|
|
24
|
+
* @param {string} [props.rowKey='id'] - 唯一标识:每一行物资的数字指纹
|
|
25
|
+
* @param {boolean} [props.loading=false] - 战场负载状态:显示加载动画
|
|
26
|
+
* @param {Array} [props.dataSource=[]] - 物理物资包:表格展示的原始数据
|
|
27
|
+
* @param {Object} [props.scroll={x:'max-content'}] - 滚动配置:默认开启横向物理溢出防护
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* <MyTable
|
|
31
|
+
* columns={columns}
|
|
32
|
+
* dataSource={data}
|
|
33
|
+
* total={100}
|
|
34
|
+
* setDataSource={setData}
|
|
35
|
+
* customSave={(row) => saveApi(row)}
|
|
36
|
+
* />
|
|
37
|
+
*/
|
|
5
38
|
export const MyTable = ({ size, query, total, search, autoScroll, onChange, pagination, rowClassName, customSave, setDataSource, lineFormChange, columns = [], rowSelection, rowKey = 'id', loading = false, dataSource = [], scroll = { x: 'max-content' }, ...restProps }) => {
|
|
6
39
|
|
|
7
40
|
const tableRef = useRef(null)
|
|
8
41
|
const hasScrolledRef = useRef(false)
|
|
9
42
|
|
|
43
|
+
/**
|
|
44
|
+
* @description [权限侦察] 自动扫描列配置。若存在 editType 型号,则激活“骇入模式(编辑模式)”。
|
|
45
|
+
*/
|
|
10
46
|
const isEditable = useMemo(() => columns.some(col => col.editType), [columns])
|
|
11
47
|
|
|
48
|
+
/**
|
|
49
|
+
* @function handleSave
|
|
50
|
+
* @description [原子级物理封存] 执行数据更新协议。
|
|
51
|
+
* 采用函数式状态更新(prev => ...)以彻底消除闭航陷阱(Stale Closure),确保数据高频修改时的绝对准确。
|
|
52
|
+
*
|
|
53
|
+
* @param {Object} row - 修改后的行片段
|
|
54
|
+
*/
|
|
12
55
|
const handleSave = useCallback((row) => {
|
|
13
56
|
setDataSource(prev => {
|
|
14
57
|
const newData = [...prev]
|
|
@@ -20,6 +63,10 @@ export const MyTable = ({ size, query, total, search, autoScroll, onChange, pagi
|
|
|
20
63
|
})
|
|
21
64
|
}, [rowKey, customSave, setDataSource])
|
|
22
65
|
|
|
66
|
+
/**
|
|
67
|
+
* @constant components
|
|
68
|
+
* @description [零部件注册] 注入地堡特有的可编辑 Row 和 Cell 单元。
|
|
69
|
+
*/
|
|
23
70
|
const components = useMemo(() => ({
|
|
24
71
|
body: {
|
|
25
72
|
row: EditableRow,
|
|
@@ -27,6 +74,11 @@ export const MyTable = ({ size, query, total, search, autoScroll, onChange, pagi
|
|
|
27
74
|
}
|
|
28
75
|
}), [])
|
|
29
76
|
|
|
77
|
+
/**
|
|
78
|
+
* @constant mergedColumns
|
|
79
|
+
* @description [元数据重组] 将指挥官的静态列定义转化为带逻辑注入的“动态作战列”。
|
|
80
|
+
* 自动处理 Cell 级的 disabled 状态和 editable 权限。
|
|
81
|
+
*/
|
|
30
82
|
const mergedColumns = useMemo(() => {
|
|
31
83
|
return columns.map(col => {
|
|
32
84
|
if (!col.editType) {
|
|
@@ -44,6 +96,7 @@ export const MyTable = ({ size, query, total, search, autoScroll, onChange, pagi
|
|
|
44
96
|
dataIndex: col.dataIndex,
|
|
45
97
|
placeholder: col.placeholder,
|
|
46
98
|
defaultEdit: col.defaultEdit,
|
|
99
|
+
// 💡 条件防御逻辑:支持针对单行的物理锁定
|
|
47
100
|
disabled: col.specialDisabled ? record.disabled : col.disabled,
|
|
48
101
|
editable: col.specialEditType ? record.needEdit && !!col.editType : !!col.editType,
|
|
49
102
|
}),
|
|
@@ -51,6 +104,11 @@ export const MyTable = ({ size, query, total, search, autoScroll, onChange, pagi
|
|
|
51
104
|
})
|
|
52
105
|
}, [columns, handleSave])
|
|
53
106
|
|
|
107
|
+
/**
|
|
108
|
+
* @constant paginationConfig
|
|
109
|
+
* @description [后勤对齐协议] 自动将地堡的 search 状态映射为 Antd 分页模型。
|
|
110
|
+
* 具备类型自适应能力:支持 boolean 开关或 Object 属性覆盖。
|
|
111
|
+
*/
|
|
54
112
|
const paginationConfig = useMemo(() => {
|
|
55
113
|
|
|
56
114
|
if (!pagination) return false
|
|
@@ -65,15 +123,21 @@ export const MyTable = ({ size, query, total, search, autoScroll, onChange, pagi
|
|
|
65
123
|
}
|
|
66
124
|
}, [total, search, pagination])
|
|
67
125
|
|
|
126
|
+
/**
|
|
127
|
+
* @description [视觉追踪协议] 当检测到 query.rowIndex 信号时,执行“物理重定向”。
|
|
128
|
+
* 表格将自动平滑滚动至目标行并注入高亮背景(#e6f4ff)。
|
|
129
|
+
*/
|
|
68
130
|
useEffect(() => {
|
|
69
131
|
if (!autoScroll || !dataSource?.length || query?.rowIndex === undefined) return
|
|
70
132
|
if (hasScrolledRef.current === query.rowIndex) return
|
|
71
133
|
const timer = setTimeout(() => {
|
|
72
134
|
const tableElement = tableRef.current
|
|
73
135
|
if (!tableElement) return
|
|
136
|
+
// 物理侦察:查找底层 Antd 生成的行节点
|
|
74
137
|
const trList = tableElement.getElementsByClassName('ant-table-row')
|
|
75
138
|
const tr = trList[Number(query.rowIndex)]
|
|
76
139
|
if (tr) {
|
|
140
|
+
// 物理定位:平滑引导视觉中心
|
|
77
141
|
tr.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
78
142
|
tr.style.backgroundColor = '#e6f4ff'
|
|
79
143
|
hasScrolledRef.current = query.rowIndex
|
|
@@ -96,6 +160,7 @@ export const MyTable = ({ size, query, total, search, autoScroll, onChange, pagi
|
|
|
96
160
|
rowSelection={rowSelection}
|
|
97
161
|
pagination={paginationConfig}
|
|
98
162
|
components={isEditable ? components : undefined}
|
|
163
|
+
// 💡 信号注入:将行监听协议绑定至底层 tr
|
|
99
164
|
onRow={(record, index) => ({ record, index, onValuesChange: lineFormChange })}
|
|
100
165
|
rowClassName={(record, index) => {
|
|
101
166
|
const externalClass = typeof rowClassName === 'function' ? rowClassName(record, index) : rowClassName
|
|
@@ -3,12 +3,39 @@ import { useState, useEffect } from 'react'
|
|
|
3
3
|
import { UploadOutlined } from '@ant-design/icons'
|
|
4
4
|
import { Button, Tree, Radio, Input, Upload, Select, Cascader, Checkbox, DatePicker, InputNumber, TreeSelect, AutoComplete } from 'antd'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @component AliyunOSSUpload
|
|
8
|
+
* @description [地堡核心传输单元] 阿里云 OSS 高级上传组件。
|
|
9
|
+
* 集成了“物理路径自动重组”、“STS 令牌自动续期”以及“ Antd 属性无损透传”三大核心协议。
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} props - 构筑参数
|
|
12
|
+
* @param {Array} props.value - 传感器当前封存的文件列表 (符合 Antd fileList 规范)
|
|
13
|
+
* @param {Function} props.onChange - 状态反馈协议:当物理文件状态变更时同步至地堡中枢
|
|
14
|
+
* @param {Object} [props.oss] - 外部空投的初始凭证包。若缺失,组件将启动自主初始化流程。
|
|
15
|
+
* @param {string} [props.url] - 司令部通信地址:用于申请最新的物理传输令牌 (STS)
|
|
16
|
+
* @param {string} [props.path] - 默认存储扇区:定义文件在云端的物理根路径
|
|
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
|
+
*/
|
|
6
27
|
const AliyunOSSUpload = ({ value, onChange, ...restProps }) => {
|
|
7
28
|
|
|
29
|
+
// 💡 物理剥离:将地堡专用参数与 Antd 原生属性分离
|
|
8
30
|
const { oss, url, path, options, ...originUploadProps } = restProps
|
|
9
31
|
|
|
10
32
|
const [OSSData, setOSSData] = useState(oss)
|
|
11
33
|
|
|
34
|
+
/**
|
|
35
|
+
* @async
|
|
36
|
+
* @function init
|
|
37
|
+
* @description [信号握手] 执行令牌获取协议,从司令部申请最新的物理上传授权
|
|
38
|
+
*/
|
|
12
39
|
const init = async () => {
|
|
13
40
|
try {
|
|
14
41
|
const result = await initOSS(url, options)
|
|
@@ -19,6 +46,7 @@ const AliyunOSSUpload = ({ value, onChange, ...restProps }) => {
|
|
|
19
46
|
}
|
|
20
47
|
|
|
21
48
|
useEffect(() => {
|
|
49
|
+
// 💡 自愈逻辑:如果发现能源包 (OSSData) 为空,则自动触发初始化
|
|
22
50
|
if (!OSSData) {
|
|
23
51
|
init()
|
|
24
52
|
}
|
|
@@ -31,6 +59,11 @@ const AliyunOSSUpload = ({ value, onChange, ...restProps }) => {
|
|
|
31
59
|
onChange?.(files)
|
|
32
60
|
}
|
|
33
61
|
|
|
62
|
+
/**
|
|
63
|
+
* @function getExtraData
|
|
64
|
+
* @description [数据封装] 将动态生成的物理凭证与目标文件路径压入传输负载
|
|
65
|
+
* @param {Object} file - 待传输的文件对象
|
|
66
|
+
*/
|
|
34
67
|
const getExtraData = file => ({
|
|
35
68
|
key: file.url,
|
|
36
69
|
policy: OSSData?.policy,
|
|
@@ -39,15 +72,27 @@ const AliyunOSSUpload = ({ value, onChange, ...restProps }) => {
|
|
|
39
72
|
'x-oss-security-token': OSSData?.stsToken,
|
|
40
73
|
})
|
|
41
74
|
|
|
75
|
+
/**
|
|
76
|
+
* @async
|
|
77
|
+
* @function beforeUpload
|
|
78
|
+
* @description [生存期侦察] 在传输启动前执行 TTL 检测。
|
|
79
|
+
* 若令牌已失效(过期),则强行拦截并执行同步刷新协议,同时执行物理路径归一化。
|
|
80
|
+
*/
|
|
42
81
|
const beforeUpload = async file => {
|
|
43
82
|
const expire = Number(OSSData.expire) * 1000
|
|
44
83
|
if (expire < Date.now()) {
|
|
45
84
|
await init()
|
|
46
85
|
}
|
|
86
|
+
// 💡 物理路径重组:通过正则清除多余的路径分隔符,确保存储节点坐标唯一
|
|
47
87
|
file.url = `${OSSData?.dir ?? path}/${file.uid}_${file.name}`.replace(/\/\//g, '/')
|
|
48
88
|
return file
|
|
49
89
|
}
|
|
50
90
|
|
|
91
|
+
/**
|
|
92
|
+
* @constant uploadProps
|
|
93
|
+
* @description [物理合并] 最终生成的 Antd Upload 指令集:
|
|
94
|
+
* 自动优先应用 originUploadProps 中的自定义设置。
|
|
95
|
+
*/
|
|
51
96
|
const uploadProps = {
|
|
52
97
|
onRemove,
|
|
53
98
|
name: 'file',
|
|
@@ -66,6 +111,21 @@ const AliyunOSSUpload = ({ value, onChange, ...restProps }) => {
|
|
|
66
111
|
)
|
|
67
112
|
}
|
|
68
113
|
|
|
114
|
+
/**
|
|
115
|
+
* @function formNode
|
|
116
|
+
* @description 智能零部件装配器:根据构筑协议(item.type)动态产出对应的 Ant Design 交互单元。
|
|
117
|
+
*
|
|
118
|
+
* @param {Object} params - 装配参数
|
|
119
|
+
* @param {Object} params.item - 零部件配置对象
|
|
120
|
+
* @param {string} params.item.type - 构筑类型(date/number/ossUpload/select/daterange 等)
|
|
121
|
+
* @param {boolean} [params.item.readOnly] - 物理锁定开关
|
|
122
|
+
* @param {string|number} [params.item.width] - 宽度校准
|
|
123
|
+
* @param {string} [params.item.placeholder] - 视觉占位提示
|
|
124
|
+
* @param {Array} [params.item.options] - 数据字典(用于 select/radio/tree 等)
|
|
125
|
+
* @param {Object} [params.item.props] - 透传给底层组件的原始控制参数
|
|
126
|
+
*
|
|
127
|
+
* @returns {React.ReactNode} 物理装配完成的 UI 单元
|
|
128
|
+
*/
|
|
69
129
|
export const formNode = ({ item }) => {
|
|
70
130
|
|
|
71
131
|
const commonProps = {
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
{{!--
|
|
2
|
+
📡 [地堡核心驱动引擎]:逻辑链路全自动构筑
|
|
3
|
+
1. 执行“传感器对齐”:从 URL 中提取历史信号,实现状态持久化
|
|
4
|
+
2. 激活“useTableQuery”:物理封存分页、搜索、及动态列加载逻辑
|
|
5
|
+
--}}
|
|
6
|
+
|
|
1
7
|
const initParams = {{#if hasTabs}}{ type: tabs[0].key }{{else}}{}{{/if}}
|
|
2
8
|
|
|
3
9
|
const query = formatQuery(Object.fromEntries(new URLSearchParams(location.search).entries()), {{formItems}})
|
|
4
10
|
|
|
5
11
|
const { total, columns, loading, dataSource, search, setSearch, setDataSource, refresh } = useTableQuery({
|
|
6
12
|
api: async params => await request('/api/{{fileName}}', { method: 'POST', body: params }),
|
|
7
|
-
initialParams: { pageNo: 1, pageSize: 10, ...query },
|
|
8
|
-
cols:tableColumns
|
|
13
|
+
initialParams: { pageNo: 1, pageSize: 10, ...initParams, ...query },
|
|
14
|
+
cols:tableColumns,
|
|
9
15
|
formatResponse: response => {
|
|
10
16
|
return {
|
|
11
17
|
data: response?.data ?? [],
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
|
|
2
|
+
{{!-- 📡 [地堡状态模组] 物理信号初始化:驱动弹窗、页签及数据勾选等核心交互单元 --}}
|
|
2
3
|
const rowKey = 'id'
|
|
3
4
|
const [form] = Form.useForm()
|
|
4
|
-
const [modal, setModal] = useState({ visible:false, title:'', formItems:
|
|
5
|
+
const [modal, setModal] = useState({ visible:false, title:'', formItems:modalItems })
|
|
5
6
|
{{#if hasRowSelection}}
|
|
6
7
|
const [selectedRows, setSelectedRows] = useState([])
|
|
7
8
|
{{/if}}
|
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import { useRef, useState, useEffect, useCallback } from 'react'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @hook useTableQuery
|
|
5
|
+
* @description [地堡核心逻辑引擎] 高级异步数据驱动钩子。
|
|
6
|
+
* 负责全自动管理表格的生命周期,包括数据抓取、状态同步、动态列指纹识别及“竞态病毒”物理免疫。
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} params - 引擎配置包
|
|
9
|
+
* @param {Function} params.api - 任务指令:必须返回 Promise 的数据抓取函数
|
|
10
|
+
* @param {Array} [params.cols] - 初始战备物资:前端预设的静态列配置
|
|
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 {Array} .columns - 当前生效的动态列配置(已通过指纹协议校验)
|
|
17
|
+
* @returns {boolean} .loading - 引擎负载状态
|
|
18
|
+
* @returns {Array} .dataSource - 已解压的物理物资列表
|
|
19
|
+
* @returns {Object} .search - 当前导航参数(pageNo, pageSize 等)
|
|
20
|
+
* @returns {Function} .setSearch - 坐标修改器
|
|
21
|
+
* @returns {Function} .setLoading - 负载状态手动干预句柄
|
|
22
|
+
* @returns {Function} .setDataSource - 物资流手动修改句柄(用于行内编辑等乐观更新)
|
|
23
|
+
* @returns {Function} .refresh - 强制重启协议:立即执行一次数据抓取
|
|
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
|
+
*/
|
|
3
32
|
export const useTableQuery = ({ api, cols, initialParams = {}, formatResponse }) => {
|
|
4
33
|
|
|
5
34
|
const [total, setTotal] = useState(0)
|
|
@@ -8,23 +37,41 @@ export const useTableQuery = ({ api, cols, initialParams = {}, formatResponse })
|
|
|
8
37
|
const [dataSource, setDataSource] = useState([])
|
|
9
38
|
const [search, setSearch] = useState(initialParams)
|
|
10
39
|
|
|
40
|
+
// 💡 引用锁定协议:确保异步回调中永远能指向最新的指令地址
|
|
11
41
|
const apiRef = useRef(api)
|
|
42
|
+
// 💡 [核心防御] 信号 ID 计数器:用于物理拦截由于网络时延导致的“竞态病毒(Race Condition)”
|
|
12
43
|
const fetchIdRef = useRef(0)
|
|
13
44
|
|
|
14
45
|
useEffect(() => {
|
|
15
46
|
apiRef.current = api
|
|
16
47
|
}, [api])
|
|
17
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @function getColumnSchema
|
|
51
|
+
* @description [指纹识别协议] 对列配置执行“结构化脱水”。
|
|
52
|
+
* 通过提取标题、字段和排序状态生成物理指纹,用于判定列结构是否发生“基因变异”。
|
|
53
|
+
* @param {Array} cols - 待扫描的列配置
|
|
54
|
+
* @returns {string} 物理指纹字符串
|
|
55
|
+
*/
|
|
18
56
|
const getColumnSchema = cols => cols.map(col => `${col.title}-${col.dataIndex}-${col.sortOrder}`).join('|')
|
|
19
57
|
|
|
58
|
+
/**
|
|
59
|
+
* @async
|
|
60
|
+
* @function fetchData
|
|
61
|
+
* @description [数据装配任务] 执行核心的数据抓取与物理封存逻辑。
|
|
62
|
+
* 具备信号溯源能力,确保只有最后一次发出的指令能修改地堡状态。
|
|
63
|
+
*/
|
|
20
64
|
const fetchData = useCallback(async () => {
|
|
21
65
|
if (!apiRef.current) return
|
|
22
66
|
setLoading(true)
|
|
67
|
+
// 💡 信号加标:为本次请求分配唯一的物理 ID
|
|
23
68
|
const currentFetchId = ++fetchIdRef.current
|
|
24
69
|
try {
|
|
25
70
|
const response = await apiRef.current(search)
|
|
71
|
+
// 💡 [物理拦截] 信号校准:若当前 ID 不是最新 ID,说明该信号已过期,执行丢弃协议
|
|
26
72
|
if (currentFetchId !== fetchIdRef.current) return
|
|
27
73
|
const { data, total, columns: newCols } = formatResponse(response ?? {})
|
|
74
|
+
// 💡 [性能装甲] 只有当列指纹发生变化时,才触发 React 的重绘逻辑
|
|
28
75
|
if (newCols && getColumnSchema(newCols) !== getColumnSchema(columns)) {
|
|
29
76
|
setColumns(newCols)
|
|
30
77
|
}
|
|
@@ -33,6 +80,7 @@ export const useTableQuery = ({ api, cols, initialParams = {}, formatResponse })
|
|
|
33
80
|
} catch (error) {
|
|
34
81
|
console.error('查询失败', error)
|
|
35
82
|
} finally {
|
|
83
|
+
// 💡 状态闭环:确保只有最新的信号能解除 Loading 锁定
|
|
36
84
|
if (currentFetchId === fetchIdRef.current) {
|
|
37
85
|
setLoading(false)
|
|
38
86
|
}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import dayjs from 'dayjs'
|
|
2
2
|
import { request } from './request'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
*/
|
|
6
17
|
export const parseExcel = async file => {
|
|
7
18
|
/* const arrayBuffer = await file.arrayBuffer()
|
|
8
19
|
const XLSX = require('xlsx')
|
|
@@ -13,6 +24,14 @@ export const parseExcel = async file => {
|
|
|
13
24
|
return excelData.filter(item => item.length > 0) */
|
|
14
25
|
}
|
|
15
26
|
|
|
27
|
+
/**
|
|
28
|
+
* @async
|
|
29
|
+
* @function initOSS
|
|
30
|
+
* @description [出口协议] 阿里云 OSS 凭证初始化:从司令部申请 STS 令牌并计算物理传输签名。
|
|
31
|
+
* @param {string} url - 令牌申请地址
|
|
32
|
+
* @param {Object} options - 请求配置参数
|
|
33
|
+
* @returns {Promise<Object>} 包含 OSS 客户端实例、上传策略及 Host 节点的配置包
|
|
34
|
+
*/
|
|
16
35
|
export const initOSS = async (url, options) => {
|
|
17
36
|
/* const OSS = require('ali-oss')
|
|
18
37
|
const crypto = require('crypto-js')
|
|
@@ -29,6 +48,13 @@ export const initOSS = async (url, options) => {
|
|
|
29
48
|
return { client, policy, signature,host: `https://${bucket}.${endpoint.split('//')[1]}`, ...response?.data } */
|
|
30
49
|
}
|
|
31
50
|
|
|
51
|
+
/**
|
|
52
|
+
* @function formatQuery
|
|
53
|
+
* @description [数据脱水] 搜索参数校准协议:根据零部件类型(如日期)自动执行类型转换(String -> Number)。
|
|
54
|
+
* @param {Object} params - 原始表单/URL 传感器数据
|
|
55
|
+
* @param {Array} formItems - 对应的零部件配置清单
|
|
56
|
+
* @returns {Object} 经过物理清洗、可直接用于 API 请求的参数包
|
|
57
|
+
*/
|
|
32
58
|
export const formatQuery = (params, formItems) => {
|
|
33
59
|
const cleanParams = {}
|
|
34
60
|
Object.keys(params).forEach(key => {
|
|
@@ -41,6 +67,15 @@ export const formatQuery = (params, formItems) => {
|
|
|
41
67
|
return cleanParams
|
|
42
68
|
}
|
|
43
69
|
|
|
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
|
+
*/
|
|
44
79
|
export const timeRender = ({ time, date, minute }) => {
|
|
45
80
|
if (!time) {
|
|
46
81
|
return ''
|
|
@@ -55,6 +90,16 @@ export const timeRender = ({ time, date, minute }) => {
|
|
|
55
90
|
}
|
|
56
91
|
}
|
|
57
92
|
|
|
93
|
+
/**
|
|
94
|
+
* @async
|
|
95
|
+
* @function exportDataToExcel
|
|
96
|
+
* @description [物资外运] 数据表格导出协议:支持复杂的渲染逻辑回溯,将页面数据封存为 .xlsx 格式。
|
|
97
|
+
* @param {string} url - 数据源地址
|
|
98
|
+
* @param {Object} options - 请求参数
|
|
99
|
+
* @param {Array} columns - 表格列配置,支持 exportRender 优先级
|
|
100
|
+
* @param {string} fileName - 导出的文件名
|
|
101
|
+
* @param {Function} [formatter] - 数据预处理回调
|
|
102
|
+
*/
|
|
58
103
|
export const exportDataToExcel = async (url, options, columns, fileName, formatter) => {
|
|
59
104
|
/* const response = await request(url, options)
|
|
60
105
|
const result = formatter ? formatter(response) : (response?.data ?? [])
|
|
@@ -80,6 +125,16 @@ export const exportDataToExcel = async (url, options, columns, fileName, formatt
|
|
|
80
125
|
} */
|
|
81
126
|
}
|
|
82
127
|
|
|
128
|
+
/**
|
|
129
|
+
* @async
|
|
130
|
+
* @function streamDownload
|
|
131
|
+
* @description [后勤传输] 智能流式下载协议:具备“语义侦察”能力,能自动识别被伪装成二进制流的 JSON 报错信息。
|
|
132
|
+
* @param {Object} params - 传输配置
|
|
133
|
+
* @param {string} params.url - 物资下载地址
|
|
134
|
+
* @param {Object} [params.options] - Fetch 配置
|
|
135
|
+
* @param {Object} [params.headers] - 自定义请求头
|
|
136
|
+
* @param {string} [fileName='下载'] - 预设文件名(支持从 Content-Disposition 自动对齐)
|
|
137
|
+
*/
|
|
83
138
|
export const streamDownload = async ({ url, options, headers = {} }, fileName = '下载') => {
|
|
84
139
|
|
|
85
140
|
const response = await fetch(url, { ...options, headers })
|
|
@@ -96,6 +151,7 @@ export const streamDownload = async ({ url, options, headers = {} }, fileName =
|
|
|
96
151
|
return
|
|
97
152
|
}
|
|
98
153
|
|
|
154
|
+
// 💡 语义侦察逻辑:检查前 10 个字节,判断是否为 JSON 报错信息
|
|
99
155
|
const slice = blob.slice(0, 10)
|
|
100
156
|
const text = (await slice.text()).trim()
|
|
101
157
|
|
|
@@ -110,6 +166,7 @@ export const streamDownload = async ({ url, options, headers = {} }, fileName =
|
|
|
110
166
|
return
|
|
111
167
|
}
|
|
112
168
|
|
|
169
|
+
// 物理解析 Content-Disposition 头部,获取指挥官预设的文件名
|
|
113
170
|
const disposition = response.headers.get('content-disposition')
|
|
114
171
|
let name = fileName
|
|
115
172
|
|
|
@@ -133,6 +190,17 @@ export const streamDownload = async ({ url, options, headers = {} }, fileName =
|
|
|
133
190
|
setTimeout(() => URL.revokeObjectURL(urlObj), 1000)
|
|
134
191
|
}
|
|
135
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
|
+
*/
|
|
136
204
|
export const moneyRender = (value, { decimals = 2, currency = 'CNY', language = 'zh-CN', showSymbol = true } = {}) => {
|
|
137
205
|
if (value === null || value === undefined || value === '') return '-'
|
|
138
206
|
const number = Number(value)
|
|
@@ -143,6 +211,16 @@ export const moneyRender = (value, { decimals = 2, currency = 'CNY', language =
|
|
|
143
211
|
return new Intl.NumberFormat(language, options).format(number)
|
|
144
212
|
}
|
|
145
213
|
|
|
214
|
+
/**
|
|
215
|
+
* @function formatUnit
|
|
216
|
+
* @description [视觉渲染] 单位自动进阶协议:实现 B/KB/MB... 等数据的物理量级自动折算。
|
|
217
|
+
* @param {number} value - 原始字节数值
|
|
218
|
+
* @param {Object} [options] - 配置参数
|
|
219
|
+
* @param {number} [options.k=1024] - 进位系数
|
|
220
|
+
* @param {Array} [options.units] - 单位阶梯
|
|
221
|
+
* @param {number} [options.decimals=2] - 保留小数位
|
|
222
|
+
* @returns {string} 带单位的物理量级文本
|
|
223
|
+
*/
|
|
146
224
|
export const formatUnit = (value, { k = 1024, units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'], decimals = 2 } = {}) => {
|
|
147
225
|
if (value === null || value === undefined || isNaN(value) || value === 0) {
|
|
148
226
|
return `0 ${units[0] ?? ''}`
|