不多说废话直接上代码
父组件
// index.jsx
/**
* @description 此ProTable是根据ProComponents里的ProTable模仿封装的简易版本
* */
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react'
import { Card, Table } from 'antd'
import dayjs from 'dayjs'
import { connect } from 'dva'
import { cloneDeep } from 'lodash-es'
import './index.less'
import SearchForm from './components/SearchForm'
/**
* 默认分页选择
* */
const defaultTableProps = {
pageSizeOptions: ['10', '20', '50', '100'], // 指定每页展示多少条
bordered: true,
}
let isFirstRequest = true
/**
* @description Table结合搜索封装组件
*
* @function SearchForm
* @param {object} props 父组件传参
* @param {array} props.columns 列表&搜索类型参数集合
* @param {string | null} props.size 表格大小
* @default small
* @param {array | null} props.pageSizeOptions 分页每页选择条数
* @param {boolean} props.search 是否展示搜索
* @param {array} props.optionsButton 操作按钮
* @param {function} props.request 列表请求
* @param {boolean} props.bordered 列表是否有边框
* @param {ReactNode} props.middleDOM
* @param {object} props.initialValues Form默认参数
* @param {function | null} props.handlePrecedenceFatherLaterChildren 用于父组件执行结束之后再执行当前组件函数
* @param {object | null} props.proTable 状态存储搜索参数
* */
const ProTable = forwardRef((props, ref) => {
const {
columns = [],
tableKey = 'code',
size = 'small',
pageSizeOptions = defaultTableProps.pageSizeOptions,
bordered = defaultTableProps.bordered,
search = true,
optionsButton = [],
middleDOM,
initialValues,
request,
handlePrecedenceFatherLaterChildren,
proTable,
dispatch,
rowSelection,
loading,
scroll,
} = props
const [current, setCurrent] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [total, setTotal] = useState(0)
const [dataSource, setDataSource] = useState([])
const [searchValues, setSearchValues] = useState({})
const [tableColumns, setTableColumns] = useState([])
const [tableLoading, setTableLoading] = useState(false)
const tableProps = {
showQuickJumper: true,
showSizeChanger: true,
total,
current,
pageSize,
onShowSizeChange: (page, size) => {
setCurrent(page)
setPageSize(size)
},
showTotal: totals => `共${totals}条记录`,
onChange: (page, size) => {
setCurrent(page)
setPageSize(size)
},
}
useEffect(() => {
isFirstRequest = false
const list = cloneDeep(columns)
?.filter(item => !item?.hideInTable)
?.map(item => {
return { ...item, width: item?.width ?? getTextWidth(item?.title) + 20 }
})
setTableColumns(() => [...list])
// 结构状态参数,并判断是否需要保存数据
let { key, values } = proTable
if (key !== location.pathname) {
values = {}
setSearchValues({})
} else {
setSearchValues(val => {
return { ...val, ...values }
})
}
// 放入异步队列,让执行顺序小于父组件
if (handlePrecedenceFatherLaterChildren) {
handlePrecedenceFatherLaterChildren().then(async () => {
await handleSearch({ ...initialValues, ...values })
})
} else {
handleSearch({ ...initialValues, ...values }).then()
}
}, [])
useEffect(() => {
const {
proTable: { values },
} = props
if (
(current !== values.current && values.current > 0) ||
(pageSize !== values.pageSize && values.pageSize > 0)
) {
isFirstRequest = true
handleSearch({ ...initialValues, ...searchValues }).then(res => res)
}
}, [current, pageSize])
// 此方法 实际计算出来结果 会比手动计算大8px左右
const getTextWidth = (text, font = '14px Microsoft YaHei') => {
const canvas = document.createElement('canvas')
let context = canvas.getContext('2d')
context.font = font
let textmetrics = context.measureText(`${text}:`)
return textmetrics.width
}
// 遍历查询转换时间格式化
const handleMapSearchTime = values => {
for (let key in values) {
if (key?.indexOf(',') > 0 && key?.indexOf('.') < 0) {
// 判断某个参数是否传入默认format,如果没传默认为 YYYY-MM-DD
let format =
initialValues && initialValues[`${key}.format`]
? initialValues[`${key}.format`]
: 'YYYY-MM-DD'
// 格式化搜索参数
let paramsList = key.split(',')
if (paramsList?.length > 0 && values[key]?.length > 0) {
values = {
...values,
[paramsList[0]]: dayjs(values[key][0]).format(format),
[paramsList[1]]: dayjs(values[key][1]).format(format),
}
delete values[key]
}
}
}
return values
}
// 列表搜索
const handleSearch = useCallback(
async ({ current: searchCurrent, pageSize: searchPageSize, ...values }) => {
if (loading.global || values?.isSearch || isFirstRequest) {
delete values.isSearch
setTableLoading(true)
}
try {
setSearchValues(val => ({ ...values }))
if (searchCurrent || searchPageSize) {
setCurrent(searchCurrent)
setPageSize(searchPageSize)
}
dispatch({
type: 'proTable/setSearchFormValues',
payload: {
key: location.pathname,
values: {
current: searchCurrent ?? current,
pageSize: searchPageSize ?? pageSize,
...values,
},
},
})
const resValues = handleMapSearchTime(values)
const res = await request({
current: searchCurrent ?? current,
pageSize: searchPageSize ?? pageSize,
values: resValues,
})
const { total: resTotal = 0, dataSource: resDataSource = [] } = res
setTotal(resTotal)
setDataSource(() => [...resDataSource])
} catch (e) {
console.error('获取列表错误:', e)
} finally {
setTableLoading(false)
}
},
[current, pageSize, searchValues]
)
// 暴漏子组件参数
useImperativeHandle(ref, () => ({
handleSearch,
}))
// DOM
return (
<div className='pro-table'>
{search && (
<Card>
<SearchForm handleSearch={handleSearch} {...props} />
</Card>
)}
<div>{middleDOM}</div>
<Card
className='table-card'
extra={optionsButton?.length > 0 ? optionsButton?.map(item => item) : ''}
>
<Table
loading={tableLoading}
columns={tableColumns}
rowKey={tableKey}
dataSource={dataSource}
pagination={{ ...tableProps, pageSizeOptions }}
size={size}
bordered={bordered}
sticky={true}
rowSelection={rowSelection}
scroll={scroll}
className='table'
/>
</Card>
</div>
)
})
// 由于ref被Hoc高阶组件{connect}隔离了
// 所以我们需要使用函数进行包裹
function wrap(Component) {
const ForwardRefComp = props => {
const { forwardedRef, ...rest } = props
return <Component ref={forwardedRef} {...rest} />
}
const StateComp = connect(state => state)(ForwardRefComp)
return forwardRef((props, ref) => <StateComp {...props} forwardedRef={ref} />)
}
export default wrap(ProTable)
子组件
/**
* @description 此ProTable是根据ProComponents里的ProTable模仿封装的简易版本,搜索组件
* */
import React, { useEffect, useState } from 'react'
import { Button, Col, DatePicker, Form, Input, Row, Select, Space } from 'antd'
import { DownOutlined } from '@ant-design/icons'
import dayjs from 'dayjs'
import { cloneDeep } from 'lodash-es'
import '../index.less'
/**
* form表单默认参数
* */
const defaultFormProps = {
labelAlign: 'left',
colon: true,
buttonProps: {
searchDisabled: false,
resetDisabled: false,
searchButtonText: '查询',
searchResetText: '重置',
},
}
// 日期解析
const { RangePicker } = DatePicker
// 默认布局
const rowCols = { xl: 8, lg: 8, md: 12, sm: 24 }
// 时间选择快捷键
const defaultPresets = [
{ label: '今天', value: [dayjs(), dayjs()] },
{ label: '昨天', value: [dayjs().subtract(1, 'day'), dayjs().subtract(1, 'day')] },
{ label: '本周', value: [dayjs().startOf('week'), dayjs()] },
{
label: '上周',
value: [
dayjs()
.subtract(1, 'week')
.startOf('week'),
dayjs()
.subtract(1, 'week')
.endOf('week'),
],
},
{ label: '本月', value: [dayjs().startOf('month'), dayjs()] },
{
label: '上月',
value: [
dayjs()
.subtract(1, 'month')
.startOf('month'),
dayjs()
.subtract(1, 'month')
.endOf('month'),
],
},
{ label: '今年', value: [dayjs().startOf('year'), dayjs()] },
{
label: '去年',
value: [
dayjs()
.subtract(1, 'year')
.startOf('year'),
dayjs()
.subtract(1, 'year')
.endOf('year'),
],
},
]
/**
* @description 搜索组件
*
* @function SearchForm
* @param {object} props 父组件传参
* @param {array} props.columns 列表&搜索类型参数集合
* @param {object | null} props.buttonAuth Form操作按钮规则按钮
* @param {function} props.handleSearch 搜索
* @param {function} props.dispatch dva 状态管理
* @param {object | null} props.initialValues Form初始化参数
* @param {string} props.labelAlign 搜索默认布局
* @param {object | null} props.proTable 状态存储搜索参数
* */
const SearchForm = props => {
const {
columns = [],
handleSearch,
labelAlign = defaultFormProps.labelAlign,
colon = defaultFormProps.colon,
initialValues,
pageSizeOptions,
buttonProps: fatherButtonProps = defaultFormProps.buttonProps,
proTable,
} = props
const buttonProps = { ...defaultFormProps.buttonProps, ...fatherButtonProps }
const [form] = Form.useForm() // 初始化搜索实例
const [expand, setExpand] = useState(false)
const [puckOrOpen, setPuckOrOpen] = useState('展开')
const [formList, setFormList] = useState([])
useEffect(() => {
const { key, values } = proTable
if (key !== location?.pathname) {
form.setFieldsValue({ ...initialValues })
} else {
form.setFieldsValue({ ...values })
}
}, [])
// 根据columns更新列表
useEffect(() => {
if (columns?.length > 0) {
getFormItemLabelWidth()
}
}, [columns])
// 计算字体长度,返回最大宽度
const getFormItemLabelWidth = () => {
// sm 屏幕 ≥ 576px
// md 屏幕 ≥ 768px
// lg 屏幕 ≥ 992px
// xl 屏幕 ≥ 1200px
let cols = 1
if (window.innerWidth >= 1200) {
cols = 3
} else if (window.innerWidth >= 992) {
cols = 3
} else if (window.innerWidth >= 768) {
cols = 2
}
// 处理columns
let columnsFilter = cloneDeep(columns).filter(({ hideInSearch = false }) => !hideInSearch)
// 处理每行列数
const groupedColumns = new Array(cols).fill(null).map(() => [])
// 处理每行
columnsFilter?.forEach((item, index) => {
const columnIndex = index % cols
groupedColumns[columnIndex].push(item)
})
// 处理每列
const maxColumnWidths = groupedColumns.map(column => {
return column.reduce((maxWidth, item) => {
const label = item?.searchTitle ?? item?.title
return label ? Math.max(maxWidth, getTextWidth(label)) : maxWidth
}, 0)
})
// 处理每列标签宽度
const resColumns = columnsFilter?.map((item, index) => {
const columnIndex = index % cols
item.width = `${maxColumnWidths[columnIndex]}px`
return { ...item, width: `${maxColumnWidths[columnIndex]}px` }
})
setFormList(() => [...resColumns])
}
// 此方法 实际计算出来结果 会比手动计算大8px左右
const getTextWidth = (text, font = '14px Microsoft YaHei') => {
const canvas = document.createElement('canvas')
let context = canvas.getContext('2d')
context.font = font
let textmetrics = context.measureText(`${text}:`)
return textmetrics.width
}
/**
* 组件类型
* @param {object} params 仅需要搜索组件参数
* @param {string} params.searchType 搜索组件类型 - Input|Select|
* @param {array} params.options 搜索参数数组展示
* @param {string} params.placeholder 占位符
* @param {string} params.title 名称
* @param {string} params.searchTitle 指定搜索名称
* @param {string} params.mode 搜索多选模式
* @param {string} params.picker 日期选择模式
* @param {object} params.DatePickerOptions 日期时间参数
* @param {array} params.RangePickerOptions 日期区间默认时间
* @param {object} params.presets 快捷键设置
* @param {string} params.searchIndex 搜索参数
* @param {string} params.dataIndex 搜索&table参数
* @param {object} params.selectOptions 选择框参数
* @param {string} params.relevanceIndex 日期关联参数,用于日期限制
* @param {string} params.relevanceTitle 日期关联名称,用于选择提示
* */
const componentsFormItem = params => {
const {
searchType,
placeholder,
searchTitle,
title,
selectOptions,
picker,
rangePickerOptions,
datePickerOptions,
presets = defaultPresets,
renderExtraFooterText,
searchIndex,
dataIndex,
timeRangeDay = 31,
} = params
switch (searchType) {
case 'Select':
return (
<Select
{...selectOptions}
mode={selectOptions?.mode ?? ''}
allowClear
key='value'
options={
selectOptions?.options
? selectOptions.options?.map(item => ({
value: item[selectOptions?.props?.value ?? 'value'],
label: item[selectOptions?.props?.label ?? 'desc'],
}))
: []
}
onChange={selectOptions?.onChange}
placeholder={placeholder ?? `请选择${searchTitle ?? title}`}
style={{ width: '100%' }}
/>
)
case 'DatePicker':
return (
<DatePicker
{...datePickerOptions}
picker={picker}
placeholder={placeholder ?? `请选择${searchTitle ?? title}`}
style={{ width: '100%' }}
/>
)
case 'RangePicker':
return (
<RangePicker
{...rangePickerOptions}
renderExtraFooter={() =>
renderExtraFooterText ?? (
<div style={{ color: 'red' }}>注:最长可选择时间范围 {timeRangeDay} 天</div>
)
}
presets={presets}
picker={picker}
disabledDate={
timeRangeDay
? current =>
handleDisabledDateRangePicker(current, searchIndex ?? dataIndex, timeRangeDay)
: null
}
onChange={val => handleRangePickerChange(val, searchIndex ?? dataIndex)}
onCalendarChange={val => handleOnCalendarChange(val, searchIndex ?? dataIndex)}
onOpenChange={open => handleOnOpenChange(open, searchIndex ?? dataIndex)}
placeholder={placeholder ?? ['开始时间', '结束时间']}
style={{ width: '100%' }}
/>
)
default:
return (
<Input
allowClear
placeholder={placeholder ?? `请输入${searchTitle ?? title}`}
style={{ width: '100%' }}
/>
)
}
}
/**
* 日期组件打开时操作
* @function handleOnOpenChange
* @param {boolean} open 是否打开了参数
* @param {string | T | any} searchIndex 搜索参数
* */
const handleOnOpenChange = (open, searchIndex) => {
if (open) {
setTimeout(() => {
form.setFieldsValue({ [`${searchIndex}`]: [null, null] })
})
} else {
const date = form.getFieldValue(searchIndex)
if ((!date || !date[0] || !date[1]) && initialValues && initialValues[`${searchIndex}`]) {
form.setFieldsValue({ [`${searchIndex}`]: [...initialValues[`${searchIndex}`]] })
}
}
}
/**
* 待选日期发生变化时回调
* @function handleOnCalendarChange
* @param {array[dayjs]} values 时间选择参数
* @param {string | T | any} searchIndex 搜索参数
* */
const handleOnCalendarChange = (values, searchIndex) => {
form.setFieldValue(searchIndex, values)
}
/**
* 时间区间选择设置关联参数,返回可选择范围
* @function handleDisabledDateRangePicker
* @param {dayjs | any} current 时间
* @param {string | T | any} searchIndex 搜索参数
* @param {number} timeRangeDay 禁用范围天数
* */
const handleDisabledDateRangePicker = (current, searchIndex, timeRangeDay) => {
if (!form.getFieldValue(searchIndex) || !form.getFieldValue(searchIndex)[0]) {
return current && current > dayjs().endOf('day')
}
let tooLate =
form.getFieldValue(searchIndex)[0] &&
current?.diff(form.getFieldValue(searchIndex)[0], 'days') >= timeRangeDay
let tooEarly =
form.getFieldValue(searchIndex)[1] &&
form.getFieldValue(searchIndex)[1].diff(current, 'days') >= timeRangeDay
return !!tooEarly || !!tooLate || (current && current > dayjs().endOf('day'))
}
/**
* 时间选择格式化输出
* @function handleRangePickerChange
* @param {array} values 时间框输出时间
* @param {string | T | any} searchIndex 输出搜索参数
// * @param {string} format 日期格式化 , format = 'YYYY-MM-DD'
* */
const handleRangePickerChange = (values, searchIndex) => {
form.setFieldValue(searchIndex, values)
}
// FormItem子组件内容展示
const renderFormItemChildren = () => {
return (
<>
{formList?.map(item => {
const { renderFormItem, title, searchTitle, dataIndex, searchIndex, width } = item
if (renderFormItem) {
return (
<Col {...rowCols} key={searchIndex ?? dataIndex} style={{ marginTop: 10 }}>
<Form.Item
label={searchTitle ?? title}
name={searchIndex ?? dataIndex}
labelCol={{ style: { minWidth: width } }}
key={searchIndex ?? dataIndex}
>
{renderFormItem()}
</Form.Item>
</Col>
)
}
return (
<Col {...rowCols} key={searchIndex ?? dataIndex} style={{ marginTop: 10 }}>
<Form.Item
label={searchTitle ?? title}
name={searchIndex ?? dataIndex}
labelCol={{ style: { minWidth: width } }}
key={searchIndex ?? dataIndex}
>
{componentsFormItem(item)}
</Form.Item>
</Col>
)
})}
</>
)
}
const handleIsShow = () => {
return renderFormItemChildren()?.props?.children?.length >= 3
}
// 搜索
const handleFormSearch = values => {
handleSearch({
...values,
pageSize: pageSizeOptions?.pageSize ?? 20,
current: pageSizeOptions?.current ?? 1,
isSearch: true,
})
}
// 重置
const handleFormReset = () => {
form.resetFields()
handleSearch({
...initialValues,
pageSize: pageSizeOptions?.pageSize ?? 20,
current: pageSizeOptions?.current ?? 1,
})
}
// DOM
return (
<Form
layout='inline'
form={form}
name='advanced_search'
onFinish={handleFormSearch}
labelAlign={labelAlign}
colon={colon}
initialValues={{ ...initialValues }}
className='search-form'
>
<Row gutter={[16, 16]} className='row-search'>
<Row style={{ width: '100%' }}>
<Row style={{ display: expand ? 'inline-flex' : 'none' }}>
{renderFormItemChildren()?.props?.children?.map(item => item)}
</Row>
<Row style={{ display: !expand ? 'inline-flex' : 'none' }}>
{renderFormItemChildren()
?.props?.children?.slice(0, 2)
?.map(item => item)}
</Row>
</Row>
{(!expand || renderFormItemChildren()?.props?.children?.length % 3 !== 0) && (
<Col {...rowCols} className='col-left__one' style={{ bottom: 0 }}>
<Space size='small'>
<Button
onClick={handleFormReset}
disabled={buttonProps.resetDisabled}
style={{ display: handleIsShow() ? 'flex' : 'none' }}
>
{buttonProps.searchResetText}
</Button>
<Button type='primary' htmlType='submit' disabled={buttonProps.searchDisabled}>
{buttonProps.searchButtonText}
</Button>
<a
onClick={() => {
setExpand(!expand)
setPuckOrOpen(expand ? '展开' : '收起')
}}
className='button-open'
style={{ display: handleIsShow() ? 'flex' : 'none' }}
>
<DownOutlined rotate={expand ? 180 : 0} /> {puckOrOpen}
</a>
</Space>
</Col>
)}
</Row>
{expand && renderFormItemChildren()?.props?.children?.length % 3 === 0 && (
<Row className='row-button' style={{ marginTop: expand ? 16 : -32 }}>
<Col span={24} className='col-left__two'>
<Space size='small'>
<Button onClick={handleFormReset} disabled={buttonProps.resetDisabled}>
{buttonProps.searchResetText}
</Button>
<Button type='primary' htmlType='submit' disabled={buttonProps.searchDisabled}>
{buttonProps.searchButtonText}
</Button>
<a
onClick={() => {
setExpand(!expand)
setPuckOrOpen(expand ? '展开' : '收起')
}}
className='button-open'
>
<DownOutlined rotate={expand ? 180 : 0} /> {puckOrOpen}
</a>
</Space>
</Col>
</Row>
)}
</Form>
)
}
export default SearchForm
这里改变了FormItem的label宽度,是计算的,这里是每行三列,切每列的字数最多的为这列的label的宽度,这里看大家需求去改变就可以了
less文件
// index.less
.pro-table {
.table-card {
margin-top: 10px;
.ant-card-body {
padding: 10px;
}
.ant-card-extra{
width: 100%;
}
}
.search-form {
position: relative;
.row-search {
width: 100%;
.ant-row{
width: 100%;
}
.col-left__one {
position: absolute;
right: 10px;
display: flex;
justify-content: flex-end;
}
}
.row-button {
width: 100%;
margin-top: 10px;
.col-left__two {
display: flex;
justify-content: flex-end;
}
}
}
.button-open {
font-size: 12px;
color: #2e85dd;
}
}
代码就这么多,基本上都写了,备注也有,大家自己看吧
不嫌弃可以进啦看看点击