@yorha2b-lab/autodev 7.7.4 → 7.7.6
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/src/workshop/react/blueprint/molds/MySearchForm.hbs +1 -1
- package/src/workshop/react/blueprint/molds/handleBlock.hbs +5 -1
- package/src/workshop/react/blueprint/molds/stateBlock.hbs +2 -0
- package/src/workshop/react/briefings/raider.js +1 -1
- package/src/workshop/react/briefings/striker.js +1 -1
- package/src/workshop/react/builder/index.js +2 -1
- package/src/workshop/react/protocols/striker.json +2 -2
- package/src/workshop/react/standardParts/AliyunOSSUpload.js +114 -0
- package/src/workshop/react/standardParts/DraggableContainer.js +5 -2
- package/src/workshop/react/standardParts/EditableCell.js +5 -2
- package/src/workshop/react/standardParts/MyForm.js +19 -12
- package/src/workshop/react/standardParts/MyModalForm.js +7 -10
- package/src/workshop/react/standardParts/MySearchForm.js +1 -1
- package/src/workshop/react/standardParts/MyTable.js +61 -26
- package/src/workshop/react/standardParts/index.js +31 -143
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<MySearchForm form={form} search={search}
|
|
1
|
+
<MySearchForm form={form} search={search} loading={loading} formItems={ {{formItems}} } initialParams={initialParams} initialValues={query} setSearch={setSearch} />
|
|
@@ -53,7 +53,7 @@ module.exports = `
|
|
|
53
53
|
- 必填校验: 若图片中 label 前有红色星号,必须加入 rules:[{required:true,message:'xxx不能为空'}]。
|
|
54
54
|
|
|
55
55
|
## 3. 页签 (Tabs)
|
|
56
|
-
- 结构: tabs: [{
|
|
56
|
+
- 结构: tabs: [{tab:'页签名',key:'页签英文'}]
|
|
57
57
|
|
|
58
58
|
## 4. 下拉选项字典 (OptionDict)
|
|
59
59
|
- 结构: optionDict: { 字段英文名Options: [] }
|
|
@@ -18,7 +18,7 @@ module.exports = `
|
|
|
18
18
|
## 零部件构筑规范 (Component Specifications)
|
|
19
19
|
|
|
20
20
|
## 标签页 (tabs)
|
|
21
|
-
- **基础格式**: [{
|
|
21
|
+
- **基础格式**: [{ tab: '文本', key: 'englishName' }]
|
|
22
22
|
- **识别条件**: 底部有显著长横线 (Ink Bar) 或呈现包裹感 (Card style) 时判定。
|
|
23
23
|
- **排除项**: 表格上方的独立按钮必须归类为 [functionButton]。
|
|
24
24
|
|
|
@@ -50,8 +50,8 @@ module.exports = ({ template, logistics, headquarters }) => {
|
|
|
50
50
|
|
|
51
51
|
const hooksLib = ['useTableQuery']
|
|
52
52
|
const utilsLib = ['timeRender', 'moneyRender']
|
|
53
|
-
const reactLib = ['useState', 'useEffect', 'useRef', 'useMemo']
|
|
54
53
|
const componentsLib = ['MyTable', 'MyImage', 'MyModalForm', 'MySearchForm']
|
|
54
|
+
const reactLib = ['useRef', 'useMemo', 'useState', 'useEffect', 'useContext']
|
|
55
55
|
const antdLib = ['Tag', 'Card', 'Badge', 'Space', 'Modal', 'Alert', 'Image', 'Table', 'Input', 'Select', 'Button']
|
|
56
56
|
|
|
57
57
|
const usedAntd = antdLib.filter(name => new RegExp(`\\b${name}\\b`).test(bodyCode))
|
|
@@ -67,6 +67,7 @@ module.exports = ({ template, logistics, headquarters }) => {
|
|
|
67
67
|
...usedComps.map(comp => `import { ${comp} } from '../../components/${comp}'`),
|
|
68
68
|
...(module === 'index' ? [
|
|
69
69
|
`import { request } from '../../utils/request'`,
|
|
70
|
+
`import { GlobalContext } from '../../layouts'`,
|
|
70
71
|
`import { formatQuery } from '../../utils/utils'`,
|
|
71
72
|
`import { ${hasTabs ? 'tabs, ' : ''}${hasFormItems ? 'formItems, ' : ''}modalItems, tableColumns} from './resource'`
|
|
72
73
|
] : [
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Button, Upload } from 'antd'
|
|
2
|
+
import { initOSS } from '../utils/utils'
|
|
3
|
+
import { useState, useEffect } from 'react'
|
|
4
|
+
import { UploadOutlined } from '@ant-design/icons'
|
|
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
|
+
export const AliyunOSSUpload = ({ value, onChange, ...restProps }) => {
|
|
28
|
+
|
|
29
|
+
// 💡 物理剥离:将地堡专用参数与 Antd 原生属性分离
|
|
30
|
+
const { oss, url, path, options, expireField = 'expire', ...originUploadProps } = restProps
|
|
31
|
+
|
|
32
|
+
const [OSSData, setOSSData] = useState(oss)
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @async
|
|
36
|
+
* @function init
|
|
37
|
+
* @description [信号握手] 执行令牌获取协议,从司令部申请最新的物理上传授权
|
|
38
|
+
*/
|
|
39
|
+
const init = async () => {
|
|
40
|
+
try {
|
|
41
|
+
const result = await initOSS(url, options)
|
|
42
|
+
setOSSData(result)
|
|
43
|
+
return result
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.log(err)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
// 💡 自愈逻辑:如果发现能源包 (OSSData) 为空,则自动触发初始化
|
|
51
|
+
if (!OSSData) {
|
|
52
|
+
init()
|
|
53
|
+
}
|
|
54
|
+
}, [])
|
|
55
|
+
|
|
56
|
+
const handleChange = ({ fileList }) => onChange?.([...fileList])
|
|
57
|
+
|
|
58
|
+
const onRemove = file => {
|
|
59
|
+
const files = (value || []).filter(v => v.uid !== file.uid)
|
|
60
|
+
onChange?.(files)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @function getExtraData
|
|
65
|
+
* @description [数据封装] 将动态生成的物理凭证与目标文件路径压入传输负载
|
|
66
|
+
* @param {Object} file - 待传输的文件对象
|
|
67
|
+
*/
|
|
68
|
+
const getExtraData = file => ({
|
|
69
|
+
key: file.url,
|
|
70
|
+
policy: OSSData?.policy,
|
|
71
|
+
Signature: OSSData?.signature,
|
|
72
|
+
OSSAccessKeyId: OSSData?.accessKeyId,
|
|
73
|
+
'x-oss-security-token': OSSData?.stsToken,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @async
|
|
78
|
+
* @function beforeUpload
|
|
79
|
+
* @description [生存期侦察] 在传输启动前执行 TTL 检测。
|
|
80
|
+
* 若令牌已失效(过期),则强行拦截并执行同步刷新协议,同时执行物理路径归一化。
|
|
81
|
+
*/
|
|
82
|
+
const beforeUpload = async file => {
|
|
83
|
+
let currentOSS = OSSData
|
|
84
|
+
const expire = Number(OSSData?.[expireField] ?? 0) * 1000
|
|
85
|
+
if (expire < Date.now()) {
|
|
86
|
+
currentOSS = await init()
|
|
87
|
+
}
|
|
88
|
+
// 💡 物理路径重组:通过正则清除多余的路径分隔符,确保存储节点坐标唯一
|
|
89
|
+
file.url = `${currentOSS?.dir ?? path}/${file.uid}_${file.name}`.replace(/\/\//g, '/')
|
|
90
|
+
return file
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @constant uploadProps
|
|
95
|
+
* @description [物理合并] 最终生成的 Antd Upload 指令集:
|
|
96
|
+
* 自动优先应用 originUploadProps 中的自定义设置。
|
|
97
|
+
*/
|
|
98
|
+
const uploadProps = {
|
|
99
|
+
onRemove,
|
|
100
|
+
name: 'file',
|
|
101
|
+
beforeUpload,
|
|
102
|
+
fileList: value,
|
|
103
|
+
data: getExtraData,
|
|
104
|
+
action: OSSData?.host,
|
|
105
|
+
onChange: handleChange,
|
|
106
|
+
...originUploadProps
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<Upload {...uploadProps}>
|
|
111
|
+
<Button icon={<UploadOutlined />}>选择文件</Button>
|
|
112
|
+
</Upload>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
@@ -9,11 +9,14 @@ const arrayMoveImmutable = (array, oldIndex, newIndex) => {
|
|
|
9
9
|
return newArray
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export const DraggableContainer = ({ props, setDataSource }) => {
|
|
12
|
+
export const DraggableContainer = ({ props, customSave, setDataSource, dataSourceRef }) => {
|
|
13
13
|
|
|
14
14
|
const onSortEnd = ({ oldIndex, newIndex }) => {
|
|
15
15
|
if (oldIndex !== newIndex) {
|
|
16
|
-
|
|
16
|
+
const currentData = dataSourceRef?.current || []
|
|
17
|
+
const sortedDataSource = arrayMoveImmutable(currentData.slice(), oldIndex, newIndex)
|
|
18
|
+
setDataSource(sortedDataSource)
|
|
19
|
+
customSave?.(sortedDataSource)
|
|
17
20
|
}
|
|
18
21
|
}
|
|
19
22
|
|
|
@@ -60,7 +60,7 @@ export const EditableRow = ({ index, record, ...props }) => {
|
|
|
60
60
|
* @param {boolean} [props.defaultEdit] - 持久化模式:是否默认始终开启编辑状态
|
|
61
61
|
* @param {string} [props.editType='text'] - 零部件型号(text/number/select/checkbox)
|
|
62
62
|
*/
|
|
63
|
-
export const EditableCell = ({ title, record, editable, disabled, children, dataIndex, placeholder, handleSave, defaultEdit, rules = [], options = [], editType = 'text', ...restProps }) => {
|
|
63
|
+
export const EditableCell = ({ title, record, editable, disabled, children, dataIndex, placeholder, handleSave, defaultEdit, editRender, rules = [], options = [], editType = 'text', ...restProps }) => {
|
|
64
64
|
|
|
65
65
|
const inputRef = useRef(null)
|
|
66
66
|
const form = useContext(EditableContext)
|
|
@@ -125,6 +125,9 @@ export const EditableCell = ({ title, record, editable, disabled, children, data
|
|
|
125
125
|
case 'number':
|
|
126
126
|
inputNode = <InputNumber ref={inputRef} onPressEnter={save} {...commonProps} />
|
|
127
127
|
break
|
|
128
|
+
case 'custom':
|
|
129
|
+
inputNode = editRender({ form, record, save, dataIndex, disabled, toggleEdit, commonProps })
|
|
130
|
+
break
|
|
128
131
|
case 'checkbox':
|
|
129
132
|
inputNode = (
|
|
130
133
|
<div tabIndex={-1} onBlur={e => {
|
|
@@ -151,7 +154,7 @@ export const EditableCell = ({ title, record, editable, disabled, children, data
|
|
|
151
154
|
{editable ? (
|
|
152
155
|
editing || defaultEdit ? (
|
|
153
156
|
// 骇入模式:渲染输入零件
|
|
154
|
-
<Form.Item {...(['checkbox'].includes(editType) ? { style: formProps.style } : formProps)}>
|
|
157
|
+
<Form.Item {...(['custom', 'checkbox'].includes(editType) ? { style: formProps.style } : formProps)}>
|
|
155
158
|
{inputNode}
|
|
156
159
|
</Form.Item>
|
|
157
160
|
) : (
|
|
@@ -43,10 +43,10 @@ const ACTION_PROCESSORS = {
|
|
|
43
43
|
// prefix: (item) => ({ props: { prefix: <Icon type={item.prefix} /> } }),
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const FormRenderer = ({ form, formItems, tableProps = {} }) => {
|
|
46
|
+
const FormRenderer = ({ form, options, formItems, renderAction, tableProps = {} }) => {
|
|
47
47
|
|
|
48
48
|
// 定义核心渲染逻辑,供递归使用
|
|
49
|
-
const renderCore = items => <FormRenderer form={form} formItems={items} tableProps={tableProps} />
|
|
49
|
+
const renderCore = items => <FormRenderer form={form} options={options} formItems={items} renderAction={renderAction} tableProps={tableProps} />
|
|
50
50
|
|
|
51
51
|
// 定义单项动作注入逻辑
|
|
52
52
|
const getRenderItemProps = item => {
|
|
@@ -61,28 +61,35 @@ const FormRenderer = ({ form, formItems, tableProps = {} }) => {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
return formItems.map((item, index) => {
|
|
64
|
+
// 💡 保持纯粹不可变:通过解构浅拷贝,绝不污染原始 formItems 对象!
|
|
65
|
+
const processedItem = {
|
|
66
|
+
...item,
|
|
67
|
+
...(item.renderAction && renderAction?.[item.name] ? { render: renderAction[item.name] } : {}),
|
|
68
|
+
...(['select', 'radio', 'checkbox'].includes(item.type) && !item.options ? { options: options?.[item.name] || [] } : {})
|
|
69
|
+
}
|
|
70
|
+
|
|
64
71
|
// 情况 A:发现容器型零件 (含有 layoutType)
|
|
65
|
-
if (
|
|
66
|
-
const Layout = LAYOUT_PROCESSORS[
|
|
72
|
+
if (processedItem.layoutType && LAYOUT_PROCESSORS[processedItem.layoutType]) {
|
|
73
|
+
const Layout = LAYOUT_PROCESSORS[processedItem.layoutType] ?? LAYOUT_PROCESSORS.default
|
|
67
74
|
return (
|
|
68
75
|
<Layout
|
|
69
|
-
item={
|
|
70
|
-
key={
|
|
71
|
-
renderCore={renderCore}
|
|
76
|
+
item={processedItem}
|
|
77
|
+
key={processedItem.name || index}
|
|
78
|
+
renderCore={renderCore}
|
|
72
79
|
/>
|
|
73
80
|
)
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
// 情况 B:标准作战零件
|
|
77
84
|
return (
|
|
78
|
-
<Col span={
|
|
79
|
-
<MyBaseForm item={{ ...
|
|
85
|
+
<Col span={processedItem.span ?? 6} key={processedItem.name || index}>
|
|
86
|
+
<MyBaseForm item={{ ...processedItem, ...getRenderItemProps(processedItem) }} form={form} />
|
|
80
87
|
</Col>
|
|
81
88
|
)
|
|
82
89
|
})
|
|
83
90
|
}
|
|
84
91
|
|
|
85
|
-
export const MyForm = ({ formItems, externalForm, labelCol, wrapperCol, tableProps, layout = 'horizontal',
|
|
92
|
+
export const MyForm = ({ formItems = [], externalForm, labelCol, wrapperCol, tableProps, onValuesChange, layout = 'horizontal', options = {}, renderAction = {} }) => {
|
|
86
93
|
|
|
87
94
|
const [internalForm] = Form.useForm()
|
|
88
95
|
const form = externalForm || internalForm
|
|
@@ -90,8 +97,8 @@ export const MyForm = ({ formItems, externalForm, labelCol, wrapperCol, tablePro
|
|
|
90
97
|
return (
|
|
91
98
|
<Form form={form} layout={layout} preserve={false} labelCol={labelCol} wrapperCol={wrapperCol} onValuesChange={onValuesChange} scrollToFirstError={{ behavior: 'smooth', block: 'center' }}>
|
|
92
99
|
<Row gutter={[24, 0]}>
|
|
93
|
-
<FormRenderer form={form} formItems={formItems} tableProps={tableProps} />
|
|
100
|
+
<FormRenderer form={form} options={options} formItems={formItems} renderAction={renderAction} tableProps={tableProps} />
|
|
94
101
|
</Row>
|
|
95
102
|
</Form>
|
|
96
103
|
)
|
|
97
|
-
}
|
|
104
|
+
}
|
|
@@ -36,17 +36,12 @@ const flattenFormItems = items => {
|
|
|
36
36
|
* title='构筑新模块'
|
|
37
37
|
* visible={visible}
|
|
38
38
|
* submit={async (vals) => await save(vals)}
|
|
39
|
-
*
|
|
39
|
+
* modalItems={[{ label: '截止日期', name: 'deadline', type: 'date' }]}
|
|
40
40
|
* />
|
|
41
41
|
*/
|
|
42
42
|
export const MyModalForm = ({ extra, width, title, layout, okText, options, footer, submit, record, visible, setModal, labelCol, modalItems, wrapperCol, cancelText, tableConfig, okButtonProps, renderAction, onValuesChange, handleModalTableOk }) => {
|
|
43
43
|
|
|
44
44
|
const rowKey = tableConfig?.rowKey || 'id'
|
|
45
|
-
const formItems = useMemo(() => modalItems.map(item => ({
|
|
46
|
-
...item,
|
|
47
|
-
...(item.renderAction ? { render: renderAction?.[item.name] } : {}),
|
|
48
|
-
...(['select'].includes(item.type) ? { options: item.options ?? options?.[item.name] ?? [] } : {})
|
|
49
|
-
})), [options, modalItems, renderAction])
|
|
50
45
|
|
|
51
46
|
const [form] = Form.useForm()
|
|
52
47
|
const [pending, setPending] = useState(false)
|
|
@@ -62,7 +57,7 @@ export const MyModalForm = ({ extra, width, title, layout, okText, options, foot
|
|
|
62
57
|
useEffect(() => {
|
|
63
58
|
if (visible && !prevVisibleRef.current) {
|
|
64
59
|
if (record && Object.keys(record).length > 0) {
|
|
65
|
-
const itemMap = new Map(flattenFormItems(
|
|
60
|
+
const itemMap = new Map(flattenFormItems(modalItems).map(i => [i.name, i]))
|
|
66
61
|
const initialData = {}
|
|
67
62
|
Object.entries(record).forEach(([key, value]) => {
|
|
68
63
|
const config = itemMap.get(key)
|
|
@@ -86,7 +81,7 @@ export const MyModalForm = ({ extra, width, title, layout, okText, options, foot
|
|
|
86
81
|
}
|
|
87
82
|
}
|
|
88
83
|
prevVisibleRef.current = visible
|
|
89
|
-
}, [visible, record, form,
|
|
84
|
+
}, [visible, record, form, modalItems])
|
|
90
85
|
|
|
91
86
|
/**
|
|
92
87
|
* @async
|
|
@@ -98,7 +93,7 @@ export const MyModalForm = ({ extra, width, title, layout, okText, options, foot
|
|
|
98
93
|
try {
|
|
99
94
|
const values = await form.validateFields()
|
|
100
95
|
const formattedValues = { ...values }
|
|
101
|
-
flattenFormItems(
|
|
96
|
+
flattenFormItems(modalItems).forEach(item => {
|
|
102
97
|
const val = formattedValues[item.name]
|
|
103
98
|
if (item.type?.includes('date') && val) {
|
|
104
99
|
if (Array.isArray(val)) {
|
|
@@ -159,10 +154,12 @@ export const MyModalForm = ({ extra, width, title, layout, okText, options, foot
|
|
|
159
154
|
{extra?.header?.(ctx)}
|
|
160
155
|
<MyForm
|
|
161
156
|
layout={layout}
|
|
157
|
+
options={options}
|
|
162
158
|
externalForm={form}
|
|
163
159
|
labelCol={labelCol}
|
|
164
|
-
formItems={
|
|
160
|
+
formItems={modalItems}
|
|
165
161
|
wrapperCol={wrapperCol}
|
|
162
|
+
renderAction={renderAction}
|
|
166
163
|
tableProps={{ tableConfig, setModalTable, setSelectedTableRows }}
|
|
167
164
|
onValuesChange={(changed, all) => onValuesChange?.({ changed, all, form, record })}
|
|
168
165
|
/>
|
|
@@ -32,7 +32,7 @@ import { SearchOutlined, RedoOutlined } from '@ant-design/icons'
|
|
|
32
32
|
* formItems={[{ label: '状态', name: 'status', type: 'select', options: [...] }]}
|
|
33
33
|
* />
|
|
34
34
|
*/
|
|
35
|
-
export const MySearchForm = ({ form, extra, search, options, loading, labelCol, setSearch, formItems, showLimit = 7, initialValues, initialParams, customReset, customFinish, renderAction, onValuesChange, syncUrlParams = true, defaultPageSize = 10 }) => {
|
|
35
|
+
export const MySearchForm = ({ form, extra, search, options, loading, labelCol, setSearch, formItems, showLimit = 7, initialValues = {}, initialParams = {}, customReset, customFinish, renderAction, onValuesChange, syncUrlParams = true, defaultPageSize = 10 }) => {
|
|
36
36
|
|
|
37
37
|
const [limit, setLimit] = useState(showLimit)
|
|
38
38
|
|
|
@@ -55,15 +55,18 @@ import { useRef, useMemo, useEffect, useCallback } from 'react'
|
|
|
55
55
|
* }}
|
|
56
56
|
* />
|
|
57
57
|
*/
|
|
58
|
-
export const MyTable = ({ size, query, total, search, autoScroll, onChange, pagination, renderAction, rowClassName, customSave, setDataSource, lineFormChange, columns = [], rowSelection, rowKey = 'id', loading = false, dataSource = [], scroll = { x: 'max-content' }, isLocalPaging = false, ...restProps }) => {
|
|
58
|
+
export const MyTable = ({ size, query, total, search, options, autoScroll, onChange, pagination, editRender = {}, renderAction, rowClassName, customSave, setDataSource, lineFormChange, columns = [], rowSelection, rowKey = 'id', loading = false, dataSource = [], draggable = false, scroll = { x: 'max-content' }, isLocalPaging = false, ...restProps }) => {
|
|
59
59
|
|
|
60
60
|
const tableRef = useRef(null)
|
|
61
61
|
const hasScrolledRef = useRef(false)
|
|
62
|
+
const dataSourceRef = useRef(dataSource)
|
|
63
|
+
dataSourceRef.current = dataSource
|
|
62
64
|
|
|
63
65
|
/**
|
|
64
66
|
* @description [权限侦察] 自动扫描列配置。若存在 editType 型号,则激活“骇入模式(编辑模式)”。
|
|
65
67
|
*/
|
|
66
68
|
const isEditable = useMemo(() => columns.some(col => col.editType), [columns])
|
|
69
|
+
const hasCustomComponents = isEditable || draggable
|
|
67
70
|
|
|
68
71
|
/**
|
|
69
72
|
* @function handleSave
|
|
@@ -73,27 +76,36 @@ export const MyTable = ({ size, query, total, search, autoScroll, onChange, pagi
|
|
|
73
76
|
* @param {Object} row - 修改后的行片段
|
|
74
77
|
*/
|
|
75
78
|
const handleSave = useCallback((row) => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
const prev = dataSourceRef.current
|
|
80
|
+
const index = prev.findIndex(item => row[rowKey] === item[rowKey])
|
|
81
|
+
if (index === -1) return
|
|
82
|
+
const item = prev[index]
|
|
83
|
+
const updatedRow = { ...item, ...row }
|
|
84
|
+
const newData = [...prev]
|
|
85
|
+
newData.splice(index, 1, updatedRow)
|
|
86
|
+
setDataSource(newData)
|
|
87
|
+
customSave?.(updatedRow, newData)
|
|
84
88
|
}, [rowKey, customSave, setDataSource])
|
|
85
89
|
|
|
86
90
|
/**
|
|
87
91
|
* @constant components
|
|
88
92
|
* @description [零部件注册] 注入地堡特有的可编辑 Row 和 Cell 单元。
|
|
89
93
|
*/
|
|
90
|
-
const components = useMemo(() =>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
const components = useMemo(() => {
|
|
95
|
+
if (!hasCustomComponents) return undefined
|
|
96
|
+
const c = { body: {} }
|
|
97
|
+
// 1. 只有有编辑列时,才挂载行内编辑单元
|
|
98
|
+
if (isEditable) {
|
|
99
|
+
c.body.row = EditableRow
|
|
100
|
+
c.body.cell = EditableCell
|
|
101
|
+
}
|
|
102
|
+
// 2. 只有开启了拖拽,才挂载拖拽容器
|
|
103
|
+
if (draggable) {
|
|
104
|
+
c.body.wrapper = props => <DraggableContainer props={props} customSave={customSave} setDataSource={setDataSource} dataSourceRef={dataSourceRef} />
|
|
95
105
|
}
|
|
96
|
-
|
|
106
|
+
return c
|
|
107
|
+
}, [isEditable, draggable, customSave, setDataSource])
|
|
108
|
+
|
|
97
109
|
|
|
98
110
|
/**
|
|
99
111
|
* @constant mergedColumns
|
|
@@ -103,30 +115,53 @@ export const MyTable = ({ size, query, total, search, autoScroll, onChange, pagi
|
|
|
103
115
|
const mergedColumns = useMemo(() => {
|
|
104
116
|
return columns.map(col => {
|
|
105
117
|
if (!col.editType) {
|
|
118
|
+
let currentCol = { ...col }
|
|
106
119
|
if (col.renderAction && renderAction?.[col.dataIndex]) {
|
|
107
|
-
|
|
120
|
+
currentCol.render = renderAction[col.dataIndex]
|
|
121
|
+
}
|
|
122
|
+
if (col.search) {
|
|
123
|
+
currentCol = {
|
|
124
|
+
...currentCol,
|
|
125
|
+
filterSearch: true,
|
|
126
|
+
onFilter: (value, record) => col.remote ? undefined : col.customFilter({ value, record, dataIndex: col.dataIndex }),
|
|
127
|
+
filterDropdown: ({ close, confirm, selectedKeys, clearFilters, setSelectedKeys }) => (
|
|
128
|
+
<Space direction='vertical' style={{ padding: 12 }}>
|
|
129
|
+
{col.customFilterDropdown({ dataIndex: col.dataIndex, selectedKeys, setSelectedKeys })}
|
|
130
|
+
<div style={{ display: 'flex' }}>
|
|
131
|
+
<a style={{ marginLeft: 'auto', marginRight: 8 }} onClick={() => {
|
|
132
|
+
if (col.remote) {
|
|
133
|
+
setSearch(prev => ({ ...prev, pageNo: 1, [col.dataIndex]: col.formatFilter?.(selectedKeys) ?? selectedKeys?.toString() }))
|
|
134
|
+
}
|
|
135
|
+
confirm()
|
|
136
|
+
}}>确定</a>
|
|
137
|
+
<a onClick={() => {
|
|
138
|
+
if (col.remote) {
|
|
139
|
+
setSearch(prev => ({ ...prev, pageNo: 1, [col.dataIndex]: undefined }))
|
|
140
|
+
}
|
|
141
|
+
clearFilters()
|
|
142
|
+
}}>重置</a>
|
|
143
|
+
</div>
|
|
144
|
+
</Space>
|
|
145
|
+
),
|
|
146
|
+
}
|
|
108
147
|
}
|
|
109
|
-
return
|
|
148
|
+
return currentCol
|
|
110
149
|
}
|
|
111
150
|
return {
|
|
112
151
|
...col,
|
|
113
152
|
onCell: (record) => ({
|
|
153
|
+
...col,
|
|
114
154
|
record,
|
|
115
155
|
handleSave,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
options: col.options,
|
|
119
|
-
editType: col.editType,
|
|
120
|
-
dataIndex: col.dataIndex,
|
|
121
|
-
placeholder: col.placeholder,
|
|
122
|
-
defaultEdit: col.defaultEdit,
|
|
156
|
+
editRender: editRender?.[col.dataIndex],
|
|
157
|
+
options: col.options ?? options?.[col.dataIndex],
|
|
123
158
|
// 💡 条件防御逻辑:支持针对单行的物理锁定
|
|
124
159
|
disabled: col.specialDisabled ? record.disabled : col.disabled,
|
|
125
160
|
editable: col.specialEditType ? record.needEdit && !!col.editType : !!col.editType,
|
|
126
161
|
}),
|
|
127
162
|
}
|
|
128
163
|
})
|
|
129
|
-
}, [columns, handleSave, renderAction])
|
|
164
|
+
}, [columns, options, handleSave, editRender, renderAction])
|
|
130
165
|
|
|
131
166
|
/**
|
|
132
167
|
* @constant paginationConfig
|
|
@@ -183,9 +218,9 @@ export const MyTable = ({ size, query, total, search, autoScroll, onChange, pagi
|
|
|
183
218
|
columns={mergedColumns}
|
|
184
219
|
dataSource={dataSource}
|
|
185
220
|
size={size ?? 'middle'}
|
|
221
|
+
components={components}
|
|
186
222
|
rowSelection={rowSelection}
|
|
187
223
|
pagination={paginationConfig}
|
|
188
|
-
components={isEditable ? components : undefined}
|
|
189
224
|
// 💡 信号注入:将行监听协议绑定至底层 tr
|
|
190
225
|
onRow={(record, index) => ({ record, index, ...(typeof lineFormChange === 'function' ? { onValuesChange: lineFormChange } : {}) })}
|
|
191
226
|
rowClassName={(record, index) => {
|
|
@@ -1,117 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { UploadOutlined } from '@ant-design/icons'
|
|
4
|
-
import { Button, Tree, Radio, Input, Upload, Select, Cascader, Checkbox, DatePicker, InputNumber, TreeSelect, AutoComplete } from 'antd'
|
|
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
|
-
const AliyunOSSUpload = ({ value, onChange, ...restProps }) => {
|
|
28
|
-
|
|
29
|
-
// 💡 物理剥离:将地堡专用参数与 Antd 原生属性分离
|
|
30
|
-
const { oss, url, path, options, expireField = 'expire', ...originUploadProps } = restProps
|
|
31
|
-
|
|
32
|
-
const [OSSData, setOSSData] = useState(oss)
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @async
|
|
36
|
-
* @function init
|
|
37
|
-
* @description [信号握手] 执行令牌获取协议,从司令部申请最新的物理上传授权
|
|
38
|
-
*/
|
|
39
|
-
const init = async () => {
|
|
40
|
-
try {
|
|
41
|
-
const result = await initOSS(url, options)
|
|
42
|
-
setOSSData(result)
|
|
43
|
-
return result
|
|
44
|
-
} catch (err) {
|
|
45
|
-
console.log(err)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
// 💡 自愈逻辑:如果发现能源包 (OSSData) 为空,则自动触发初始化
|
|
51
|
-
if (!OSSData) {
|
|
52
|
-
init()
|
|
53
|
-
}
|
|
54
|
-
}, [])
|
|
55
|
-
|
|
56
|
-
const handleChange = ({ fileList }) => onChange?.([...fileList])
|
|
57
|
-
|
|
58
|
-
const onRemove = file => {
|
|
59
|
-
const files = (value || []).filter(v => v.uid !== file.uid)
|
|
60
|
-
onChange?.(files)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* @function getExtraData
|
|
65
|
-
* @description [数据封装] 将动态生成的物理凭证与目标文件路径压入传输负载
|
|
66
|
-
* @param {Object} file - 待传输的文件对象
|
|
67
|
-
*/
|
|
68
|
-
const getExtraData = file => ({
|
|
69
|
-
key: file.url,
|
|
70
|
-
policy: OSSData?.policy,
|
|
71
|
-
Signature: OSSData?.signature,
|
|
72
|
-
OSSAccessKeyId: OSSData?.accessKeyId,
|
|
73
|
-
'x-oss-security-token': OSSData?.stsToken,
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* @async
|
|
78
|
-
* @function beforeUpload
|
|
79
|
-
* @description [生存期侦察] 在传输启动前执行 TTL 检测。
|
|
80
|
-
* 若令牌已失效(过期),则强行拦截并执行同步刷新协议,同时执行物理路径归一化。
|
|
81
|
-
*/
|
|
82
|
-
const beforeUpload = async file => {
|
|
83
|
-
let currentOSS = OSSData
|
|
84
|
-
const expire = Number(OSSData?.[expireField] ?? 0) * 1000
|
|
85
|
-
if (expire < Date.now()) {
|
|
86
|
-
currentOSS = await init()
|
|
87
|
-
}
|
|
88
|
-
// 💡 物理路径重组:通过正则清除多余的路径分隔符,确保存储节点坐标唯一
|
|
89
|
-
file.url = `${currentOSS?.dir ?? path}/${file.uid}_${file.name}`.replace(/\/\//g, '/')
|
|
90
|
-
return file
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* @constant uploadProps
|
|
95
|
-
* @description [物理合并] 最终生成的 Antd Upload 指令集:
|
|
96
|
-
* 自动优先应用 originUploadProps 中的自定义设置。
|
|
97
|
-
*/
|
|
98
|
-
const uploadProps = {
|
|
99
|
-
onRemove,
|
|
100
|
-
name: 'file',
|
|
101
|
-
beforeUpload,
|
|
102
|
-
fileList: value,
|
|
103
|
-
data: getExtraData,
|
|
104
|
-
action: OSSData?.host,
|
|
105
|
-
onChange: handleChange,
|
|
106
|
-
...originUploadProps
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return (
|
|
110
|
-
<Upload {...uploadProps}>
|
|
111
|
-
<Button icon={<UploadOutlined />}>选择文件</Button>
|
|
112
|
-
</Upload>
|
|
113
|
-
)
|
|
114
|
-
}
|
|
1
|
+
import { AliyunOSSUpload } from './AliyunOSSUpload'
|
|
2
|
+
import { Tree, Form, Radio, Input, Upload, Select, Cascader, Checkbox, DatePicker, InputNumber, TreeSelect, AutoComplete } from 'antd'
|
|
115
3
|
|
|
116
4
|
/**
|
|
117
5
|
* @function formNode
|
|
@@ -130,6 +18,18 @@ const AliyunOSSUpload = ({ value, onChange, ...restProps }) => {
|
|
|
130
18
|
*/
|
|
131
19
|
export const formNode = ({ item }) => {
|
|
132
20
|
|
|
21
|
+
if (item.type === 'group' && Array.isArray(item.groups)) {
|
|
22
|
+
return (
|
|
23
|
+
<Input.Group compact style={{ width: item.width ?? '100%', display: 'flex', ...item.style }}>
|
|
24
|
+
{item.groups.map(subItem => (
|
|
25
|
+
<Form.Item noStyle key={subItem.name} name={subItem.name} rules={subItem.rules} initialValue={subItem.value}>
|
|
26
|
+
{formNode({ item: subItem })}
|
|
27
|
+
</Form.Item>
|
|
28
|
+
))}
|
|
29
|
+
</Input.Group>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
133
33
|
const commonProps = {
|
|
134
34
|
disabled: item.readOnly,
|
|
135
35
|
style: { width: item.width ?? '100%' },
|
|
@@ -137,34 +37,22 @@ export const formNode = ({ item }) => {
|
|
|
137
37
|
...item.props
|
|
138
38
|
}
|
|
139
39
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
case 'tree':
|
|
156
|
-
return <Tree checkable treeData={item.options} {...commonProps} />
|
|
157
|
-
case 'textarea':
|
|
158
|
-
return <Input.TextArea autoSize={{ minRows: 4 }} {...commonProps} />
|
|
159
|
-
case 'daterange':
|
|
160
|
-
return <DatePicker.RangePicker {...commonProps} placeholder={undefined} />
|
|
161
|
-
case 'cascader':
|
|
162
|
-
return <Cascader showSearch allowClear options={item.options} {...commonProps} />
|
|
163
|
-
case 'upload':
|
|
164
|
-
return <Upload {...item.uploadProps} {...commonProps}>{item.content ?? '上传文件'}</Upload>
|
|
165
|
-
case 'select':
|
|
166
|
-
return <Select allowClear showSearch mode={item.mode} options={item.options} optionFilterProp='label' {...commonProps} />
|
|
167
|
-
default:
|
|
168
|
-
return <Input allowClear {...commonProps} />
|
|
40
|
+
const componentDict = {
|
|
41
|
+
date: <DatePicker {...commonProps} />,
|
|
42
|
+
number: <InputNumber {...commonProps} />,
|
|
43
|
+
default: <Input allowClear {...commonProps} />,
|
|
44
|
+
ossUpload: <AliyunOSSUpload {...commonProps} />,
|
|
45
|
+
auto: <AutoComplete options={item.options} {...commonProps} />,
|
|
46
|
+
radio: <Radio.Group options={item.options} {...commonProps} />,
|
|
47
|
+
tree: <Tree checkable treeData={item.options} {...commonProps} />,
|
|
48
|
+
treeSelect: <TreeSelect treeData={item.options} {...commonProps} />,
|
|
49
|
+
checkbox: <Checkbox.Group options={item.options} {...commonProps} />,
|
|
50
|
+
textarea: <Input.TextArea autoSize={{ minRows: 4 }} {...commonProps} />,
|
|
51
|
+
daterange: <DatePicker.RangePicker {...commonProps} placeholder={undefined} />,
|
|
52
|
+
cascader: <Cascader showSearch allowClear options={item.options} {...commonProps} />,
|
|
53
|
+
upload: <Upload {...item.uploadProps} {...commonProps}>{item.content ?? '上传文件'}</Upload>,
|
|
54
|
+
select: <Select allowClear showSearch mode={item.mode} options={item.options} optionFilterProp='label' {...commonProps} />,
|
|
169
55
|
}
|
|
56
|
+
|
|
57
|
+
return componentDict[item.type] ?? componentDict.default
|
|
170
58
|
}
|