/** * @file 文件上传弹窗组件 <默认支持xls,xlsx上传> * @author luozhengxuan <51071534@qq.com> * */ import React, { useCallback, useMemo, useState, useImperativeHandle } from 'react'; import classNames from 'classnames'; import Axios from 'axios'; import io from 'socket.io-client'; import { DeleteOutlined, UploadOutlined } from '@ant-design/icons'; import locale from 'antd/es/locale/zh_CN'; import ConfigProvider from 'antd/es/config-provider'; import Modal from 'antd/es/modal'; import 'antd/es/modal/style/index'; import Button from 'antd/es/button'; import 'antd/es/button/style/index'; import Upload from 'antd/es/upload'; import 'antd/es/upload/style/index'; import Progress from 'antd/es/progress'; import 'antd/es/progress/style/index'; import message from 'antd/es/message'; import 'antd/es/message/style/index'; import Form from 'antd/es/form'; import 'antd/es/form/style/index'; import { checkType, loadFile } from '@jy-fe/utils'; import { env } from '@jy-fe/business'; import { XuiModalFileProps, XuiModalFileHandles } from './xui-modal-file.d'; export const XuiModalFile: React.ForwardRefRenderFunction< XuiModalFileHandles, XuiModalFileProps > = ( { modalStyle = 'admin', title: ModalTilte, hideDownloadTemplate = false, stepNodes = [], fileStepTitles = ['选择文件', '导入数据'], loadTip = '请先下载Excel模板', tip = '选择 Microsoft Office Excel 文件(支持xls, xlsx格式)', downloadHref, uploadURL, uploadExtraConf = {}, typeIntercept = { accept: [ 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ], acceptName: ['xls', 'xlsx'], }, maxSize, maxSizeTip, isTureProgress = false, isCover, success, fail, afterClose, }, ref, ) => { const classname = 'xui-ant__modal-file'; const [visible, setVisible] = useState(false); const [step, setStep] = useState(0); const [nextStepLoading, setNextStepLoading] = useState(false); const [uploadLoading, setUploadLoading] = useState(false); const [stepDataMap, setStepDataMap] = useState(new Map()); const [fileList, setFileList] = useState([]); const [percent, setPercent] = useState('0'); const [isimport, setIsimport] = useState(false); const [progressStatus, setProgressStatus] = useState< 'normal' | 'exception' | 'active' | 'success' >('active'); const [progressTip, setProgressTip] = useState(''); const [resData, setResData] = useState<{ [key: string]: any } | null>(null); const [form] = Form.useForm(); const stepTitle = useMemo(() => stepNodes.map(item => item.title).concat(fileStepTitles), [ stepNodes, fileStepTitles, ]); /** 额外步骤的相关渲染dom集合 */ const stepMap = useMemo(() => { const currentStepNodeMap: Map = new Map(); const currentStepHandleMap: Map< number, ((values: { [key: string]: any }) => Promise) | undefined > = new Map(); const formNodes: JSX.Element[] = []; stepNodes.forEach((item, index) => { const { title, defaultValues, formInfo, onNextStep } = item; currentStepHandleMap.set(index, onNextStep); if (formInfo.length > 0) { const formProps: any = { className: classNames(`${classname}_${modalStyle}--form`), key: title, form, labelCol: { span: 6 }, wrapperCol: { span: 16 }, }; if (defaultValues) { formProps.initialValues = defaultValues; } formNodes.push(
{formInfo.map(f => { const { label, name, rules, children, extra } = f; return ( {children} ); })}
, ); } }); formNodes.forEach((item, index) => { currentStepNodeMap.set(index, item); }); return { stepNodeMap: currentStepNodeMap, stepHandleMap: currentStepHandleMap, }; }, [stepNodes, form, modalStyle]); const internalBeforeUpload = (file: any) => { const { accept, acceptName } = typeIntercept; // 部分文件获取的type有可能为'' // 用后缀名判断 if (file.type) { const n = file.name; const extension = n.split('.').pop(); if (!acceptName.includes(extension)) { message.error(`只支持${acceptName.join('、')}格式`); return false; } } if (file.type && !accept.includes(file.type)) { message.error(`只支持${acceptName.join('、')}格式`); return false; } if (maxSize && maxSizeTip && file.size > maxSize) { message.error(maxSizeTip); return false; } if (fileList.length >= 1) { if (isCover) { setFileList([file]); } else { message.warn('一次仅支持上传一个文件'); } } else { setFileList([...fileList, file]); } return false; }; // eslint-disable-next-line consistent-return const fetchCreateUpload = async () => { try { const { data: extraData, ...otherExtraConf } = uploadExtraConf; const formData = new FormData(); if (extraData instanceof Object) { Object.entries(extraData).forEach(entry => { formData.append(entry[0], entry[1]); }); } fileList.forEach(file => { formData.append('file', file); }); setUploadLoading(true); if (isTureProgress) { // 真实进度条 const socket = io.connect(env.jySaasSocketURL, { transports: ['websocket', 'polling'], reconnection: true, }); socket.on('sessionId:', async (sessionId: string) => { formData.append('sessionId', sessionId); const { data } = await Axios({ url: uploadURL, method: 'POST', data: formData, ...(otherExtraConf || {}), }); // 这里如果使用 return Promise.reject 会进入到 axios 的捕获里 // TODO: 待优化 if (data.status) { throw data; } else { setUploadLoading(false); setStep(stepTitle.length - 1); } return data; }); socket.on( 'message:', async ( progressRes: { status: string; percentage: string; msg: string; }, resultRes: { allNum: number; errorNum: number; errorMsg: string; rightNum: number; url: string; }, ) => { const { status, percentage, msg } = progressRes; const progressStatusMap = new Map([ ['wait', 'active'], ['success', 'success'], ['fail', 'exception'], ]); setUploadLoading(false); setStep(stepTitle.length - 1); setPercent(percentage); setProgressTip(percentage === '100' ? '' : msg); setIsimport(status === 'wait'); setProgressStatus(progressStatusMap.get(status) as any); setResData(resultRes); if (percentage === '100') { socket.disconnect(); if (success) success(resultRes); } }, ); } else { const { data } = await Axios({ url: uploadURL, method: 'POST', data: formData, ...(otherExtraConf || {}), }); // 这里如果使用 return Promise.reject 会进入到 axios 的捕获里 // TODO: 待优化 if (data.status) { throw data; } else { setUploadLoading(false); setStep(stepTitle.length - 1); setPercent('100'); setProgressStatus('success'); setResData(data.data); message.success('上传成功'); if (success) success(data.data); } return data; } } catch (err) { setUploadLoading(false); setPercent('0'); if (err.msg) { message.warn(err.msg); } else { message.error('网络异常'); } if (fail) fail(err); } }; const handleDeleteFile = (index: number) => { const currentFileList = fileList.slice(); currentFileList.splice(index, 1); setFileList(currentFileList); }; const hanldeNextStep = useCallback(() => { form.validateFields().then(async values => { let through = true; const f = stepMap.stepHandleMap.get(step); if (f && checkType(f) === 'Function') { setNextStepLoading(true); try { await f(values); setNextStepLoading(false); } catch (error) { through = false; setNextStepLoading(false); } } if (through) { stepDataMap.set(step, values); setStepDataMap(stepDataMap); setStep(step + 1); } }); }, [step, form, stepMap, stepDataMap]); const hanldeBackStep = useCallback(() => { setStep(step - 1); }, [step]); const handleClose = useCallback(() => { setVisible(false); }, []); const reset = useCallback(() => { setVisible(false); setStep(0); setNextStepLoading(false); setUploadLoading(false); setStepDataMap(new Map()); setFileList([]); setPercent('0'); setProgressStatus('active'); setProgressTip(''); setResData(null); }, []); const onAfterClose = useCallback(() => { form.resetFields(); reset(); afterClose && afterClose(); }, [form, afterClose, reset]); useImperativeHandle(ref, () => ({ form, show: () => setVisible(true), destory: () => setVisible(false), changeNextStepLoading: value => setNextStepLoading(value), })); return ( {/* 不是最后一步时才显示取消按钮 */} {step !== stepTitle.length - 1 && } {/* 除第一步和最后一步都有上一步按钮 */} {step !== 0 && step !== stepTitle.length - 1 && ( )} {/* 只有总步数超过2步,且当前不为最后两步时显示下一步 */} {stepTitle.length > 2 && step < stepTitle.length - 2 && ( )} {/* 只有倒数第二步时才显示导入按钮 */} {step === stepTitle.length - 2 && ( )} {/* 只有最后一步时才显示导入按钮 */} {step === stepTitle.length - 1 && ( )} } > {modalStyle === 'admin' ? ( <>
{stepTitle.map((item, index) => (
{item}
))}
{stepTitle.length > 2 && step < stepTitle.length - 2 && stepMap.stepNodeMap.get(step)} {step === stepTitle.length - 2 ? (

选择 Excel{' '} {!hideDownloadTemplate && ( loadFile(downloadHref)}>下载导入模板 )}

`.${type}`).join(',')} >

{tip}

{fileList.map((file, i) => (

{file.name} handleDeleteFile(i)}>

))}
) : null} ) : ( <> {step === stepTitle.length - 2 ? (
步骤二:
`.${type}`).join(',')} >

{tip}

{fileList.map((file, i) => (

{file.name} handleDeleteFile(i)}>

))}
) : null} )} {step === stepTitle.length - 1 ? (
{progressTip} {!progressTip && resData ? (

{resData.errorMsg || `总计导入${resData.allNum}条数据,成功${resData.rightNum}条,失败${resData.errorNum} 条。`}     {resData.errorNum ? ( loadFile(resData.url)}>下载错误报告 ) : null}

) : null}
) : null}
); }; export default React.forwardRef(XuiModalFile);