import React, { useState, useCallback } from 'react'; import { Modal, Upload, Table, Button, Space, Checkbox, Alert, Tag, Typography, Progress, Spin, message, } from 'antd'; import { UploadOutlined, DownloadOutlined, ImportOutlined, CloudServerOutlined, CheckCircleOutlined, CloseCircleOutlined, } from '@ant-design/icons'; import * as XLSX from 'xlsx'; import Papa from 'papaparse'; import api from '../api'; import { designTokens } from '../config/theme'; import CloseButton from './CloseButton'; const { Text, Title } = Typography; function NetworkCardImportModal({ visible, onClose, onSuccess }) { const [importPreview, setImportPreview] = useState([]); const [importErrors, setImportErrors] = useState([]); const [importProgress, setImportProgress] = useState({ current: 0, total: 0 }); const [importing, setImporting] = useState(false); const [skipExisting, setSkipExisting] = useState(false); const [updateExisting, setUpdateExisting] = useState(false); const generateUniqueNicId = () => { return `NIC-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; }; const handleFileUpload = (file, onSuccess) => { const reader = new FileReader(); reader.onload = async e => { try { const data = e.target.result; let parsedData = []; if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) { const workbook = XLSX.read(data, { type: 'binary' }); const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; parsedData = XLSX.utils.sheet_to_json(worksheet); } else if (file.name.endsWith('.csv')) { parsedData = Papa.parse(data, { header: true, skipEmptyLines: true, }).data; } else { message.error('不支持的文件格式,请上传 .xlsx 或 .csv 文件'); return; } if (parsedData.length === 0) { message.warning('文件内容为空,请检查文件内容'); return; } const validatedResult = await validateImportData(parsedData); setImportPreview(validatedResult.validData); setImportErrors(validatedResult.errors); setImportProgress({ current: 0, total: validatedResult.validData.length }); if (onSuccess) onSuccess(); } catch (error) { message.error('文件解析失败'); console.error('文件解析失败:', error); } }; if (file.name.endsWith('.csv')) { reader.readAsText(file); } else { reader.readAsBinaryString(file); } }; const validateImportData = async data => { const validData = []; const allErrors = []; for (let i = 0; i < data.length; i++) { const row = data[i]; const result = await validateNetworkCardRow(row, i); if (result.valid) { validData.push(row); } else { allErrors.push(...result.errors.map(err => ({ ...err, originalRow: row }))); } } if (allErrors.length > 0) { message.warning({ content: `发现 ${allErrors.length} 个数据错误,请查看错误详情并修正后重新导入`, duration: 5, }); } return { validData, errors: allErrors }; }; const validateNetworkCardRow = async (row, index) => { const rowNum = index + 2; const errors = []; if (!row['设备SN']) { errors.push({ row: rowNum, field: '设备SN', value: row['设备SN'] || '(空)', error: '缺少必填字段', suggestion: '请填写设备SN,格式如:SNAA0001', }); } if (!row['网卡名称']) { errors.push({ row: rowNum, field: '网卡名称', value: row['网卡名称'] || '(空)', error: '缺少必填字段', suggestion: '请填写网卡名称,格式如:网卡1、eth0', }); } if (row['插槽编号']) { const slotPattern = /^\d+$/; if (!slotPattern.test(row['插槽编号'])) { errors.push({ row: rowNum, field: '插槽编号', value: row['插槽编号'], error: '插槽编号格式错误', suggestion: '插槽编号必须为数字,如:1、2', }); } } if (errors.length > 0) { return { valid: false, errors }; } return { valid: true }; }; const handleBatchImport = async () => { if (importPreview.length === 0) { message.warning('请先选择要导入的数据'); return; } setImporting(true); setImportProgress({ current: 0, total: importPreview.length }); let progressInterval; let currentProgress = 0; try { const networkCardsData = importPreview.map((row, index) => ({ nicId: generateUniqueNicId(), deviceSn: row['设备SN'], name: row['网卡名称'], slotNumber: row['插槽编号'] ? parseInt(row['插槽编号']) : null, model: row['网卡型号'] || null, manufacturer: row['制造商'] || null, description: row['描述'] || null, })); progressInterval = setInterval(() => { currentProgress = Math.min(currentProgress + Math.random() * 15, 85); setImportProgress(prev => ({ ...prev, current: Math.floor((currentProgress / 100) * importPreview.length), })); }, 200); const response = await api.post('/network-cards/batch', { networkCards: networkCardsData, skipExisting, updateExisting, }); const { total, success, failed, skipped = 0, updated = 0, errors } = response; clearInterval(progressInterval); setImportProgress({ current: importPreview.length, total: importPreview.length }); let msgContent = ''; if (updated > 0) { msgContent += `更新 ${updated} 个,`; } if (skipped > 0) { msgContent += `跳过 ${skipped} 个,`; } if (success > 0) { msgContent += `新增 ${success - updated} 个,`; } if (failed > 0) { msgContent += `失败 ${failed} 个`; Modal.error({ title: '导入失败详情', content: (
), width: 600, }); } if (failed > 0) { message.warning(`导入完成,但有 ${failed} 个失败。${msgContent}`); } else { message.success({ content: `导入完成!${msgContent}`, icon: , }); } if (onSuccess) { onSuccess(); } setImportPreview([]); setImportErrors([]); onClose(); } catch (error) { clearInterval(progressInterval); console.error('批量导入失败:', error); message.error('批量导入失败,请检查数据格式'); } finally { clearInterval(progressInterval); setImporting(false); } }; const handleDownloadTemplate = () => { const templateData = [ { 设备SN: 'SN-20230001', 网卡名称: '网卡1', 插槽编号: '1', 网卡型号: 'Intel X710', 制造商: 'Intel', 描述: '主网卡', }, { 设备SN: 'SN-20230001', 网卡名称: '网卡2', 插槽编号: '2', 网卡型号: 'Intel X710', 制造商: 'Intel', 描述: '备网卡', }, ]; const worksheet = XLSX.utils.json_to_sheet(templateData); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, '网卡数据'); XLSX.writeFile(workbook, '网卡导入模板.xlsx'); }; const handleClose = () => { setImportPreview([]); setImportErrors([]); setImportProgress({ current: 0, total: 0 }); setSkipExisting(false); setUpdateExisting(false); onClose(); }; return (
批量导入网卡 } open={visible} closeIcon={} onCancel={handleClose} width={900} footer={[ , , , ]} >
适用范围:批量导入网卡仅适用于 服务器设备 ,交换机设备请直接在端口管理中导入端口
前置条件:请先在 设备管理 中添加目标服务器,确保设备SN已存在
操作步骤:
1. 点击「下载模板」获取标准Excel/CSV文件
2. 按模板格式填写网卡信息, 设备SN网卡名称为必填项
3. 点击上传区域选择文件,或直接拖拽文件到上传区域
4. 系统自动校验数据,可预览前10条数据及错误详情
5. 选择导入策略(跳过/更新已存在),点击「开始导入」
字段说明:
设备SN(必填):服务器的唯一序列号,如SN-20230001
网卡名称(必填):网卡的名称或标识,如eth0、网卡1
插槽编号(选填):网卡所在的插槽位置,必须为数字
网卡型号(选填):如Intel X710、BCM57414
制造商(选填):如Intel、Mellanox
描述(选填):备注信息
注意事项:
• 同一设备下网卡名称不可重复
• 导入后需在网卡管理中为网卡添加端口
} type="info" showIcon style={{ borderRadius: designTokens.borderRadius.md, background: designTokens.colors.info.bg, border: `1px solid ${designTokens.colors.info.light}40`, marginBottom: '16px', }} /> { handleFileUpload(file, null); return false; }} style={{ borderRadius: designTokens.borderRadius.lg, border: `2px dashed ${designTokens.colors.primary.light}`, background: designTokens.colors.primary.bg, }} >

点击或拖拽文件到此处上传

支持 .xlsx, .xls, .csv 格式文件

setSkipExisting(e.target.checked)}> 跳过已存在的网卡 setUpdateExisting(e.target.checked)}> 更新已存在的网卡
{importErrors.length > 0 && (
{row}, }, { title: '字段', dataIndex: 'field', key: 'field', width: 100, render: field => {field}, }, { title: '错误值', dataIndex: 'value', key: 'value', width: 120, render: val => {val}, }, { title: '错误原因', dataIndex: 'error', key: 'error', render: err => {err}, }, { title: '修正建议', dataIndex: 'suggestion', key: 'suggestion', render: sug => {sug}, }, ]} dataSource={importErrors} rowKey={(record, index) => `error-${index}`} pagination={{ pageSize: 5, size: 'small', showSizeChanger: false, showTotal: total => `共 ${total} 条错误`, }} size="small" scroll={{ x: 600 }} style={{ marginTop: '8px' }} /> } type="error" showIcon style={{ borderRadius: designTokens.borderRadius.md }} /> )} {importPreview.length > 0 && (
成功解析 {importPreview.length} 条有效数据 {importErrors.length > 0 && ( ({importErrors.length} 条错误) )} } type={importErrors.length > 0 ? 'warning' : 'success'} showIcon style={{ marginBottom: '16px', borderRadius: designTokens.borderRadius.md }} />
数据预览(前10条)
`import-row-${index}`} pagination={false} size="small" scroll={{ x: 700 }} style={{ borderRadius: designTokens.borderRadius.md }} /> {importPreview.length > 10 && (
仅显示前10条数据,共 {importPreview.length} 条
)} )} {importing && (
正在导入 {importProgress.current} / {importProgress.total} 条数据...
)} ); } export default NetworkCardImportModal;