先看最终代码:
import React, { useState, useMemo, useEffect } from 'react'
interface PageAbout {
current: number
pageSize: number
}
// 配合antd的table,生产table所需函数方法及state
export function useTableData<
TableParams extends SearchData & PageAbout = any,
SearchData extends object = any,
Datasource = any
>({
tableParamsInit = { pageSize: 10, current: 1 } as any,
searchDataInit = {},
pullData = () => {
// console.log('pullData')
},
getTotal = () => {
return 1
},
}: {
tableParamsInit?: TableParams
searchDataInit?: SearchData | {}
// 调用接口的方法,接受setDatasource作为参数, 在获取数据后,通过其将数据注回useTableData
pullData: (
params: TableParams,
setDatasource: React.Dispatch<React.SetStateAction<Datasource[] | []>>
) => void
// 从结果中获取数据总数
getTotal?: (res: any) => number
}) {
const [datasource, setDatasource] = useState<Datasource[] | []>([])
const [tableParams, setTableParams] = useState<TableParams>(tableParamsInit)
const [searchData, setSearchData] = useState<SearchData | {}>(searchDataInit)
const [total, setTotal] = useState(1)
//表格接口依赖变动,调用接口,拉取新的数据
useEffect(() => {
const res = pullData(tableParams, setDatasource)
setTotal(getTotal(res))
}, [tableParams])
// 搜索时,将搜索state set至表格接口依赖,表格接口依赖变化,自动触发接口调用
const handleSearch = () => {
setTableParams((s) => ({ ...s, current: 1, ...searchData }))
}
// 重置,将搜索依赖及表格接口依赖重置为init
const handleReset = () => {
setTableParams(tableParamsInit)
setSearchData(searchDataInit)
}
// 页码变更
const handlePageChange = (v: number) => {
setTableParams((s) => ({ ...s, current: v }))
}
// 执行删除操作,数据源出现变动,如果删除的时当前页最后一条数据,则退回有数据的页数,当前页为第一页则只刷新
// count删除的数据量
const handleAfterDel = (count?: number) => {
// 第一页时,直接获取新数据
if (datasource?.length > (count ?1) || tableParams.current === 1) {
const res = pullData(tableParams, setDatasource)
setTotal(getTotal(res))
} else {
// 被删除的页数,用于计算需要返回的页数
const deletedPage = Math.floor(
Number(
(
((count ?1) - (datasource?.length ?0)) /
tableParams.pageSize
).toFixed(0)
)
)
setTableParams((s) => ({
...s,
current: s.current - deletedPage > 1 s.current - deletedPage : 1,
}))
}
}
return {
// 数据总数
total,
setTotal,
// 表格数据源
datasource,
setDatasource,
// 表格接口依赖
tableParams,
setTableParams,
// 搜索依赖state
searchData,
setSearchData,
// 搜索操作
handleSearch,
// 重置操作
handleReset,
// 页面变化
handlePageChange,
// 删除数据后执行,参数为删除的条数
handleAfterDel,
}
}
封装思路
hooks提供了以往react以往风格不方便实现的逻辑复用能力,笔者使用hooks于生产实践中也有近俩月了,写的后台项目中非常多的表格,遂基于antd的table组件,封装了配套的可复用的逻辑。
-
参数部分
一般来说,所有表格将会出现的不同部分需要作为参数传入hooks中。
1. get表格数据所需的params参数初始值:tableParamsInit (默认需要翻页操作)
2. 查询、筛选、搜索等功能所依赖的参数初始值:searchDataInit
3. 调用接口,获取表格数据的方法
4. 获取数据总数的方法(如果后台的接口格式几乎一样,这个也可以写死,不用传入)
{
tableParamsInit = { pageSize: 10, current: 1 } as any,
searchDataInit = {},
pullData = () => {
// console.log('pullData')
},
getTotal = () => {
return 1
},
}: {
tableParamsInit?: TableParams
searchDataInit?: SearchData | {}
// 调用接口的方法,接受setDatasource作为参数, 在获取数据后,通过其将数据注回useTableData
pullData: (
params: TableParams,
setDatasource: React.Dispatch<React.SetStateAction<Datasource[] | []>>
) => void
// 从结果中获取数据总数
getTotal?: (res: any) => number
}
-
状态state
1.dataSource用来存储表格的数据
2.tableParams是用于发送接口请求的最终参数
3.searchData是查询状态的state
const [datasource, setDatasource] = useState<Datasource[] | []>([])
const [tableParams, setTableParams] = useState<TableParams>(tableParamsInit)
const [searchData, setSearchData] = useState<SearchData | {}>(searchDataInit)
-
监听effect
- 监听tableParams即接口参数的变化,接口依赖的参数变了就请求接口获取新的数据,这种调用接口的时机是实际生产当中笔者总结出的逻辑比较清晰易懂不容易出错的一种。
- 值得注意的是,用于查询搜索的那些参数,只有当用户点击查询或者搜索按钮时,才会被添加到tableParams中,触发接口获取数据。如果读者在生产中,产品要求搜索内容变化就要立即获取数据的话,则应该把搜索所依赖的参数直接收纳于tableParams中,而不应当再作为searchData中的内容。
useEffect(() => {
const res = pullData(tableParams, setDatasource)
setTotal(getTotal(res))
}, [tableParams])
-
方法handler
- handleSearch 这个方法一般被笔者直接添加到查询的按钮上,通过把searchData的值赋值到tableParams中,改变tableParams,触发pullData获取新的数据。
注意:current也被笔者重新赋值为1,是因为,查询或筛选后的数据一般会少于总数据。这样的话如果不把页数重置为1则会出现查询到的结果不符合预期的情况。
- handleSearch 这个方法一般被笔者直接添加到查询的按钮上,通过把searchData的值赋值到tableParams中,改变tableParams,触发pullData获取新的数据。
// 搜索时,将搜索state set至表格接口依赖,表格接口依赖变化,自动触发接口调用
const handleSearch = () => {
setTableParams((s) => ({ ...s, current: 1, ...searchData }))
}
- handleReset 这个方法一般被笔者放在重置按钮上,通过把tableParams及searchData的值重新赋值为init的值,触发pullData,覆盖原有的数据。
// 重置,将搜索依赖及表格接口依赖重置为init
const handleReset = () => {
setTableParams(tableParamsInit)
setSearchData(searchDataInit)
}
- handlePageChange 翻页,直接给antD的Table组件的pagination 的onChange上就ok
// 页码变更
const handlePageChange = (v: number) => {
setTableParams((s) => ({ ...s, current: v }))
}
-
- handleAfterDel 当表格具有删除数据的功能时,不得不考虑删除数据后的一些处理逻辑。
- 第一页时,用当前参数直接获取新数据
- 当前数据量大于被删除的数据量,用当前参数直接获取新数据
- 计算出被删除的页数,以及应该具有数据的最后一页,出现0或者负则应该为第一页
// 执行删除操作,数据源出现变动,如果删除的时当前页最后一条数据,则退回有数据的页数,当前页为第一页则只刷新
// count删除的数据量
const handleAfterDel = (count?: number) => {
// 第一页时,直接获取新数据
if (datasource?.length > (count ?1) || tableParams.current === 1) {
const res = pullData(tableParams, setDatasource)
setTotal(getTotal(res))
} else {
// 被删除的页数,用于计算需要返回的页数
const deletedPage = Math.floor(
Number(
(
((count ?1) - (datasource?.length ?0)) /
tableParams.pageSize
).toFixed(0)
)
)
setTableParams((s) => ({
...s,
current: s.current - deletedPage > 1 s.current - deletedPage : 1,
}))
}
}
-
使用
...
const {
total,
setTotal,
// 表格数据源
datasource,
setDatasource,
// 表格接口依赖
tableParams,
setTableParams,
// 搜索依赖state
searchData,
setSearchData,
// 搜索操作
handleSearch,
// 重置操作
handleReset,
// 页面变化
handlePageChange,
// 删除数据后执行,参数为删除的条数
handleAfterDel,
} = useTableData<any, any, any>({
tableParamsInit: { pageSize: 10, current: 1 },
searchDataInit: { discount_type: 0 },
getTotal() {
return 100
},
pullData({ pageSize: limit, current: page, ...rest }, setData) {
dispatch({
type: `${namespace}/${ActionTypes.publicCodeGet}`,
payload: {
params: {
limit,
page,
...rest,
},
onSuccess(res) {
setData(res?.data)
},
},
})
setData(MOCK.publicCodeTableData)
},
})
return <>
<Form className={`${styles.search} mt20`} layout="inline">
<Form.Item label="通用码名称">
<Input
placeholder="请输入通用码名称"
value={searchData.keywords}
onPressEnter={handleSearch}
onChange={({ target: { value } }) => {
setSearchData((s) => ({ ...s, keywords: value }))
}}
></Input>
</Form.Item>
<Form.Item label="优惠方式">
<div style={{ width: 180 }}>
<Select
className={`w100`}
placeholder="请选择优惠方式"
options={[{ label: '全部', value: 0 }, ...types.discount.list]}
value={searchData.discount_type}
onChange={(v) => {
setSearchData((s) => ({ ...s, discount_type: v }))
}}
></Select>
</div>
</Form.Item>
<Form.Item>
<Button onClick={handleSearch} type="primary">
查询
</Button>
</Form.Item>
<Form.Item>
<Button type="ghost" onClick={handleReset}>
重置
</Button>
</Form.Item>
</Form>
<Table
loading={tableLoading}
dataSource={datasource}
rowKey="id"
pagination={{
total,
pageSize: tableParams.pageSize,
current: tableParams.current,
showTotal: (total) => `共有${total}条数据`,
showSizeChanger: false,
onChange: handlePageChange,
}}
columns={[
{ title: '通用码名称', dataIndex: 'name' },
{
title: '适用范围',
dataIndex: 'range',
render(_, record) {
return (
<div>
<div className={`p16`}>
{types.goods.data[record.range.goods_type]}
</div>
<div className={`p12g`}>{record.range.name}</div>
</div>
)
},
},
{ title: '优惠码', dataIndex: 'code' },
{ title: '优惠方式', align: 'center', dataIndex: 'path' },
{ title: '可用数量', align: 'center', dataIndex: 'count' },
{
title: '有效期',
dataIndex: 'time',
render(time) {
return (
<div>
<div>起:{time[0]}</div>
<div>止:{time[1]}</div>
</div>
)
},
},
{
title: '状态',
align: 'center',
dataIndex: 'is_enable',
render(text) {
return text === 1 '启用' : '禁用'
},
},
{
title: '操作',
dataIndex: 'option',
align: 'center',
key: 'option',
width: 120,
render: (text: any, record: any, index) => (
<span className={styles.operate}>
<a
onClick={() => {
router.push({
pathname: '/marketing/publicCode/form',
query: { id: record.id ?'' },
})
}}
>
编辑
</a>
<Divider type="vertical" />
<Popover
placement="bottom"
content={
<div>
<p
className="hoverActive cp pb4"
onClick={() => {
router.push({
pathname: '/marketing/publicCode/detail',
query: { id: record.id },
})
}}
>
使用情况
</p>
<p
className="hoverActive cp pb4"
onClick={() => {
// handleDel(record.id);
handleDel(record.id, index)
}}
>
删除
</p>
</div>
}
trigger="hover"
>
<a onClick={() => {}}>更多</a>
</Popover>
</span>
),
},
]}
/ >
</>
...
总结:类型检查等处还有待优化。笔者水平有限,还请各位读者大佬斧正。