import React, { useState } from 'react'; import { Modal, Upload, Button, Progress, message, Table, Alert, Space, Spin } from 'antd'; import { UploadOutlined, DownloadOutlined, CheckCircleOutlined, WarningOutlined, FileTextOutlined } from '@ant-design/icons'; import axios from 'axios'; import { designTokens } from '../../config/theme'; const modalHeaderStyle = { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '18px', fontWeight: 600, }; const ImportModal = ({ visible, deviceFields, onImport, onCancel, }) => { const [step, setStep] = useState('upload'); const [isPreviewing, setIsPreviewing] = useState(false); const [isConfirming, setIsConfirming] = useState(false); const [previewLoading, setPreviewLoading] = useState(false); const [previewData, setPreviewData] = useState(null); const [selectedFile, setSelectedFile] = useState(null); const [importProgress, setImportProgress] = useState(0); const [importPhase, setImportPhase] = useState(''); const [importResult, setImportResult] = useState(null); const api = axios.create({ baseURL: '/api', }); api.interceptors.request.use((config) => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); const resetState = () => { setStep('upload'); setIsPreviewing(false); setIsConfirming(false); setPreviewLoading(false); setPreviewData(null); setSelectedFile(null); setImportProgress(0); setImportPhase(''); setImportResult(null); }; const handleClose = () => { resetState(); onCancel(); }; const handlePreview = async (file) => { const actualFile = file.originFileObj || file; setSelectedFile(actualFile); setPreviewLoading(true); setIsPreviewing(true); try { const formData = new FormData(); formData.append('csvFile', actualFile); const response = await api.post('/devices/import-preview', formData, { headers: { 'Content-Type': 'multipart/form-data', }, }); if (response.data.success) { setPreviewData(response.data.data); setStep('preview'); } else { message.error(response.data.error || '预览失败'); resetState(); } } catch (error) { console.error('预览失败:', error); message.error(error.response?.data?.error || '预览失败,请检查文件格式'); resetState(); } finally { setPreviewLoading(false); } return false; }; const handleConfirmImport = async () => { if (!selectedFile) { message.error('请先选择文件'); return; } setIsConfirming(true); setImportProgress(0); setImportPhase('正在上传文件...'); try { await onImport(selectedFile, { onProgress: (progress, phase) => { setImportProgress(progress); setImportPhase(phase); }, onSuccess: (result) => { setImportResult(result); setImportProgress(100); setImportPhase('导入完成'); setIsConfirming(false); setStep('result'); }, onError: (error) => { setImportResult({ success: false, statistics: { total: 0, success: 0, failed: 1, errors: [{ row: 0, error: error.message || '导入失败' }], }, }); setIsConfirming(false); setStep('result'); }, }); } catch (error) { setIsConfirming(false); message.error('导入失败'); resetState(); } }; const requiredFields = deviceFields.filter((f) => f.visible && f.required); const optionalFields = deviceFields.filter((f) => f.visible && !f.required); const previewColumns = previewData?.fieldList ? previewData.fieldList.map((field) => ({ title: field.displayName + (field.required ? ' *' : ''), dataIndex: field.fieldName, key: field.fieldName, width: 120, ellipsis: true, })) : [ { title: '行号', dataIndex: '_rowNum', key: '_rowNum', width: 60 }, { title: '设备名称', dataIndex: 'name', key: 'name', width: 120 }, { title: '设备类型', dataIndex: 'type', key: 'type', width: 80 }, { title: '品牌', dataIndex: 'model', key: 'model', width: 100 }, { title: '序列号', dataIndex: 'serialNumber', key: 'serialNumber', width: 140 }, { title: '所在机房', dataIndex: 'roomName', key: 'roomName', width: 100 }, { title: '所在机柜', dataIndex: 'rackName', key: 'rackName', width: 100 }, { title: '状态', dataIndex: 'status', key: 'status', width: 80 }, ]; const getRowClassName = (record) => { if (record._hasError) { return 'ant-table-row-error'; } return ''; }; const renderUploadStep = () => (

请上传CSV格式的设备数据文件

支持的编码格式:GBK

CSV文件格式要求:

{requiredFields.length > 0 && (
必填字段: {requiredFields.map((f) => f.displayName).join('、')}
)} {optionalFields.length > 0 && (
可选字段: {optionalFields.map((f) => f.displayName).join('、')}
)}
  • 设备类型:server(服务器)、switch(交换机)、router(路由器)、storage(存储设备)、other(其他)
  • 状态值:running(运行中)、maintenance(维护中)、offline(离线)、fault(故障)
  • 日期格式:YYYY-MM-DD (例如:2023-01-01)
包含示例数据的CSV模板文件(根据当前字段配置生成)
); const renderPreviewStep = () => (
{selectedFile?.name || '已选择文件'}
共 {previewData?.total || 0} 条记录,已解析 {previewData?.previewCount || 0} 条作为预览
{previewData?.statistics?.invalid > 0 ? ( } message={`发现 ${previewData.statistics.invalid} 条数据存在错误`} description="错误行已用红色标记,请核对后确认导入" style={{ marginTop: '8px' }} /> ) : ( } message="数据验证通过" description="所有数据格式正确,可以进行导入" style={{ marginTop: '8px' }} /> )}
{previewData?.statistics?.total || 0}
总记录数
{previewData?.statistics?.valid || 0}
有效
{previewData?.statistics?.invalid || 0}
无效
`共 ${total} 条`, }} scroll={{ x: 'max-content' }} /> {previewData?.errors?.length > 0 && (

错误详情:

{previewData.errors.slice(0, 10).map((err, index) => (
第{err.row}行: {err.errors.join(',')}
))} {previewData.errors.length > 10 && (
还有 {previewData.errors.length - 10} 条错误未显示...
)}
)} ); const renderConfirmingStep = () => (

正在导入设备数据

{importPhase}

`${importProgress}%`} />
); const renderResultStep = () => (

导入完成:

{importResult?.statistics?.total || 0}
总记录数
{importResult?.statistics?.success || 0}
成功
{importResult?.statistics?.failed || 0}
失败
{importResult?.statistics?.errors?.length > 0 && (

失败记录详情:

{importResult.statistics.errors.map((item, index) => ( ))}
行号 失败原因
{item.row || index + 1} {item.error || '未知错误'}
)}
); const renderLoadingStep = () => (
正在解析文件...
); const getStepTitle = () => { switch (step) { case 'upload': return (
导入设备
); case 'preview': return (
导入设备 - 数据预览
); case 'confirming': return (
导入设备 - 导入中
); case 'result': return (
导入设备 - 结果
); default: return '导入设备'; } }; return ( {step === 'upload' && renderUploadStep()} {step === 'loading' && renderLoadingStep()} {step === 'preview' && renderPreviewStep()} {step === 'confirming' && renderConfirmingStep()} {step === 'result' && renderResultStep()} ); }; export default React.memo(ImportModal);