项目中需要导出列表功能.
设计
- 列表页一共有两个按钮,一个搜索,一个导出
- 用户点击导出按钮
-
获取当前搜索条件
-
拼接搜索条件¤t=1&pageSize=100请求后端接口,current不断自增循环请求
-
用react中的ref去累加返回的数据
-
判断当前接口返回的数据大小是否等于pageSize. 如果小于,则结束当前循环
-
如果过程中发生任何错误,显示错误信息到页面,并让用户重试
-
npm包的选择
- react-csv github.com/react-csv/r…
- react-csv-downloader
github.com/dolezel/rea…
这两个npm下载量都比较大,但第一个react-csv已经好几年不更新. 本来想用第一个,发现他也不支持typescript. ignore后发现又报了其他错误. 具体什么错误,我也忘记了.最后换了react-csv-downloader. 发现这个使用起来更方便一些
补充: 以上只是一些demo使用. 实际生产中还需要考虑以下几点
- 导出进度条的显示.
- 导出可取消. 如果用户导出大量数据不能取消, 体验很差,浪费时间,只能刷新页面,或重新打开
- 导出不能阻塞页面的其他动作
- 特殊符号的转义. csv是以逗号分割数据的,所以这些符号需要转义.
- 时间数据的转义, 注意时区的处理
- 组件的封装,因为基本每个列表都需要导出功能,所以需要将以上再次封装为一个业务组件
- csv数据转义函数
function escapeCsvField(field) {
if (field.includes('"')) {
field = field.replace(/"/g, '""');
}
if (field.includes(',') || field.includes('\n') || field.includes('\r')) {
field = `"${field}"`;
}
return field;
}
- 时间的处理函数
new Date().toLocaleString() //'2024/8/3 18:52:27'
new Date().toISOString() //'2024-08-03T10:53:18.681Z'
new Date().toJSON() // '2024-08-03T10:53:46.224Z'
可以看见以上时间转字符, 只有第一个是正确的时区, 但第一个不能显示毫秒
后端接口返回的是'2024-08-03T10:53:18.681Z'
前端对日期的几种处理方法
- toLocaleString()
//1 直接toLocaleString()
new Date('2024-08-03T10:53:18.681Z').toLocaleString()
- ISO date+ 时区补偿 是否会纳闷有点多此一举, 并没有,因为toLocaleString只能显示到秒,但这种方案可以显示到毫秒
let date = new Date('2024-08-03T10:53:18.681Z');
date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
// toJSON 的时区补偿
date.toJSON().substring(0, 22).replace(/[T]/g, ' ');;
导出组件,伪代码
import {
forwardRef,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import CsvDownloader from 'react-csv-downloader'
export const delay = (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms))
}
const columns = [
{
id: 'name',
displayName: 'name',
},
{
id: 'created_at',
displayName: 'created_at',
},
{
id: 'updated_at',
displayName: 'updated_at',
},
]
const CsvExportModal = forwardRef(
(_props, ref) => {
const [progress, setProgress] = useState()
const [visible, setVisible] = useState(false)
const [searchParams, setSearchParams] = useState()
const dataRef = useRef([])
const interruptExportRef = useRef(true)
const btnRef = useRef<any>(null)
const pageSizeRef = useRef(200)
const [confirmLoading, setConfirmLoading] = useState(false)
const confirmSearchParams = useMemo(() => {
if (searchParams && Object.keys(searchParams).length > 0) {
return Object.entries(searchParams)
.filter(
([, v]) => v !== undefined && v !== null && v !== ''
)
.map(([k, v]) => {
return {
label: k,
children: <p>{v}</p>,
}
})
}
return undefined
}, [searchParams])
const open = (params) => {
setSearchParams(params)
setVisible(true)
}
const handleOk = async () => {
setConfirmLoading(true)
interruptExportRef.current = false
setProgress('start fetch data')
let _currentPage = 1
let _count = pageSizeRef.current
while (
_count === pageSizeRef.current &&
!interruptExportRef.current
) {
const data = await fetch({
...searchParams,
current: _currentPage,
pageSize: pageSizeRef.current,
}).catch((e) => {
console.error(e)
return {
list: [],
total: undefined,
}
})
const { list, total } = data
await delay(500)
if (interruptExportRef.current) {
return handleCancel(undefined, true)
}
_count = list.length
_currentPage++
list.forEach((row) => {
dataRef.current.push(row)
})
setProgress(
`start fetch ${total} row:${dataRef.current.length} had ok`
)
await delay(1000)
if (interruptExportRef.current) {
return handleCancel(undefined, true)
}
}
if (interruptExportRef.current) {
return handleCancel(undefined, true)
}
await delay(1000)
if (interruptExportRef.current) {
return handleCancel(undefined, true)
}
setProgress('ok will export')
delay(500).then(() => {
if (interruptExportRef.current) {
return handleCancel(undefined, true)
} else if (dataRef.current.length === 0) {
setProgress('current date is null')
return handleCancel(true)
}
btnRef?.current?.handleClick?.()
handleCancel(undefined, undefined, true)
})
}
const handleCancel = (
visible?: boolean,
showCancelMsg?: boolean,
showOkMsg?: boolean
) => {
setConfirmLoading(false)
interruptExportRef.current = true
dataRef.current = []
if (showCancelMsg) {
console.info('cancel')
}
if (showOkMsg) {
console.info('success')
}
if (!visible) {
setProgress(undefined)
setVisible(() => false)
}
}
useImperativeHandle(ref, () => {
return {
open,
}
})
return (
<Modal
title={'export'}
open={visible}
onOk={handleOk}
maskClosable={false}
onCancel={() => handleCancel()}
confirmLoading={confirmLoading}
>
<CsvDownloader
style={{
visibility: 'hidden',
}}
ref={btnRef}
filename={filename}
extension=".csv"
wrapColumnChar=""
columns={columns}
datas={dataRef.current as any}
text="DOWNLOAD"
/>
<div>
{confirmSearchParams}
</div>
<div>
{progress}
</div>
</Modal>
)
}
)
export default CsvExportModal
blog.csdn.net/weixin_4324… developer.mozilla.org/zh-CN/docs/…