@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
|
@@ -3,13 +3,6 @@ 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
|
-
*/
|
|
13
6
|
const getValueFromEvent = e => {
|
|
14
7
|
if (Array.isArray(e)) {
|
|
15
8
|
return e
|
|
@@ -17,33 +10,20 @@ const getValueFromEvent = e => {
|
|
|
17
10
|
return e?.fileList
|
|
18
11
|
}
|
|
19
12
|
|
|
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
|
-
*/
|
|
30
|
-
const renderFormContent = ({ item, prefixName = [] }) => {
|
|
13
|
+
const renderFormContent = ({ item, form, prefixName = [] }) => {
|
|
31
14
|
|
|
32
|
-
// 💡 物理占位逻辑:若无 name 属性,则直接输出原始 value 文本
|
|
33
15
|
if (!item.name) {
|
|
34
|
-
return item.value
|
|
16
|
+
return <span dangerouslySetInnerHTML={{ __html: item.value }} />
|
|
35
17
|
}
|
|
36
18
|
|
|
37
|
-
// 💡 路径自动对齐:支持字符串或数组格式的 name,自动合并前缀实现“逻辑寻址”
|
|
38
19
|
const namePath = Array.isArray(prefixName) ? [...prefixName, ...(Array.isArray(item.name) ? item.name : [item.name])] : item.name
|
|
39
20
|
|
|
40
21
|
const inputNode = (
|
|
41
22
|
<Form.Item noStyle name={namePath} rules={item.rules} dependencies={item.dependencies} initialValue={item.value} {...(['upload', 'ossUpload'].includes(item.type) ? { getValueFromEvent, valuePropName: 'fileList' } : { valuePropName: item.valuePropName })}>
|
|
42
|
-
{formNode({ item })}
|
|
23
|
+
{formNode({ item, form })}
|
|
43
24
|
</Form.Item>
|
|
44
25
|
)
|
|
45
26
|
|
|
46
|
-
// 💡 视觉单位挂件逻辑:实现类似“金额(元)”的自动横向排版
|
|
47
27
|
if (item.unit) {
|
|
48
28
|
return (
|
|
49
29
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
@@ -56,43 +36,21 @@ const renderFormContent = ({ item, prefixName = [] }) => {
|
|
|
56
36
|
return inputNode
|
|
57
37
|
}
|
|
58
38
|
|
|
59
|
-
/**
|
|
60
|
-
* @component MyBaseForm
|
|
61
|
-
* @description [地堡通用零部件单元] 表单系统的物理外壳。
|
|
62
|
-
* 具备“单兵作战(Item)”与“集群协同(List)”两种模式切换能力。
|
|
63
|
-
*
|
|
64
|
-
* @param {Object} props - 组件属性
|
|
65
|
-
* @param {Object} props.item - 构筑协议对象
|
|
66
|
-
* @param {Object} props.form - Antd Form 实例,用于实现复杂的跨组件联动
|
|
67
|
-
* @param {boolean} [props.item.isList] - 模式切换:是否开启动态增减列表模式
|
|
68
|
-
* @param {Function} [props.item.render] - 逻辑劫持:支持自定义渲染函数,可完全改写渲染行为
|
|
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
|
-
*/
|
|
77
39
|
export const MyBaseForm = ({ item, form }) => {
|
|
78
40
|
return (
|
|
79
41
|
item.isList ?
|
|
80
|
-
// 💡 集群模式:处理动态数组类型的表单构筑
|
|
81
42
|
<Form.List name={item.name} initialValue={item.value}>
|
|
82
43
|
{(fields, { add, remove }) => (
|
|
83
44
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
84
45
|
{fields.map((field, index) => (
|
|
85
46
|
<Fragment key={field.key}>
|
|
86
|
-
{/* 物理映射:将 field 控制器回传给指挥官自定义的 render 逻辑 */}
|
|
87
47
|
{item.render(field, index, { add, remove }, form, renderFormContent)}
|
|
88
48
|
</Fragment>
|
|
89
49
|
))}
|
|
90
|
-
{/* 零信号兜底:若列表为空,显示添加按钮 */}
|
|
91
50
|
{fields.length === 0 && <Button type='dashed' onClick={() => add()} block icon={<PlusOutlined />}>添加{item.label || '项'}</Button>}
|
|
92
51
|
</div>
|
|
93
52
|
)}
|
|
94
53
|
</Form.List> :
|
|
95
|
-
// 💡 单兵模式:标准 Form.Item 构筑
|
|
96
54
|
<Form.Item
|
|
97
55
|
label={item.label}
|
|
98
56
|
tooltip={item.tooltip}
|
|
@@ -102,12 +60,7 @@ export const MyBaseForm = ({ item, form }) => {
|
|
|
102
60
|
required={item.required ?? item.rules?.some(v => v.required)}
|
|
103
61
|
style={{ marginBottom: !item.name ? 0 : undefined, ...item.style }}
|
|
104
62
|
>
|
|
105
|
-
{
|
|
106
|
-
逻辑优先级:
|
|
107
|
-
1. 优先使用 item.render 执行语义劫持
|
|
108
|
-
2. 否则使用 renderFormContent 进行标准构筑
|
|
109
|
-
*/}
|
|
110
|
-
{item.render ? (typeof item.render === 'function' ? item.render(item, form) : item.render) : renderFormContent({ item })}
|
|
63
|
+
{item.render ? (typeof item.render === 'function' ? item.render(item, form) : item.render) : renderFormContent({ item, form })}
|
|
111
64
|
</Form.Item>
|
|
112
65
|
)
|
|
113
66
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { Form, Descriptions } from 'antd'
|
|
2
2
|
import { MyBaseForm } from './MyBaseForm'
|
|
3
3
|
|
|
4
|
-
export const MyDescription = ({ title, column, descriptions, form: externalForm, onValuesChange }) => {
|
|
4
|
+
export const MyDescription = ({ title, column, layout = 'vertical', labelStyle, contentStyle, descriptions, form: externalForm, onValuesChange }) => {
|
|
5
5
|
|
|
6
6
|
const [internalForm] = Form.useForm()
|
|
7
7
|
const form = externalForm || internalForm
|
|
8
8
|
|
|
9
9
|
return (
|
|
10
10
|
<Form form={form} preserve={false} component={false} onValuesChange={onValuesChange}>
|
|
11
|
-
<Descriptions title={title} column={column}>
|
|
11
|
+
<Descriptions title={title} column={column} layout={layout} labelStyle={labelStyle} contentStyle={contentStyle}>
|
|
12
12
|
{descriptions.map(item => (
|
|
13
|
-
<Descriptions.Item key={item.label} label={item.name ? undefined : item.label}>
|
|
13
|
+
<Descriptions.Item key={item.label} span={item.span ?? 1} label={item.name ? undefined : item.label}>
|
|
14
14
|
{item.name ? <MyBaseForm item={item} form={form} /> : item.value}
|
|
15
15
|
</Descriptions.Item>))}
|
|
16
16
|
</Descriptions>
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import dayjs from 'dayjs'
|
|
1
2
|
import { MyBaseForm } from './MyBaseForm'
|
|
3
|
+
import { flattenFormItems } from '../utils/utils'
|
|
2
4
|
import { Row, Col, Form, Button, Collapse } from 'antd'
|
|
5
|
+
import { useEffect, useContext, createContext } from 'react'
|
|
6
|
+
|
|
7
|
+
const FormContext = createContext({})
|
|
3
8
|
|
|
4
9
|
const LAYOUT_PROCESSORS = {
|
|
5
10
|
section: ({ item, renderCore }) => (
|
|
@@ -24,15 +29,17 @@ const LAYOUT_PROCESSORS = {
|
|
|
24
29
|
default: ({ item, renderCore }) => <Row gutter={[24, 0]}>{renderCore(item.childItems || [])}</Row>
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
// 定义“零部件增强协议”处理器,以后加任何新功能,只需要在这个对象里加一个 Key,不需要动主逻辑!
|
|
28
32
|
const ACTION_PROCESSORS = {
|
|
29
|
-
|
|
30
|
-
addonAfter: (item, { tableConfig, setModalTable, setSelectedTableRows }) => ({
|
|
33
|
+
addonAfter: (item, { customClick, tableConfig = {}, setModalTable, setSelectedTableRows }) => ({
|
|
31
34
|
props: {
|
|
32
35
|
addonAfter: (
|
|
33
36
|
<Button onClick={() => {
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
if (typeof customClick === 'function') {
|
|
38
|
+
customClick()
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
setSelectedTableRows?.([])
|
|
42
|
+
setModalTable?.({ visible: true, title: `选择${item.label}`, ...tableConfig[item.name] })
|
|
36
43
|
}}>
|
|
37
44
|
{item.addonAfter}
|
|
38
45
|
</Button>
|
|
@@ -43,12 +50,12 @@ const ACTION_PROCESSORS = {
|
|
|
43
50
|
// prefix: (item) => ({ props: { prefix: <Icon type={item.prefix} /> } }),
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
const FormRenderer = ({ form, options, formItems
|
|
53
|
+
const FormRenderer = ({ form, options, formItems }) => {
|
|
54
|
+
|
|
55
|
+
const { tableProps, renderAction } = useContext(FormContext)
|
|
47
56
|
|
|
48
|
-
|
|
49
|
-
const renderCore = items => <FormRenderer form={form} options={options} formItems={items} renderAction={renderAction} tableProps={tableProps} />
|
|
57
|
+
const renderCore = items => <FormRenderer form={form} options={options} formItems={items} />
|
|
50
58
|
|
|
51
|
-
// 定义单项动作注入逻辑
|
|
52
59
|
const getRenderItemProps = item => {
|
|
53
60
|
if (!item.renderAction) return {}
|
|
54
61
|
const props = {}
|
|
@@ -61,14 +68,12 @@ const FormRenderer = ({ form, options, formItems, renderAction, tableProps = {}
|
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
return formItems.map((item, index) => {
|
|
64
|
-
// 💡 保持纯粹不可变:通过解构浅拷贝,绝不污染原始 formItems 对象!
|
|
65
71
|
const processedItem = {
|
|
66
72
|
...item,
|
|
67
73
|
...(item.renderAction && renderAction?.[item.name] ? { render: renderAction[item.name] } : {}),
|
|
68
74
|
...(['select', 'radio', 'checkbox'].includes(item.type) && !item.options ? { options: options?.[item.name] || [] } : {})
|
|
69
75
|
}
|
|
70
76
|
|
|
71
|
-
// 情况 A:发现容器型零件 (含有 layoutType)
|
|
72
77
|
if (processedItem.layoutType && LAYOUT_PROCESSORS[processedItem.layoutType]) {
|
|
73
78
|
const Layout = LAYOUT_PROCESSORS[processedItem.layoutType] ?? LAYOUT_PROCESSORS.default
|
|
74
79
|
return (
|
|
@@ -80,7 +85,6 @@ const FormRenderer = ({ form, options, formItems, renderAction, tableProps = {}
|
|
|
80
85
|
)
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
// 情况 B:标准作战零件
|
|
84
88
|
return (
|
|
85
89
|
<Col span={processedItem.span ?? 6} key={processedItem.name || index}>
|
|
86
90
|
<MyBaseForm item={{ ...processedItem, ...getRenderItemProps(processedItem) }} form={form} />
|
|
@@ -89,16 +93,42 @@ const FormRenderer = ({ form, options, formItems, renderAction, tableProps = {}
|
|
|
89
93
|
})
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
export const MyForm = ({ formItems = [], externalForm, labelCol, wrapperCol, tableProps, onValuesChange, layout = 'horizontal', options = {}, renderAction = {} }) => {
|
|
96
|
+
export const MyForm = ({ record, formItems = [], externalForm, labelCol, wrapperCol, tableProps, onValuesChange, layout = 'horizontal', options = {}, renderAction = {} }) => {
|
|
93
97
|
|
|
94
98
|
const [internalForm] = Form.useForm()
|
|
95
99
|
const form = externalForm || internalForm
|
|
96
100
|
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (record && Object.keys(record).length > 0) {
|
|
103
|
+
const itemMap = new Map(flattenFormItems(formItems).map(i => [i.name, i]))
|
|
104
|
+
const initialData = {}
|
|
105
|
+
Object.entries(record).forEach(([key, value]) => {
|
|
106
|
+
const config = itemMap.get(key)
|
|
107
|
+
if (config?.type?.includes('date') && value) {
|
|
108
|
+
if (Array.isArray(value)) {
|
|
109
|
+
initialData[key] = value.map(v => dayjs(v))
|
|
110
|
+
} else if (typeof value === 'string' && value.includes(',')) {
|
|
111
|
+
initialData[key] = value.split(',').map(v => dayjs(v))
|
|
112
|
+
} else {
|
|
113
|
+
initialData[key] = dayjs(value)
|
|
114
|
+
}
|
|
115
|
+
} else if ((config?.type === 'select' && config.mode) || config?.type === 'checkbox') {
|
|
116
|
+
initialData[key] = Array.isArray(value) ? value : value?.split(config.separator ?? ',')
|
|
117
|
+
} else {
|
|
118
|
+
initialData[key] = value
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
form.setFieldsValue(initialData)
|
|
122
|
+
}
|
|
123
|
+
}, [form, record, formItems])
|
|
124
|
+
|
|
97
125
|
return (
|
|
98
|
-
<
|
|
99
|
-
<
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
126
|
+
<FormContext.Provider value={{ tableProps, renderAction }}>
|
|
127
|
+
<Form form={form} layout={layout} preserve={false} labelCol={labelCol} wrapperCol={wrapperCol} onValuesChange={onValuesChange} scrollToFirstError={{ behavior: 'smooth', block: 'center' }}>
|
|
128
|
+
<Row gutter={[24, 0]}>
|
|
129
|
+
<FormRenderer form={form} options={options} formItems={formItems} />
|
|
130
|
+
</Row>
|
|
131
|
+
</Form>
|
|
132
|
+
</FormContext.Provider>
|
|
103
133
|
)
|
|
104
134
|
}
|
|
@@ -1,44 +1,9 @@
|
|
|
1
|
-
import dayjs from 'dayjs'
|
|
2
1
|
import { MyForm } from './MyForm'
|
|
3
2
|
import { Form, Modal } from 'antd'
|
|
3
|
+
import { useMemo, useState } from 'react'
|
|
4
4
|
import { MyModalTable } from './MyModalTable'
|
|
5
|
-
import {
|
|
5
|
+
import { flattenFormItems } from '../utils/utils'
|
|
6
6
|
|
|
7
|
-
const flattenFormItems = items => {
|
|
8
|
-
if (!Array.isArray(items)) return []
|
|
9
|
-
return items.flatMap(item => {
|
|
10
|
-
if (item.childItems) {
|
|
11
|
-
return flattenFormItems(item.childItems)
|
|
12
|
-
}
|
|
13
|
-
return item
|
|
14
|
-
})
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* @component MyModalForm
|
|
19
|
-
* @description [地堡核心构筑舱] 通用弹窗表单组件。
|
|
20
|
-
* 集成了“语义回显自愈”、“自动时间戳转换”及“原子级提交锁定”三大核心协议。
|
|
21
|
-
*
|
|
22
|
-
* @param {Object} props - 构筑参数
|
|
23
|
-
* @param {string} [props.title] - 舱体视觉标题
|
|
24
|
-
* @param {Object} [props.labelCol] - 标签布局校准
|
|
25
|
-
* @param {Object} [props.wrapperCol] - 控件布局校准
|
|
26
|
-
* @param {string|number} [props.width] - 舱体物理宽度
|
|
27
|
-
* @param {boolean} props.visible - 激活信号:控制弹窗的物理显示状态
|
|
28
|
-
* @param {Array} props.modalItems - 零部件清单:定义表单内部的输入单元
|
|
29
|
-
* @param {Object} [props.record] - 初始物资包:用于编辑模式的数据回显
|
|
30
|
-
* @param {Function} props.submit - 数据发射协议:提交表单后的核心回调函数
|
|
31
|
-
* @param {Function} props.setModal - 指令中心:用于更新弹窗状态(开启/关闭)
|
|
32
|
-
* @param {Function} [props.onValuesChange] - 联动传感:捕获表单内部的信号波动
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* <MyModalForm
|
|
36
|
-
* title='构筑新模块'
|
|
37
|
-
* visible={visible}
|
|
38
|
-
* submit={async (vals) => await save(vals)}
|
|
39
|
-
* modalItems={[{ label: '截止日期', name: 'deadline', type: 'date' }]}
|
|
40
|
-
* />
|
|
41
|
-
*/
|
|
42
7
|
export const MyModalForm = ({ extra, width, title, layout, okText, options, footer, submit, record, visible, setModal, labelCol, modalItems, wrapperCol, cancelText, tableConfig, okButtonProps, renderAction, onValuesChange, handleModalTableOk }) => {
|
|
43
8
|
|
|
44
9
|
const rowKey = tableConfig?.rowKey || 'id'
|
|
@@ -48,47 +13,6 @@ export const MyModalForm = ({ extra, width, title, layout, okText, options, foot
|
|
|
48
13
|
const [modalTable, setModalTable] = useState({})
|
|
49
14
|
const [selectedTableRows, setSelectedTableRows] = useState([])
|
|
50
15
|
|
|
51
|
-
const prevVisibleRef = useRef(false)
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* @description [数据回显协议] 当构筑舱开启时,自动对初始物资进行“语义格式化”。
|
|
55
|
-
* 将后端传输的字符串/数字时间戳重新转化为地堡可读的 `dayjs` 对象。
|
|
56
|
-
*/
|
|
57
|
-
useEffect(() => {
|
|
58
|
-
if (visible && !prevVisibleRef.current) {
|
|
59
|
-
if (record && Object.keys(record).length > 0) {
|
|
60
|
-
const itemMap = new Map(flattenFormItems(modalItems).map(i => [i.name, i]))
|
|
61
|
-
const initialData = {}
|
|
62
|
-
Object.entries(record).forEach(([key, value]) => {
|
|
63
|
-
const config = itemMap.get(key)
|
|
64
|
-
// 语义识别:若零部件类型为日期且数值存在,执行物理转化
|
|
65
|
-
if (config?.type?.includes('date') && value) {
|
|
66
|
-
if (Array.isArray(value)) {
|
|
67
|
-
// 处理范围日期
|
|
68
|
-
initialData[key] = value.map(v => dayjs(v))
|
|
69
|
-
} else if (typeof value === 'string' && value.includes(',')) {
|
|
70
|
-
// 处理逗号分隔的字符串日期
|
|
71
|
-
initialData[key] = value.split(',').map(v => dayjs(v))
|
|
72
|
-
} else {
|
|
73
|
-
// 处理单日期
|
|
74
|
-
initialData[key] = dayjs(value)
|
|
75
|
-
}
|
|
76
|
-
} else {
|
|
77
|
-
initialData[key] = value
|
|
78
|
-
}
|
|
79
|
-
})
|
|
80
|
-
form.setFieldsValue(initialData)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
prevVisibleRef.current = visible
|
|
84
|
-
}, [visible, record, form, modalItems])
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* @async
|
|
88
|
-
* @function handleOk
|
|
89
|
-
* @description [物理加压提交] 执行表单校验,并自动启动“时间戳转换协议”。
|
|
90
|
-
* 确保发射至后端的数据包符合标准 Unix 时间戳(毫秒)规范。
|
|
91
|
-
*/
|
|
92
16
|
const handleOk = async (extraParams = {}) => {
|
|
93
17
|
try {
|
|
94
18
|
const values = await form.validateFields()
|
|
@@ -97,10 +21,8 @@ export const MyModalForm = ({ extra, width, title, layout, okText, options, foot
|
|
|
97
21
|
const val = formattedValues[item.name]
|
|
98
22
|
if (item.type?.includes('date') && val) {
|
|
99
23
|
if (Array.isArray(val)) {
|
|
100
|
-
// 物理压实:将范围日期转化为逗号分隔的时间戳字符串
|
|
101
24
|
formattedValues[item.name] = val.map(v => v.valueOf()).join(',')
|
|
102
25
|
} else {
|
|
103
|
-
// 物理转化:将 dayjs 对象还原为原始数值(毫秒)
|
|
104
26
|
formattedValues[item.name] = val.valueOf()
|
|
105
27
|
}
|
|
106
28
|
}
|
|
@@ -118,10 +40,6 @@ export const MyModalForm = ({ extra, width, title, layout, okText, options, foot
|
|
|
118
40
|
|
|
119
41
|
const modalTableOk = () => handleModalTableOk?.({ form, selectedTableRows, setModalTable })
|
|
120
42
|
|
|
121
|
-
/**
|
|
122
|
-
* @function handleCancel
|
|
123
|
-
* @description 执行“撤退协议”:关闭构筑舱并清除当前逻辑残留。
|
|
124
|
-
*/
|
|
125
43
|
const handleCancel = () => {
|
|
126
44
|
setModal({ visible: false })
|
|
127
45
|
form.resetFields()
|
|
@@ -130,7 +48,7 @@ export const MyModalForm = ({ extra, width, title, layout, okText, options, foot
|
|
|
130
48
|
const tableRowSelection = useMemo(() => ({
|
|
131
49
|
selectedRowKeys: selectedTableRows.map(row => row[rowKey]),
|
|
132
50
|
onChange: (selectedRowKeys, selectedRows) => setSelectedTableRows(selectedRows),
|
|
133
|
-
}), [rowKey,
|
|
51
|
+
}), [rowKey, selectedTableRows])
|
|
134
52
|
|
|
135
53
|
const ctx = { form, record, pending, handleOk, handleCancel, setModal }
|
|
136
54
|
|
|
@@ -153,6 +71,7 @@ export const MyModalForm = ({ extra, width, title, layout, okText, options, foot
|
|
|
153
71
|
{modalTable.visible && <MyModalTable rowKey={rowKey} {...modalTable} rowSelection={tableRowSelection} onOk={modalTableOk} setModal={setModalTable} />}
|
|
154
72
|
{extra?.header?.(ctx)}
|
|
155
73
|
<MyForm
|
|
74
|
+
record={record}
|
|
156
75
|
layout={layout}
|
|
157
76
|
options={options}
|
|
158
77
|
externalForm={form}
|
|
@@ -4,58 +4,17 @@ import { MySearchForm } from './MySearchForm'
|
|
|
4
4
|
import { Form, Modal, Space, Button } from 'antd'
|
|
5
5
|
import { useTableQuery } from '../hooks/useTableQuery'
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* @component MyModalTable
|
|
9
|
-
* @description [地堡支援型作战单元] 弹窗数据选择平台。
|
|
10
|
-
* 专门用于执行“关联选择”、“子数据审计”或“二级逻辑骇入”任务。
|
|
11
|
-
* 具备独立的数据获取链路、可选的搜索过滤系统以及与主构筑舱(setModalForm)的跨维协同能力。
|
|
12
|
-
*
|
|
13
|
-
* @param {Object} props - 支援参数
|
|
14
|
-
* @param {string} [props.title] - 舱体视觉标题
|
|
15
|
-
* @param {Function} props.api - 弹窗内独立的数据源指令
|
|
16
|
-
* @param {string|number} [props.width] - 舱体物理宽度
|
|
17
|
-
* @param {Object} [props.rowSelection] - 战术勾选:用于多选物资
|
|
18
|
-
* @param {boolean} props.visible - 激活信号:控制支援单元的显示状态
|
|
19
|
-
* @param {Function} props.setModal - 单元注销中心:用于关闭当前弹窗
|
|
20
|
-
* @param {Array} props.columns - 零部件初始清单 (别名: modalColumns)
|
|
21
|
-
* @param {Function} [props.onOk] - 战果物理确认:按下确认按钮后的异步回调协议
|
|
22
|
-
* @param {Object} [props.initialParams={}] - 初始战备物资:定义的初始搜索参数
|
|
23
|
-
* @param {React.ReactNode} [props.footer] - 底座指令区:支持完全自定义操作按钮
|
|
24
|
-
* @param {Array} [props.formItems] - 局部搜索零部件:用于在弹窗内执行精准情报过滤
|
|
25
|
-
* @param {Function} props.formatResponse - 数据清洗协议:对 API 吐出的原始物资进行格式化
|
|
26
|
-
* @param {boolean|Object} [props.modalPagination=true] - 局部后勤协议:弹窗内是否开启分页
|
|
27
|
-
* @param {Array} [props.functionButtons] - 功能挂件组:位于表格上方的操作按钮,可直接操控表格状态
|
|
28
|
-
* @param {Function} props.operate - 动态火力配置:生成操作列的函数,接收 { refresh, setModalForm } 句柄
|
|
29
|
-
* @param {Function} [props.setModalForm] - [重要] 跨维调用:由主页面注入的表单控制句柄,实现“弹窗开弹窗”的非耦合交互
|
|
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
|
-
*/
|
|
39
7
|
export const MyModalTable = ({ api, onOk, title, width, footer, visible, operate, setModal, formItems, setModalForm, rowSelection, formatResponse, functionButtons, columns: modalColumns, rowKey = 'id', scroll = { y: 400 }, initialParams = {}, modalPagination = true, isLocalPaging = false }) => {
|
|
40
8
|
|
|
41
9
|
const [form] = Form.useForm()
|
|
42
10
|
const [pending, setPending] = useState(false)
|
|
43
11
|
|
|
44
|
-
/**
|
|
45
|
-
* @description [独立信号链路]
|
|
46
|
-
* 在弹窗内部启动专属的 useTableQuery,实现了与主页面逻辑的“物理隔绝”。
|
|
47
|
-
*/
|
|
48
12
|
const { total, loading, columns, dataSource, search, refresh, setLoading, setSearch, setDataSource } = useTableQuery({ api, cols: modalColumns, initialParams, formatResponse, isLocalPaging })
|
|
49
13
|
|
|
50
|
-
const handleSearch = values => setSearch({ ...
|
|
14
|
+
const handleSearch = values => setSearch(prev => ({ ...prev, ...values, pageNo: 1 }))
|
|
51
15
|
|
|
52
|
-
const handleTableChange = (pagination, filters, sorter) => setSearch({ ...
|
|
16
|
+
const handleTableChange = (pagination, filters, sorter) => setSearch(prev => ({ ...prev, pageNo: pagination.current, pageSize: pagination.pageSize, orderBy: sorter.column ? sorter.field : undefined }))
|
|
53
17
|
|
|
54
|
-
/**
|
|
55
|
-
* @async
|
|
56
|
-
* @function handleOk
|
|
57
|
-
* @description [确认指令闭环] 执行确认操作并启动原子级锁定(Pending),防止重复指令干扰。
|
|
58
|
-
*/
|
|
59
18
|
const handleOk = async () => {
|
|
60
19
|
if (onOk) {
|
|
61
20
|
try {
|
|
@@ -69,25 +28,13 @@ export const MyModalTable = ({ api, onOk, title, width, footer, visible, operate
|
|
|
69
28
|
}
|
|
70
29
|
}
|
|
71
30
|
|
|
72
|
-
/**
|
|
73
|
-
* @constant finalColumns
|
|
74
|
-
* @description [火力全开] 将 Hook 生成的动态列与指挥官注入的 operate 操作列进行物理合并。
|
|
75
|
-
*/
|
|
76
31
|
const finalColumns = useMemo(() => {
|
|
77
32
|
return operate ? columns.concat(operate({ refresh, setModalForm })) : columns
|
|
78
33
|
}, [columns, operate])
|
|
79
34
|
|
|
80
35
|
return (
|
|
81
36
|
<Modal centered destroyOnClose title={title} width={width} open={visible} onOk={handleOk} footer={footer?.({ setModal })} onCancel={() => setModal({ visible: false })} confirmLoading={pending} bodyStyle={{ paddingBottom: 0 }}>
|
|
82
|
-
{/*
|
|
83
|
-
💡 [地堡战术细节]
|
|
84
|
-
syncUrlParams={false}:确保弹窗内的搜索行为不会干扰全局 URL 信号。
|
|
85
|
-
*/}
|
|
86
37
|
{formItems?.length > 0 && <MySearchForm form={form} search={search} formItems={formItems} setSearch={handleSearch} syncUrlParams={false} />}
|
|
87
|
-
{/*
|
|
88
|
-
💡 [高阶赋能逻辑]
|
|
89
|
-
functionButtons 能够获得对当前表格状态 (refresh, setLoading...) 的完全控制权。
|
|
90
|
-
*/}
|
|
91
38
|
{functionButtons?.length > 0 && <Space style={{ marginBottom: 12 }}>{functionButtons.map(item => <Button key={item.name} type={item.type} loading={loading} disabled={item.disabled} onClick={() => item.onClick({ refresh, setLoading, setSearch, setDataSource })}>{item.name}</Button>)}</Space>}
|
|
92
39
|
<MyTable
|
|
93
40
|
total={total}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Popover } from 'antd'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { MyTable } from './MyTable'
|
|
4
|
+
|
|
5
|
+
export const MyPopoverTable = ({ api, columns, children, formatter }) => {
|
|
6
|
+
|
|
7
|
+
const [loading, setLoading] = useState(false)
|
|
8
|
+
const [dataSource, setDataSource] = useState([])
|
|
9
|
+
|
|
10
|
+
const fetchData = async () => {
|
|
11
|
+
if (loading) return
|
|
12
|
+
setLoading(true)
|
|
13
|
+
const response = await api()
|
|
14
|
+
setLoading(false)
|
|
15
|
+
const datas = formatter(response)
|
|
16
|
+
setDataSource(datas)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Popover trigger='click' content={<MyTable columns={columns} loading={loading} dataSource={dataSource} />}>
|
|
21
|
+
<span onClick={fetchData}>{children}</span>
|
|
22
|
+
</Popover>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -4,35 +4,7 @@ import { useMemo, useState, useEffect } from 'react'
|
|
|
4
4
|
import { Row, Col, Form, Button, Space } from 'antd'
|
|
5
5
|
import { SearchOutlined, RedoOutlined } from '@ant-design/icons'
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
* @component MySearchForm
|
|
9
|
-
* @description [地堡战术搜索中心] 高级搜索表单组件。
|
|
10
|
-
* 集成了“URL 状态同步(持久化)”、“日期范围物理拆解”以及“动态伸缩布局”三大核心能力。
|
|
11
|
-
*
|
|
12
|
-
* @param {Object} props - 指挥官指令包
|
|
13
|
-
* @param {Object} props.form - Antd Form 实例,表单的主控舵盘
|
|
14
|
-
* @param {Function} [props.customReset] - 逻辑劫持:自定义重置协议
|
|
15
|
-
* @param {Function} [props.customFinish] - 逻辑劫持:自定义提交协议
|
|
16
|
-
* @param {Array} props.formItems - 零部件清单:定义所有的搜索输入单元
|
|
17
|
-
* @param {number} [props.showLimit=7] - 初始可见能级:默认展示的零部件数量
|
|
18
|
-
* @param {number} [props.defaultPageSize=10] - 默认吞吐量:重置时的初始每页条数
|
|
19
|
-
* @param {Function} [props.onValuesChange] - 联动传感:当表单值变动时的即时反馈
|
|
20
|
-
* @param {Object} props.initialValues - 初始信号:从 URL 或缓存中解析出的初始参数
|
|
21
|
-
* @param {React.ReactNode} [props.extra] - 额外挂件:在按钮区域注入自定义战术单元
|
|
22
|
-
* @param {Function} props.setSearch - 状态分发器:用于更新全局搜索参数并触发重新构筑
|
|
23
|
-
* @param {Object} props.search - 当前的全局搜索状态(包含 pageNo, pageSize 及各过滤项)
|
|
24
|
-
* @param {boolean} props.loading - 战场负载状态:请求中时锁定按钮,防止由于重复指令导致的逻辑冲突
|
|
25
|
-
* @param {boolean} [props.syncUrlParams=true] - 物理同步开关:是否将搜索状态实时镜像至 URL 链接中
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* <MySearchForm
|
|
29
|
-
* form={form}
|
|
30
|
-
* search={search}
|
|
31
|
-
* setSearch={setSearch}
|
|
32
|
-
* formItems={[{ label: '状态', name: 'status', type: 'select', options: [...] }]}
|
|
33
|
-
* />
|
|
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 }) => {
|
|
7
|
+
export const MySearchForm = ({ form, extra, options, loading, labelCol, setSearch, formItems, showLimit = 7, initialValues = {}, initialParams = {}, customReset, customFinish, renderAction, onValuesChange, syncUrlParams = true, defaultPageSize = 10 }) => {
|
|
36
8
|
|
|
37
9
|
const [limit, setLimit] = useState(showLimit)
|
|
38
10
|
|
|
@@ -42,55 +14,39 @@ export const MySearchForm = ({ form, extra, search, options, loading, labelCol,
|
|
|
42
14
|
...(['select'].includes(item.type) ? { options: item.options ?? options?.[item.name] ?? [] } : {})
|
|
43
15
|
})), [options, formItems, renderAction])
|
|
44
16
|
|
|
45
|
-
/**
|
|
46
|
-
* @function handleReset
|
|
47
|
-
* @description 执行“归零协议”:清除当前所有搜索信号,并可选择性擦除 URL 中的物理记录。
|
|
48
|
-
*/
|
|
49
17
|
const handleReset = () => {
|
|
50
18
|
form.resetFields()
|
|
51
19
|
if (syncUrlParams) {
|
|
52
|
-
// 💡 修正逻辑:只有当 formItems 里【没有任何一项】包含这个 key 时,才视为默认参数
|
|
53
20
|
const defaultQueryEntries = Object.entries(initialValues).filter(([key, _]) => !formItems.some(item => item.name.includes(key)))
|
|
54
21
|
const defaultQuery = Object.fromEntries(defaultQueryEntries)
|
|
55
22
|
const params = new URLSearchParams(defaultQuery)
|
|
56
|
-
// 💡 执行物理重写:将 URL 恢复为仅含默认参数的状态
|
|
57
23
|
const newUrl = params.toString() ? `${window.location.pathname}?${params.toString()}` : window.location.pathname
|
|
58
24
|
window.history.replaceState(null, '', newUrl)
|
|
59
25
|
}
|
|
60
26
|
if (customReset) {
|
|
61
27
|
customReset()
|
|
62
28
|
} else {
|
|
63
|
-
|
|
64
|
-
setSearch({ pageNo: 1, pageSize: search.pageSize ?? defaultPageSize, ...initialParams })
|
|
29
|
+
setSearch(prev => ({ pageNo: 1, pageSize: prev.pageSize ?? defaultPageSize, ...initialParams }))
|
|
65
30
|
}
|
|
66
31
|
}
|
|
67
32
|
|
|
68
|
-
/**
|
|
69
|
-
* @function handleFinish
|
|
70
|
-
* @description 执行“数据发射协议”:对原始表单数据进行脱水和转换,尤其处理复杂的日期范围协议。
|
|
71
|
-
* @param {Object} values - 原始表单数据包
|
|
72
|
-
*/
|
|
73
33
|
const handleFinish = values => {
|
|
74
34
|
const formattedValues = { ...values }
|
|
75
35
|
Object.keys(formattedValues).forEach(key => {
|
|
76
36
|
const val = formattedValues[key]
|
|
77
37
|
const isDateType = formItems.find(item => item.name === key)?.type?.includes('date')
|
|
78
|
-
// 💡 [黑科技 1] 日期自动转换与拆解
|
|
79
38
|
if (isDateType && val) {
|
|
80
39
|
if (Array.isArray(val) && val.length > 0) {
|
|
81
|
-
// 物理拆分:当 name 为 'start,end' 格式时,自动拆解为两个独立的后端参数
|
|
82
40
|
const [startKey, endKey] = key.split(',')
|
|
83
|
-
formattedValues[startKey] = val[0].startOf('day').valueOf()
|
|
84
|
-
formattedValues[endKey] = val[1].endOf('day').valueOf()
|
|
41
|
+
formattedValues[startKey] = val[0].startOf('day').valueOf()
|
|
42
|
+
formattedValues[endKey] = val[1].endOf('day').valueOf()
|
|
85
43
|
delete formattedValues[key]
|
|
86
44
|
} else {
|
|
87
|
-
// 单日转换:转化为毫秒级数字
|
|
88
45
|
formattedValues[key] = val.valueOf()
|
|
89
46
|
}
|
|
90
47
|
}
|
|
91
48
|
})
|
|
92
49
|
if (syncUrlParams) {
|
|
93
|
-
// 💡 [黑科技 2] 镜像至 URL
|
|
94
50
|
const currentQuery = Object.fromEntries(new URLSearchParams(window.location.search).entries())
|
|
95
51
|
const params = new URLSearchParams(Object.fromEntries(Object.entries({ ...currentQuery, ...formattedValues }).filter(([key, value]) => !['', null, undefined].includes(value))))
|
|
96
52
|
window.history.replaceState(null, '', `${window.location.pathname}?${params.toString()}`)
|
|
@@ -98,31 +54,24 @@ export const MySearchForm = ({ form, extra, search, options, loading, labelCol,
|
|
|
98
54
|
if (customFinish) {
|
|
99
55
|
customFinish(formattedValues)
|
|
100
56
|
} else {
|
|
101
|
-
setSearch({ ...
|
|
57
|
+
setSearch(prev => ({ ...prev, ...formattedValues, pageNo: 1 }))
|
|
102
58
|
}
|
|
103
59
|
}
|
|
104
60
|
|
|
105
|
-
/**
|
|
106
|
-
* @description [信号自愈] 初始化逻辑:
|
|
107
|
-
* 将来自 URL 的字符串信号重新转化为地堡可读的 `dayjs` 对象。
|
|
108
|
-
*/
|
|
109
61
|
useEffect(() => {
|
|
110
62
|
if (Object.values(initialValues).length > 0 && formItems.length > 0) {
|
|
111
63
|
const newValues = {}
|
|
112
64
|
formItems.forEach(item => {
|
|
113
|
-
// 处理单字段回显
|
|
114
65
|
if (initialValues[item.name] !== undefined) {
|
|
115
66
|
const value = initialValues[item.name]
|
|
116
67
|
newValues[item.name] = item.type?.includes('date') ? dayjs(Number(value)) : value
|
|
117
68
|
}
|
|
118
|
-
// 处理双字段(日期范围)回显:执行物理重组
|
|
119
69
|
if (item.name?.includes(',') && item.type?.includes('date')) {
|
|
120
70
|
const [startKey, endKey] = item.name.split(',')
|
|
121
71
|
if (initialValues[startKey] && initialValues[endKey]) {
|
|
122
72
|
newValues[item.name] = [dayjs(Number(initialValues[startKey])), dayjs(Number(initialValues[endKey]))]
|
|
123
73
|
}
|
|
124
74
|
}
|
|
125
|
-
// 处理多选框回显
|
|
126
75
|
if (item.hasOwnProperty('mode') && !!initialValues[item.name]) {
|
|
127
76
|
newValues[item.name] = initialValues[item.name]?.split(',')
|
|
128
77
|
}
|
|
@@ -136,22 +85,15 @@ export const MySearchForm = ({ form, extra, search, options, loading, labelCol,
|
|
|
136
85
|
<Row gutter={24}>
|
|
137
86
|
{searchItems.slice(0, limit)?.map((item, ind) => (
|
|
138
87
|
<Col key={ind} span={item.span ?? 6}>
|
|
139
|
-
{/* 物理装配:利用底层 MyBaseForm 单元进行渲染 */}
|
|
140
88
|
<MyBaseForm item={item} form={form} />
|
|
141
89
|
</Col>
|
|
142
90
|
))}
|
|
143
|
-
{/*
|
|
144
|
-
战术控制区:
|
|
145
|
-
1. 'flex=auto' 确保按钮组始终靠右对齐。
|
|
146
|
-
2. 'showLimit' 控制页面纵向熵值,防止过多搜索项挤占战场视野。
|
|
147
|
-
*/}
|
|
148
91
|
<Col flex='auto' style={{ textAlign: 'right' }}>
|
|
149
92
|
<Form.Item>
|
|
150
93
|
<Space>
|
|
151
94
|
<Button type='primary' icon={<SearchOutlined />} loading={loading} htmlType='submit'>查询</Button>
|
|
152
95
|
<Button icon={<RedoOutlined />} onClick={handleReset} loading={loading} htmlType='reset'>重置</Button>
|
|
153
96
|
{extra}
|
|
154
|
-
{/* 动态伸缩协议:根据当前零部件数量决定是否显示展开/收起按钮 */}
|
|
155
97
|
{formItems.length > showLimit && (<a onClick={() => setLimit(formItems.length > limit ? formItems.length : showLimit)}>{formItems.length > limit ? '展开' : '收起'}</a>)}
|
|
156
98
|
</Space>
|
|
157
99
|
</Form.Item>
|