feat(机柜管理): 优化机柜导入导出功能
- 新增机柜导入预览功能,支持数据验证和错误提示 - 支持按选中机柜导出,优化导出文件名和提示信息 - 重构导入逻辑,分离Excel解析和验证逻辑 - 修复机柜关联设备查询问题,使用正确的关系别名 - 更新API文档,添加导入预览和选中导出接口说明
This commit is contained in:
@@ -69,6 +69,9 @@ api.interceptors.request.use(
|
||||
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
if (response.config.responseType === 'blob') {
|
||||
return response;
|
||||
}
|
||||
return response.data;
|
||||
},
|
||||
error => {
|
||||
@@ -96,7 +99,9 @@ api.interceptors.response.use(
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(data.message || '请求失败');
|
||||
error.friendlyMessage = data.message || '请求失败';
|
||||
error.message = data.message || '请求失败';
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
|
||||
@@ -155,7 +155,7 @@ const ImportModal = ({ visible, deviceFields, onImport, onCancel }) => {
|
||||
const handleDownloadTemplate = async () => {
|
||||
try {
|
||||
const response = await deviceAPI.getImportTemplate();
|
||||
const blob = new Blob([response], { type: 'text/csv; charset=gbk' });
|
||||
const blob = new Blob([response.data], { type: 'text/csv; charset=gbk' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
|
||||
@@ -0,0 +1,848 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Upload,
|
||||
Button,
|
||||
Progress,
|
||||
message,
|
||||
Steps,
|
||||
Card,
|
||||
Alert,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
Divider,
|
||||
Table,
|
||||
Spin,
|
||||
} from 'antd';
|
||||
import {
|
||||
UploadOutlined,
|
||||
DownloadOutlined,
|
||||
CheckCircleOutlined,
|
||||
WarningOutlined,
|
||||
FileExcelOutlined,
|
||||
InboxOutlined,
|
||||
DatabaseOutlined,
|
||||
FileTextOutlined,
|
||||
ArrowRightOutlined,
|
||||
CloseCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { designTokens } from '../../config/theme';
|
||||
import CloseButton from '../CloseButton';
|
||||
import api from '../../api';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { Step } = Steps;
|
||||
const { Dragger } = Upload;
|
||||
|
||||
/**
|
||||
* 机柜导入弹窗组件
|
||||
* @param {boolean} visible - 弹窗显示状态
|
||||
* @param {Function} onClose - 关闭回调
|
||||
* @param {Function} onImport - 导入回调
|
||||
* @param {Function} onDownloadTemplate - 下载模板回调
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
function RackImportModal({ visible, onClose, onImport, onDownloadTemplate }) {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [importProgress, setImportProgress] = useState(0);
|
||||
const [importPhase, setImportPhase] = useState('');
|
||||
const [importResult, setImportResult] = useState(null);
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
const [previewData, setPreviewData] = useState(null);
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setCurrentStep(0);
|
||||
setIsImporting(false);
|
||||
setImportProgress(0);
|
||||
setImportPhase('');
|
||||
setImportResult(null);
|
||||
setSelectedFile(null);
|
||||
setPreviewData(null);
|
||||
setPreviewLoading(false);
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
resetState();
|
||||
onClose();
|
||||
}, [resetState, onClose]);
|
||||
|
||||
/**
|
||||
* 调用后端接口解析 Excel 文件获取预览数据
|
||||
* @param {File} file - 上传的文件
|
||||
*/
|
||||
const fetchPreviewData = useCallback(async file => {
|
||||
setPreviewLoading(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await api.post('/racks/import-preview', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setPreviewData(response.data);
|
||||
setCurrentStep(1);
|
||||
} else {
|
||||
message.error(response.error || '预览失败');
|
||||
setSelectedFile(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('预览失败:', error);
|
||||
const errMsg = error.response?.data?.error || error.message || '预览失败,请检查文件格式';
|
||||
message.error(errMsg);
|
||||
setSelectedFile(null);
|
||||
} finally {
|
||||
setPreviewLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleBeforeUpload = useCallback(
|
||||
file => {
|
||||
const isXlsx =
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||||
file.type === 'application/vnd.ms-excel';
|
||||
|
||||
if (!isXlsx) {
|
||||
message.error('请上传 Excel 文件(.xlsx 或 .xls 格式)');
|
||||
return false;
|
||||
}
|
||||
|
||||
setSelectedFile(file);
|
||||
fetchPreviewData(file);
|
||||
return false;
|
||||
},
|
||||
[fetchPreviewData]
|
||||
);
|
||||
|
||||
const handleStartImport = useCallback(async () => {
|
||||
if (!selectedFile) {
|
||||
message.warning('请先选择文件');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsImporting(true);
|
||||
setCurrentStep(2);
|
||||
setImportProgress(0);
|
||||
setImportPhase('正在解析文件...');
|
||||
|
||||
try {
|
||||
const result = await onImport(selectedFile, {
|
||||
onProgress: (progress, phase) => {
|
||||
setImportProgress(progress);
|
||||
setImportPhase(phase);
|
||||
},
|
||||
});
|
||||
|
||||
setImportResult(result);
|
||||
setImportProgress(100);
|
||||
setImportPhase('导入完成');
|
||||
setCurrentStep(3);
|
||||
} catch (error) {
|
||||
message.error(error?.message || '导入失败');
|
||||
setImportPhase('导入失败');
|
||||
} finally {
|
||||
setIsImporting(false);
|
||||
}
|
||||
}, [selectedFile, onImport]);
|
||||
|
||||
const steps = [
|
||||
{ title: '上传文件', icon: <UploadOutlined /> },
|
||||
{ title: '数据预览', icon: <FileTextOutlined /> },
|
||||
{ title: '导入中', icon: <DatabaseOutlined /> },
|
||||
{ title: '完成', icon: <CheckCircleOutlined /> },
|
||||
];
|
||||
|
||||
const renderUploadStep = () => (
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
{/* 文件上传区域 */}
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: designTokens.borderRadius.large,
|
||||
border: `2px dashed ${designTokens.colors.border.light}`,
|
||||
background: designTokens.colors.background.secondary,
|
||||
}}
|
||||
bodyStyle={{ padding: '40px 24px' }}
|
||||
>
|
||||
<Dragger
|
||||
name="file"
|
||||
accept=".xlsx,.xls"
|
||||
showUploadList={false}
|
||||
beforeUpload={handleBeforeUpload}
|
||||
maxCount={1}
|
||||
disabled={previewLoading}
|
||||
style={{ background: 'transparent', border: 'none' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '80px',
|
||||
height: '80px',
|
||||
borderRadius: '50%',
|
||||
background: designTokens.colors.primary.bg,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
{previewLoading ? (
|
||||
<Spin size="large" />
|
||||
) : (
|
||||
<InboxOutlined
|
||||
style={{ fontSize: '36px', color: designTokens.colors.primary.main }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Title level={5} style={{ margin: '0 0 8px', color: designTokens.colors.text.primary }}>
|
||||
{previewLoading ? '正在解析文件...' : '点击或拖拽文件到此处上传'}
|
||||
</Title>
|
||||
<Paragraph style={{ color: designTokens.colors.text.tertiary, margin: 0 }}>
|
||||
支持 .xlsx、.xls 格式,文件大小不超过 10MB
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Dragger>
|
||||
</Card>
|
||||
|
||||
{/* 模板下载 */}
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: designTokens.borderRadius.large,
|
||||
border: `1px solid ${designTokens.colors.border.light}`,
|
||||
}}
|
||||
bodyStyle={{ padding: '20px 24px' }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
background: designTokens.colors.success.bg,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<FileExcelOutlined
|
||||
style={{ fontSize: '24px', color: designTokens.colors.success.main }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text strong style={{ fontSize: '15px', color: designTokens.colors.text.primary }}>
|
||||
下载导入模板
|
||||
</Text>
|
||||
<Paragraph style={{ color: designTokens.colors.text.secondary, margin: '4px 0 0', fontSize: '13px' }}>
|
||||
包含标准字段和示例数据,帮助您快速准备导入文件
|
||||
</Paragraph>
|
||||
</div>
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={onDownloadTemplate}
|
||||
style={{
|
||||
height: '40px',
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
borderColor: designTokens.colors.primary.main,
|
||||
color: designTokens.colors.primary.main,
|
||||
}}
|
||||
>
|
||||
下载模板
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 格式说明 */}
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
icon={<InfoCircleOutlined />}
|
||||
message="Excel 文件格式要求"
|
||||
description={
|
||||
<ul style={{ margin: '8px 0 0', paddingLeft: '20px', lineHeight: '1.8' }}>
|
||||
<li>
|
||||
<Text strong>必填字段:</Text>
|
||||
机柜ID、机柜名称、所属机房名称、高度(U)、最大功率(W)、状态
|
||||
</li>
|
||||
<li>
|
||||
<Text strong>状态值:</Text>
|
||||
<Tag color="success">active(在用)</Tag>
|
||||
<Tag color="warning">maintenance(维护中)</Tag>
|
||||
<Tag>inactive(停用)</Tag>
|
||||
</li>
|
||||
<li>
|
||||
<Text strong>数据格式:</Text>
|
||||
高度和功率必须是数字格式,编码为 UTF-8
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
style={{
|
||||
borderRadius: designTokens.borderRadius.large,
|
||||
border: `1px solid ${designTokens.colors.info.light}40`,
|
||||
background: designTokens.colors.info.bg,
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
|
||||
const renderPreviewStep = () => {
|
||||
if (!previewData) return null;
|
||||
|
||||
const columns = [
|
||||
{ title: '行号', dataIndex: '_rowNum', key: '_rowNum', width: 60 },
|
||||
{ title: '机柜ID', dataIndex: 'rackId', key: 'rackId', width: 100 },
|
||||
{ title: '机柜名称', dataIndex: 'name', key: 'name', width: 120 },
|
||||
{ title: '所属机房', dataIndex: 'roomName', key: 'roomName', width: 100 },
|
||||
{ title: '高度(U)', dataIndex: 'height', key: 'height', width: 80 },
|
||||
{ title: '最大功率(W)', dataIndex: 'maxPower', key: 'maxPower', width: 100 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 80 },
|
||||
];
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
{/* 统计卡片 */}
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||
gap: '12px',
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: designTokens.borderRadius.large,
|
||||
background: designTokens.colors.primary.bg,
|
||||
border: `1px solid ${designTokens.colors.primary.light}30`,
|
||||
}}
|
||||
bodyStyle={{ padding: '16px', textAlign: 'center' }}
|
||||
>
|
||||
<Text style={{ fontSize: '24px', fontWeight: 700, color: designTokens.colors.primary.main }}>
|
||||
{previewData.total}
|
||||
</Text>
|
||||
<div style={{ fontSize: '12px', color: designTokens.colors.text.secondary, marginTop: '4px' }}>
|
||||
总记录数
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: designTokens.borderRadius.large,
|
||||
background: designTokens.colors.success.bg,
|
||||
border: `1px solid ${designTokens.colors.success.light}30`,
|
||||
}}
|
||||
bodyStyle={{ padding: '16px', textAlign: 'center' }}
|
||||
>
|
||||
<Text style={{ fontSize: '24px', fontWeight: 700, color: designTokens.colors.success.main }}>
|
||||
{previewData.valid}
|
||||
</Text>
|
||||
<div style={{ fontSize: '12px', color: designTokens.colors.text.secondary, marginTop: '4px' }}>
|
||||
有效数据
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: designTokens.borderRadius.large,
|
||||
background: previewData.invalid > 0 ? designTokens.colors.error.bg : designTokens.colors.success.bg,
|
||||
border: `1px solid ${previewData.invalid > 0 ? designTokens.colors.error.light : designTokens.colors.success.light}30`,
|
||||
}}
|
||||
bodyStyle={{ padding: '16px', textAlign: 'center' }}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
fontWeight: 700,
|
||||
color: previewData.invalid > 0 ? designTokens.colors.error.main : designTokens.colors.success.main,
|
||||
}}
|
||||
>
|
||||
{previewData.invalid}
|
||||
</Text>
|
||||
<div style={{ fontSize: '12px', color: designTokens.colors.text.secondary, marginTop: '4px' }}>
|
||||
无效数据
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 验证结果提示 */}
|
||||
{previewData.invalid > 0 ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
icon={<WarningOutlined />}
|
||||
message={`发现 ${previewData.invalid} 条数据存在错误`}
|
||||
description="错误行已用红色标记,请核对后确认导入。建议修复错误后重新上传。"
|
||||
style={{ borderRadius: designTokens.borderRadius.large }}
|
||||
/>
|
||||
) : (
|
||||
<Alert
|
||||
type="success"
|
||||
showIcon
|
||||
icon={<CheckCircleOutlined />}
|
||||
message="数据验证通过"
|
||||
description="所有数据格式正确,可以进行导入。"
|
||||
style={{ borderRadius: designTokens.borderRadius.large }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 数据预览表格 */}
|
||||
<Card
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<FileTextOutlined style={{ color: designTokens.colors.primary.main }} />
|
||||
<Text strong>数据预览</Text>
|
||||
<Tag color="blue">共 {previewData.preview.length} 条</Tag>
|
||||
</div>
|
||||
}
|
||||
style={{
|
||||
borderRadius: designTokens.borderRadius.large,
|
||||
border: `1px solid ${designTokens.colors.border.light}`,
|
||||
}}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={previewData.preview}
|
||||
rowKey="_rowNum"
|
||||
size="small"
|
||||
pagination={false}
|
||||
scroll={{ x: 'max-content' }}
|
||||
rowClassName={record => (record._hasError ? 'ant-table-row-error' : '')}
|
||||
style={{
|
||||
borderRadius: `0 0 ${designTokens.borderRadius.large} ${designTokens.borderRadius.large}`,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 错误详情 */}
|
||||
{previewData.errors && previewData.errors.length > 0 && (
|
||||
<Card
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<CloseCircleOutlined style={{ color: designTokens.colors.error.main }} />
|
||||
<Text strong style={{ color: designTokens.colors.error.main }}>
|
||||
错误详情
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
style={{
|
||||
borderRadius: designTokens.borderRadius.large,
|
||||
border: `1px solid ${designTokens.colors.error.light}50`,
|
||||
background: designTokens.colors.error.bg,
|
||||
}}
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
>
|
||||
{previewData.errors.map((err, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
background: '#fff',
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
marginBottom: index < previewData.errors.length - 1 ? '8px' : 0,
|
||||
border: `1px solid ${designTokens.colors.error.light}30`,
|
||||
}}
|
||||
>
|
||||
<Text strong style={{ color: designTokens.colors.error.main }}>
|
||||
第 {err.row} 行:
|
||||
</Text>
|
||||
<Text style={{ color: designTokens.colors.text.secondary }}>
|
||||
{err.errors.join(';')}
|
||||
</Text>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px' }}>
|
||||
<Button
|
||||
size="large"
|
||||
onClick={() => {
|
||||
setSelectedFile(null);
|
||||
setPreviewData(null);
|
||||
setCurrentStep(0);
|
||||
}}
|
||||
style={{ borderRadius: designTokens.borderRadius.medium }}
|
||||
>
|
||||
重新选择
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<ArrowRightOutlined />}
|
||||
onClick={handleStartImport}
|
||||
disabled={previewData.invalid > 0}
|
||||
style={{
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
background: previewData.invalid > 0 ? undefined : designTokens.colors.primary.gradient,
|
||||
border: 'none',
|
||||
}}
|
||||
>
|
||||
确认导入
|
||||
</Button>
|
||||
</div>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
const renderImportingStep = () => (
|
||||
<div style={{ textAlign: 'center', padding: '40px 0' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '80px',
|
||||
height: '80px',
|
||||
borderRadius: '50%',
|
||||
background: designTokens.colors.primary.gradient,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: '24px',
|
||||
animation: 'pulse 2s infinite',
|
||||
}}
|
||||
>
|
||||
<DatabaseOutlined style={{ fontSize: '36px', color: '#fff' }} />
|
||||
</div>
|
||||
<Title level={4} style={{ margin: '0 0 8px', color: designTokens.colors.text.primary }}>
|
||||
正在导入机柜数据
|
||||
</Title>
|
||||
<Text style={{ color: designTokens.colors.primary.main, fontSize: '15px' }}>
|
||||
{importPhase}
|
||||
</Text>
|
||||
<div style={{ marginTop: '32px', maxWidth: '400px', margin: '32px auto 0' }}>
|
||||
<Progress
|
||||
percent={importProgress}
|
||||
status="active"
|
||||
strokeColor={{
|
||||
'0%': designTokens.colors.primary.main,
|
||||
'100%': designTokens.colors.purple.main,
|
||||
}}
|
||||
strokeWidth={8}
|
||||
style={{ borderRadius: '4px' }}
|
||||
/>
|
||||
</div>
|
||||
<style>{`
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.05); opacity: 0.9; }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderResultStep = () => {
|
||||
if (!importResult) return null;
|
||||
|
||||
const isSuccess = importResult.failedCount === 0;
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
{/* 结果图标和标题 */}
|
||||
<div style={{ textAlign: 'center', padding: '20px 0' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '80px',
|
||||
height: '80px',
|
||||
borderRadius: '50%',
|
||||
background: isSuccess ? designTokens.colors.success.gradient : designTokens.colors.warning.gradient,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
{isSuccess ? (
|
||||
<CheckCircleOutlined style={{ fontSize: '40px', color: '#fff' }} />
|
||||
) : (
|
||||
<WarningOutlined style={{ fontSize: '40px', color: '#fff' }} />
|
||||
)}
|
||||
</div>
|
||||
<Title level={4} style={{ margin: '0 0 8px' }}>
|
||||
{isSuccess ? '导入成功' : '导入完成(部分失败)'}
|
||||
</Title>
|
||||
<Text style={{ color: designTokens.colors.text.secondary }}>
|
||||
{isSuccess
|
||||
? '所有机柜数据已成功导入系统'
|
||||
: '部分数据导入失败,请查看下方错误详情'}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||
gap: '12px',
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: designTokens.borderRadius.large,
|
||||
background: designTokens.colors.primary.bg,
|
||||
border: `1px solid ${designTokens.colors.primary.light}30`,
|
||||
}}
|
||||
bodyStyle={{ padding: '16px', textAlign: 'center' }}
|
||||
>
|
||||
<Text style={{ fontSize: '24px', fontWeight: 700, color: designTokens.colors.primary.main }}>
|
||||
{importResult.total || 0}
|
||||
</Text>
|
||||
<div style={{ fontSize: '12px', color: designTokens.colors.text.secondary, marginTop: '4px' }}>
|
||||
总记录数
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: designTokens.borderRadius.large,
|
||||
background: designTokens.colors.success.bg,
|
||||
border: `1px solid ${designTokens.colors.success.light}30`,
|
||||
}}
|
||||
bodyStyle={{ padding: '16px', textAlign: 'center' }}
|
||||
>
|
||||
<Text style={{ fontSize: '24px', fontWeight: 700, color: designTokens.colors.success.main }}>
|
||||
{importResult.successCount || 0}
|
||||
</Text>
|
||||
<div style={{ fontSize: '12px', color: designTokens.colors.text.secondary, marginTop: '4px' }}>
|
||||
成功导入
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
style={{
|
||||
borderRadius: designTokens.borderRadius.large,
|
||||
background:
|
||||
importResult.failedCount > 0
|
||||
? designTokens.colors.error.bg
|
||||
: designTokens.colors.success.bg,
|
||||
border: `1px solid ${importResult.failedCount > 0 ? designTokens.colors.error.light : designTokens.colors.success.light}30`,
|
||||
}}
|
||||
bodyStyle={{ padding: '16px', textAlign: 'center' }}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
fontWeight: 700,
|
||||
color:
|
||||
importResult.failedCount > 0
|
||||
? designTokens.colors.error.main
|
||||
: designTokens.colors.success.main,
|
||||
}}
|
||||
>
|
||||
{importResult.failedCount || 0}
|
||||
</Text>
|
||||
<div style={{ fontSize: '12px', color: designTokens.colors.text.secondary, marginTop: '4px' }}>
|
||||
导入失败
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 新增机柜列表 */}
|
||||
{importResult.createdRacks && importResult.createdRacks.length > 0 && (
|
||||
<Card
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />
|
||||
<Text strong>本次新增机柜</Text>
|
||||
<Tag color="success">{importResult.createdRacks.length} 个</Tag>
|
||||
</div>
|
||||
}
|
||||
style={{
|
||||
borderRadius: designTokens.borderRadius.large,
|
||||
border: `1px solid ${designTokens.colors.success.light}50`,
|
||||
background: designTokens.colors.success.bg,
|
||||
}}
|
||||
bodyStyle={{ padding: '16px', maxHeight: '200px', overflow: 'auto' }}
|
||||
>
|
||||
{importResult.createdRacks.map((rack, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
background: '#fff',
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
marginBottom: idx < importResult.createdRacks.length - 1 ? '8px' : 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<DatabaseOutlined style={{ color: designTokens.colors.primary.main }} />
|
||||
<Text strong>{rack.rackId}</Text>
|
||||
<Text type="secondary">-</Text>
|
||||
<Text>{rack.name}</Text>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 跳过机柜列表 */}
|
||||
{importResult.skippedRacks && importResult.skippedRacks.length > 0 && (
|
||||
<Card
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<WarningOutlined style={{ color: designTokens.colors.warning.main }} />
|
||||
<Text strong>已跳过机柜(已存在)</Text>
|
||||
<Tag color="warning">{importResult.skippedRacks.length} 个</Tag>
|
||||
</div>
|
||||
}
|
||||
style={{
|
||||
borderRadius: designTokens.borderRadius.large,
|
||||
border: `1px solid ${designTokens.colors.warning.light}50`,
|
||||
background: designTokens.colors.warning.bg,
|
||||
}}
|
||||
bodyStyle={{ padding: '16px', maxHeight: '200px', overflow: 'auto' }}
|
||||
>
|
||||
{importResult.skippedRacks.map((rack, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
background: '#接口',
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
marginBottom: idx < importResult.skippedRacks.length - 1 ? '8px' : 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<DatabaseOutlined style={{ color: designTokens.colors.warning.main }} />
|
||||
<Text strong>{rack.rackId}</Text>
|
||||
<Text type="secondary">-</Text>
|
||||
<Text>{rack.name}</Text>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 错误详情 */}
|
||||
{importResult.errors && importResult.errors.length > 0 && (
|
||||
<Card
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<CloseCircleOutlined style={{ color: designTokens.colors.error.main }} />
|
||||
<Text strong style={{ color: designTokens.colors.error.main }}>
|
||||
错误详情
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
style={{
|
||||
borderRadius: designTokens.borderRadius.large,
|
||||
border: `1px solid ${designTokens.colors.error.light}50`,
|
||||
background: designTokens.colors.error.bg,
|
||||
}}
|
||||
bodyStyle={{ padding: '16px', maxHeight: '250px', overflow: 'auto' }}
|
||||
>
|
||||
{importResult.errors.slice(0, 5).map((err, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
style={{
|
||||
padding: '10px 12px',
|
||||
background: '#fff',
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
marginBottom: idx < Math.min(importResult.errors.length, 5) - 1 ? '8px' : 0,
|
||||
border: `1px solid ${designTokens.colors.error.light}30`,
|
||||
}}
|
||||
>
|
||||
<Text strong style={{ color: designTokens.colors.error.main }}>
|
||||
第 {err.row || idx + 1} 行
|
||||
</Text>
|
||||
<Divider type="vertical" style={{ margin: '0 8px' }} />
|
||||
<Text style={{ color: designTokens.colors.text.secondary }}>{err.error}</Text>
|
||||
</div>
|
||||
))}
|
||||
{importResult.errors.length > 5 && (
|
||||
<div style={{ textAlign: 'center', marginTop: '8px' }}>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
还有 {importResult.errors.length - 5} 处错误未显示...
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 完成按钮 */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: '8px 0' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handleClose}
|
||||
style={{
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
background: designTokens.colors.primary.gradient,
|
||||
border: 'none',
|
||||
minWidth: '120px',
|
||||
}}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</div>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStepContent = () => {
|
||||
switch (currentStep) {
|
||||
case 0:
|
||||
return renderUploadStep();
|
||||
case 1:
|
||||
return renderPreviewStep();
|
||||
case 2:
|
||||
return renderImportingStep();
|
||||
case 3:
|
||||
return renderResultStep();
|
||||
default:
|
||||
return renderUploadStep();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', paddingRight: '32px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
background: designTokens.colors.primary.gradient,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
<UploadOutlined />
|
||||
</div>
|
||||
<span style={{ fontSize: '18px', fontWeight: 600 }}>导入机柜</span>
|
||||
</div>
|
||||
}
|
||||
open={visible}
|
||||
closeIcon={<CloseButton />}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={720}
|
||||
destroyOnHidden
|
||||
styles={{
|
||||
header: {
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
padding: '16px 24px',
|
||||
position: 'relative',
|
||||
},
|
||||
body: { padding: '24px' },
|
||||
}}
|
||||
style={{ borderRadius: designTokens.borderRadius.large }}
|
||||
>
|
||||
{/* 步骤条 */}
|
||||
<Steps
|
||||
current={currentStep}
|
||||
items={steps}
|
||||
style={{ marginBottom: '24px' }}
|
||||
size="small"
|
||||
/>
|
||||
|
||||
{/* 步骤内容 */}
|
||||
{renderStepContent()}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(RackImportModal);
|
||||
@@ -224,9 +224,9 @@ const BackupManagement = () => {
|
||||
|
||||
const handleDownload = async filename => {
|
||||
try {
|
||||
const blob = await backupAPI.download(filename);
|
||||
const response = await backupAPI.download(filename);
|
||||
// 创建临时链接并触发下载
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const url = window.URL.createObjectURL(response.data);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
Checkbox,
|
||||
Dropdown,
|
||||
Statistic,
|
||||
Upload,
|
||||
Spin,
|
||||
Pagination,
|
||||
} from 'antd';
|
||||
@@ -51,6 +50,7 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { designTokens } from '../config/theme';
|
||||
import CloseButton from '../components/CloseButton';
|
||||
import RackImportModal from '../components/rack/RackImportModal';
|
||||
import api from '../api';
|
||||
|
||||
const { Option } = Select;
|
||||
@@ -312,10 +312,6 @@ function RackManagement() {
|
||||
showTotal: total => `共 ${total} 条记录`,
|
||||
});
|
||||
const [form] = Form.useForm();
|
||||
const [importProgress, setImportProgress] = useState(0);
|
||||
const [importPhase, setImportPhase] = useState('');
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [importResult, setImportResult] = useState(null);
|
||||
|
||||
const fetchRacks = useCallback(
|
||||
async (page = 1, pageSize = 10) => {
|
||||
@@ -475,7 +471,7 @@ function RackManagement() {
|
||||
});
|
||||
|
||||
// 创建下载链接
|
||||
const blob = new Blob([response], {
|
||||
const blob = new Blob([response.data], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
@@ -499,14 +495,16 @@ function RackManagement() {
|
||||
// 导出租机柜数据
|
||||
const handleExport = useCallback(async () => {
|
||||
try {
|
||||
message.loading('正在导出租机柜数据...', 0);
|
||||
const hasSelection = selectedRackIds.length > 0;
|
||||
message.loading(hasSelection ? '正在导出选中机柜数据...' : '正在导出全部机柜数据...', 0);
|
||||
|
||||
const response = await api.get('/racks/export', {
|
||||
params: hasSelection ? { rackIds: selectedRackIds.join(',') } : {},
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
// 创建下载链接
|
||||
const blob = new Blob([response], {
|
||||
const blob = new Blob([response.data], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
@@ -530,85 +528,82 @@ function RackManagement() {
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
message.destroy();
|
||||
message.success('机柜导出成功');
|
||||
message.success(hasSelection ? `已导出 ${selectedRackIds.length} 个选中机柜` : '已导出全部机柜');
|
||||
} catch (error) {
|
||||
message.destroy();
|
||||
message.error('机柜导出失败');
|
||||
console.error('机柜导出失败:', error);
|
||||
}
|
||||
}, []);
|
||||
}, [selectedRackIds]);
|
||||
|
||||
const handleImport = useCallback(
|
||||
async file => {
|
||||
try {
|
||||
setIsImporting(true);
|
||||
setImportProgress(0);
|
||||
setImportPhase('正在上传文件...');
|
||||
setImportResult(null);
|
||||
(file, { onProgress } = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const doImport = async () => {
|
||||
try {
|
||||
onProgress?.(10, '正在上传文件...');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await api.post('/racks/import', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
onProgress?.(50, '正在处理数据...');
|
||||
|
||||
setImportProgress(100);
|
||||
setImportPhase('导入完成');
|
||||
const response = await api.post('/racks/import', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
|
||||
const resData = response;
|
||||
const importResult = {
|
||||
total: resData.total || 0,
|
||||
successCount: resData.imported || 0,
|
||||
duplicates: resData.duplicates || 0,
|
||||
failedCount: 0,
|
||||
errors: [],
|
||||
createdRacks: resData.createdRacks || [],
|
||||
skippedRacks: resData.skippedRacks || [],
|
||||
onProgress?.(100, '导入完成');
|
||||
|
||||
const resData = response;
|
||||
const importResult = {
|
||||
total: resData.total || 0,
|
||||
successCount: resData.imported || 0,
|
||||
duplicates: resData.duplicates || 0,
|
||||
failedCount: 0,
|
||||
errors: [],
|
||||
createdRacks: resData.createdRacks || [],
|
||||
skippedRacks: resData.skippedRacks || [],
|
||||
};
|
||||
|
||||
if (resData.details && Array.isArray(resData.details)) {
|
||||
importResult.failedCount = resData.details.length;
|
||||
importResult.errors = resData.details.map(item => ({
|
||||
row: item.row,
|
||||
error: item.errors.join(';'),
|
||||
}));
|
||||
}
|
||||
|
||||
if (resData.success) {
|
||||
message.success('机柜导入成功');
|
||||
} else {
|
||||
message.warning(resData.message || '部分记录导入失败');
|
||||
}
|
||||
|
||||
fetchRacks();
|
||||
resolve(importResult);
|
||||
} catch (error) {
|
||||
const errorData = error.response?.data;
|
||||
if (errorData?.details && Array.isArray(errorData.details)) {
|
||||
const importResult = {
|
||||
total: errorData.total || 0,
|
||||
successCount: 0,
|
||||
failedCount: errorData.details.length,
|
||||
errors: errorData.details.map(item => ({
|
||||
row: item.row,
|
||||
error: item.errors.join(';'),
|
||||
})),
|
||||
};
|
||||
resolve(importResult);
|
||||
} else {
|
||||
message.error(errorData?.error || errorData?.message || '机柜导入失败');
|
||||
console.error('机柜导入失败:', error);
|
||||
reject(new Error(errorData?.error || errorData?.message || '机柜导入失败'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (resData.details && Array.isArray(resData.details)) {
|
||||
importResult.failedCount = resData.details.length;
|
||||
importResult.errors = resData.details.map(item => ({
|
||||
row: item.row,
|
||||
error: item.errors.join(';'),
|
||||
}));
|
||||
}
|
||||
|
||||
setImportResult(importResult);
|
||||
setIsImporting(false);
|
||||
|
||||
if (resData.success) {
|
||||
message.success('机柜导入成功');
|
||||
} else {
|
||||
message.warning(resData.message || '部分记录导入失败');
|
||||
}
|
||||
|
||||
fetchRacks();
|
||||
return false;
|
||||
} catch (error) {
|
||||
setIsImporting(false);
|
||||
setImportProgress(0);
|
||||
|
||||
const errorData = error.response?.data;
|
||||
if (errorData?.details && Array.isArray(errorData.details)) {
|
||||
const importResult = {
|
||||
total: errorData.total || 0,
|
||||
successCount: 0,
|
||||
failedCount: errorData.details.length,
|
||||
errors: errorData.details.map(item => ({
|
||||
row: item.row,
|
||||
error: item.errors.join(';'),
|
||||
})),
|
||||
};
|
||||
setImportResult(importResult);
|
||||
setImportPhase('导入失败');
|
||||
} else {
|
||||
message.error(errorData?.error || errorData?.message || '机柜导入失败');
|
||||
console.error('机柜导入失败:', error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
doImport();
|
||||
});
|
||||
},
|
||||
[fetchRacks]
|
||||
);
|
||||
@@ -911,8 +906,12 @@ function RackManagement() {
|
||||
>
|
||||
导入
|
||||
</Button>
|
||||
<Button icon={<DownloadOutlined />} onClick={handleExport} style={actionButtonStyle}>
|
||||
导出
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleExport}
|
||||
style={actionButtonStyle}
|
||||
>
|
||||
{selectedRackIds.length > 0 ? `导出选中 (${selectedRackIds.length})` : '导出全部'}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -1273,284 +1272,12 @@ function RackManagement() {
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', paddingRight: '32px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '4px',
|
||||
height: '20px',
|
||||
background: designTokens.colors.primary.gradient,
|
||||
borderRadius: '2px',
|
||||
}}
|
||||
/>
|
||||
导入机柜
|
||||
</div>
|
||||
}
|
||||
open={importModalVisible}
|
||||
closeIcon={<CloseButton />}
|
||||
onCancel={() => {
|
||||
setImportModalVisible(false);
|
||||
setImportProgress(0);
|
||||
setImportPhase('');
|
||||
setImportResult(null);
|
||||
setIsImporting(false);
|
||||
}}
|
||||
footer={null}
|
||||
width={650}
|
||||
destroyOnHidden
|
||||
styles={{
|
||||
body: { padding: '24px' },
|
||||
header: {
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
padding: '16px 24px',
|
||||
position: 'relative',
|
||||
},
|
||||
}}
|
||||
style={{ borderRadius: '16px' }}
|
||||
>
|
||||
{!isImporting && !importResult ? (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
padding: '16px',
|
||||
background: designTokens.colors.primary.bgGradient,
|
||||
borderRadius: '12px',
|
||||
marginBottom: '20px',
|
||||
border: '1px solid #e8e8e8',
|
||||
}}
|
||||
>
|
||||
<p style={{ margin: '0 0 12px 0', fontWeight: '600', color: '#262626' }}>
|
||||
请上传XLSX格式的机柜数据文件
|
||||
</p>
|
||||
<p style={{ margin: 0, color: '#8c8c8c', fontSize: '12px' }}>支持的编码格式:UTF-8</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<p style={{ fontWeight: '600', marginBottom: '12px', color: '#262626' }}>
|
||||
Excel文件格式要求:
|
||||
</p>
|
||||
<ul
|
||||
style={{
|
||||
paddingLeft: '20px',
|
||||
marginBottom: '10px',
|
||||
color: '#595959',
|
||||
lineHeight: '1.8',
|
||||
}}
|
||||
>
|
||||
<li>必填字段:机柜ID、机柜名称、所属机房名称、高度(U)、最大功率(W)、状态</li>
|
||||
<li>状态值:active(在用)、maintenance(维护中)、inactive(停用)</li>
|
||||
<li>高度和功率必须是数字格式</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
padding: '16px',
|
||||
background: '#f6f6f6',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleDownloadTemplate}
|
||||
style={actionButtonStyle}
|
||||
>
|
||||
下载导入模板
|
||||
</Button>
|
||||
<span style={{ color: '#8c8c8c', fontSize: '12px' }}>包含示例数据的XLSX模板文件</span>
|
||||
</div>
|
||||
|
||||
<Upload
|
||||
name="file"
|
||||
accept=".xlsx, .xls"
|
||||
showUploadList={false}
|
||||
beforeUpload={handleImport}
|
||||
maxCount={1}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<UploadOutlined />}
|
||||
block
|
||||
style={{
|
||||
...primaryButtonStyle,
|
||||
height: '48px',
|
||||
fontSize: '16px',
|
||||
}}
|
||||
>
|
||||
选择Excel文件
|
||||
</Button>
|
||||
</Upload>
|
||||
</div>
|
||||
) : isImporting ? (
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: '50%',
|
||||
background: designTokens.colors.primary.gradient,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: '16px',
|
||||
color: '#fff',
|
||||
fontSize: '20px',
|
||||
}}
|
||||
>
|
||||
<UploadOutlined spin />
|
||||
</div>
|
||||
<div>
|
||||
<p
|
||||
style={{
|
||||
margin: '0 0 4px 0',
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
fontSize: '16px',
|
||||
}}
|
||||
>
|
||||
正在导入机柜数据
|
||||
</p>
|
||||
<p style={{ margin: 0, color: designTokens.colors.primary.main, fontSize: '14px' }}>
|
||||
{importPhase}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Progress
|
||||
percent={importProgress}
|
||||
status="active"
|
||||
strokeColor={{
|
||||
'0%': '#667eea',
|
||||
'100%': '#764ba2',
|
||||
}}
|
||||
format={() => `${importProgress}%`}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
importResult && (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
padding: '20px',
|
||||
background: '#f6f6f6',
|
||||
borderRadius: '12px',
|
||||
marginBottom: '20px',
|
||||
}}
|
||||
>
|
||||
<p style={{ marginBottom: '12px', fontWeight: '600' }}>导入结果:</p>
|
||||
<p style={{ margin: '8px 0', color: '#52c41a' }}>
|
||||
✓ 总记录数:{importResult.total || 0}
|
||||
</p>
|
||||
<p style={{ margin: '8px 0', color: '#52c41a' }}>
|
||||
✓ 成功导入:{importResult.successCount || 0}
|
||||
</p>
|
||||
{importResult.failedCount > 0 && (
|
||||
<p style={{ margin: '8px 0', color: '#ff4d4f' }}>
|
||||
✗ 导入失败:{importResult.failedCount}
|
||||
</p>
|
||||
)}
|
||||
{importResult.duplicates > 0 && (
|
||||
<p style={{ margin: '8px 0', color: '#faad14' }}>
|
||||
⚠ 跳过(已存在):{importResult.duplicates}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{importResult.createdRacks && importResult.createdRacks.length > 0 && (
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<p style={{ fontWeight: '600', marginBottom: '8px', color: '#52c41a' }}>
|
||||
✓ 本次新增机柜({importResult.createdRacks.length}):
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
maxHeight: '150px',
|
||||
overflow: 'auto',
|
||||
background: '#f6ffed',
|
||||
border: '1px solid #b7eb8f',
|
||||
borderRadius: '8px',
|
||||
padding: '12px',
|
||||
}}
|
||||
>
|
||||
{importResult.createdRacks.map((rack, idx) => (
|
||||
<div key={idx} style={{ fontSize: '13px', marginBottom: '4px' }}>
|
||||
• {rack.rackId} - {rack.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{importResult.skippedRacks && importResult.skippedRacks.length > 0 && (
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<p style={{ fontWeight: '600', marginBottom: '8px', color: '#faad14' }}>
|
||||
⚠ 已跳过机柜({importResult.skippedRacks.length}):
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
maxHeight: '150px',
|
||||
overflow: 'auto',
|
||||
background: '#fffbe6',
|
||||
border: '1px solid #ffe58f',
|
||||
borderRadius: '8px',
|
||||
padding: '12px',
|
||||
}}
|
||||
>
|
||||
{importResult.skippedRacks.map((rack, idx) => (
|
||||
<div key={idx} style={{ fontSize: '13px', marginBottom: '4px' }}>
|
||||
• {rack.rackId} - {rack.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{importResult.errors && importResult.errors.length > 0 && (
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<p style={{ fontWeight: '600', marginBottom: '8px', color: '#ff4d4f' }}>
|
||||
错误详情:
|
||||
</p>
|
||||
{importResult.errors.slice(0, 5).map((err, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
background: '#fff2f0',
|
||||
borderRadius: '6px',
|
||||
marginBottom: '8px',
|
||||
fontSize: '13px',
|
||||
color: '#cf1322',
|
||||
}}
|
||||
>
|
||||
第{err.row || idx + 1}行:{err.error}
|
||||
</div>
|
||||
))}
|
||||
{importResult.errors.length > 5 && (
|
||||
<p style={{ color: '#8c8c8c', fontSize: '12px' }}>
|
||||
还有 {importResult.errors.length - 5} 处错误...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setImportModalVisible(false);
|
||||
setImportResult(null);
|
||||
fetchRacks();
|
||||
}}
|
||||
style={primaryButtonStyle}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</Modal>
|
||||
<RackImportModal
|
||||
visible={importModalVisible}
|
||||
onClose={() => setImportModalVisible(false)}
|
||||
onImport={handleImport}
|
||||
onDownloadTemplate={handleDownloadTemplate}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user