@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,6 +1,6 @@
1
1
  {
2
2
  "name": "@hzab/form-render",
3
- "version": "1.6.8-beta1",
3
+ "version": "1.6.8-beta2",
4
4
  "description": "",
5
5
  "main": "src",
6
6
  "scripts": {
@@ -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 type { SelectProps } from "antd/lib/select";
5
- import type { AvatarProps } from "antd/lib/avatar";
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: "orgName" },
51
- { label: "主驻点:", key: "userName" },
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
- setList((prev) => (isScrollLoad ? [...prev, ...list] : list));
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 = (itemId: string | number) => {
121
- const currentValue = selectProps.value;
122
- const isMultiple = selectProps.mode === "multiple";
123
-
124
- // 更新值
125
- if (isMultiple) {
126
- const isValueArr = Array.isArray(currentValue);
127
- let newValue = null;
128
-
129
- if (isValueArr) {
130
- const currentValueIds = currentValue.map((it) => it.value);
131
- newValue = currentValueIds.includes(itemId)
132
- ? currentValueIds.filter((v) => v !== itemId)
133
- : uniq([...currentValue, itemId]);
134
- } else {
135
- newValue = [itemId];
136
- }
137
-
138
- selectProps.onChange?.(newValue, currentValue);
139
- } else {
140
- const newValue = itemId === (currentValue as LableValue)?.value ? undefined : itemId;
141
- selectProps.onChange?.(newValue, currentValue);
142
-
143
- // 单选模式下关闭下拉框
144
- if (selectInstance.current) {
145
- selectInstance.current.blur();
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
- let isSelected;
159
-
160
- if (selectProps.mode === "multiple") {
161
- isSelected = (selectProps.value || [])
162
- .map((it) => (isObject(it) ? it?.value || it?.userId : it))
163
- .includes(item.userId);
164
- } else {
165
- isSelected = isObject(selectProps.value) ? (selectProps.value as LableValue)?.value === item.userId : false;
166
- }
167
-
168
- const disabled = item?.[disabledKey] || false;
169
- const getDisabledStyle: React.CSSProperties =
170
- typeof disabledStyle === "function"
171
- ? disabledStyle(disabled)
172
- : {
173
- cursor: disabled ? "not-allowed" : "pointer",
174
- backgroundColor: disabled ? "#f0f0f0" : isSelected ? "#e6f7ff" : "inherit",
175
- color: disabled ? "#999" : "inherit",
176
- filter: disabled ? "grayscale(100%)" : "none",
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
- key={item.userId}
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
- <Avatar alt={item?.userName} size={60} {...avataProps} src={item.avatar} />
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 = (menu: React.ReactNode) => (
210
- <div ref={listRef}>
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, field: any) => {
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
+ }