管理后台实现各种业务离不开列表展示,列表也都会具有基本功能诸如分页、搜索、选择、操作等,而各业务重复着这些逻辑就会造成代码臃肿,难以阅读和维护,React高阶组件恰好可以运用于此,解决这些问题。
Ant-Design-Pro框架提供了高级表格Protable功能,并提供了配套的实践方案,个人觉得功能较为臃肿,使用起来配置麻烦,故而借鉴其高级表格实现了自定义版本的ProTable。
一. 需要解决的问题
项目所用的UI框架是React后台管理项目中比较通用的Ant-Design,其本身提供的Table组件已经支持了对列表数据的排序、搜索、分页、批量、自定义操作等功能,这里需要做的就是再其基础上进行一层封装,做一些预设并开放自定义的行为,总结如下:
- 可配置的请求方法。
- 表单搜索数据,可以根据列表项字段属性配置类型,生成搜索表单。
- 继承Ant-Design的Table组件Api和方法。
- 暴露一些组件内部方法如选择、列表数据重载、字段导出等。
最终实现,在页面中引入该高阶组件,简单配置接口、列表字段属性便可以一键生成增删改查的页面模板。
二. 功能实现
1. 界面样式
这里的界面仿照了Ant-Design-Pro后台管理框架的表格设计方式,上方为列表字段搜索区域,当搜索项较多可以展开和收起。下方为分页列表内容展示区域,可以进行分页、排序、刷新、列表项选择、单项以及批量操作等。
2. 参数接口设计
Protable组件参数,ProTable是在antd的Table上进行的一层封装,支持了一些预设,这里只列出和Antd Table不同的Api:
- title: String,非必须,表格标题或名称。
- description: String,非必须,表格描述。
- toolBarRender: Function (ref, rows: {selectedRowKeys?:(string | number)[], selectedRows?:T[]} => React.ReactNodes[]),非必须,渲染工具栏。
- beforeSearchSubmit: Function (params: T) => T,非必须,提交表单前的数据处理,当列表字段值和后端定义的搜索字段不一致可在此转换处理。
- request: Function (params: searchParams)=> Promise,必须,请求接口获取数据。
- columns: Object ProTableColumnProps[],必须,字段设置。
- rowSelection: Object ProTableRowSelection,非必须,内部自动控制,行选择设置。
- ref: (ref: Actions) => void,非必须,获取刷新、重置列表、获取搜索参数的对象。
Columns配置参数,也是原columns属性基础上拓展了列表字段搜索、列表样式等配置,这里同样列出和Antd Table Cloumns不同的Api:
- type: String(可填入类型控件为input | select | multiSelect | datePicker | …),非必须,可拓展,若配置类型值后在列表上方自动生成该字段匹配类型的查询表单控件。
- formItemProps: Object,非必须,确定type类型控件后,配置查询表单控件的一些属性。
- ellipsis: Boolean | (values: searchFormValue) => boolean,非必须,是否单元格超长字符省略。
- isCopy: Boolean | (values: searchFormValue) => boolean,非必须,是否单击复制单元格内容。
- hideInTable: boolean | (values: searchFormValue) => boolean,非必须,控制字段是否在列表中显示。
- hideInSearch:boolean | (values: searchFormValue) => boolean,非必须,控制字段是否在搜索表单中显示
3. ProTable组件编写
import React from "react";
import { Table, Card, Icon, Tooltip, Button, message } from "antd";
import TableForm from "./TableForm"; //列表搜索表单
import { isEqual, debounce } from "lodash";
import ColumnsSetting from "./columnsSetting";
import styled from "styled-components";
const Layout = styled.div`
.table-title {
color: #555555;
font-weight: 500;
font-size: 15px;
.table-title-description {
display: inline-block;
margin-left: 15px;
cursor: pointer;
}
.left-selectionNum{
display: inline-block;
margin-left: 15px;
font-size: 14px;
color: #555555;
transition: all 0.3s;
.selection-num{
display: inline-block;
color: #ff4d4f;
margin: 0 2px;
}
}
}
.copyStyle {
display: inline-block;
cursor: pointer;
&:hover {
.ant-typography-copy {
visibility: visible !important;
}
}
.ant-typography-copy {
visibility: hidden !important;
}
}
.table-operation {
cursor: pointer;
margin-left: 20px;
display: inline-block;
}
/* 多行文本省略 */
.tabel-more-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
word-break: break-word;
white-space: normal;
}
`;
/**
* @name 标准列表页组件
* @description 包含 搜素表单 列表分页 工具条 这三部分生成
* ProTable 封装了分页和搜索表单的 table组件
*/
class ProTable extends React.Component {
constructor(props) {
super(props);
this.fetchData = debounce(this.fetchData, 30, { leading: true, trailing: false, maxWait: 300 });
}
state = {
// table密度
density: "default", // default | middle | small
tableHeight: window.innerHeight,
// data
dataSource: [],
total: 0,
loading: false,
// pagination
pagination: {
pageNo: 1, //搜索起始页
pageSize: 10, //每页条数
},
// rowSelection
selectedRowKeys: [],
selectedRows: [],
// form
searchFormValue: { FIRST_TIME_LOADING_TAG: true },
forceUpdate: false,
isShowSearch: false,
};
componentDidMount() {
this.props.onRef && this.props.onRef(this);
this.setState({ tableColumns: this.props.columns });
}
componentDidUpdate(prevProps, prevState) {
if (
!isEqual(prevState.pagination, this.state.pagination) ||
!isEqual(prevState.searchFormValue, this.state.searchFormValue) ||
(!isEqual(prevState.forceUpdate, this.state.forceUpdate) && this.state.forceUpdate === true)
) {
this.fetchData();
}
}
//设置初始化选中值
setInitialSelectionRowKeys = (selectedRowKeys) => {
this.setState({ selectedRowKeys });
};
//设置分页
setPagination = (pageNo, pageSize) => {
this.setState({ pagination: { pageNo, pageSize } });
};
//请求列表数据
fetchData = async (params = {}) => {
const { request } = this.props;
const { searchFormValue, pagination } = this.state;
if (!request) return;
this.setState({ loading: true });
try {
const data = await request({ ...searchFormValue, ...pagination, ...params });
// 如果查总条数 小于 pageNo*pageSize
if (data.total < (pagination.pageNo - 1) * pagination.pageSize + 1 && data.total !== 0) {
this.setState(
(prevState) => {
return {
pagination: {
pageNo: prevState.pagination.pageNo - 1,
pageSize: prevState.pagination.pageSize,
},
};
},
() => {
this.fetchData();
}
);
}
this.setState({ dataSource: data.data, total: data.total, loading: false, forceUpdate: false });
} catch (error) {
console.warn(error);
message.warn(error.msg);
this.setState({ dataSource: [], loading: false, forceUpdate: false });
}
};
//设置搜索表单数据
setSearchFormValues = (values) => {
this.setState({ searchFormValue: values });
};
//重置表格选择
resetRowSelection = () => {
this.setState({ selectedRowKeys: [], selectedRows: [] });
};
getSearchFormValue = () => {
const { searchFormValue, pagination } = this.state;
return { params: { ...searchFormValue, ...pagination }, searchFormValue, pagination };
};
//列表刷新
reload = () => {
this.fetchData();
this.resetRowSelection();
};
//搜索重置
reset = () => {
if (this.formRef && this.formRef.reset) {
this.setState({ forceUpdate: true });
this.formRef.reset();
this.resetRowSelection();
}
};
//列排序以及界面增删显示
handleColumnsChange = (val) => {
const { columns } = this.props;
const tableColumns = [];
for (let i of columns) {
for (let j of val) {
if (i.dataIndex == j) {
tableColumns.push(i);
}
}
}
this.setState({ tableColumns: [...tableColumns, columns[columns.length - 1]] });
};
render() {
const { pagination, total, dataSource, selectedRowKeys, selectedRows, loading, tableColumns = [], density } = this.state;
const {
title = "高级表格",
description = "",
request,
toolBarRender,
beforeSearchSubmit = (v) => v,
rowSelection,
pagination: tablePaginationConfig,
rowKey,
columns,
columnsSettingDisabled = false,
density: densityDisabled,
...other
} = this.props;
const filterColumns = columns.filter((o) => o.type && !o.hideInSearch);
return (
<Layout>
<div className="filter-wrap" hidden={filterColumns.length == 0}>
<Card bordered={false}>
<TableForm
wrappedComponentRef={(ref) => (this.formRef = ref)}
onSubmit={(values) => {
this.setPagination(1, pagination.pageSize);
this.resetRowSelection();
this.setSearchFormValues(beforeSearchSubmit(values));
}}
filterForm={filterColumns}
isShowSearch={this.state.isShowSearch}
/>
</Card>
</div>
<Card bordered={false}>
{toolBarRender && (
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div className="table-title">
<span style={{ fontWeight: "bold" }}>{title}</span>
{description && (
<span className="table-title-description">
<Tooltip placement="right" title={description}>
<Icon style={{ color: "#666666" }} type="question-circle" theme="filled" />
</Tooltip>
</span>
)}
{selectedRowKeys.length > 0 && (
<span className="left-selectionNum">
已选中<span className="selection-num">{selectedRowKeys.length}</span>项
</span>
)}
</div>
{
<div style={{ textAlign: "right" }}>
{toolBarRender(this, { selectedRowKeys, selectedRows, dataSource })}
{filterColumns.length > 0 ? (
<div className="table-operation" onClick={() => this.setState({ isShowSearch: true })}>
<Button>
查询
</Button>
</div>
) : null}
<div className="table-operation" onClick={() => this.reset()}>
<Button
style={{
border: "1px solid #3571FF",
background: "rgba(53, 113, 255, 0.07)",
color: "#3571FF",
}}
type="primary"
icon="redo"
>
刷新
</Button>
</div>
{!columnsSettingDisabled && (
<Tooltip title="列设置">
<div className="table-operation">
<ColumnsSetting columns={columns} columnsChange={this.handleColumnsChange} />
</div>
</Tooltip>
)}
</div>
}
</div>
)}
{/* {selectedRowKeys.length > 0 && `已选中${selectedRowKeys.length}项`} */}
<Table
{...other}
size={densityDisabled || density}
loading={loading}
rowKey={rowKey}
dataSource={dataSource}
columns={tableColumns
.filter((o) => !o.hideInTable)
.map((o) => {
if (!o.render) {
if (o.type == "select" && !o.render && o.formItemProps && Array.isArray(o.formItemProps.options)) {
o.render = (text) => {
try {
const target = o.formItemProps.options.find((item) => item.value == text);
return (target ? target.label : text) || "-";
} catch (e) {
return typeof text == "undefined" || text == null ? "-" : text;
}
};
} else {
o.render = (text) => (
<div
onClick={() => {
if (!o.isCopy) return;
let inputDom = document.createElement("input");
document.body.appendChild(inputDom);
inputDom.value = text;
inputDom.select(); // 选中
document.execCommand("copy", false);
inputDom.remove(); //移除
message.success("复制成功");
}}
>
{typeof text === "undefined" || text == null || text === "" ? "-" : text}
</div>
);
}
}
if (o.ellipsis) {
o.ellipsis = false;
let render = o.render;
o.render = (text, record, index) => (
<div className="tabel-more-ellipsis">
<Tooltip placement="topLeft" title={render(text, record, index)}>
{render(text, record, index)}
</Tooltip>
</div>
);
}
return o;
})}
pagination={{
...tablePaginationConfig,
total,
showSizeChanger: true,
showQuickJumper: true,
current: pagination.pageNo,
pageSize: pagination.pageSize,
showTotal: (total, range) => `共${total}条`,
onChange: (pageNo, pageSize) => {
this.setPagination(pageNo, pageSize);
},
onShowSizeChange: (current, pageSize) => {
this.setPagination(current, pageSize);
},
}}
rowSelection={
rowSelection
? {
selectedRowKeys,
...rowSelection,
onChange: (selectedRowKeys, selectedRows) => {
if (rowSelection && rowSelection.onChange) {
rowSelection.onChange(selectedRowKeys, selectedRows);
}
this.setState({ selectedRowKeys, selectedRows });
},
getCheckboxProps: (record) => {
if (rowSelection && rowSelection.getCheckboxProps) {
return rowSelection.getCheckboxProps(record, {
selectedRowKeys,
selectedRows,
dataSource,
});
}
return undefined;
},
}
: undefined
}
/>
</Card>
</Layout>
);
}
}
export default ProTable;
3. TableForm组件编写
TableForm组件为列表的搜索表单,包括列表搜索重置功能,可以根据Cloumns配置项的type类型加载不同类型搜索控件,目前支持的搜索控件有单行文本/数字输入、单项/多项选择器、时间/日期选择器。采用的简单工厂模式设计也方便后续拓展其他类型控件,以下是TableForm组件代码:
import React from 'react'
import { Form, Button, Row, Col } from 'antd'
import styled from 'styled-components';
import FilterControl from './filterControl'
const Layout = styled.div`
height: 100%;
background: #ffffff;
.collapse {
color: #4569d4;
text-decoration: none;
transition: opacity .2s;
outline: none;
cursor: pointer;
display: inline-block;
margin-left: 16px;
}
.table-form-anticon{
margin-left: 0.5em;
transition: all 0.3s ease 0s;
transform: rotate(0);
display: inline-block;
color: inherit;
font-style: normal;
line-height: 0;
text-align: center;
text-transform: none;
vertical-align: -.125em;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
}
`;
class FilterForm extends React.Component {
constructor(props) {
super(props)
this.state = {
isCollapse: true, // 默认 收起
collapseNum: 3 // 展开收起的数量
}
}
componentDidMount() {
this.submit()
}
submit = () => {
const { getFieldsValue } = this.props.form
const values = getFieldsValue()
if (this.props.onSubmit) {
this.props.onSubmit(values)
}
}
reset = () => {
const { resetFields } = this.props.form
resetFields()
this.submit()
}
render() {
const { getFieldDecorator } = this.props.form
const { filterForm = [] } = this.props
const { isCollapse, collapseNum } = this.state
const formItemLayout = {
labelCol: {
xs: { span: 8 },
sm: { span: 6 },
},
wrapperCol: {
xs: { span: 16 },
sm: { span: 18 },
},
}
return (
<Layout>
<Form
layout="inline"
onSubmit={e => {
e.preventDefault()
this.submit()
}}
onReset={e => {
e.preventDefault()
this.reset()
}}
{...formItemLayout}
>
<Row gutter={[24, 24]} type="flex" align="middle">
{filterForm.map((item, index) => (
(((index + 1) > collapseNum && !isCollapse) || (index + 1) <= collapseNum) && (
<Col xxl={6} xl={8} md={12} xs={24} key={index}>
<Form.Item style={{ width: '100%' }} label={item.formItemProps?.labelTitle || item.title}>
{getFieldDecorator(
item.key || item.dataIndex,
{ initialValue: item.initialValue }
)(FilterControl.getControlComponent(item.type, { ...item }))}
</Form.Item>
</Col>
)
))}
{filterForm.length > 0 && (
<Col xxl={6} xl={8} md={12} xs={24}>
<div style={{ marginLeft: '27px', padding: "0 20px 0 0" }}>
<Button htmlType="submit" type="primary" style={{ marginLeft: 8 }}>查询</Button>
<Button htmlType="reset" type="reset" style={{ marginLeft: 8 }}>重置</Button>
{filterForm.length > collapseNum && <div className="collapse" onClick={() => {
this.setState((state) => { return { isCollapse: !state.isCollapse } })
}}>
{isCollapse ? '展开' : '收起'}
</div>}
</div>
</Col>
)}
</Row>
</Form>
</Layout>
)
}
}
export default Form.create()(FilterForm)
4. FilterControl控件编写
FilterControl集合了基本的搜索控件,并在index中导出: index.js:
import React from "react";
import SingleInput from "./singleInput"; //单行输入
import SingleSelect from "./singleSelect"; //单项选择
import RangePickerSelect from './rangePickerSelect'; //时间范围选择
import MultiSelect from './multiSelect'; //多项选择
import SearchSelect from './searchSelect' //远程搜索选择const ControlUI = new Map();
ControlUI.set("input", SingleInput)
ControlUI.set("select", SingleSelect)
ControlUI.set("rangePickerSelect", RangePickerSelect)
ControlUI.set("multiSelect", MultiSelect)
ControlUI.set("searchSelect", SearchSelect)
export default class ControlFactory {
static getControlComponent(uiCode, args) {
if (ControlUI.has(uiCode)) {
return React.createElement(ControlUI.get(uiCode), { ...args });
} else {
return (<>开发中</>)
}
}
}
这里的基础控件,仅展示单行输入写法,接受value属性值以及传递onChange方法,表单值即可被Form收集。 SingleInput.js:
import React from 'react'
import { Input } from 'antd';
const SingleInput = (props, ref) => {
const {
value,
onChange,
formItemProps: {
placeholder = '请输入'
} = {}
} = props; return (
<Input
style={{ width: "100%" }}
placeholder={placeholder}
value={value}
onChange={(e) => {
onChange(e.target.value);
}} />
)}
export default React.forwardRef(SingleInput);
三. 如何使用
组件的代码已经编写完成,使用起来也是比较简单,调用组件request方法请求异步数据,分页搜索方法已经在组件内部实现,配置columns字段类型可以实现当前字段控件搜索,行内的操作也可以从action中获取组件内部的状态值和方法:
<ProTable
title={"示例表格"}
description={"这是一个示例的带查询分页表格,无需编写逻辑代码,只需简单配置就可以生成增删改查的页面模板"}
rowKey="id"
ref={(ref) => (this.actions = ref)}
request={(params) => {
return new Promise((resolve, reject) => {
getAction("/api/demoList", params).then((res) => {
resolve({
data: res.data,
total: res.total,
});
});
});
}} columns={[
{
dataIndex: "name",
title: "姓名",
type: "input",
},
{
dataIndex: "sex",
title: "性别",
type: "select",
formItemProps: {
options: [
{
value: false,
label: "女",
},
{
value: true,
label: "男",
},
],
},
},
{
dataIndex: "age",
title: "年龄",
sorter: true
},
{
dataIndex: "birthday",
title: "生日",
type: 'datePicker'
},
{
title: "操作",
width: 100,
render: (text, record) => (
<div> <a onClick={() => {}>编辑</a> <Divider type="vertical" /> <Popconfirm title={"确认删除该项吗?"} onConfirm={() => { console.log("删除的操作项!"); }} okText="确定" cancelText="取消" > <a style={{ color: "red" }}>删除</a> </Popconfirm> <Divider type="vertical" /> <Dropdown overlay={ <Menu> <Menu.Item key="1"> <a onClick={() => { }}>查看</a> </Menu.Item> <Menu.Item key="2"> <a onClick={() => { }}>指派</a> </Menu.Item> </Menu> } > <a onClick={(e) => e.preventDefault()}>更多</a> </Dropdown> </div>
),
},
] }
toolBarRender={(actions, { selectedRowKeys, selectedRows }) => (
<> <Button type="primary" onClick={() => {}} >新增</Button> </>
)}
rowSelection={() => {}}
/>
四. 总结
大概就这些,感觉自己的表述能力有所不足,这里提供一个hooks版本的组件使用范例,范例中有较为完整的使用代码。项目使用了dva和mock环境,模拟请求数据,希望可以提供一些帮助。
水平有限,如果一些错误的地方不自知,还请希望大家指正。
今天的文章React分页搜索列表高阶组件分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/23505.html