不多说废话直接上代码
父组件
// 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,} = propsconst [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 = falseconst list = cloneDeep(columns)?.filter(item => !item?.hideInTable)?.map(item => {return { ...item, width: item?.width ?? getTextWidth(item?.title) + 20 }})setTableColumns(() => [...list])// 结构状态参数,并判断是否需要保存数据let { key, values } = proTableif (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 },} = propsif ((current !== values.current && values.current > 0) ||(pageSize !== values.pageSize && values.pageSize > 0)) {isFirstRequest = truehandleSearch({ ...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 = fontlet 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-DDlet 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.isSearchsetTableLoading(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 = [] } = ressetTotal(resTotal)setDataSource(() => [...resDataSource])} catch (e) {console.error('获取列表错误:', e)} finally {setTableLoading(false)}},[current, pageSize, searchValues])// 暴漏子组件参数useImperativeHandle(ref, () => ({handleSearch,}))// DOMreturn (<div className='pro-table'>{search && (<Card><SearchForm handleSearch={handleSearch} {...props} /></Card>)}<div>{middleDOM}</div><CardclassName='table-card'extra={optionsButton?.length > 0 ? optionsButton?.map(item => item) : ''}><Tableloading={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 } = propsreturn <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,} = propsconst 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 } = proTableif (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 屏幕 ≥ 1200pxlet cols = 1if (window.innerWidth >= 1200) {cols = 3} else if (window.innerWidth >= 992) {cols = 3} else if (window.innerWidth >= 768) {cols = 2}// 处理columnslet columnsFilter = cloneDeep(columns).filter(({ hideInSearch = false }) => !hideInSearch)// 处理每行列数const groupedColumns = new Array(cols).fill(null).map(() => [])// 处理每行columnsFilter?.forEach((item, index) => {const columnIndex = index % colsgroupedColumns[columnIndex].push(item)})// 处理每列const maxColumnWidths = groupedColumns.map(column => {return column.reduce((maxWidth, item) => {const label = item?.searchTitle ?? item?.titlereturn label ? Math.max(maxWidth, getTextWidth(label)) : maxWidth}, 0)})// 处理每列标签宽度const resColumns = columnsFilter?.map((item, index) => {const columnIndex = index % colsitem.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 = fontlet 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,} = paramsswitch (searchType) {case 'Select':return (<Select{...selectOptions}mode={selectOptions?.mode ?? ''}allowClearkey='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 (<InputallowClearplaceholder={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] &¤t?.diff(form.getFieldValue(searchIndex)[0], 'days') >= timeRangeDaylet tooEarly =form.getFieldValue(searchIndex)[1] &&form.getFieldValue(searchIndex)[1].diff(current, 'days') >= timeRangeDayreturn !!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 } = itemif (renderFormItem) {return (<Col {...rowCols} key={searchIndex ?? dataIndex} style={{ marginTop: 10 }}><Form.Itemlabel={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.Itemlabel={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,})}// DOMreturn (<Formlayout='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'><ButtononClick={handleFormReset}disabled={buttonProps.resetDisabled}style={{ display: handleIsShow() ? 'flex' : 'none' }}>{buttonProps.searchResetText}</Button><Button type='primary' htmlType='submit' disabled={buttonProps.searchDisabled}>{buttonProps.searchButtonText}</Button><aonClick={() => {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><aonClick={() => {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;}
}
代码就这么多,基本上都写了,备注也有,大家自己看吧
不嫌弃可以进啦看看点击