@hzab/form-render 1.6.8-beta1 → 1.6.8-beta2
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
CHANGED
@@ -1,62 +1,34 @@
|
|
1
1
|
import React, { useEffect, useState, useRef, useCallback, useMemo } from "react";
|
2
2
|
import { Select, List, Avatar, Spin } from "antd";
|
3
3
|
import { connect, mapProps } from "@formily/react";
|
4
|
-
import
|
5
|
-
import type {
|
6
|
-
import { debounce, isObject, uniq } from "lodash";
|
4
|
+
import { debounce, isObject } from "lodash";
|
5
|
+
import type { LableValue, Person, ScrollPagination, RemoteSelectProps } from "./type";
|
7
6
|
|
8
7
|
import "./index.less";
|
9
8
|
|
10
|
-
export interface Person {
|
11
|
-
id: string | number;
|
12
|
-
name: string;
|
13
|
-
avatar?: string;
|
14
|
-
email?: string;
|
15
|
-
description?: string;
|
16
|
-
disabled?: boolean;
|
17
|
-
[key: string]: any;
|
18
|
-
}
|
19
|
-
|
20
|
-
interface ScrollPagination {
|
21
|
-
pageNum: number;
|
22
|
-
pageSize: number;
|
23
|
-
current?: number;
|
24
|
-
total?: number;
|
25
|
-
}
|
26
|
-
|
27
|
-
type LableValue = {
|
28
|
-
label: string;
|
29
|
-
value: string | number;
|
30
|
-
[key: string]: any;
|
31
|
-
};
|
32
|
-
|
33
|
-
interface RemoteSelectProps extends Omit<SelectProps<any>, "options" | "children"> {
|
34
|
-
loadOptions: (
|
35
|
-
search: string,
|
36
|
-
pagination: ScrollPagination,
|
37
|
-
) => Promise<{ list: Person[]; pagination: ScrollPagination }>;
|
38
|
-
renderItem?: (item: Person) => React.ReactNode;
|
39
|
-
initialPagination?: Partial<ScrollPagination>;
|
40
|
-
avataProps?: AvatarProps;
|
41
|
-
value: LableValue[] | LableValue;
|
42
|
-
listItemConfigs?: { label: string; key: string }[];
|
43
|
-
disabledKey?: string;
|
44
|
-
disabledStyle?: (disabled: boolean) => React.CSSProperties;
|
45
|
-
customItemNode?: React.ReactNode;
|
46
|
-
}
|
47
|
-
|
48
9
|
const defaultListItemConfigs = [
|
49
10
|
{ label: "姓名:", key: "userName" },
|
50
|
-
{ label: "主部门:", key: "
|
51
|
-
{ label: "主驻点:", key: "
|
11
|
+
{ label: "主部门:", key: "parentName" },
|
12
|
+
{ label: "主驻点:", key: "orgName" },
|
52
13
|
{ label: "手机号:", key: "phoneNumber" },
|
53
14
|
{ label: "身份证号:", key: "idnumber" },
|
54
15
|
];
|
55
16
|
|
17
|
+
const transfromLabelInValueData = (list: Person[], labelKey: string, valueKey: string): Person[] => {
|
18
|
+
try {
|
19
|
+
return list.map((item) => ({ ...item, label: item?.[labelKey], value: item?.[valueKey] }));
|
20
|
+
} catch (e) {
|
21
|
+
console.error(e);
|
22
|
+
return list;
|
23
|
+
}
|
24
|
+
};
|
25
|
+
|
56
26
|
const RemoteSelect: React.FC<RemoteSelectProps> = ({
|
57
27
|
loadOptions,
|
58
28
|
renderItem,
|
59
29
|
listItemConfigs,
|
30
|
+
labelKey = "userName",
|
31
|
+
valueKey = "userId",
|
60
32
|
disabledKey = "disabled",
|
61
33
|
initialPagination = {},
|
62
34
|
avataProps = {},
|
@@ -73,8 +45,8 @@ const RemoteSelect: React.FC<RemoteSelectProps> = ({
|
|
73
45
|
pageSize: 10,
|
74
46
|
...initialPagination,
|
75
47
|
});
|
76
|
-
const listRef = useRef<HTMLDivElement>(null);
|
77
48
|
const selectInstance = useRef<any>(null);
|
49
|
+
const total = useRef(0);
|
78
50
|
|
79
51
|
const debounceLoadData = debounce(async (isScrollLoad = false) => {
|
80
52
|
try {
|
@@ -84,18 +56,23 @@ const RemoteSelect: React.FC<RemoteSelectProps> = ({
|
|
84
56
|
...pagination,
|
85
57
|
pageNum: isScrollLoad ? pagination.pageNum + 1 : 1,
|
86
58
|
});
|
87
|
-
const { list } = result;
|
59
|
+
const { list, pagination: resPagination } = result;
|
60
|
+
|
61
|
+
total.current = resPagination.total;
|
88
62
|
|
89
|
-
|
63
|
+
const transformatList: Person[] = transfromLabelInValueData(list, labelKey, valueKey);
|
64
|
+
|
65
|
+
setList((prev) => (isScrollLoad ? [...prev, ...transformatList] : transformatList));
|
90
66
|
} finally {
|
91
67
|
isScrollLoad ? setLoadingMore(false) : setLoading(false);
|
92
68
|
}
|
93
69
|
}, 500);
|
70
|
+
|
94
71
|
const debounceHandleScroll = async (e: React.UIEvent<HTMLDivElement>) => {
|
95
72
|
const { scrollTop, clientHeight, scrollHeight } = e.currentTarget;
|
96
73
|
const isNearBottom = scrollHeight - scrollTop - clientHeight < 30;
|
97
74
|
|
98
|
-
if (isNearBottom && !loadingMore) {
|
75
|
+
if (isNearBottom && !loadingMore && list.length <= total.current) {
|
99
76
|
await loadData(true);
|
100
77
|
setPagination((pre) => ({ ...pre, pageNum: pagination.pageNum + 1 }));
|
101
78
|
}
|
@@ -106,8 +83,15 @@ const RemoteSelect: React.FC<RemoteSelectProps> = ({
|
|
106
83
|
// 处理滚动加载
|
107
84
|
const handleScroll = useCallback(debounceHandleScroll, [loadingMore, loadData]);
|
108
85
|
|
86
|
+
// 重置list滚动条
|
87
|
+
const resetListScroll = () => {
|
88
|
+
const listNode = document.getElementById("abt-list");
|
89
|
+
listNode && listNode.scrollTo({ top: 0, behavior: "auto" });
|
90
|
+
};
|
91
|
+
|
109
92
|
// 处理搜索
|
110
93
|
const handleSearch = debounce((value: string) => {
|
94
|
+
resetListScroll();
|
111
95
|
setSearch(value);
|
112
96
|
setPagination((prev) => ({ ...prev, pageNum: 1 }));
|
113
97
|
}, 1000);
|
@@ -116,37 +100,38 @@ const RemoteSelect: React.FC<RemoteSelectProps> = ({
|
|
116
100
|
loadData();
|
117
101
|
}, [search]);
|
118
102
|
|
119
|
-
//
|
120
|
-
const handleItemClick = (
|
121
|
-
const currentValue = selectProps
|
122
|
-
const
|
123
|
-
|
124
|
-
//
|
125
|
-
|
126
|
-
const
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
103
|
+
// 处理选项点击事件
|
104
|
+
const handleItemClick = (item: Person) => {
|
105
|
+
const { value: currentValue, mode, onChange } = selectProps;
|
106
|
+
const itemId = item.userId;
|
107
|
+
|
108
|
+
// 多选模式处理
|
109
|
+
const handleMultipleSelection = () => {
|
110
|
+
const selectedOptions = Array.isArray(currentValue) ? currentValue : [];
|
111
|
+
const isSelected = selectedOptions.some((opt) => opt.value === itemId);
|
112
|
+
|
113
|
+
return isSelected
|
114
|
+
? selectedOptions.filter((opt) => opt.value !== itemId) // 移除已选项
|
115
|
+
: [...selectedOptions, item]; // 添加新选项
|
116
|
+
};
|
117
|
+
|
118
|
+
// 单选模式处理
|
119
|
+
const handleSingleSelection = () => {
|
120
|
+
const isCurrentSelected = (currentValue as LableValue)?.value === itemId;
|
121
|
+
return isCurrentSelected ? undefined : item;
|
122
|
+
};
|
123
|
+
|
124
|
+
// 执行模式对应处理
|
125
|
+
const newValue = mode === "multiple" ? handleMultipleSelection() : handleSingleSelection();
|
126
|
+
|
127
|
+
// 触发变更回调
|
128
|
+
onChange?.(newValue, currentValue);
|
129
|
+
|
130
|
+
// 单选模式自动关闭下拉框
|
131
|
+
if (mode !== "multiple" && selectInstance.current) selectInstance.current.blur();
|
148
132
|
};
|
149
133
|
|
134
|
+
// 合并配置
|
150
135
|
const itemConfigs = useMemo(() => {
|
151
136
|
if (Array.isArray(listItemConfigs) && listItemConfigs?.length) return listItemConfigs;
|
152
137
|
return defaultListItemConfigs;
|
@@ -155,48 +140,53 @@ const RemoteSelect: React.FC<RemoteSelectProps> = ({
|
|
155
140
|
// 默认渲染项(增加选中状态和点击处理)
|
156
141
|
const defaultRenderItem = useCallback(
|
157
142
|
(item: Person) => {
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
.
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
143
|
+
const { mode, value: selectedValue } = selectProps;
|
144
|
+
const { userId, [disabledKey]: isDisabled = false } = item;
|
145
|
+
// 选中状态判断
|
146
|
+
const getSelectionStatus = () => {
|
147
|
+
if (mode === "multiple") {
|
148
|
+
if (!Array.isArray(selectedValue)) return false;
|
149
|
+
const selectedValues = (selectedValue || [])?.map((opt) => (isObject(opt) ? opt?.value ?? opt?.userId : opt));
|
150
|
+
return selectedValues.includes(userId);
|
151
|
+
}
|
152
|
+
return isObject(selectedValue) ? (selectedValue as LableValue)?.value === userId : false;
|
153
|
+
};
|
154
|
+
|
155
|
+
// 禁用样式生成
|
156
|
+
const generateStyle = (disabled: boolean): React.CSSProperties => {
|
157
|
+
if (typeof disabledStyle === "function") {
|
158
|
+
return disabledStyle(disabled);
|
159
|
+
}
|
160
|
+
|
161
|
+
return {
|
162
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
163
|
+
backgroundColor: disabled ? "#f0f0f0" : getSelectionStatus() ? "#e6f7ff" : "inherit",
|
164
|
+
color: disabled ? "#999" : "inherit",
|
165
|
+
filter: disabled ? "grayscale(100%)" : "none",
|
166
|
+
};
|
167
|
+
};
|
168
|
+
|
169
|
+
// 头像组件
|
170
|
+
const avatarComponent = <Avatar alt={item.userName} size={60} {...avataProps} src={item.avatar} />;
|
171
|
+
|
172
|
+
// 用户信息列表
|
173
|
+
const userInfoContent = (
|
174
|
+
<ul className="abt-user-item-info">
|
175
|
+
{itemConfigs.map((configItem, index) => (
|
176
|
+
<li key={configItem?.key || index} className="abt-user-item-info__item">
|
177
|
+
<span>{configItem?.label}</span>
|
178
|
+
<span>{item?.[configItem.key]}</span>
|
179
|
+
</li>
|
180
|
+
))}
|
181
|
+
</ul>
|
182
|
+
);
|
178
183
|
|
179
184
|
return (
|
180
|
-
<List.Item
|
181
|
-
|
182
|
-
onClick={() => (disabled ? null : handleItemClick(item.userId))}
|
183
|
-
style={getDisabledStyle}
|
184
|
-
>
|
185
|
-
{customItemNode ? (
|
186
|
-
customItemNode
|
187
|
-
) : (
|
185
|
+
<List.Item key={userId} onClick={() => !isDisabled && handleItemClick(item)} style={generateStyle(isDisabled)}>
|
186
|
+
{customItemNode || (
|
188
187
|
<div className="abt-user-item">
|
189
|
-
<div className="abt-user-item-avatar">
|
190
|
-
|
191
|
-
</div>
|
192
|
-
<ul className="abt-user-item-info">
|
193
|
-
{itemConfigs.map((configItem) => (
|
194
|
-
<li className="abt-user-item-info__item">
|
195
|
-
<span>{configItem.label}</span>
|
196
|
-
<span>{item?.[configItem.key]}</span>
|
197
|
-
</li>
|
198
|
-
))}
|
199
|
-
</ul>
|
188
|
+
<div className="abt-user-item-avatar">{avatarComponent}</div>
|
189
|
+
{userInfoContent}
|
200
190
|
</div>
|
201
191
|
)}
|
202
192
|
</List.Item>
|
@@ -206,10 +196,11 @@ const RemoteSelect: React.FC<RemoteSelectProps> = ({
|
|
206
196
|
);
|
207
197
|
|
208
198
|
// 自定义下拉内容
|
209
|
-
const dropdownRender = (
|
210
|
-
<div
|
199
|
+
const dropdownRender = () => (
|
200
|
+
<div>
|
211
201
|
<Spin spinning={loading}>
|
212
202
|
<List
|
203
|
+
id={"abt-list"}
|
213
204
|
loading={loadingMore}
|
214
205
|
dataSource={list}
|
215
206
|
renderItem={renderItem ? renderItem : defaultRenderItem}
|
@@ -226,27 +217,27 @@ const RemoteSelect: React.FC<RemoteSelectProps> = ({
|
|
226
217
|
{...selectProps}
|
227
218
|
ref={selectInstance}
|
228
219
|
showSearch
|
220
|
+
labelInValue
|
229
221
|
onSearch={handleSearch}
|
230
222
|
filterOption={false}
|
231
223
|
dropdownRender={dropdownRender}
|
232
224
|
placeholder="请选择人员"
|
233
|
-
// 增加虚拟的options用于显示选中标签
|
234
225
|
options={list.map((item) => ({
|
235
226
|
value: item.userId,
|
236
227
|
label: item.userName,
|
228
|
+
...item,
|
237
229
|
}))}
|
238
230
|
/>
|
239
231
|
);
|
240
232
|
};
|
241
233
|
|
242
|
-
// Formily集成
|
243
234
|
const PersonnelSelect = connect(
|
244
235
|
RemoteSelect,
|
245
236
|
mapProps(
|
246
237
|
{
|
247
238
|
loading: true,
|
248
239
|
},
|
249
|
-
(props
|
240
|
+
(props) => {
|
250
241
|
return {
|
251
242
|
...props,
|
252
243
|
};
|
@@ -0,0 +1,76 @@
|
|
1
|
+
import type { SelectProps } from "antd/lib/select";
|
2
|
+
import React from "react";
|
3
|
+
import type { AvatarProps } from "antd/lib/avatar";
|
4
|
+
|
5
|
+
export interface Person {
|
6
|
+
userId: string | number;
|
7
|
+
userName: string;
|
8
|
+
avatar?: string;
|
9
|
+
email?: string;
|
10
|
+
description?: string;
|
11
|
+
disabled?: boolean;
|
12
|
+
[key: string]: any;
|
13
|
+
}
|
14
|
+
|
15
|
+
export interface ScrollPagination {
|
16
|
+
pageNum: number;
|
17
|
+
pageSize: number;
|
18
|
+
current?: number;
|
19
|
+
total?: number;
|
20
|
+
}
|
21
|
+
|
22
|
+
export type LableValue = {
|
23
|
+
label: string;
|
24
|
+
value: string | number;
|
25
|
+
[key: string]: any;
|
26
|
+
};
|
27
|
+
|
28
|
+
export interface RemoteSelectProps extends Omit<SelectProps<any>, "options" | "children"> {
|
29
|
+
/**
|
30
|
+
* @title 请求函数
|
31
|
+
* */
|
32
|
+
loadOptions: (
|
33
|
+
search: string,
|
34
|
+
pagination: ScrollPagination,
|
35
|
+
) => Promise<{ list: Person[]; pagination: ScrollPagination }>;
|
36
|
+
/**
|
37
|
+
* @title 自定义列表项
|
38
|
+
* */
|
39
|
+
renderItem?: (item: Person) => React.ReactNode;
|
40
|
+
/**
|
41
|
+
* @title 分页参数
|
42
|
+
* */
|
43
|
+
initialPagination?: Partial<ScrollPagination>;
|
44
|
+
/**
|
45
|
+
* @title antd头像组件props
|
46
|
+
* */
|
47
|
+
avataProps?: AvatarProps;
|
48
|
+
/**
|
49
|
+
* @title 下拉框值
|
50
|
+
* */
|
51
|
+
value: LableValue[] | LableValue;
|
52
|
+
/**
|
53
|
+
* @title 列表项内部展示的字段内容
|
54
|
+
* */
|
55
|
+
listItemConfigs?: { label: string; key: string }[];
|
56
|
+
/**
|
57
|
+
* @title 动态disabledKey
|
58
|
+
* */
|
59
|
+
disabledKey?: string;
|
60
|
+
/**
|
61
|
+
* @title 动态labelKey
|
62
|
+
* */
|
63
|
+
labelKey?: string;
|
64
|
+
/**
|
65
|
+
* @title 动态valueKey
|
66
|
+
* */
|
67
|
+
valueKey?: string;
|
68
|
+
/**
|
69
|
+
* @title disabledStyle
|
70
|
+
* */
|
71
|
+
disabledStyle?: (disabled: boolean) => React.CSSProperties;
|
72
|
+
/**
|
73
|
+
* @title 自定义列表项中的样式
|
74
|
+
* */
|
75
|
+
customItemNode?: React.ReactNode;
|
76
|
+
}
|