feat(机柜管理): 添加机柜导入导出功能并优化分页查询

- 添加xlsx依赖支持Excel文件处理
- 实现机柜数据的导入导出功能,包括模板下载
- 优化机柜列表的分页查询接口
- 修复机柜可视化页面U位标记位置计算问题
- 更新背景设置和渐变颜色样式
This commit is contained in:
zhang1106
2025-12-22 08:59:44 +08:00
parent 130b2ad5f7
commit c60d13a0df
11 changed files with 726 additions and 38 deletions
+3 -2
View File
@@ -43,8 +43,9 @@ function Dashboard() {
// 获取所有机柜
const racksRes = await axios.get('/api/racks');
const racks = racksRes.data;
const totalRacks = racks.length;
// 机柜API返回的是包含total和racks数组的对象
const totalRacks = racksRes.data.total;
const racks = racksRes.data.racks || [];
// 获取所有机房
const roomsRes = await axios.get('/api/rooms');
+4 -3
View File
@@ -195,7 +195,8 @@ function DeviceManagement() {
const fetchRacks = async () => {
try {
const response = await axios.get('/api/racks');
setRacks(response.data);
// 现在API返回的格式是 { racks: [], total: number }
setRacks(response.data.racks || []);
} catch (error) {
message.error('获取机柜列表失败');
console.error('获取机柜列表失败:', error);
@@ -1006,7 +1007,7 @@ function DeviceManagement() {
}}
footer={null}
width={600}
destroyOnClose
destroyOnHidden
>
{!isImporting && !importResult ? (
<div>
@@ -1131,7 +1132,7 @@ function DeviceManagement() {
</Button>
]}
width={800}
destroyOnClose
destroyOnHidden
>
{selectedDevice && (
<div>
+210 -9
View File
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { Table, Button, Modal, Form, Input, Select, message, Card, Space, InputNumber, Upload, Progress } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, UploadOutlined, DownloadOutlined } from '@ant-design/icons';
import axios from 'axios';
import * as XLSX from 'xlsx';
const { Option } = Select;
@@ -12,15 +13,38 @@ function RackManagement() {
const [modalVisible, setModalVisible] = useState(false);
const [editingRack, setEditingRack] = useState(null);
const [form] = Form.useForm();
// 分页状态
const [pagination, setPagination] = useState({
current: 1,
pageSize: 10,
total: 0,
pageSizeOptions: ['10', '20', '30', '50', '100'],
showSizeChanger: true,
showTotal: (total) => `${total} 条记录`
});
// 导入模态框状态
const [importModalVisible, setImportModalVisible] = useState(false);
const [importProgress, setImportProgress] = useState(0);
const [isImporting, setIsImporting] = useState(false);
const [importResult, setImportResult] = useState(null);
// 获取所有机柜
const fetchRacks = async () => {
const fetchRacks = async (page = 1, pageSize = 10) => {
try {
setLoading(true);
const response = await axios.get('/api/racks');
setRacks(response.data);
const response = await axios.get('/api/racks', {
params: {
page,
pageSize
}
});
// 假设API返回格式为 { racks: [], total: number }
const { racks: data, total } = response.data;
setRacks(data);
// 更新分页状态
setPagination(prev => ({ ...prev, current: page, pageSize, total }));
} catch (error) {
message.error('获取机柜列表失败');
console.error('获取机柜列表失败:', error);
@@ -40,8 +64,13 @@ function RackManagement() {
}
};
// 处理表格分页变化
const handleTableChange = (pagination) => {
fetchRacks(pagination.current, pagination.pageSize);
};
useEffect(() => {
fetchRacks();
fetchRacks(pagination.current, pagination.pageSize);
fetchRooms();
}, []);
@@ -105,11 +134,86 @@ function RackManagement() {
});
};
// 下载导入模板
const handleDownloadTemplate = () => {
// 调用后端API下载模板
window.open('/api/racks/import-template', '_blank');
message.success('模板下载成功');
};
// 导入机柜数据
const handleImport = async (file) => {
try {
setIsImporting(true);
setImportProgress(0);
setImportResult(null);
const formData = new FormData();
formData.append('file', file);
// 模拟进度
const progressInterval = setInterval(() => {
setImportProgress(prev => {
if (prev >= 90) {
clearInterval(progressInterval);
return prev;
}
return prev + 10;
});
}, 500);
// 发送文件到后端处理
const response = await axios.post('/api/racks/import', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
clearInterval(progressInterval);
setImportProgress(100);
setImportResult(response.data);
setIsImporting(false);
// 根据导入结果显示不同的消息
if (response.data.success) {
const { imported, duplicates, total } = response.data;
if (duplicates > 0) {
message.warning(`导入完成,但有 ${duplicates} 条重复记录被跳过`);
} else {
message.success('所有记录导入成功');
}
} else {
message.error(response.data.message || '导入失败');
}
// 阻止自动上传
return false;
} catch (error) {
setIsImporting(false);
let errorMessage = '机柜导入失败';
let errorDetails = null;
if (error.response) {
// 服务器返回了错误响应
errorMessage = error.response.data.message || error.response.data.error || errorMessage;
errorDetails = error.response.data.details;
} else if (error.request) {
// 请求已发送但没有收到响应
errorMessage = '网络错误,服务器没有响应';
} else {
// 请求配置错误
errorMessage = `请求错误: ${error.message}`;
}
setImportResult({ success: false, message: errorMessage, details: errorDetails });
message.error(errorMessage);
console.error('机柜导入失败:', error);
// 阻止自动上传
return false;
}
};
// 状态标签映射
const statusMap = {
@@ -195,6 +299,9 @@ function RackManagement() {
<Button type="primary" icon={<PlusOutlined />} onClick={() => showModal()}>
添加机柜
</Button>
<Button icon={<UploadOutlined />} onClick={() => setImportModalVisible(true)}>
导入机柜
</Button>
</Space>
}>
<Table
@@ -202,7 +309,9 @@ function RackManagement() {
dataSource={racks}
rowKey="rackId"
loading={loading}
pagination={{ pageSize: 10 }}
pagination={pagination}
onChange={handleTableChange}
scroll={{ x: 'max-content' }}
/>
</Card>
@@ -287,6 +396,98 @@ function RackManagement() {
</Form>
</Modal>
{/* 导入机柜模态框 */}
<Modal
title="导入机柜"
open={importModalVisible}
onCancel={() => {
setImportModalVisible(false);
setImportProgress(0);
setImportResult(null);
setIsImporting(false);
}}
footer={null}
width={600}
destroyOnClose
>
{!isImporting && !importResult ? (
<div>
<p>请上传XLSX格式的机柜数据文件</p>
<p style={{ color: '#999', fontSize: '12px', marginBottom: 20 }}>支持的编码格式UTF-8</p>
<div style={{ marginBottom: 20 }}>
<p style={{ fontWeight: 'bold', marginBottom: 8 }}>Excel文件格式要求</p>
<ul style={{ paddingLeft: 20, marginBottom: 10 }}>
<li>必填字段机柜ID机柜名称所属机房名称高度(U)最大功率(W)状态</li>
<li>状态值active(在用)maintenance(维护中)inactive(停用)</li>
<li>高度和功率必须是数字格式</li>
</ul>
</div>
<div style={{ marginBottom: 20 }}>
<Button icon={<DownloadOutlined />} onClick={handleDownloadTemplate} style={{ marginRight: 10 }}>
下载导入模板
</Button>
<span style={{ color: '#999', fontSize: '12px' }}>包含示例数据的XLSX模板文件</span>
</div>
<Upload
name="file"
accept=".xlsx, .xls"
showUploadList={false}
beforeUpload={handleImport}
maxCount={1}
>
<Button type="primary" icon={<UploadOutlined />} block>
选择Excel文件
</Button>
</Upload>
</div>
) : (
<div>
<p>正在导入数据...</p>
<Progress percent={importProgress} status="active" style={{ marginBottom: 20 }} />
{importResult && (
<div>
<p style={{ marginBottom: 10 }}>导入完成</p>
<p>总记录数{importResult.total || 0}</p>
<p>成功{importResult.imported || 0}</p>
<p>重复{importResult.duplicates || 0}</p>
{importResult.details && importResult.details.length > 0 && (
<div>
<p style={{ marginTop: 20, fontWeight: 'bold' }}>导入失败记录</p>
<ul style={{ maxHeight: '300px', overflowY: 'auto', textAlign: 'left' }}>
{importResult.details.map((item, index) => (
<li key={index} style={{ marginBottom: '8px' }}>
<strong> {item.row} </strong>
<ul style={{ margin: '4px 0 0 20px' }}>
{item.errors.map((err, errIndex) => (
<li key={errIndex} style={{ color: '#ff4d4f' }}>{err}</li>
))}
</ul>
</li>
))}
</ul>
</div>
)}
<Button
type="primary"
onClick={() => {
setImportModalVisible(false);
setImportProgress(0);
setImportResult(null);
setIsImporting(false);
fetchRacks();
}}
style={{ marginTop: 20 }}
>
确定
</Button>
</div>
)}
</div>
)}
</Modal>
</div>
);
+13 -16
View File
@@ -152,10 +152,13 @@ function RackVisualization() {
try {
setLoading(true);
setError(null);
const response = await axios.get('/api/racks');
// 为机柜可视化页面传递较大的pageSize以获取所有机柜
const response = await axios.get('/api/racks', { params: { pageSize: 1000 } });
// 验证数据结构并过滤有效机柜
const validRacks = response.data.filter(rack =>
// 机柜API返回的是包含total和racks数组的对象
const racksArray = response.data.racks || [];
const validRacks = racksArray.filter(rack =>
rack &&
typeof rack === 'object' &&
rack.rackId &&
@@ -439,7 +442,7 @@ function RackVisualization() {
key={i}
className="u-mark"
style={{
top: `${(height - i) * uHeight + 1}px`,
top: `${(height - i) * uHeight}px`,
height: '1px',
backgroundColor: i % 5 === 0 ? '#1890ff' : '#999',
width: '100%',
@@ -469,14 +472,11 @@ function RackVisualization() {
const generateLeftUMarks = (height) => {
const marks = [];
const uHeight = 18;
const rackContentHeight = height * uHeight; // 机柜内容总高度
const containerPadding = 6; // 前面板上下padding总和
for (let u = 1; u <= height; u++) {
// 计算从上到下的位置,考虑前面板的padding
// U1在最底部,所以从底部开始计算
const bottomToTop = (u - 1) * uHeight;
const topPosition = rackContentHeight - bottomToTop - uHeight + containerPadding / 2;
// 使用与设备位置计算一致的逻辑
// U1在最底部,计算U位标记的顶部位置(从机柜顶部算起)
const topPosition = (height - u) * uHeight;
marks.push(
<div key={`left-${u}`}>
@@ -515,14 +515,11 @@ function RackVisualization() {
const generateRightUMarks = (height) => {
const marks = [];
const uHeight = 18;
const rackContentHeight = height * uHeight; // 机柜内容总高度
const containerPadding = 6; // 前面板上下padding总和
for (let u = 1; u <= height; u++) {
// 计算从上到下的位置,考虑前面板的padding
// U1在最底部,所以从底部开始计算
const bottomToTop = (u - 1) * uHeight;
const topPosition = rackContentHeight - bottomToTop - uHeight + containerPadding / 2;
// 使用与设备位置计算一致的逻辑
// U1在最底部,计算U位标记的顶部位置(从机柜顶部算起)
const topPosition = (height - u) * uHeight;
marks.push(
<div key={`right-${u}`}>
@@ -834,7 +831,7 @@ function RackVisualization() {
backgroundColor: '#f0f2f5',
background: backgroundType === 'image' && backgroundImage
? `url(${backgroundImage})`
: 'linear-gradient(135deg, #ff0000 0%, #ff7f00 14%, #ffff00 28%, #00ff00 42%, #0000ff 57%, #4b0082 71%, #8b00ff 100%)',
: 'linear-gradient(135deg, #ffcccc 0%, #ffd6b3 14%, #ffffcc 28%, #ccffcc 42%, #cce6ff 57%, #d6ccff 71%, #e6ccff 100%)',
backgroundSize: backgroundType === 'image' ? backgroundSize : 'auto',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',