lu-lowcode-package-form 0.11.33 → 0.11.35
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/.cursorignore +7 -0
- package/dist/index.cjs.js +385 -396
- package/dist/index.es.js +100176 -102713
- package/dist/style.css +2 -2
- package/package.json +2 -1
- package/src/App.jsx +659 -125
- package/src/components/form-container/index.jsx +18 -1
- package/src/components/index.jsx +2 -1
- package/src/components/optimized-form-container/index.jsx +281 -0
- package/src/utils/formula.js +29 -6
@@ -1,5 +1,6 @@
|
|
1
1
|
import React, { forwardRef, useEffect } from "react";
|
2
2
|
import { Form, Row, Col, message } from "antd";
|
3
|
+
import { ConfigProvider } from 'antd';
|
3
4
|
|
4
5
|
import { debounce, isEqual, throttle } from 'lodash';
|
5
6
|
import { evalFormula } from '../../utils/formula'
|
@@ -799,12 +800,28 @@ const FormContainer = forwardRef(({ cols = 1, children, mode = "view" }, ref) =>
|
|
799
800
|
};
|
800
801
|
|
801
802
|
return (
|
802
|
-
<
|
803
|
+
<ConfigProvider
|
804
|
+
theme={{
|
805
|
+
components: {
|
806
|
+
Table: {
|
807
|
+
rowHoverBg: '#ebebeb',
|
808
|
+
// "fontSize": 14,
|
809
|
+
// "cellPaddingBlock": 6
|
810
|
+
},
|
811
|
+
},
|
812
|
+
token: {
|
813
|
+
colorBgContainerDisabled: 'rgba(0, 0, 0, 0.02)', // 设置更浅的灰色背景
|
814
|
+
colorTextDisabled: '#000',
|
815
|
+
}
|
816
|
+
}}
|
817
|
+
>
|
818
|
+
<Form form={form} className={"form-container fp-0 fw-full fh-full box-border fflex fflex-col " + (mode == "desgin" ? " fp-6" : "")} onFieldsChange={handleFieldsChange}>
|
803
819
|
<Form.Item name="__id" hidden={true}>
|
804
820
|
<input type="hidden" />
|
805
821
|
</Form.Item>
|
806
822
|
{formContent}
|
807
823
|
</Form>
|
824
|
+
</ConfigProvider>
|
808
825
|
);
|
809
826
|
});
|
810
827
|
|
package/src/components/index.jsx
CHANGED
@@ -3,6 +3,7 @@ import '../App.css';
|
|
3
3
|
import { TreeSelect, Select, WithSingleSelect,WithMultipleSelect, SingleSelect, MultipleSelect, SearchSelect, UserSelect, DeptSelect, PostSelect } from './field/select/index.jsx'
|
4
4
|
import Custom from './field/custom/index.jsx'
|
5
5
|
import { FormContainer, FormContainerWrapper,LayoutFormRow,LayoutFormGroupTitle } from './form-container/index.jsx'
|
6
|
+
import OptimizedFormContainer from './optimized-form-container/index.jsx'
|
6
7
|
import { Checkbox ,CheckboxTree, CheckboxGroup } from './field/checkbox/index.jsx'
|
7
8
|
import { default as RadioGroup } from './field/radio/index.jsx'
|
8
9
|
import { UploadFile,UploadImage } from './field/upload'
|
@@ -61,7 +62,7 @@ Object.keys(Show).forEach(key => {
|
|
61
62
|
Show[key].displayName = `Show.${key}`;
|
62
63
|
});
|
63
64
|
|
64
|
-
export { FormContainer, Field ,FormContainerWrapper,Layout,Show }
|
65
|
+
export { FormContainer, Field ,FormContainerWrapper,Layout,Show,OptimizedFormContainer }
|
65
66
|
import { default as OptionSetter} from './setter/optionsetter'
|
66
67
|
const Setter = {
|
67
68
|
OptionSetter
|
@@ -0,0 +1,281 @@
|
|
1
|
+
import React, { forwardRef, useState, useEffect, useRef, useCallback } from "react";
|
2
|
+
import { Form, Row, Col, message } from "antd";
|
3
|
+
import { debounce } from "lodash";
|
4
|
+
import { evalFormula } from "../../utils/formula";
|
5
|
+
import { nanoid } from "nanoid";
|
6
|
+
|
7
|
+
/**
|
8
|
+
* 简化版的分组方法,将子组件按列数分组,并对不可见项补位
|
9
|
+
*/
|
10
|
+
function batchElements(elements, groupSize, dependencyMap) {
|
11
|
+
const groupedElements = [];
|
12
|
+
let tempArray = [];
|
13
|
+
|
14
|
+
const fillWithPlaceholder = (size, array) => {
|
15
|
+
while (array.length < size) {
|
16
|
+
array.push(<div key={nanoid()} />);
|
17
|
+
}
|
18
|
+
};
|
19
|
+
|
20
|
+
elements.forEach(element => {
|
21
|
+
const compName = element.type?.displayName || element.props?._componentName;
|
22
|
+
// Layout 组件单独一组
|
23
|
+
if (compName && compName.startsWith("Layout.")) {
|
24
|
+
if (tempArray.length > 0) {
|
25
|
+
fillWithPlaceholder(groupSize, tempArray);
|
26
|
+
groupedElements.push(tempArray);
|
27
|
+
tempArray = [];
|
28
|
+
}
|
29
|
+
groupedElements.push([element]);
|
30
|
+
} else {
|
31
|
+
tempArray.push(element);
|
32
|
+
// 计算当前分组中可见的字段数量
|
33
|
+
const visibleCount = tempArray.filter(el => {
|
34
|
+
const identifier = el.props?.componentId || el.props?.__id;
|
35
|
+
const isVisible =
|
36
|
+
!dependencyMap?.has(identifier) || dependencyMap.get(identifier)?.show;
|
37
|
+
return isVisible && !el.props?.calcHidden;
|
38
|
+
}).length;
|
39
|
+
|
40
|
+
if (visibleCount === groupSize) {
|
41
|
+
groupedElements.push(tempArray);
|
42
|
+
tempArray = [];
|
43
|
+
}
|
44
|
+
}
|
45
|
+
});
|
46
|
+
|
47
|
+
if (tempArray.length > 0) {
|
48
|
+
fillWithPlaceholder(groupSize, tempArray);
|
49
|
+
groupedElements.push(tempArray);
|
50
|
+
}
|
51
|
+
return groupedElements;
|
52
|
+
}
|
53
|
+
|
54
|
+
/**
|
55
|
+
* 构建字段依赖关系 Map
|
56
|
+
* 针对每个组件,根据其 props 中的信息记录 identifier、级联规则、显隐函数等信息
|
57
|
+
*/
|
58
|
+
const buildDependencyMap = (children) => {
|
59
|
+
const dependencyMap = new Map();
|
60
|
+
const fields = [];
|
61
|
+
|
62
|
+
// 递归遍历生成 fields 数组
|
63
|
+
const traverse = (node, parentIdentifier = "") => {
|
64
|
+
const compName = node.type?.displayName || node.props?._componentName;
|
65
|
+
const { props } = node;
|
66
|
+
if (compName && (compName.startsWith("Field.") || compName.startsWith("Layout."))) {
|
67
|
+
let identifier = props?.componentId || props?.__id;
|
68
|
+
if (parentIdentifier) {
|
69
|
+
identifier = `${parentIdentifier}.${identifier}`;
|
70
|
+
}
|
71
|
+
const withIds = props.withId
|
72
|
+
? [props.withId]
|
73
|
+
: (props.withIds ? [...props.withIds] : []);
|
74
|
+
fields.push({ identifier, withIds, compName, fillRules: props?.fillRules, visibleFunc: props?.withVisibleFunc });
|
75
|
+
}
|
76
|
+
if (props?.children) {
|
77
|
+
React.Children.forEach(props.children, child => {
|
78
|
+
traverse(child, compName && compName.startsWith("Field.") ? (props?.componentId || props?.__id) : "");
|
79
|
+
});
|
80
|
+
}
|
81
|
+
};
|
82
|
+
|
83
|
+
React.Children.forEach(children, child => traverse(child));
|
84
|
+
|
85
|
+
fields.forEach(field => {
|
86
|
+
dependencyMap.set(field.identifier, {
|
87
|
+
...field,
|
88
|
+
show: true, // 默认全部显示,后续根据 visibleFunc 或计算公式更新
|
89
|
+
children: fields.filter(item => item.withIds.includes(field.identifier))
|
90
|
+
});
|
91
|
+
});
|
92
|
+
return dependencyMap;
|
93
|
+
};
|
94
|
+
|
95
|
+
/**
|
96
|
+
* 根据当前表单值和指定公式更新字段显示状态(返回 true 表示状态发生了变化)
|
97
|
+
*/
|
98
|
+
const computeFieldVisibility = (identifier, dependencyMap, fieldValues, withDatas = []) => {
|
99
|
+
if (!dependencyMap.has(identifier)) return false;
|
100
|
+
const dep = dependencyMap.get(identifier);
|
101
|
+
if (dep.visibleFunc && typeof dep.visibleFunc === "function") {
|
102
|
+
const visible = dep.visibleFunc(fieldValues);
|
103
|
+
if (dep.show !== visible) {
|
104
|
+
dep.show = visible;
|
105
|
+
return true;
|
106
|
+
}
|
107
|
+
}
|
108
|
+
// 可以加入根据 fillRules 计算公式的逻辑:这里简化为仅支持 evalFormula 计算
|
109
|
+
if (dep.fillRules && Array.isArray(dep.fillRules) && dep.fillRules.length > 0) {
|
110
|
+
const formula = dep.fillRules.map(rule => {
|
111
|
+
// 此处只处理简单字符串和取值逻辑,复杂情况可以在此扩展
|
112
|
+
return typeof rule === "string" ? rule : "";
|
113
|
+
});
|
114
|
+
const result = evalFormula(formula) === "true";
|
115
|
+
if (dep.show !== result) {
|
116
|
+
dep.show = result;
|
117
|
+
return true;
|
118
|
+
}
|
119
|
+
}
|
120
|
+
return false;
|
121
|
+
};
|
122
|
+
|
123
|
+
/**
|
124
|
+
* 完全实现原有依赖、级联更新和动态布局的表单组件
|
125
|
+
*/
|
126
|
+
const OptimizedFormContainer = forwardRef(({ cols = 1, children, mode = "view" }, ref) => {
|
127
|
+
const [form] = Form.useForm();
|
128
|
+
const [formContent, setFormContent] = useState(null);
|
129
|
+
|
130
|
+
// 存储依赖关系的 Map
|
131
|
+
const dependencyMap = useRef(new Map());
|
132
|
+
// 记录上一次字段值,用于判断是否需要更新
|
133
|
+
const lastFormValues = useRef({});
|
134
|
+
// 暂存所有字段变化
|
135
|
+
const changedFieldsRef = useRef({});
|
136
|
+
|
137
|
+
// 初始化依赖 map(当 children 发生变化时重新计算)
|
138
|
+
const initDependencyMap = useCallback(() => {
|
139
|
+
dependencyMap.current = buildDependencyMap(children);
|
140
|
+
}, [children]);
|
141
|
+
|
142
|
+
// 统一更新依赖显示状态,并根据需要重新渲染
|
143
|
+
const updateDependencies = useCallback(() => {
|
144
|
+
const fieldValues = form.getFieldsValue();
|
145
|
+
let needUpdate = false;
|
146
|
+
dependencyMap.current.forEach((_, identifier) => {
|
147
|
+
const changed = computeFieldVisibility(identifier, dependencyMap.current, fieldValues);
|
148
|
+
needUpdate = needUpdate || changed;
|
149
|
+
});
|
150
|
+
if (needUpdate) {
|
151
|
+
setFormContent(renderChildren());
|
152
|
+
}
|
153
|
+
}, [form]);
|
154
|
+
|
155
|
+
// 针对字段变化的统一处理函数(加去抖)
|
156
|
+
const handleFieldsChangeDebounced = useCallback(
|
157
|
+
debounce(() => {
|
158
|
+
const fieldValues = form.getFieldsValue();
|
159
|
+
Object.keys(changedFieldsRef.current).forEach(key => {
|
160
|
+
// 简单比较变化值
|
161
|
+
if (changedFieldsRef.current[key] !== fieldValues[key]) {
|
162
|
+
lastFormValues.current[key] = fieldValues[key];
|
163
|
+
}
|
164
|
+
});
|
165
|
+
changedFieldsRef.current = {};
|
166
|
+
updateDependencies();
|
167
|
+
}, 200),
|
168
|
+
[form, updateDependencies]
|
169
|
+
);
|
170
|
+
|
171
|
+
// 字段变化时记录变化
|
172
|
+
const onFieldsChange = (...[, allValues]) => {
|
173
|
+
// allValues 为整个表单的值
|
174
|
+
Object.assign(changedFieldsRef.current, allValues);
|
175
|
+
handleFieldsChangeDebounced();
|
176
|
+
};
|
177
|
+
|
178
|
+
// 生成实际渲染的子组件结构
|
179
|
+
const renderChildren = useCallback(() => {
|
180
|
+
const childrenArray = React.Children.toArray(children);
|
181
|
+
const groupedChildren = batchElements(childrenArray, cols, dependencyMap.current);
|
182
|
+
return groupedChildren.map((group, rowIndex) => (
|
183
|
+
<Row key={`row-${rowIndex}`} gutter={[24, 24]}>
|
184
|
+
{group.map((child, colIndex) => {
|
185
|
+
const { componentId, __id, _componentName, calcHidden, ...props } = child.props;
|
186
|
+
const compName = child.type?.displayName || _componentName;
|
187
|
+
// 标识字段ID,用于依赖管理
|
188
|
+
const identifier = componentId || __id;
|
189
|
+
// 缓存更新 key 避免受不必要的渲染影响
|
190
|
+
const updateKey = nanoid();
|
191
|
+
const isLayoutComponent = compName && compName.startsWith("Layout.");
|
192
|
+
const isVisible =
|
193
|
+
!dependencyMap.current.has(identifier) ||
|
194
|
+
dependencyMap.current.get(identifier)?.show;
|
195
|
+
// 不在设计模式下,对 calcHidden 或依赖为 false 时隐藏
|
196
|
+
const hidden = (calcHidden || !isVisible) && mode !== "desgin";
|
197
|
+
const rules = [];
|
198
|
+
if (props.isRequired) {
|
199
|
+
rules.push({ required: true, message: `${props.label}必须填写` });
|
200
|
+
}
|
201
|
+
if (props.rules) {
|
202
|
+
// 支持数组与正则字符串
|
203
|
+
const pattern =
|
204
|
+
Array.isArray(props.rules) && props.rules.length > 0
|
205
|
+
? props.rules.join("|")
|
206
|
+
: props.rules;
|
207
|
+
rules.push({
|
208
|
+
pattern: new RegExp(pattern),
|
209
|
+
message: props.rulesFailMessage || `${props.label}格式错误`
|
210
|
+
});
|
211
|
+
}
|
212
|
+
let childComponent = null;
|
213
|
+
// 针对表格、布局组件做特殊处理
|
214
|
+
if (isLayoutComponent) {
|
215
|
+
childComponent = React.cloneElement(child, {
|
216
|
+
form,
|
217
|
+
fieldName: identifier,
|
218
|
+
updateKey
|
219
|
+
});
|
220
|
+
} else {
|
221
|
+
childComponent = (
|
222
|
+
<Form.Item
|
223
|
+
hidden={hidden}
|
224
|
+
label={props.label || ""}
|
225
|
+
name={identifier}
|
226
|
+
rules={isVisible ? rules : []}
|
227
|
+
>
|
228
|
+
{React.cloneElement(child, { form, fieldName: identifier, updateKey })}
|
229
|
+
</Form.Item>
|
230
|
+
);
|
231
|
+
}
|
232
|
+
return (
|
233
|
+
<Col key={identifier || `col-${colIndex}`} span={hidden ? 0 : (isLayoutComponent ? 24 : 24 / cols)}>
|
234
|
+
{childComponent}
|
235
|
+
</Col>
|
236
|
+
);
|
237
|
+
})}
|
238
|
+
</Row>
|
239
|
+
));
|
240
|
+
}, [children, cols, form, mode]);
|
241
|
+
|
242
|
+
// 初始时构建依赖 map,以及第一次渲染
|
243
|
+
useEffect(() => {
|
244
|
+
initDependencyMap();
|
245
|
+
setFormContent(renderChildren());
|
246
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
247
|
+
}, [initDependencyMap, renderChildren]);
|
248
|
+
|
249
|
+
// 对外暴露一些方法
|
250
|
+
React.useImperativeHandle(ref, () => ({
|
251
|
+
formRef: form,
|
252
|
+
setFieldsValue: (values) => {
|
253
|
+
form.setFieldsValue(values);
|
254
|
+
// 立即处理字段更新
|
255
|
+
handleFieldsChangeDebounced();
|
256
|
+
},
|
257
|
+
refreshDependencies: () => {
|
258
|
+
initDependencyMap();
|
259
|
+
updateDependencies();
|
260
|
+
}
|
261
|
+
}));
|
262
|
+
|
263
|
+
return (
|
264
|
+
<Form
|
265
|
+
form={form}
|
266
|
+
className={`form-container fp-0 fw-full fh-full box-border fflex fflex-col fgap-y-2${mode === "desgin" ? " fp-6" : ""}`}
|
267
|
+
onValuesChange={onFieldsChange}
|
268
|
+
>
|
269
|
+
<Form.Item name="__id" hidden>
|
270
|
+
<input type="hidden" />
|
271
|
+
</Form.Item>
|
272
|
+
{formContent}
|
273
|
+
</Form>
|
274
|
+
);
|
275
|
+
});
|
276
|
+
|
277
|
+
export function withWrap(Component) {
|
278
|
+
return forwardRef((props, ref) => <Component {...props} ref={ref} forwardedRef={ref} />);
|
279
|
+
}
|
280
|
+
|
281
|
+
export default OptimizedFormContainer;
|
package/src/utils/formula.js
CHANGED
@@ -1,23 +1,46 @@
|
|
1
|
+
import memoizee from 'memoizee';
|
2
|
+
|
1
3
|
// 不安全的Function函数,有时间的话需要重新编写公式编译器
|
2
4
|
// 可以使用库 expr-eval 来替换(undo)
|
3
|
-
const evalFormula =(formula) => {
|
5
|
+
const evalFormula = memoizee((formula) => {
|
4
6
|
let result = "";
|
5
|
-
let
|
7
|
+
let funcCode = "";
|
6
8
|
try {
|
7
9
|
funcCode =`return ${formula.join("")}`
|
8
|
-
|
10
|
+
console.log("funcCode",funcCode)
|
9
11
|
const func = new Function(funcCode);
|
10
12
|
result = func()
|
11
13
|
// result = eval(formula.join(""));
|
12
14
|
if (!(typeof result === "object" && result !== null))
|
13
|
-
|
15
|
+
result = result.toString();
|
14
16
|
} catch (error) {
|
15
17
|
console.error(`formula (${funcCode}) error`, error);
|
16
18
|
}
|
17
19
|
// console.log("formula result", result);
|
18
20
|
return result
|
19
|
-
}
|
20
|
-
|
21
|
+
}, {
|
22
|
+
// 配置缓存选项
|
23
|
+
primitive: true, // 使用基本类型作为缓存key
|
24
|
+
max: 1000, // 最大缓存数量
|
25
|
+
maxAge: 1000 * 60 * 1 // 缓存1分钟
|
26
|
+
});
|
27
|
+
// const evalFormula =(formula) => {
|
28
|
+
// let result = "";
|
29
|
+
// let funcCode = "";
|
30
|
+
// try {
|
31
|
+
// funcCode =`return ${formula.join("")}`
|
32
|
+
// // console.log("funcCode",funcCode)
|
33
|
+
// const func = new Function(funcCode);
|
34
|
+
// result = func()
|
35
|
+
// // result = eval(formula.join(""));
|
36
|
+
// if (!(typeof result === "object" && result !== null))
|
37
|
+
// result = result.toString();
|
38
|
+
// } catch (error) {
|
39
|
+
// console.error(`formula (${funcCode}) error`, error);
|
40
|
+
// }
|
41
|
+
// // console.log("formula result", result);
|
42
|
+
// return result
|
43
|
+
// }
|
21
44
|
|
22
45
|
// let demo = ["JSON.parse(`","{\"label\":\"1111\",\"value\":1}","`)\n\n"]
|
23
46
|
|