• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SkylerHu / antd-restful / #3

10 Jul 2025 02:38PM UTC coverage: 77.983%. First build
#3

push

web-flow
feat: 扩展组件支持远程获取数据 (#1)

扩展组件支持远程获取数据

1016 of 1392 branches covered (72.99%)

Branch coverage included in aggregate %.

1352 of 1644 new or added lines in 28 files covered. (82.24%)

1389 of 1692 relevant lines covered (82.09%)

26.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.01
/src/components/formitems/RestTreeSelect.jsx
1
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
import PropTypes from "prop-types";
3
import { Space, Spin, Tag, TreeSelect, Tooltip } from "antd";
4
import { dequal as deepEqual } from "dequal";
5
import { DEFAULT_ROWS_PATH, DEFAULT_SEPARATOR, READ_ONLY_CLASS } from "src/common/constants";
6
import { findDataByPath, findLabelFromTreeData } from "src/common/parser";
7
import { insertChildrenToTreeNode, patchTreeNodeInfo, refreshTreeKeyMap } from "src/common/treeUtils";
8
import { isArray, isEmpty, isFunction } from "src/common/typeTools";
9
import CopyView from "src/components/CopyView";
10
import { useDeepCompareMemoize } from "src/hooks";
11
import { useSafeRequest } from "src/requests";
12

13
const RestTreeSelect = ({
3✔
14
  style,
15
  className,
16
  value,
17
  onChange,
18

19
  restful,
20
  reqConfig,
21
  baseParams,
22
  fieldParent = "parent",
2✔
23
  labelTemplate,
24
  parseRowsPath = DEFAULT_ROWS_PATH,
6✔
25
  enableCopy,
26
  separator = DEFAULT_SEPARATOR,
6✔
27

28
  treeData,
29
  fieldNames,
30
  treeNodeLabelProp,
31
  disabled = false,
6✔
32
  readOnly = false,
5✔
33
  antdTreeSelectProps,
34
  antdSpaceProps,
35
}) => {
36
  const [makeRequest] = useSafeRequest();
6✔
37
  const reqConfigRef = useRef(reqConfig);
6✔
38

39
  const [innerValue, setInnerValue] = useState(value);
6✔
40
  const [loading, setLoading] = useState(false);
6✔
41
  const [treeInnerData, setTreeInnerData] = useState(treeData || []);
6✔
42
  // key: node 存储数据
43
  const treeKeyMap = useRef({});
6✔
44

45
  const fieldKey = useMemo(() => fieldNames?.value || "value", [fieldNames?.value]);
6!
46
  const fieldLabel = useMemo(
6✔
47
    () => treeNodeLabelProp || fieldNames?.label || "label",
3!
48
    [fieldNames?.label, treeNodeLabelProp]
49
  );
50
  const fieldChildren = useMemo(() => fieldNames?.children || "children", [fieldNames?.children]);
6✔
51

52
  useEffect(() => {
6✔
53
    setInnerValue((oldV) => {
3✔
54
      return deepEqual(oldV, value) ? oldV : value;
3!
55
    });
56
  }, [value]);
57

58
  const memTreeData = useDeepCompareMemoize(treeData);
6✔
59
  useEffect(() => {
6✔
60
    if (isArray(memTreeData)) {
3!
NEW
61
      setTreeInnerData((oldV) => (deepEqual(oldV, memTreeData) ? oldV : memTreeData));
×
62
    }
63
  }, [memTreeData]);
64

65
  const initDataFirstRef = useRef(true);
6✔
66

67
  useEffect(() => {
6✔
68
    // 仅第一时候初始化一级结点
69
    if (initDataFirstRef.current) {
3!
70
      initDataFirstRef.current = false;
3✔
71
      refreshByNode();
3✔
72
    }
73
  }, [value, refreshByNode]);
74

75
  const onValueChange = useCallback(
6✔
76
    (value) => {
77
      setInnerValue(value);
1✔
78
      if (isFunction(onChange)) {
1!
79
        let nodes = [];
1✔
80
        if (!isEmpty(value)) {
1!
81
          nodes = (isArray(value) ? value : [value]).map((v) => treeKeyMap.current[v]);
1!
82
        }
83
        onChange(value, nodes);
1✔
84
      }
85
    },
86
    [onChange]
87
  );
88

89
  const memBaseParams = useDeepCompareMemoize(baseParams);
6✔
90

91
  const refreshByNode = useCallback(
6✔
92
    (node, callback) => {
93
      if (!restful || disabled || readOnly) {
3✔
94
        return;
2✔
95
      }
96

97
      setLoading(true);
1✔
98
      const params = { ...memBaseParams };
1✔
99
      if (node && node[fieldKey]) {
1!
NEW
100
        params[fieldParent] = node[fieldKey];
×
101
      } else {
102
        params[`${fieldParent}__isnull`] = true;
1✔
103
      }
104

105
      // 以node[fieldKey]作为key,避免频繁刷新相同的node
106
      makeRequest({ delay: 200, key: `treeselect-${node ? node[fieldKey] : ""}` })
1!
107
        .get(restful, { params, disableNotiError: true, ...reqConfigRef.current })
108
        .then((response) => {
109
          const data = findDataByPath(response.data, parseRowsPath);
1✔
110
          patchTreeNodeInfo(data, { fieldKey, fieldChildren, labelTemplate });
1✔
111
          if (!node) {
1!
112
            setTreeInnerData(data);
1✔
113
          } else {
NEW
114
            setTreeInnerData((oldV) => {
×
NEW
115
              const nodes = insertChildrenToTreeNode(oldV, data, node[fieldKey], { fieldKey, fieldChildren });
×
NEW
116
              return [...nodes];
×
117
            });
118
          }
119
        })
120
        .finally(() => {
121
          setLoading(false);
1✔
122
          if (isFunction(callback)) {
1!
NEW
123
            callback();
×
124
          }
125
        });
126
    },
127
    [
128
      makeRequest,
129
      restful,
130
      fieldParent,
131
      disabled,
132
      readOnly,
133
      memBaseParams,
134
      parseRowsPath,
135
      labelTemplate,
136
      fieldKey,
137
      fieldChildren,
138
    ]
139
  );
140

141
  useEffect(() => {
6✔
142
    if (isArray(treeInnerData)) {
4!
143
      treeKeyMap.current = refreshTreeKeyMap(treeInnerData, { fieldKey, fieldChildren });
4✔
144
    }
145
  }, [treeInnerData, fieldKey, fieldChildren]);
146

147
  const notFoundContent = useMemo(() => {
6✔
148
    return loading ? <Spin size="small" /> : "暂无数据";
5✔
149
  }, [loading]);
150

151
  let view = null;
6✔
152

153
  if (readOnly) {
6✔
154
    let _values = [];
1✔
155
    if (isArray(innerValue)) {
1!
NEW
156
      _values = innerValue;
×
157
    } else if (!isEmpty(innerValue)) {
1!
NEW
158
      _values = [innerValue];
×
159
    }
160
    view = (
1✔
161
      <div style={style} className={className ? `${className} ${READ_ONLY_CLASS}` : READ_ONLY_CLASS}>
1!
162
        {_values.map((v) => {
NEW
163
          const label = findLabelFromTreeData(v, treeInnerData, fieldKey, fieldLabel, fieldChildren);
×
NEW
164
          return (
×
165
            <CopyView key={v} value={v} disabled={!enableCopy}>
166
              <Tooltip title={v}>
167
                {isArray(innerValue) ? <Tag>{label}</Tag> : label}
×
168
              </Tooltip>
169
            </CopyView>
170
          );
171
        })}
172
      </div>
173
    );
174
    return view;
1✔
175
  }
176

177
  view = (
5✔
178
    <TreeSelect
179
      {...(!enableCopy ? { style, className } : {})}
5✔
180
      notFoundContent={notFoundContent}
181
      allowClear
182
      showSearch
183
      filterTreeNode={(inputValue, node) => {
NEW
184
        if (inputValue) {
×
NEW
185
          const key = node[fieldKey];
×
NEW
186
          if (key && key.indexOf(inputValue) > -1) {
×
NEW
187
            return true;
×
188
          }
NEW
189
          const label = node[fieldLabel];
×
NEW
190
          if (label && label.indexOf(inputValue) > -1) {
×
NEW
191
            return true;
×
192
          }
NEW
193
          return false;
×
194
        }
NEW
195
        return true;
×
196
      }}
197
      {...antdTreeSelectProps}
198
      fieldNames={fieldNames}
199
      treeNodeLabelProp={fieldLabel}
200
      disabled={disabled}
201
      treeData={treeInnerData}
202
      value={innerValue}
203
      loadData={(node) =>
NEW
204
        new Promise((resolve) => {
×
NEW
205
          refreshByNode(node, resolve);
×
206
        })
207
      }
208
      onChange={onValueChange}
209
    />
210
  );
211

212
  if (!enableCopy) {
5✔
213
    return view;
4✔
214
  }
215

216
  return (
1✔
217
    <Space.Compact block style={style} className={className} {...antdSpaceProps}>
218
      {view}
219
      <div style={{ alignSelf: "center" }}>
220
        <CopyView value={innerValue} hiddenValue separator={separator} />
221
      </div>
222
    </Space.Compact>
223
  );
224
};
225

226
RestTreeSelect.propTypes = {
3✔
227
  // 自定义样式
228
  style: PropTypes.object,
229
  // 自定义类名
230
  className: PropTypes.string,
231
  // 当前选中的值
232
  value: PropTypes.any,
233
  // 值变化时的回调函数
234
  onChange: PropTypes.func,
235

236
  // 远程接口地址
237
  restful: PropTypes.string,
238
  // axios的配置
239
  reqConfig: PropTypes.object,
240
  // 基础请求参数
241
  baseParams: PropTypes.object,
242
  // 标签模板
243
  labelTemplate: PropTypes.string,
244
  // 父级字段名
245
  fieldParent: PropTypes.string,
246
  // 解析数据路径
247
  parseRowsPath: PropTypes.string,
248

249
  enableCopy: PropTypes.bool,
250
  // 多选时复制值之间的分隔符
251
  separator: PropTypes.string,
252

253
  // 字段映射
254
  fieldNames: PropTypes.object,
255
  treeNodeLabelProp: PropTypes.string,
256
  // 是否禁用
257
  disabled: PropTypes.bool,
258
  readOnly: PropTypes.bool,
259
  treeData: PropTypes.array,
260
  // antd TreeSelect 属性
261
  antdTreeSelectProps: PropTypes.object,
262
  antdSpaceProps: PropTypes.object,
263
};
264

265
export default RestTreeSelect;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc