场景

GitHub 源码open in new window

在后台项目中,即便使用了 antd,仍然存在太多太多的列表页面。这些列表页面大多数又是非常相似的,所以吾辈需要解决重复的简单列表的编写,避免每次都手动控制过滤器/分页之类的东西,将之抽象成配置项,然后通过配置生成列表页面。或许已经有很多人做过了这件事情,但于吾辈而言,这仍然是全新的体验,所以也便于此记录,并供之以他人参考。

理念

使用逐级递进的方式进行封装,使用者可以根据需求停留在一个合适的封装层次,使用不同封装层次的组件。

  • BasicList:高层列表封装组件
    • ListHeader:列表页顶部工具栏组件
      • CommonHeader: 通用的顶部工具栏组件
    • ListFilter:过滤器组件
      • FilterSelect:单选过滤器
      • FilterTimeRange:时间区间过滤器
      • FilterSlot:自定义过滤器
    • ListTable:列表封装组件

BasicList 使用步骤

graph TD;
A[添加 api class 实现 BasicListApi] --> B[添加配置项]
B --> C{是否需要动态修改};
C --> |是| C1[使用 useMemo 声明]
C --> |否| C2[将之抽离到组件外部声明为常量]
C1 --> D
C2 --> D
D{是否有非通用 Filter}
D --> |是| D1[自定义 slot filter]
D --> |否| D2[使用 select/time filter]
D1 --> E
D2 --> E
E{是否需要自定义表格相关功能}
E --> |是| E1[使用 filterOperate 配置]
E --> |否| E2[不声明 filter 配置]
E1 --> F
E2 --> F
F[列表渲染]

BasicList 渲染流程

graph TB
A[从 props 拿到配置项列表] --> B{header 是否为 ReactElement}
B --> |是| B1[直接渲染]
B --> |否| B2[使用 ListHeader 渲染 header 配置项]
B1 --> C
B2 --> C
C{filters 是否存在}
C --> |是| C1{filters 是否为 ReactElement}
  C1 --> |是| C1A[直接渲染]
  C1 --> |否| C1B[使用 ListFilter 渲染 filters 配置项] --> C1C[监听 initialParams 变化, 以随时修改 form]
C --> |否| C2[不渲染]
C1A --> D
C1B --> D
C2 --> D
D[渲染列表]

使用示例

使用基本 API

GitHub 代码示例open in new window

如下所示,我们想要构造下面这样一个简单的列表页面,包含一个面包屑导航列表、搜索框、过滤条件选择器和一个表格。

import * as React from 'react'
import { Button } from 'antd'
import { Link } from 'react-router-dom'
import { Moment } from 'moment'
import { LabeledValue } from 'antd/es/select'
import { userApi } from './api/UserApi'
import {
  BasicList,
  BasicListPropsType,
  FilterFieldTypeEnum,
} from '../../components/list'

type PropsType = {}

type Config = Omit<BasicListPropsType, 'params'> & {
  params?: {
    keyword?: string
    test?: number
    birthdayTimeRange?: [Moment, Moment]
  }
}
const testOptionList: LabeledValue[] = [
  { value: 0, label: '测试 0' },
  { value: 1, label: '测试 1' },
  { value: 2, label: '测试 2' },
]

//列表配置项
const config: Config = {
  header: {
    placeholder: '用户名/住址',
    list: ['用户', '列表'],
  },
  filters: [
    {
      type: FilterFieldTypeEnum.Select,
      label: '测试字段',
      field: 'test',
      options: testOptionList,
    },
    {
      type: FilterFieldTypeEnum.TimeRange,
      label: '生日',
      field: 'birthdayTimeRange',
    },
  ],
  columns: [
    { field: 'id', title: 'ID' },
    { field: 'name', title: '姓名' },
    { field: 'birthday', title: '生日' },
    {
      field: 'operate',
      title: '操作',
      slot: (param) => <Link to={`/system/user/${param.record.id}`}>详情</Link>,
    },
  ],
  api: userApi,
}

/**
 * 一个基本的列表页面
 * @constructor
 */
const BasicListExample: React.FC<PropsType> = () => {
  return <BasicList {...config} />
}

export default BasicListExample

使用自定义过滤器组件

GitHub 代码示例open in new window

事实上,总有各种奇怪的过滤器无法满足,这时候就需要添加一个自定义的过滤器了。 例如下面这个过滤器,包含了年龄的值和单位,是不是感觉很奇怪

关键代码配置如下

const config: Config = {
  filters: [
    {
      type: FilterFieldTypeEnum.Slot,
      label: '年龄',
      field: 'age',
      children: (
        <Input.Group compact>
          <Form.Item name={['age', 'value']} noStyle>
            <InputNumber style={{ width: 'calc(100% - 64px)' }} />
          </Form.Item>
          <Form.Item name={['age', 'unit']} noStyle>
            <Select style={{ width: 64 }} options={ageUnitOptionList} />
          </Form.Item>
        </Input.Group>
      ),
    },
  ],
  // 此处是为了添加过滤器的默认值
  params: {
    age: {
      unit: 0,
    },
  },
}

添加表格的额外操作

GitHub 代码示例open in new window

有时候,我们需要添加一个额外的表格操作,例如导出/导入数据/删除选中数据。

关键代码如下

async function handleBatchDelete({
  selectedRowKeys,
  setSelectedRowKeys,
  searchPage,
}: ListTableOperateParam) {
  if (selectedRowKeys.length === 0) {
    return
  }
  await userApi.batchDelete(selectedRowKeys)
  setSelectedRowKeys([])
  await searchPage()
}

const config = useMemo<Config>(
  () => ({
    tableOperate: (params) => (
      <Button onClick={() => handleBatchDelete(params)}>删除选中</Button>
    ),
  }),
  [],
)

过滤器的下拉框数据来源是异步的

GitHub 代码示例open in new window

很多时候,我们的数据来源并不是由前端写死,而是从后台获取的,这就要求我们传入的值是 react 的一个 State 而非一个固定值。

关键代码如下

const testOptionList = useAsyncMemo([], dictApi.list)
const config = useMemo<Config>(
  () => ({
    filters: [
      {
        type: FilterFieldTypeEnum.Select,
        label: '测试字段',
        field: 'test',
        options: testOptionList,
      },
    ],
  }),
  [testOptionList],
)

组件 API

BasicList

参考 src/components/common/table/js/BasicListOptions.d.ts

prop类型说明
headerBasicList.Header标题栏相关配置
[filters]BaseFilterField[]过滤器列表
((params: any, onChange: (params: any) => void) => ReactElement)
[params]Params查询参数
columnsTableColumn[]列字段列表
apiBaseListApiapi 对象
[tableOptions]TableOptions一些其他选项
[tableOperate]ListTableOperate一些其他操作

ListFilter

| prop | 类型 | 说明 | | ---------------- | -------------------- | ------------------------ | ------------------ | ---------- | | [initialValue] | any | 查询参数 | | filters | (FilterSelectType | FilterTimeRangeType | FilterSlotType)[] | 过滤器列表 | | onChange | (value: T) => void | 当过滤器的参数发生改变时 |

ListTable

prop类型说明
columnsTableColumn[]列字段列表
apiBaseListApiapi 对象
paramsParams查询参数
[tableOptions]TableOptions一些其他选项
[tableOperate]ListTableOperate一些其他选项

其他类型定义

下面是类型定义,所有的类型定义都有对应的 .d.ts 文件,请使用 C-N 搜索 class

HeaderNavItem类型说明
string导航的名字
namestring导航的名字
[link]string如果是 route 的话必须有值
Header类型说明
listHeaderNavItem[]导航元素列表
placeholderstring搜索框的提示文本

过滤器相关

FilterFieldTypeEnum类型说明
Slot1自定义 slot
Select2普通选择框
TimeRange3日期区间选择器
FilterFieldBase类型说明
typeFilterFieldTypeEnum过滤器元素类型
labelstring显示的标题
FilterSelectType类型说明
extendsFilterFieldBase继承基本过滤器配置
fieldstring字段名
optionsLabeledValue[]值列表
FilterTimeRangeType类型说明
extendsFilterFieldBase继承基本过滤器配置
fieldstring字段名
FilterSlotType类型说明
extendsFilterFieldBase继承基本过滤器配置
fieldstring字段名
childrenReactElementForm.Item 的子元素
[computed](res: Record<string, any>, value: any) => Record<string, any>自定义计算方法
Params类型说明
keywordstring查询关键字
...argsany[]其他查询参数

表格相关

TableColumn类型说明
fieldstring在数据项中对应的字段名
titlestring列标题
[formatter](v: any, record: any) => any自定义字段格式化函数
[slot](param: { text: string; record: any; i: number }) => ReactNode自定义 slot
BaseListApi类型说明
pageList(params: any) => Promise<PageRes<any>>所有 ListTable 中的 api 对象必须实现该类型
ListTableOperateParam类型说明
searchPage(page?: PageParam) => Promise<void>导航元素列表
selectedRowKeysstring[]当前选中行的主键
setSelectedRowKeys(selectedRowKeys: string[]) => void设置当前选中行的主键
pagePageData<any>分页数据信息
paramsParams过滤器及搜索参数
TableOptions类型说明
[isSelect]boolean是否可选,默认为 false
[rowKey]string行的唯一键,默认为 id

总结

相比于之前吾辈 Vue 表格封装 BasicTableVue,嗯,差距非常明显!