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

SkylerHu / antd-restful / #59

09 Sep 2025 08:33AM UTC coverage: 81.429% (-0.07%) from 81.498%
#59

push

web-flow
fix: 修复 RestSelect 不配置 restful 的情况下不应触发远程调用

1288 of 1672 branches covered (77.03%)

Branch coverage included in aggregate %.

2 of 3 new or added lines in 2 files covered. (66.67%)

1 existing line in 1 file now uncovered.

1698 of 1995 relevant lines covered (85.11%)

42.5 hits per line

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

71.81
/src/components/RestTable.jsx
1
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
2
import PropTypes from "prop-types";
3
import {
4
  Button,
5
  Col,
6
  Descriptions,
7
  Dropdown,
8
  Input,
9
  InputNumber,
10
  Row,
11
  Space,
12
  Spin,
13
  Table,
14
  Tag,
15
  Tooltip,
16
} from "antd";
17
import {
18
  CloseOutlined,
19
  DownloadOutlined,
20
  NodeExpandOutlined,
21
  ReloadOutlined,
22
  SearchOutlined,
23
  SecurityScanOutlined,
24
  SettingOutlined,
25
} from "@ant-design/icons";
26
import { dequal as deepEqual } from "dequal";
27
import { DEFAULT_PAGE, DEFAULT_PAGE_SIZE, DEFAULT_ROWS_PATH, FieldType, FilterType } from "src/common/constants";
28
import {
29
  apiSorterToTableSorterDict,
30
  commonFormat,
31
  findDataByPath,
32
  genColumnKey,
33
  tableSorterToApiSorter,
34
  toBeString,
35
  transformFilters,
36
  genFields,
37
} from "src/common/parser";
38
import { commonFilter, commonSorter } from "src/common/sorter";
39
import { isArray, isBlank, isDict, isEmpty, isFunction, isString } from "src/common/typeTools";
40
import CopyView from "src/components/CopyView";
41
import FieldsSetting from "src/components/FieldsSetting";
42
import NumberRange from "src/components/formitems/NumberRange";
43
import RangeStrPicker from "src/components/formitems/RangeStrPicker";
44
import RestSelect from "src/components/formitems/RestSelect";
45
import GridForm from "src/components/GridForm";
46
import globalConfig from "src/config";
47
import { useDeepCompareMemoize, useInterval } from "src/hooks/index";
48
import { useSafeRequest } from "src/requests";
49

50
// 处理table表头中列的筛选
51
export const getColumnSearchProps = (dataIndex, column, inputRef) => {
2✔
52
  const { filterDropdownConfig: config } = column;
24✔
53
  // 处理数组转字符串的情况
54
  const handleValue = (v) => {
24✔
55
    let _value = v;
1✔
56
    if (isArray(v) && v.length > 0 && isString(v[0]) && v[0].includes(",")) {
1!
57
      _value = v[0].split(",");
1✔
58
    }
59
    return _value;
1✔
60
  };
61
  const _props = {
24✔
62
    filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => {
63
      let searchItem = null;
36✔
64
      const placeholder = config.dropdownProps?.placeholder || "输入搜索";
36✔
65
      switch (config.type) {
36!
66
        case FieldType.INPUT: {
67
          searchItem = (
6✔
68
            <Input
69
              allowClear={true}
70
              {...config.dropdownProps}
71
              placeholder={placeholder}
72
              ref={(node) => (inputRef = node)}
×
73
              value={selectedKeys}
74
              onChange={(e) => setSelectedKeys(e.target.value ? [e.target.value] : [])}
×
75
              onPressEnter={() => confirm()}
×
76
            />
77
          );
78
          break;
6✔
79
        }
80
        case FieldType.NUMBER: {
81
          searchItem = (
×
82
            <InputNumber
83
              {...config.dropdownProps}
84
              placeholder={placeholder}
85
              value={selectedKeys}
86
              onChange={(v) => setSelectedKeys(isBlank(v) ? [] : [v])}
×
87
              onPressEnter={() => confirm()}
×
88
            />
89
          );
90
          break;
×
91
        }
92
        case FieldType.NUMBER_RANGE: {
93
          let _value = handleValue(selectedKeys);
1✔
94
          searchItem = (
1✔
95
            <NumberRange
96
              {...config.dropdownProps}
97
              placeholder={placeholder}
98
              value={_value}
99
              onChange={(v) => setSelectedKeys(isBlank(v) ? [] : isArray(v) ? v : [v])}
×
100
              onPressEnter={() => confirm()}
×
101
            />
102
          );
103
          break;
1✔
104
        }
105
        case FieldType.DATE_RANGE_PICKER: {
106
          let _value = handleValue(selectedKeys);
×
107
          searchItem = (
×
108
            <RangeStrPicker
109
              {...config.dropdownProps}
110
              placeholder={placeholder}
111
              value={_value}
112
              onChange={(v) => setSelectedKeys(isBlank(v) ? [] : isArray(v) ? v : [v])}
×
113
              onPressEnter={() => confirm()}
×
114
            />
115
          );
116
          break;
×
117
        }
118
        case FieldType.SELECT: {
119
          const isMultiple = config.dropdownProps?.mode === "multiple";
×
120
          let _value = isMultiple ? handleValue(selectedKeys) : selectedKeys;
×
121
          searchItem = (
×
122
            <RestSelect
123
              style={{ width: "100%" }}
124
              {...config.dropdownProps}
125
              value={_value}
126
              onChange={(value) => {
127
                const keys = isBlank(value) ? [] : isArray(value) ? value : [value];
×
128
                setSelectedKeys(keys);
×
129
                if (!isMultiple) {
×
130
                  // 单选时,直接确认
131
                  confirm();
×
132
                }
133
              }}
134
            />
135
          );
136
          break;
×
137
        }
138
        default:
139
          break;
29✔
140
      }
141
      if (!searchItem) {
36✔
142
        return undefined;
29✔
143
      }
144
      const direction = config.antdSpaceProps?.direction || "vertical";
7✔
145
      const view = (
146
        <Space style={{ padding: 8, ...config.style }} {...config.antdSpaceProps} direction={direction}>
7✔
147
          {searchItem}
148
          <Row gutter={10}>
149
            {direction === "vertical" ? (
7!
150
              <>
151
                <Col span={12}>
152
                  <Button
153
                    size="small"
154
                    style={{ width: "100%" }}
155
                    onClick={() => {
156
                      clearFilters();
×
157
                      confirm();
×
158
                    }}
159
                  >
160
                    重置
161
                  </Button>
162
                </Col>
163
                <Col span={12}>
164
                  <Button type="primary" size="small" style={{ width: "100%" }} onClick={() => confirm()}>
×
165
                    搜索
166
                  </Button>
167
                </Col>
168
              </>
169
            ) : (
170
              <>
171
                <Col span={24}>
172
                  <Button type="primary" size="small" style={{ width: "100%" }} onClick={() => confirm()}>
×
173
                    搜索
174
                  </Button>
175
                </Col>
176
              </>
177
            )}
178
          </Row>
179
        </Space>
180
      );
181
      return view;
7✔
182
    },
183
    filterIcon: (filtered) => <SearchOutlined style={{ color: filtered ? "#1890ff" : undefined, padding: "0 5px" }} />,
34✔
184
    filterDropdownProps: {
185
      onOpenChange: (visible) => {
186
        if (config.type === FieldType.INPUT && visible) {
×
187
          // 让输入框聚焦
188
          setTimeout(() => {
×
189
            if (inputRef) {
×
190
              inputRef.select();
×
191
            }
192
          }, 100);
193
        }
194
      },
195
    },
196
  };
197
  return _props;
24✔
198
};
199

200
export const renderRowLabel = (record, column) => {
2✔
201
  let label;
202
  if (column.fieldName) {
31✔
203
    // 用真实字段值
204
    label = findDataByPath(record, column.fieldName);
16✔
205
  } else {
206
    label = record[genColumnKey(column)];
15✔
207
  }
208
  if (isEmpty(label)) {
31✔
209
    return label;
3✔
210
  }
211
  // 处理显示的值, 转换成数组方便统一处理
212
  let data = label;
28✔
213
  let copyV; // 复制值
214
  if (!isArray(label)) {
28✔
215
    data = [label];
24✔
216
    if (column.copyProps) {
24✔
217
      copyV = column.copyField && isDict(label) ? label[column.copyField] : label;
4✔
218
    }
219
  } else {
220
    if (column.copyProps) {
4!
221
      copyV = label.map((d) => (column.copyField && isDict(d) ? d[column.copyField] : d));
×
222
    }
223
  }
224
  let show = data.map((d, i) => {
28✔
225
    // 格式化label
226
    let _label = commonFormat(column.labelTemplate, d);
33✔
227
    if (column.showTag) {
33✔
228
      // 按照Tag展示
229
      _label = (
2✔
230
        <Tag color="blue" key={i}>
231
          {_label}
232
        </Tag>
233
      );
234
    }
235
    return _label;
33✔
236
  });
237
  if (!column.showTag) {
28✔
238
    // 按照逗号展示
239
    show = toBeString(show, ",", 1);
27✔
240
  }
241
  if (column.copyProps) {
28✔
242
    // 复制值
243
    show = (
4✔
244
      <CopyView {...column.copyProps} value={copyV}>
245
        {show}
246
      </CopyView>
247
    );
248
  }
249
  return show;
28✔
250
};
251

252
const RestTable = forwardRef(
2✔
253
  (
254
    {
255
      style,
256
      className,
257

258
      restful,
259
      reqConfig,
260
      parseOptions,
261
      urlDetailTemplate,
262
      baseParams,
263
      routeParams,
264
      forceParams,
265
      fieldPage = "page",
274✔
266
      fieldPageSize = "page_size",
274✔
267
      defaultPageSize = DEFAULT_PAGE_SIZE,
274✔
268
      fieldOrdering = "ordering",
278✔
269
      parseRowsPath = DEFAULT_ROWS_PATH,
273✔
270
      parseTotalPath = "count",
273✔
271
      isActive = true,
276✔
272
      tools = true,
158✔
273
      extraTools,
274
      showHeaderTags = false,
275✔
275
      onDataSourceChange,
276
      onFiltersChange,
277

278
      rowKey = "id",
188✔
279
      columns,
280
      expandFieldPath,
281
      expandAntdProps,
282
      expandedAllRows = false,
278✔
283
      dataSource,
284
      antdTableProps,
285
      filterFormProps,
286
      antdSpaceProps,
287
    },
288
    ref
289
  ) => {
290
    const [makeRequest] = useSafeRequest();
278✔
291
    const reqConfigRef = useRef(reqConfig);
278✔
292
    const memParseOptions = useDeepCompareMemoize(parseOptions);
278✔
293

294
    const [loading, setLoading] = useState(false);
278✔
295
    // table数据源
296
    const [innerData, setInnerData] = useState({
278✔
297
      total: 0,
298
      dataSource: [],
299
    });
300

301
    // 表单筛选条件
302
    const filterFormRef = useRef();
278✔
303

304
    // 筛选参数,优先级从低到高,innerFilters 是最终用于请求的筛选条件
305
    // 基础参数
306
    const memBaseParams = useDeepCompareMemoize(baseParams);
278✔
307
    // 路由参数
308
    const memRouteParams = useDeepCompareMemoize(routeParams);
278✔
309
    // table内置header上的筛选条件
310
    const [headerFilters, setHeaderFilters] = useState({});
278✔
311
    const [formFilters, setFormFilters] = useState({});
278✔
312
    // 强制参数
313
    const memForceParams = useDeepCompareMemoize(forceParams);
278✔
314
    // 真实用于请求的筛选条件
315
    const [innerFilters, setInnerFilters] = useState({});
278✔
316

317
    // 标记字段 是否开启了 多选,用于处理query参数转化成数组
318
    const [multipleMap, setMultipleMap] = useState({});
278✔
319

320
    // 默认开启高级搜索和列显示隐藏设置
321
    const innerTools = useDeepCompareMemoize(
278✔
322
      tools ? Object.assign({ advancedSearch: true, refreshInterval: 0, settings: true }, tools) : {}
278✔
323
    );
324
    const [enableAdvancedSearch, setEnableAdvancedSearch] = useState(filterFormProps?.advancedSearch || false);
278✔
325
    const [enableRefresh, setEnableRefresh] = useState(innerTools.refreshInterval > 0);
278✔
326
    // 控制显示的表单字段
327
    const [filterFieldKeys, setFilterFieldKeys] = useState([]);
278✔
328
    // 控制显示的列
329
    const [showColumnsKeys, setShowColumnsKeys] = useState([]);
278✔
330

331
    // 因为有未使用 FieldsSettings的场景,所以不能直接使用 value 作为设置的值设置, 无法监听columns的变化
332
    const onToolsFilterChange = useCallback((_, keys) => {
278✔
333
      setFilterFieldKeys(keys);
4✔
334
    }, []);
335
    const onToolsSettingsChange = useCallback((_, keys) => {
278✔
336
      setShowColumnsKeys(keys);
48✔
337
    }, []);
338

339
    const filterFields = useMemo(() => genFields(filterFormProps?.fields, filterFieldKeys), [filterFormProps?.fields, filterFieldKeys]);
278✔
340
    const showColumns = useMemo(() => genFields(columns, showColumnsKeys), [columns, showColumnsKeys]);
278✔
341

342
    useEffect(() => {
278✔
343
      if (isArray(dataSource)) {
85✔
344
        setInnerData((oldV) => {
50✔
345
          const newV = { dataSource, total: dataSource?.length };
50✔
346
          return deepEqual(oldV, newV) ? oldV : newV;
50✔
347
        });
348
      }
349
    }, [dataSource]);
350

351
    useEffect(() => {
278✔
352
      if (isFunction(onDataSourceChange)) {
133✔
353
        onDataSourceChange(innerData);
2✔
354
      }
355
    }, [innerData, onDataSourceChange]);
356

357
    // setMultipleMap
358
    useEffect(() => {
278✔
359
      setMultipleMap((oldV) => {
84✔
360
        const newV = columns.reduce((acc, column) => {
84✔
361
          const field = column.dataIndex || column.key;
182✔
362
          if (column.filterMultiple === undefined) {
182✔
363
            if (column.filters) {
181✔
364
              // 如果开启了刷选,则默认是多选; 是table原生决定的
365
              acc[field] = true;
1✔
366
            }
367
          } else {
368
            acc[field] = column.filterMultiple;
1✔
369
          }
370
          return acc;
182✔
371
        }, {});
372
        return deepEqual(oldV, newV) ? oldV : newV;
84✔
373
      });
374
    }, [columns]);
375

376
    const pageSize = useMemo(() => {
278✔
377
      return parseInt(innerFilters[fieldPageSize] || defaultPageSize);
146✔
378
    }, [innerFilters, fieldPageSize, defaultPageSize]);
379

380
    const pageSizeOptions = useMemo(() => {
278✔
381
      let opts = antdTableProps?.pagination?.pageSizeOptions || [10, 20, 50, 100];
73✔
382
      let change = false;
73✔
383
      if (!opts.includes(pageSize)) {
73!
384
        change = true;
×
385
        opts.push(pageSize);
×
386
      }
387
      if (!opts.includes(defaultPageSize)) {
73!
388
        change = true;
×
389
        opts.push(defaultPageSize);
×
390
      }
391
      if (memBaseParams && memBaseParams[fieldPageSize] && !opts.includes(memBaseParams[fieldPageSize])) {
73!
392
        change = true;
×
393
        opts.push(memBaseParams[fieldPageSize]);
×
394
      }
395
      if (change) {
73!
396
        opts.sort((a, b) => a - b);
×
397
      }
398
      return opts;
73✔
399
    }, [pageSize, defaultPageSize, antdTableProps?.pagination?.pageSizeOptions, memBaseParams, fieldPageSize]);
400

401
    // setInnerFilters
402
    useEffect(() => {
278✔
403
      setInnerFilters((oldV) => {
80✔
404
        // 几个赋值顺序不要随意变动
405
        // basePrarams 优先级最低,可以被 route和headerFilters操作覆盖
406
        // routeParams 优先级次之,可以被 headerFilters 用户操作覆盖
407
        // formFilters 一般不与 headerFilters 同时使用, 至少表单key没有重叠的部分
408
        // forceParams 优先级最高,是用户手动设置的,不会被其他参数覆盖
409
        let newV = {
80✔
410
          // [fieldPage]: DEFAULT_PAGE,
411
          // [fieldPageSize]: defaultPageSize,
412
          ...memBaseParams,
413
          ...memRouteParams,
414
          ...headerFilters,
415
          ...formFilters,
416
          ...memForceParams,
417
        };
418
        // 避免传递过来空字符串的情况
419
        newV[fieldPage] = newV[fieldPage] || DEFAULT_PAGE;
80✔
420
        newV[fieldPageSize] = newV[fieldPageSize] || defaultPageSize;
80✔
421
        newV = transformFilters(newV, { skipEmpty: true, multipleMap });
80✔
422
        if (deepEqual(oldV, newV)) {
80✔
423
          return oldV;
6✔
424
        }
425
        return newV;
74✔
426
      });
427
    }, [
428
      fieldPage,
429
      fieldPageSize,
430
      defaultPageSize,
431
      memBaseParams,
432
      memRouteParams,
433
      memForceParams,
434
      headerFilters,
435
      multipleMap,
436
      formFilters,
437
    ]);
438

439
    const filterFormKeys = useDeepCompareMemoize(
278✔
440
      filterFormProps?.fields?.map((field) => ({
19✔
441
        key: field.key,
442
        type: field.type,
443
      })) || []
444
    );
445

446
    // 更新筛选表单的值
447
    useEffect(() => {
278✔
448
      if (filterFormRef.current) {
73✔
449
        const values = {};
4✔
450
        filterFormKeys.forEach((field) => {
4✔
451
          let v = memRouteParams ? memRouteParams[field.key] : undefined;
5!
452
          if (v === undefined) {
5!
453
            v = memBaseParams ? memBaseParams[field.key] : undefined;
5!
454
          }
455
          if (v === undefined) {
5!
456
            // 需要重置表单的值
457
            values[field.key] = null;
5✔
458
          } else {
459
            values[field.key] = v;
×
460
          }
461
          if (field.type && [FieldType.CHECKBOX, FieldType.RADIO].includes(field.type) && isBlank(values[field.key])) {
5!
462
            // 为了能够正确显示“全部”选项
463
            values[field.key] = "";
×
464
          }
465
        });
466
        delete values[fieldPage];
4✔
467
        delete values[fieldPageSize];
4✔
468

469
        if (innerTools.advancedSearch) {
4!
470
          const noEmptyKeys = Object.keys(values).filter((key) => !isBlank(values[key]));
5✔
471
          if (noEmptyKeys.length > 1) {
4!
472
            // 如果多个表单有值,则开启高级搜索
473
            setEnableAdvancedSearch(true);
×
474
          }
475
        }
476
        if (isEmpty(values)) {
4!
477
          filterFormRef.current.getFormInstance().resetFields();
×
478
        } else {
479
          filterFormRef.current.getFormInstance().setFieldsValue(values);
4✔
480
        }
481
        setFormFilters((oldV) => (deepEqual(oldV, values) ? oldV : values));
4!
482
      }
483
    }, [memRouteParams, memBaseParams, filterFormKeys, fieldPage, fieldPageSize, innerTools.advancedSearch]);
484

485
    // 处理筛选条件变化 onFiltersChange
486
    useEffect(() => {
278✔
487
      if (!innerFilters[fieldPage] || !innerFilters[fieldPageSize]) {
148✔
488
        // 因为page_size一定会赋予默认值,避免首次会多次触发回调
489
        return;
72✔
490
      }
491
      if (isFunction(onFiltersChange)) {
76✔
492
        const filters = {
9✔
493
          ...innerFilters,
494
        };
495
        // 删除默认值
496
        if (filters[fieldPage] === DEFAULT_PAGE) {
9!
497
          delete filters[fieldPage];
9✔
498
        }
499
        if (memBaseParams && memBaseParams[fieldPageSize]) {
9!
500
          if (filters[fieldPageSize] === memBaseParams[fieldPageSize]) {
×
501
            delete filters[fieldPageSize];
×
502
          }
503
        } else {
504
          if (filters[fieldPageSize] === defaultPageSize) {
9!
505
            delete filters[fieldPageSize];
9✔
506
          }
507
        }
508
        // forceParams 会覆盖其他参数,去掉相同key的
509
        if (memForceParams) {
9✔
510
          Object.keys(memForceParams).forEach((key) => {
4✔
511
            // 直接删除,肯定相等
512
            delete filters[key];
9✔
513
          });
514
        }
515
        // baseParams 相同值的可以去掉
516
        if (memBaseParams) {
9✔
517
          Object.keys(memBaseParams).forEach((key) => {
5✔
518
            if (deepEqual(filters[key], memBaseParams[key])) {
11✔
519
              delete filters[key];
8✔
520
            }
521
          });
522
        }
523
        onFiltersChange(filters);
9✔
524
      }
525
    }, [fieldPage, fieldPageSize, defaultPageSize, memBaseParams, memForceParams, innerFilters, onFiltersChange]);
526

527
    // 请求远端数据
528
    const fetchData = useCallback(() => {
278✔
529
      if (!isActive || !restful) {
78✔
530
        return;
37✔
531
      }
532
      setLoading(true);
41✔
533
      const _config = {
41✔
534
        params: innerFilters,
535
        ...reqConfigRef.current,
536
      };
537
      if (memParseOptions) {
41!
538
        _config.paramsSerializer = (p) => globalConfig.queryStringify(p, memParseOptions);
×
539
      }
540
      makeRequest({ delay: 200, key: `resttable` })
41✔
541
        .get(restful, _config)
542
        .then((response) => {
543
          const data = findDataByPath(response.data, parseRowsPath);
41✔
544
          const _total = findDataByPath(response.data, parseTotalPath);
41✔
545
          setInnerData({ dataSource: data, total: _total || 0 });
41✔
546
        })
547
        .finally(() => {
548
          setLoading(false);
41✔
549
        });
550
    }, [isActive, makeRequest, restful, parseRowsPath, parseTotalPath, innerFilters, memParseOptions]);
551

552
    useEffect(() => {
278✔
553
      if (!innerFilters[fieldPage]) {
146✔
554
        // 因为page_size一定会赋予默认值,避免首次会多请求一次
555
        return;
72✔
556
      }
557
      fetchData();
74✔
558
    }, [innerFilters, fetchData, fieldPage]);
559

560
    const [runInterval] = useInterval(() => fetchData(), innerTools.refreshInterval, innerTools.refreshInterval > 0);
278✔
561
    // 删除行
562
    const deleteRow = useCallback(
278✔
563
      (row) => {
564
        if (!restful || !row[rowKey]) {
3!
NEW
565
          return;
×
566
        }
567
        setLoading(true);
3✔
568
        let url;
569
        if (urlDetailTemplate) {
3✔
570
          url = commonFormat(urlDetailTemplate, row);
1✔
571
        } else {
572
          if (restful.endsWith("/")) {
2!
573
            url = `${restful}${row[rowKey]}/`;
×
574
          } else {
575
            url = `${restful}/${row[rowKey]}`;
2✔
576
          }
577
        }
578
        makeRequest()
3✔
579
          .delete(url)
580
          .then(() => {
581
            // 删除成功后,刷新数据
582
            fetchData();
2✔
583
          })
584
          .catch(() => {
585
            setLoading(false);
1✔
586
          });
587
      },
588
      [rowKey, restful, urlDetailTemplate, fetchData, makeRequest]
589
    );
590

591
    // 暴露给ref调用的方法
592
    useImperativeHandle(
278✔
593
      ref,
594
      () => ({
10✔
595
        refreshList: fetchData,
596
        deleteRow,
597
      }),
598
      [fetchData, deleteRow]
599
    );
600

601
    const columnSearchViewRef = useRef(null);
278✔
602

603
    // 处理table的cloumns
604
    const memColumns = useMemo(() => {
278✔
605
      const sorterDict = apiSorterToTableSorterDict(innerFilters[fieldOrdering]);
149✔
606
      const arr = showColumns
149✔
607
        .filter((item) => !item.expandable && !item.hidden)
291✔
608
        .map((column) => {
609
          // 获取唯一字段; antd默认dataIndex优先,但其取值可能是数组,不能作为key使用
610
          const field = genColumnKey(column);
290✔
611
          let newCloumn = { ...column };
290✔
612
          if (newCloumn.hidden) {
290!
613
            // 隐藏的字段后续无需处理
614
            return newCloumn;
×
615
          }
616
          if (!newCloumn.render && (column.labelTemplate || !isEmpty(column.copyProps) || column.fieldName)) {
290✔
617
            // 转换为render函数,处理显示的值
618
            newCloumn.render = (value, record) => renderRowLabel(record, column);
18✔
619
          }
620
          if (column.sorter) {
290✔
621
            // 设置是降序还是升序
622
            newCloumn.sortOrder = sorterDict[field];
4✔
623
            if (!restful && column.sorter === true) {
4!
624
              if (column.fieldName) {
4!
625
                // 如果未开启restful,且配置的是bool值,开启本地排序
626
                newCloumn.sorter = (a, b) =>
4✔
627
                  commonSorter(findDataByPath(a, column.fieldName), findDataByPath(b, column.fieldName));
×
628
              } else {
629
                // 又因为配置的dataIndex并不一定是record里的field,所以无法正确排序
630
                delete newCloumn.sorter;
×
631
              }
632
            }
633
          }
634
          if (restful) {
290✔
635
            if (column.filterDropdownConfig) {
94✔
636
              delete newCloumn.dropdownLocalConfig;
12✔
637
              newCloumn = {
12✔
638
                ...newCloumn,
639
                ...getColumnSearchProps(field, newCloumn, columnSearchViewRef.current),
640
              };
641
              delete newCloumn.filterDropdownConfig;
12✔
642
            }
643
          } else {
644
            if (!newCloumn.onFilter && (newCloumn.filters || column.dropdownLocalConfig)) {
196✔
645
              const fieldName = column.dropdownLocalConfig?.fieldName || column.fieldName || field;
4✔
646
              if (fieldName) {
4!
647
                newCloumn = {
4✔
648
                  ...newCloumn,
649
                  ...getColumnSearchProps(
650
                    field,
651
                    {
652
                      ...column,
653
                      filterDropdownConfig: { type: FieldType.INPUT, ...column.dropdownLocalConfig },
654
                    },
655
                    columnSearchViewRef.current
656
                  ),
657
                };
658
                // 支持本地筛选
659
                newCloumn.onFilter = (input, record) => {
4✔
660
                  const v = findDataByPath(record, fieldName);
×
661
                  let _filterType = column.dropdownLocalConfig?.filterType || FilterType.SEARCH;
×
662
                  if (!isEmpty(column.filters)) {
×
663
                    // 如果配置了精确筛选,则使用精确筛选
664
                    _filterType = FilterType.EQUAL;
×
665
                  }
666
                  return commonFilter(input, v, { filterType: _filterType });
×
667
                };
668
              } else {
669
                // 因为 dataIndex 可能不是 record 里的 field,所以无法正确处理筛选
670
                delete newCloumn.onFilter;
×
671
                delete newCloumn.filters;
×
672
                delete newCloumn.filterDropdown;
×
673
              }
674
              delete newCloumn.dropdownLocalConfig;
4✔
675
              delete newCloumn.filterDropdownConfig;
4✔
676
            }
677
          }
678

679
          // 初始刷选值
680
          newCloumn.filteredValue = [];
290✔
681
          if (newCloumn.filterDropdown !== undefined || newCloumn.filters !== undefined) {
290✔
682
            // 开启了刷选的字段
683
            let value = innerFilters[field];
16✔
684
            // 不是数组需要转成数组
685
            if (isBlank(value)) {
16✔
686
              value = [];
12✔
687
            } else if (!isArray(value)) {
4!
688
              value = [value];
4✔
689
            }
690
            newCloumn.filteredValue = value;
16✔
691
          }
692
          return newCloumn;
290✔
693
        });
694
      return arr.filter((item) => !item.hidden);
290✔
695
    }, [innerFilters, fieldOrdering, showColumns, restful]);
696

697
    // 表头上的筛选条件,按照Tags的形式都展示出来
698
    const headerTags = useMemo(() => {
278✔
699
      if (!showHeaderTags) {
149✔
700
        return [];
147✔
701
      }
702
      const arr = showColumns
2✔
703
        .filter(
704
          (column) => !column.hidden && (column.filterDropdownConfig || column.dropdownLocalConfig || column.filters)
6!
705
        )
706
        .map((column) => {
707
          let field = genColumnKey(column);
6✔
708
          const v = innerFilters[field];
6✔
709
          return { key: field, value: toBeString(v, ",", 1), label: column.title || field };
6!
710
        })
711
        .filter((item) => !isEmpty(item.value));
6✔
712
      return arr;
2✔
713
    }, [innerFilters, showColumns, showHeaderTags]);
714

715
    // 展开的列
716
    const memExpandableColumns = useMemo(() => {
278✔
717
      return showColumns.filter((item) => item.expandable && !item.hidden);
242!
718
    }, [showColumns]);
719
    const [isExpandedAll, setIsExpandedAll] = useState(expandedAllRows || innerTools.expandedAllRows);
278✔
720
    const [expandedRows, setExpandedRows] = useState();
278✔
721
    useEffect(() => {
278✔
722
      if (isExpandedAll) {
130!
723
        setExpandedRows(innerData.dataSource.map((row) => row[rowKey]));
×
724
      } else {
725
        setExpandedRows([]);
130✔
726
      }
727
    }, [innerData.dataSource, rowKey, isExpandedAll]);
728

729
    const expandableProps = useMemo(() => {
278✔
730
      if (memExpandableColumns.length === 0) {
195!
731
        return antdTableProps?.expandable;
195✔
732
      }
733
      return {
×
734
        expandedRowRender: (record) => {
735
          return (
×
736
            <Descriptions {...expandAntdProps}>
737
              {memExpandableColumns.map((column) => {
738
                const _key = genColumnKey(column);
×
739
                return (
×
740
                  <Descriptions.Item key={_key} {...column.expandItemProps} label={column.title}>
741
                    {renderRowLabel(record, column)}
742
                  </Descriptions.Item>
743
                );
744
              })}
745
            </Descriptions>
746
          );
747
        },
748
        rowExpandable: (record) => !expandFieldPath || findDataByPath(record, expandFieldPath),
×
749
        expandedRowKeys: expandedRows,
750
        onExpandedRowsChange: (rowKeys) => {
751
          setExpandedRows(rowKeys);
×
752
        },
753
        ...antdTableProps?.expandable,
754
      };
755
    }, [memExpandableColumns, antdTableProps?.expandable, expandAntdProps, expandFieldPath, expandedRows]);
756

757
    // 处理table的onChange事件
758
    const onTableChange = useCallback(
278✔
759
      (pagination, filters, sorter) => {
760
        // filters是字典,但是 对应的值都是数组
761
        // sorter是字典,table一般只允许一列进行排序
762
        const _filters = {
×
763
          ...filters,
764
          [fieldPage]: pagination.current,
765
          [fieldPageSize]: pagination.pageSize,
766
        };
767
        const ordering = tableSorterToApiSorter(sorter);
×
768
        // 为空的时候也要赋值,需要覆盖routeParams,触发路由变更
769
        _filters[fieldOrdering] = ordering;
×
770
        setHeaderFilters(_filters);
×
771
      },
772
      [fieldPage, fieldPageSize, fieldOrdering]
773
    );
774

775
    // 生成下载链接
776
    const genDownloadUrl = useCallback(
278✔
777
      (isAll = false) => {
×
778
        if (!restful || !innerTools.downloadKey) {
12!
779
          return "";
×
780
        }
781
        let url = restful;
12✔
782
        const query = { ...innerFilters, [innerTools.downloadKey]: 1 };
12✔
783
        if (isAll) {
12✔
784
          delete query[fieldPage];
6✔
785
          delete query[fieldPageSize];
6✔
786
        }
787
        if (isString(innerTools.downloadKey)) {
12!
788
          query[innerTools.downloadKey] = 1;
×
789
        } else {
790
          query._download = 1;
12✔
791
        }
792
        const search = globalConfig.queryStringify(query);
12✔
793
        if (search) {
12!
794
          url += `?${search}`;
12✔
795
        }
796
        return url;
12✔
797
      },
798
      [restful, innerFilters, fieldPage, fieldPageSize, innerTools.downloadKey]
799
    );
800

801
    const hasHeader = useMemo(() => {
278✔
802
      if (!isEmpty(innerTools) || extraTools) {
72✔
803
        return true;
48✔
804
      }
805
      if (restful && !isEmpty(filterFormProps)) {
24!
806
        return true;
×
807
      }
808
      return false;
24✔
809
    }, [innerTools, extraTools, filterFormProps, restful]);
810

811
    return (
278✔
812
      <Space direction="vertical" {...antdSpaceProps} style={{ width: "100%", ...antdSpaceProps?.style }}>
813
        {hasHeader && (
463✔
814
          <div style={{ position: "relative" }} className="cls-resttable-header">
815
            {restful && filterFormProps && (
341✔
816
              <Spin spinning={loading}>
817
                <GridForm
818
                  key="filterForm"
819
                  submitTitle="搜索"
820
                  enablePlaceholder={!isEmpty(innerTools) || extraTools}
16!
821
                  {...filterFormProps}
822
                  fields={filterFields}
823
                  initialValues={{ ...memBaseParams, ...filterFormProps?.initialValues }}
824
                  advancedSearch={enableAdvancedSearch}
825
                  ref={filterFormRef}
826
                  onSubmit={(values) => {
827
                    setFormFilters((oldV) => {
1✔
828
                      if (deepEqual(oldV, values)) {
1!
829
                        // 数据没有变更刷新列表
830
                        fetchData();
1✔
831
                        return oldV;
1✔
832
                      }
833
                      return values;
×
834
                    });
835
                  }}
836
                  onReset={(values) => {
837
                    setFormFilters((oldV) => {
×
838
                      if (deepEqual(oldV, values)) {
×
839
                        // 数据没有变更刷新列表
840
                        fetchData();
×
841
                        return oldV;
×
842
                      }
843
                      return values;
×
844
                    });
845
                  }}
846
                />
847
              </Spin>
848
            )}
849
            {(!isEmpty(innerTools) || extraTools) && (
370!
850
              <div
851
                style={{ position: "absolute", right: 10, bottom: 15 }}
852
                className="cls-resttable-tools"
853
              >
854
                <Space key="tools">
855
                  {extraTools}
856
                  {innerTools.expandedAllRows !== undefined && memExpandableColumns.length > 0 && (
185!
857
                    <Tooltip title={isExpandedAll ? "收起所有行" : "展开所有行"}>
×
858
                      <Button
859
                        icon={<NodeExpandOutlined />}
860
                        type={isExpandedAll ? "primary" : undefined}
×
861
                        onClick={() => {
862
                          setIsExpandedAll((oldV) => !oldV);
×
863
                        }}
864
                      />
865
                    </Tooltip>
866
                  )}
867
                  {restful && innerTools.refreshInterval >= 0 && (
462✔
868
                    <Tooltip
869
                      title={
870
                        innerTools.refreshInterval > 0
137✔
871
                          ? enableRefresh
6!
872
                            ? `点击关闭 ${innerTools.refreshInterval}ms 刷新`
873
                            : `开启间隔 ${innerTools.refreshInterval}ms 刷新`
874
                          : "点击刷新"
875
                      }
876
                    >
877
                      <Button
878
                        icon={<ReloadOutlined />}
879
                        type={enableRefresh && innerTools.refreshInterval > 0 ? "primary" : undefined}
280✔
880
                        onClick={() => {
881
                          const v = !enableRefresh;
×
882
                          setEnableRefresh(v);
×
883
                          runInterval(v && innerTools.refreshInterval > 0);
×
884
                        }}
885
                      />
886
                    </Tooltip>
887
                  )}
888
                  {restful && innerTools.downloadKey && (
331✔
889
                    <Dropdown
890
                      menu={{
891
                        items: [
892
                          {
893
                            key: "download_current",
894
                            label: (
895
                              <Button href={genDownloadUrl(false)} type="link" target="blank" size="small">
896
                                导出当前页
897
                              </Button>
898
                            ),
899
                          },
900
                          {
901
                            key: "download_all",
902
                            label: (
903
                              <Button href={genDownloadUrl(true)} type="link" target="blank" size="small">
904
                                导出全部数据
905
                              </Button>
906
                            ),
907
                          },
908
                        ],
909
                      }}
910
                      placement="bottom"
911
                    >
912
                      <Button icon={<DownloadOutlined />} />
913
                    </Dropdown>
914
                  )}
915
                  {restful && filterFormProps && innerTools.advancedSearch && (
357✔
916
                    <FieldsSetting
917
                      value={filterFormProps.fields}
918
                      title="设置搜索选项"
919
                      storageKey={isString(innerTools.advancedSearch) ? innerTools.advancedSearch : `${restful}-filter`}
16!
920
                      onChange={onToolsFilterChange}
921
                    >
922
                      <Button icon={<SecurityScanOutlined />} />
923
                    </FieldsSetting>
924
                  )}
925
                  {innerTools.settings && (
370✔
926
                    <FieldsSetting
927
                      value={columns}
928
                      title="设置列显示"
929
                      storageKey={isString(innerTools.settings) ? innerTools.settings : `${restful}-settings`}
185✔
930
                      onChange={onToolsSettingsChange}
931
                    >
932
                      <Button icon={<SettingOutlined />} />
933
                    </FieldsSetting>
934
                  )}
935
                </Space>
936
              </div>
937
            )}
938
          </div>
939
        )}
940
        {headerTags.length > 0 && (
280✔
941
          <div className="cls-resttable-header-tags">
942
            {headerTags.map((item) => {
943
              return (
2✔
944
                <Tag
945
                  key={item.key}
946
                  closable={true}
947
                  closeIcon={<CloseOutlined />}
948
                  onClose={() => {
949
                    setHeaderFilters((oldV) => {
×
950
                      return { ...oldV, [item.key]: null };
×
951
                    });
952
                  }}
953
                >
954
                  <span style={{ color: "#8c8c8c" }} className="cls-resttable-header-tag-label">
955
                    {item.label}:{" "}
956
                  </span>
957
                  <span
958
                    style={{ whiteSpace: "pre-wrap", wordBreak: "break-all" }}
959
                    className="cls-resttable-header-tag-value"
960
                  >
961
                    {item.value}
962
                  </span>
963
                </Tag>
964
              );
965
            })}
966
            <Button
967
              type="link"
968
              size="small"
969
              style={{ fontSize: 12 }}
970
              onClick={() => {
971
                setHeaderFilters((oldV) => {
×
972
                  const newV = { ...oldV };
×
973
                  headerTags.forEach((item) => {
×
974
                    newV[item.key] = null;
×
975
                  });
976
                  return newV;
×
977
                });
978
              }}
979
            >
980
              清除
981
            </Button>
982
          </div>
983
        )}
984
        <Table
985
          style={style}
986
          className={className}
987
          {...antdTableProps}
988
          loading={loading}
989
          rowKey={rowKey}
990
          columns={memColumns}
991
          dataSource={innerData.dataSource}
992
          pagination={{
993
            size: "small",
994
            showSizeChanger: true,
995
            showQuickJumper: true,
996
            showTotal: (total) => {
997
              return <span>总计:{total} 条</span>;
114✔
998
            },
999
            pageSizeOptions,
1000
            ...antdTableProps?.pagination,
1001
            current: innerFilters[fieldPage],
1002
            pageSize: innerFilters[fieldPageSize],
1003
            // 若未开启restful,则不设置总计,否则开启了本地筛选无法正确展示showTotal
1004
            total: restful ? innerData.total : undefined,
278✔
1005
          }}
1006
          onChange={(pagination, filters, sorter, extra) => {
1007
            onTableChange(pagination, filters, sorter);
×
1008
            if (isFunction(antdTableProps?.onChange)) {
×
1009
              antdTableProps.onChange(pagination, filters, sorter, extra);
×
1010
            }
1011
          }}
1012
          expandable={expandableProps}
1013
        />
1014
      </Space>
1015
    );
1016
  }
1017
);
1018

1019
RestTable.propTypes = {
2✔
1020
  style: PropTypes.object,
1021
  className: PropTypes.string,
1022

1023
  restful: PropTypes.string,
1024
  reqConfig: PropTypes.object,
1025
  // 处理query参数的选项, query-string 的配置项
1026
  parseOptions: PropTypes.object,
1027
  urlDetailTemplate: PropTypes.string,
1028
  baseParams: PropTypes.object,
1029
  // 有了baseParams,还需要routeParams,是为了处理默认参数(baseParams)不显示在地址栏的问题
1030
  // routeParams与地址栏query参数对应,在使用该组件赋值时需要去掉与baseParams重复的参数
1031
  routeParams: PropTypes.object,
1032
  // 无论路由参数、表单参数是否变化,都会被改值覆盖
1033
  forceParams: PropTypes.object,
1034
  fieldPage: PropTypes.string,
1035
  fieldPageSize: PropTypes.string,
1036
  defaultPageSize: PropTypes.number,
1037
  fieldOrdering: PropTypes.string,
1038
  parseRowsPath: PropTypes.string,
1039
  parseTotalPath: PropTypes.string,
1040
  // 是否展示表头上的筛选条件
1041
  showHeaderTags: PropTypes.bool,
1042
  // 是否激活,如果为false,则不更新数据; 主要在Tab组件中使用
1043
  isActive: PropTypes.bool,
1044
  // 工具栏的配置
1045
  tools: PropTypes.oneOfType([
1046
    PropTypes.shape({
1047
      // 开启高级搜索,默认会开启
1048
      advancedSearch: PropTypes.bool,
1049
      // 下载的key,如果为true,则使用默认的 _download
1050
      downloadKey: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
1051
      // 刷新数据的间隔,单位 ms,小于0时隐藏刷新按钮,等于0时手动刷新,大于0时自动刷新
1052
      refreshInterval: PropTypes.number,
1053
      // 默认开启列显示隐藏设置, 配置存储localStorage的key, 如果为true,则使用restful的值作为key; 当clumns配置列的key发生改动时,之前的设置会失效
1054
      settings: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
1055
      expandedAllRows: PropTypes.bool,
1056
    }),
1057
    PropTypes.bool,
1058
  ]),
1059
  // 其他工具
1060
  extraTools: PropTypes.node,
1061

1062
  onFiltersChange: PropTypes.func,
1063
  onDataSourceChange: PropTypes.func,
1064

1065
  rowKey: PropTypes.string,
1066
  columns: PropTypes.arrayOf(
1067
    PropTypes.shape({
1068
      // 若是某列是字典,可以用此模板格式化显示
1069
      labelTemplate: PropTypes.string,
1070
      // 用于配置字段展示,可配合labelTemplate使用
1071
      fieldName: PropTypes.string,
1072
      // 开启复制功能
1073
      copyProps: PropTypes.object,
1074
      // 如果某列根据dataIndex获取到的是字典,则需要指定字段中某个字段值用于copy
1075
      copyField: PropTypes.string,
1076
      // 是否按照Tag展示,数据是数组时有用
1077
      showTag: PropTypes.bool,
1078
      // 下来筛选自定义view的设置
1079
      filterDropdownConfig: PropTypes.shape({
1080
        style: PropTypes.object,
1081
        // 可以控制输入组件和按钮的排列位置
1082
        antdSpaceProps: PropTypes.object,
1083
        type: PropTypes.oneOf(FieldType.map((o) => o.value)),
24✔
1084
        dropdownProps: PropTypes.object,
1085
      }),
1086
      // 标记字段开启了多值筛选,处理query参数转化成数组
1087
      filterMultiple: PropTypes.bool,
1088
      // 禁用restful下,开启下拉选择的配置; 非restful情况下会覆盖 filterDropdownConfig
1089
      dropdownLocalConfig: PropTypes.shape({
1090
        // 配置本地筛选字段,比外层配置优先级高
1091
        fieldName: PropTypes.string,
1092
        filterType: PropTypes.oneOf(FilterType.map((o) => o.value)),
6✔
1093
      }),
1094
      // 是否默认显示
1095
      hidden: PropTypes.bool,
1096
      // 是否开启排序,得配置 dataIndex 字段
1097
      sorter: PropTypes.bool,
1098
      // 是否开启展开功能
1099
      expandable: PropTypes.bool,
1100
      expandableItemProps: PropTypes.object,
1101
    })
1102
  ).isRequired,
1103
  // 设置是否开启展开功能的字段
1104
  expandFieldPath: PropTypes.string,
1105
  expandAntdProps: PropTypes.object,
1106
  // 未启用tools时也可以配置展开所有行
1107
  expandedAllRows: PropTypes.bool,
1108
  dataSource: PropTypes.array,
1109
  // 筛选表单的配置
1110
  filterFormProps: PropTypes.object,
1111

1112
  antdTableProps: PropTypes.object,
1113
  antdSpaceProps: PropTypes.object,
1114
};
1115
RestTable.displayName = "RestTable";
2✔
1116

1117
export default RestTable;
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