feat(device): 增强导出功能,支持按筛选条件导出全部设备
- 导出接口从 GET 改为 POST,避免 URL 长度限制 - 新增 filters 参数支持按关键词、状态、类型、机房、机柜筛选导出 - 修复删除/编辑设备后分页跳回第一页的问题 - 导出模态框改为传递计数而非数组,支持显示总数 - 使用唯一文件名避免并发导出冲突 - 补充 idle 状态映射
This commit is contained in:
@@ -13,16 +13,27 @@ const modalHeaderStyle = {
|
||||
fontWeight: 600,
|
||||
};
|
||||
|
||||
/**
|
||||
* 设备导出模态框
|
||||
* @param {boolean} visible - 是否显示
|
||||
* @param {number} selectedCount - 已选择的设备数量
|
||||
* @param {number} currentPageCount - 当前页设备数量
|
||||
* @param {number} totalCount - 符合筛选条件的设备总数
|
||||
* @param {Function} onExport - 导出回调
|
||||
* @param {Function} onCancel - 取消回调
|
||||
*/
|
||||
const ExportModal = ({
|
||||
visible,
|
||||
selectedDevices,
|
||||
currentPageDevices,
|
||||
allDevices,
|
||||
selectedCount,
|
||||
currentPageCount,
|
||||
totalCount,
|
||||
onExport,
|
||||
onCancel,
|
||||
}) => {
|
||||
const hasSelection = selectedCount > 0;
|
||||
const [exportFormat, setExportFormat] = useState('csv');
|
||||
const [exportScope, setExportScope] = useState('selected');
|
||||
// 有选中设备时默认导出选中行,否则默认导出全部
|
||||
const [exportScope, setExportScope] = useState(hasSelection ? 'selected' : 'all');
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
|
||||
const handleExport = async () => {
|
||||
@@ -41,11 +52,11 @@ const ExportModal = ({
|
||||
const getScopeLabel = () => {
|
||||
switch (exportScope) {
|
||||
case 'selected':
|
||||
return `选择的行 (${selectedDevices.length} 个)`;
|
||||
return `选择的行 (${selectedCount} 个)`;
|
||||
case 'currentPage':
|
||||
return `当前页 (${currentPageDevices.length} 个)`;
|
||||
return `当前页 (${currentPageCount} 个)`;
|
||||
case 'all':
|
||||
return `全部设备 (${allDevices.length} 个)`;
|
||||
return `全部设备 (${totalCount} 个)`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -114,9 +125,11 @@ const ExportModal = ({
|
||||
</Form.Item>
|
||||
<Form.Item label="导出范围">
|
||||
<Select value={exportScope} onChange={setExportScope} style={{ width: '100%' }}>
|
||||
<Option value="selected">{getScopeLabel()}</Option>
|
||||
<Option value="currentPage">当前页 ({currentPageDevices.length} 个)</Option>
|
||||
<Option value="all">全部设备 ({allDevices.length} 个)</Option>
|
||||
{hasSelection && (
|
||||
<Option value="selected">{getScopeLabel()}</Option>
|
||||
)}
|
||||
<Option value="currentPage">当前页 ({currentPageCount} 个)</Option>
|
||||
<Option value="all">全部设备 ({totalCount} 个)</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<div style={{ color: '#666', fontSize: '13px' }}>
|
||||
|
||||
@@ -145,6 +145,14 @@ function DeviceManagement() {
|
||||
const { devices: deviceList, total } = response.data;
|
||||
|
||||
const processedDevices = deviceList.map(processDeviceData);
|
||||
|
||||
// 当前页无数据且不是第 1 页时,回退到前一页
|
||||
if (processedDevices.length === 0 && page > 1 && total > 0) {
|
||||
const prevPage = Math.ceil(total / pageSize) || 1;
|
||||
fetchDevices(prevPage, pageSize);
|
||||
return;
|
||||
}
|
||||
|
||||
setAllDevices(processedDevices);
|
||||
setPagination(prev => ({ ...prev, current: page, pageSize, total }));
|
||||
} catch (error) {
|
||||
@@ -266,7 +274,7 @@ function DeviceManagement() {
|
||||
}
|
||||
|
||||
setModalVisible(false);
|
||||
fetchDevices(1, pagination.pageSize);
|
||||
fetchDevices(pagination.current, pagination.pageSize);
|
||||
setEditingDevice(null);
|
||||
} catch (error) {
|
||||
const errorMsg = error.response?.data?.error || error.message || '未知错误';
|
||||
@@ -349,7 +357,7 @@ function DeviceManagement() {
|
||||
message.success(response.data.message || '成功删除所有设备');
|
||||
setSelectedDevices([]);
|
||||
setSelectAll(false);
|
||||
fetchDevices(1, 50);
|
||||
fetchDevices(1, pagination.pageSize);
|
||||
} catch (error) {
|
||||
message.error('删除所有设备失败');
|
||||
console.error('删除所有设备失败:', error);
|
||||
@@ -397,7 +405,7 @@ function DeviceManagement() {
|
||||
try {
|
||||
await axios.delete(`/api/devices/${deviceId}`);
|
||||
message.success('设备删除成功');
|
||||
fetchDevices(1, pagination.pageSize);
|
||||
fetchDevices(pagination.current, pagination.pageSize);
|
||||
} catch (error) {
|
||||
message.error('设备删除失败');
|
||||
console.error('设备删除失败:', error);
|
||||
@@ -453,33 +461,45 @@ function DeviceManagement() {
|
||||
};
|
||||
|
||||
const showExportModal = () => {
|
||||
if (selectedDevices.length === 0) {
|
||||
message.warning('请先选择要导出的设备');
|
||||
return;
|
||||
}
|
||||
setExportModalVisible(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理设备数据导出
|
||||
* @param {Object} options - 导出选项
|
||||
* @param {string} options.format - 导出格式(csv/json)
|
||||
* @param {string} options.scope - 导出范围(selected/currentPage/all)
|
||||
*/
|
||||
const handleEnhancedExport = async ({ format, scope }) => {
|
||||
let deviceIds = [];
|
||||
let requestBody = { format };
|
||||
|
||||
if (scope === 'selected') {
|
||||
deviceIds = selectedDevices;
|
||||
// 导出选中的设备
|
||||
if (selectedDevices.length === 0) {
|
||||
message.warning('没有可导出的设备');
|
||||
return;
|
||||
}
|
||||
requestBody.deviceIds = selectedDevices;
|
||||
} else if (scope === 'currentPage') {
|
||||
deviceIds = allDevices.map(device => device.deviceId);
|
||||
// 导出当前页设备
|
||||
const currentPageIds = allDevices.map(device => device.deviceId);
|
||||
if (currentPageIds.length === 0) {
|
||||
message.warning('没有可导出的设备');
|
||||
return;
|
||||
}
|
||||
requestBody.deviceIds = currentPageIds;
|
||||
} else if (scope === 'all') {
|
||||
deviceIds = allDevices.map(device => device.deviceId);
|
||||
// 按筛选条件导出全部设备(由后端查询,不受分页限制)
|
||||
requestBody.filters = {
|
||||
keyword: debouncedKeyword || undefined,
|
||||
status: status !== 'all' ? status : undefined,
|
||||
type: type !== 'all' ? type : undefined,
|
||||
roomId: roomId !== 'all' ? roomId : undefined,
|
||||
rackId: rackId !== 'all' ? rackId : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (deviceIds.length === 0) {
|
||||
message.warning('没有可导出的设备');
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
deviceIds.forEach(id => params.append('deviceIds', id));
|
||||
params.append('format', format);
|
||||
|
||||
const response = await axios.get(`/api/devices/enhanced-export?${params.toString()}`, {
|
||||
const response = await axios.post('/api/devices/enhanced-export', requestBody, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
@@ -494,7 +514,7 @@ function DeviceManagement() {
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
message.success(`成功导出 ${deviceIds.length} 个设备`);
|
||||
message.success('设备数据导出成功');
|
||||
};
|
||||
|
||||
const handleImport = async (file, callbacks) => {
|
||||
@@ -1294,9 +1314,9 @@ function DeviceManagement() {
|
||||
|
||||
<ExportModal
|
||||
visible={exportModalVisible}
|
||||
selectedDevices={selectedDevices}
|
||||
currentPageDevices={allDevices}
|
||||
allDevices={allDevices}
|
||||
selectedCount={selectedDevices.length}
|
||||
currentPageCount={allDevices.length}
|
||||
totalCount={pagination.total}
|
||||
onExport={handleEnhancedExport}
|
||||
onCancel={() => setExportModalVisible(false)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user