feat(device): 增强导出功能,支持按筛选条件导出全部设备
- 导出接口从 GET 改为 POST,避免 URL 长度限制 - 新增 filters 参数支持按关键词、状态、类型、机房、机柜筛选导出 - 修复删除/编辑设备后分页跳回第一页的问题 - 导出模态框改为传递计数而非数组,支持显示总数 - 使用唯一文件名避免并发导出冲突 - 补充 idle 状态映射
This commit is contained in:
+95
-17
@@ -1877,9 +1877,10 @@ router.put('/batch-move', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 增强导出设备数据(支持所有字段和自定义字段)
|
// 增强导出设备数据(支持所有字段和自定义字段)
|
||||||
router.get('/enhanced-export', async (req, res) => {
|
// 改为 POST 请求,支持按筛选条件导出全部设备,避免 URL 长度限制
|
||||||
|
router.post('/enhanced-export', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { deviceIds, format = 'csv' } = req.query;
|
const { deviceIds, format = 'csv', filters } = req.body;
|
||||||
|
|
||||||
// 从数据库读取所有字段配置(不过滤 visible,以导出所有信息)
|
// 从数据库读取所有字段配置(不过滤 visible,以导出所有信息)
|
||||||
const allFields = await DeviceField.findAll({
|
const allFields = await DeviceField.findAll({
|
||||||
@@ -1896,32 +1897,103 @@ router.get('/enhanced-export', async (req, res) => {
|
|||||||
|
|
||||||
// 构建查询条件
|
// 构建查询条件
|
||||||
const where = {};
|
const where = {};
|
||||||
if (deviceIds) {
|
|
||||||
const ids = Array.isArray(deviceIds) ? deviceIds : [deviceIds];
|
// 按指定 deviceIds 导出(选择的行 / 当前页)
|
||||||
where.deviceId = { [Op.in]: ids };
|
if (deviceIds && Array.isArray(deviceIds) && deviceIds.length > 0) {
|
||||||
|
where.deviceId = { [Op.in]: deviceIds };
|
||||||
|
} else if (filters) {
|
||||||
|
// 按筛选条件导出全部设备(复用列表查询的筛选逻辑)
|
||||||
|
const { keyword, status, type, rackId, roomId } = filters;
|
||||||
|
|
||||||
|
if (keyword) {
|
||||||
|
const escapedKeyword = keyword
|
||||||
|
.replace(/\\/g, '\\\\')
|
||||||
|
.replace(/'/g, "''")
|
||||||
|
.replace(/%/g, '\\%')
|
||||||
|
.replace(/_/g, '\\_');
|
||||||
|
|
||||||
|
const searchConditions = [
|
||||||
|
{ deviceId: { [Op.like]: `%${escapedKeyword}%` } },
|
||||||
|
{ name: { [Op.like]: `%${escapedKeyword}%` } },
|
||||||
|
{ type: { [Op.like]: `%${escapedKeyword}%` } },
|
||||||
|
{ model: { [Op.like]: `%${escapedKeyword}%` } },
|
||||||
|
{ serialNumber: { [Op.like]: `%${escapedKeyword}%` } },
|
||||||
|
{ ipAddress: { [Op.like]: `%${escapedKeyword}%` } },
|
||||||
|
{ description: { [Op.like]: `%${escapedKeyword}%` } },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 动态获取文本类型的自定义字段
|
||||||
|
const customFields = await DeviceField.findAll({
|
||||||
|
where: {
|
||||||
|
isSystem: false,
|
||||||
|
fieldType: { [Op.in]: ['string', 'textarea'] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (customFields.length > 0) {
|
||||||
|
const jsonConditions = customFields.map(field => {
|
||||||
|
const safeFieldName = field.fieldName
|
||||||
|
.replace(/\\/g, '\\\\')
|
||||||
|
.replace(/"/g, '\\"')
|
||||||
|
.replace(/'/g, "''");
|
||||||
|
|
||||||
|
if (dbDialect === 'mysql') {
|
||||||
|
return sequelize.literal(
|
||||||
|
`JSON_UNQUOTE(JSON_EXTRACT(customFields, '$."${safeFieldName}"')) LIKE '%${escapedKeyword}%' ESCAPE '\\\\'`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return sequelize.literal(
|
||||||
|
`CAST(json_extract(customFields, '$.${safeFieldName}') AS TEXT) LIKE '%${escapedKeyword}%' ESCAPE '\\'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
searchConditions.push(...jsonConditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询设备数据
|
where[Op.or] = searchConditions;
|
||||||
const devices = await Device.findAll({
|
}
|
||||||
where,
|
|
||||||
include: [
|
if (status && status !== 'all') {
|
||||||
|
where.status = status;
|
||||||
|
}
|
||||||
|
if (type && type !== 'all') {
|
||||||
|
where.type = type;
|
||||||
|
}
|
||||||
|
if (rackId && rackId !== 'all') {
|
||||||
|
where.rackId = rackId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询设备数据(不分页,导出全部匹配结果)
|
||||||
|
const includeConfig = [
|
||||||
{
|
{
|
||||||
model: Rack,
|
model: Rack,
|
||||||
include: [{ model: Room }],
|
include: [{ model: Room }],
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
|
|
||||||
|
// 如果按筛选条件导出且有机房筛选,添加 Rack 的 where 条件
|
||||||
|
if (filters && filters.roomId && filters.roomId !== 'all') {
|
||||||
|
includeConfig[0].where = { roomId: filters.roomId };
|
||||||
|
}
|
||||||
|
|
||||||
|
const devices = await Device.findAll({
|
||||||
|
where,
|
||||||
|
include: includeConfig,
|
||||||
|
distinct: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (devices.length === 0) {
|
if (devices.length === 0) {
|
||||||
return res.status(404).json({ error: '未找到指定的设备' });
|
return res.status(404).json({ error: '未找到指定的设备' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 状态和类型映射
|
// 状态和类型映射(补充 idle 状态)
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
running: '运行中',
|
running: '运行中',
|
||||||
maintenance: '维护中',
|
maintenance: '维护中',
|
||||||
offline: '离线',
|
offline: '离线',
|
||||||
fault: '故障',
|
fault: '故障',
|
||||||
|
idle: '空闲',
|
||||||
};
|
};
|
||||||
const typeMap = {
|
const typeMap = {
|
||||||
server: '服务器',
|
server: '服务器',
|
||||||
@@ -2019,8 +2091,12 @@ router.get('/enhanced-export', async (req, res) => {
|
|||||||
return res.status(400).json({ error: '没有可导出的字段' });
|
return res.status(400).json({ error: '没有可导出的字段' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用唯一文件名避免并发导出冲突
|
||||||
|
const exportFileName = `enhanced_export_${Date.now()}_${Math.random().toString(36).slice(2, 8)}.csv`;
|
||||||
|
const exportFilePath = path.join(__dirname, '../temp', exportFileName);
|
||||||
|
|
||||||
const csvWriter = createObjectCsvWriter({
|
const csvWriter = createObjectCsvWriter({
|
||||||
path: path.join(__dirname, '../temp/enhanced_export.csv'),
|
path: exportFilePath,
|
||||||
header: headers,
|
header: headers,
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
});
|
});
|
||||||
@@ -2031,17 +2107,19 @@ router.get('/enhanced-export', async (req, res) => {
|
|||||||
|
|
||||||
await csvWriter.writeRecords(exportData);
|
await csvWriter.writeRecords(exportData);
|
||||||
|
|
||||||
const csvContent = fs.readFileSync(
|
const csvContent = fs.readFileSync(exportFilePath, 'utf8');
|
||||||
path.join(__dirname, '../temp/enhanced_export.csv'),
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
const gbkContent = iconv.encode(csvContent, 'gbk');
|
const gbkContent = iconv.encode(csvContent, 'gbk');
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'text/csv');
|
res.setHeader('Content-Type', 'text/csv');
|
||||||
res.setHeader('Content-Disposition', 'attachment; filename=devices.csv');
|
res.setHeader('Content-Disposition', 'attachment; filename=devices.csv');
|
||||||
res.send(gbkContent);
|
res.send(gbkContent);
|
||||||
|
|
||||||
fs.unlinkSync(path.join(__dirname, '../temp/enhanced_export.csv'));
|
// 清理临时文件
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(exportFilePath);
|
||||||
|
} catch (cleanupErr) {
|
||||||
|
logger.warn('清理导出临时文件失败', { error: cleanupErr.message });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// JSON 导出
|
// JSON 导出
|
||||||
res.setHeader('Content-Type', 'application/json');
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
|||||||
+25
-16
@@ -786,30 +786,39 @@ paths:
|
|||||||
description: 导入成功
|
description: 导入成功
|
||||||
|
|
||||||
/api/devices/enhanced-export:
|
/api/devices/enhanced-export:
|
||||||
get:
|
post:
|
||||||
summary: 增强导出设备数据
|
summary: 增强导出设备数据(支持按筛选条件导出全部设备)
|
||||||
tags: [devices]
|
tags: [devices]
|
||||||
parameters:
|
requestBody:
|
||||||
- name: deviceIds
|
required: true
|
||||||
in: query
|
content:
|
||||||
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
deviceIds:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
type: string
|
type: string
|
||||||
- name: format
|
description: 指定导出的设备ID列表(与filters二选一)
|
||||||
in: query
|
format:
|
||||||
schema:
|
|
||||||
type: string
|
type: string
|
||||||
enum: [csv, json]
|
enum: [csv, json]
|
||||||
default: csv
|
default: csv
|
||||||
- name: fields
|
filters:
|
||||||
in: query
|
type: object
|
||||||
schema:
|
description: 筛选条件(导出全部设备时使用)
|
||||||
|
properties:
|
||||||
|
keyword:
|
||||||
type: string
|
type: string
|
||||||
description: JSON格式的字段列表
|
status:
|
||||||
- name: fieldLabels
|
type: string
|
||||||
in: query
|
type:
|
||||||
schema:
|
type: string
|
||||||
|
roomId:
|
||||||
|
type: string
|
||||||
|
rackId:
|
||||||
type: string
|
type: string
|
||||||
description: JSON格式的字段标签映射
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: 导出成功
|
description: 导出成功
|
||||||
|
|||||||
+11
-7
@@ -646,17 +646,21 @@ POST /api/devices/import
|
|||||||
### 增强导出设备数据
|
### 增强导出设备数据
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /api/devices/enhanced-export
|
POST /api/devices/enhanced-export
|
||||||
```
|
```
|
||||||
|
|
||||||
**查询参数:**
|
**请求体参数:**
|
||||||
|
|
||||||
| 参数名 | 类型 | 描述 |
|
| 参数名 | 类型 | 描述 |
|
||||||
|--------|------|------|
|
|--------|------|------|
|
||||||
| deviceIds | string/array | 指定导出的设备ID |
|
| deviceIds | array | 指定导出的设备ID列表(与filters二选一) |
|
||||||
| format | string | 导出格式(csv/json) |
|
| format | string | 导出格式(csv/json),默认csv |
|
||||||
| fields | string | JSON格式的字段列表 |
|
| filters | object | 筛选条件(导出全部设备时使用) |
|
||||||
| fieldLabels | string | JSON格式的字段标签映射 |
|
| filters.keyword | string | 搜索关键词 |
|
||||||
|
| filters.status | string | 设备状态筛选 |
|
||||||
|
| filters.type | string | 设备类型筛选 |
|
||||||
|
| filters.roomId | string | 机房ID筛选 |
|
||||||
|
| filters.rackId | string | 机柜ID筛选 |
|
||||||
|
|
||||||
### 获取设备的工单列表
|
### 获取设备的工单列表
|
||||||
|
|
||||||
@@ -2239,7 +2243,7 @@ GET /health
|
|||||||
| /api/devices/import-template | GET | 获取设备导入模板 |
|
| /api/devices/import-template | GET | 获取设备导入模板 |
|
||||||
| /api/devices/export | GET | 导出设备数据 |
|
| /api/devices/export | GET | 导出设备数据 |
|
||||||
| /api/devices/import | POST | 导入设备数据 |
|
| /api/devices/import | POST | 导入设备数据 |
|
||||||
| /api/devices/enhanced-export | GET | 增强导出设备数据 |
|
| /api/devices/enhanced-export | POST | 增强导出设备数据 |
|
||||||
| /api/devices/:deviceId/tickets | GET | 获取设备的工单列表 |
|
| /api/devices/:deviceId/tickets | GET | 获取设备的工单列表 |
|
||||||
| /api/deviceFields | GET/POST | 设备字段列表/创建 |
|
| /api/deviceFields | GET/POST | 设备字段列表/创建 |
|
||||||
| /api/deviceFields/:id | PUT/DELETE | 设备字段更新/删除 |
|
| /api/deviceFields/:id | PUT/DELETE | 设备字段更新/删除 |
|
||||||
|
|||||||
@@ -13,16 +13,27 @@ const modalHeaderStyle = {
|
|||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备导出模态框
|
||||||
|
* @param {boolean} visible - 是否显示
|
||||||
|
* @param {number} selectedCount - 已选择的设备数量
|
||||||
|
* @param {number} currentPageCount - 当前页设备数量
|
||||||
|
* @param {number} totalCount - 符合筛选条件的设备总数
|
||||||
|
* @param {Function} onExport - 导出回调
|
||||||
|
* @param {Function} onCancel - 取消回调
|
||||||
|
*/
|
||||||
const ExportModal = ({
|
const ExportModal = ({
|
||||||
visible,
|
visible,
|
||||||
selectedDevices,
|
selectedCount,
|
||||||
currentPageDevices,
|
currentPageCount,
|
||||||
allDevices,
|
totalCount,
|
||||||
onExport,
|
onExport,
|
||||||
onCancel,
|
onCancel,
|
||||||
}) => {
|
}) => {
|
||||||
|
const hasSelection = selectedCount > 0;
|
||||||
const [exportFormat, setExportFormat] = useState('csv');
|
const [exportFormat, setExportFormat] = useState('csv');
|
||||||
const [exportScope, setExportScope] = useState('selected');
|
// 有选中设备时默认导出选中行,否则默认导出全部
|
||||||
|
const [exportScope, setExportScope] = useState(hasSelection ? 'selected' : 'all');
|
||||||
const [exportLoading, setExportLoading] = useState(false);
|
const [exportLoading, setExportLoading] = useState(false);
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
@@ -41,11 +52,11 @@ const ExportModal = ({
|
|||||||
const getScopeLabel = () => {
|
const getScopeLabel = () => {
|
||||||
switch (exportScope) {
|
switch (exportScope) {
|
||||||
case 'selected':
|
case 'selected':
|
||||||
return `选择的行 (${selectedDevices.length} 个)`;
|
return `选择的行 (${selectedCount} 个)`;
|
||||||
case 'currentPage':
|
case 'currentPage':
|
||||||
return `当前页 (${currentPageDevices.length} 个)`;
|
return `当前页 (${currentPageCount} 个)`;
|
||||||
case 'all':
|
case 'all':
|
||||||
return `全部设备 (${allDevices.length} 个)`;
|
return `全部设备 (${totalCount} 个)`;
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -114,9 +125,11 @@ const ExportModal = ({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="导出范围">
|
<Form.Item label="导出范围">
|
||||||
<Select value={exportScope} onChange={setExportScope} style={{ width: '100%' }}>
|
<Select value={exportScope} onChange={setExportScope} style={{ width: '100%' }}>
|
||||||
|
{hasSelection && (
|
||||||
<Option value="selected">{getScopeLabel()}</Option>
|
<Option value="selected">{getScopeLabel()}</Option>
|
||||||
<Option value="currentPage">当前页 ({currentPageDevices.length} 个)</Option>
|
)}
|
||||||
<Option value="all">全部设备 ({allDevices.length} 个)</Option>
|
<Option value="currentPage">当前页 ({currentPageCount} 个)</Option>
|
||||||
|
<Option value="all">全部设备 ({totalCount} 个)</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<div style={{ color: '#666', fontSize: '13px' }}>
|
<div style={{ color: '#666', fontSize: '13px' }}>
|
||||||
|
|||||||
@@ -145,6 +145,14 @@ function DeviceManagement() {
|
|||||||
const { devices: deviceList, total } = response.data;
|
const { devices: deviceList, total } = response.data;
|
||||||
|
|
||||||
const processedDevices = deviceList.map(processDeviceData);
|
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);
|
setAllDevices(processedDevices);
|
||||||
setPagination(prev => ({ ...prev, current: page, pageSize, total }));
|
setPagination(prev => ({ ...prev, current: page, pageSize, total }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -266,7 +274,7 @@ function DeviceManagement() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
fetchDevices(1, pagination.pageSize);
|
fetchDevices(pagination.current, pagination.pageSize);
|
||||||
setEditingDevice(null);
|
setEditingDevice(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error.response?.data?.error || error.message || '未知错误';
|
const errorMsg = error.response?.data?.error || error.message || '未知错误';
|
||||||
@@ -349,7 +357,7 @@ function DeviceManagement() {
|
|||||||
message.success(response.data.message || '成功删除所有设备');
|
message.success(response.data.message || '成功删除所有设备');
|
||||||
setSelectedDevices([]);
|
setSelectedDevices([]);
|
||||||
setSelectAll(false);
|
setSelectAll(false);
|
||||||
fetchDevices(1, 50);
|
fetchDevices(1, pagination.pageSize);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('删除所有设备失败');
|
message.error('删除所有设备失败');
|
||||||
console.error('删除所有设备失败:', error);
|
console.error('删除所有设备失败:', error);
|
||||||
@@ -397,7 +405,7 @@ function DeviceManagement() {
|
|||||||
try {
|
try {
|
||||||
await axios.delete(`/api/devices/${deviceId}`);
|
await axios.delete(`/api/devices/${deviceId}`);
|
||||||
message.success('设备删除成功');
|
message.success('设备删除成功');
|
||||||
fetchDevices(1, pagination.pageSize);
|
fetchDevices(pagination.current, pagination.pageSize);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('设备删除失败');
|
message.error('设备删除失败');
|
||||||
console.error('设备删除失败:', error);
|
console.error('设备删除失败:', error);
|
||||||
@@ -453,33 +461,45 @@ function DeviceManagement() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showExportModal = () => {
|
const showExportModal = () => {
|
||||||
if (selectedDevices.length === 0) {
|
|
||||||
message.warning('请先选择要导出的设备');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setExportModalVisible(true);
|
setExportModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理设备数据导出
|
||||||
|
* @param {Object} options - 导出选项
|
||||||
|
* @param {string} options.format - 导出格式(csv/json)
|
||||||
|
* @param {string} options.scope - 导出范围(selected/currentPage/all)
|
||||||
|
*/
|
||||||
const handleEnhancedExport = async ({ format, scope }) => {
|
const handleEnhancedExport = async ({ format, scope }) => {
|
||||||
let deviceIds = [];
|
let requestBody = { format };
|
||||||
if (scope === 'selected') {
|
|
||||||
deviceIds = selectedDevices;
|
|
||||||
} else if (scope === 'currentPage') {
|
|
||||||
deviceIds = allDevices.map(device => device.deviceId);
|
|
||||||
} else if (scope === 'all') {
|
|
||||||
deviceIds = allDevices.map(device => device.deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deviceIds.length === 0) {
|
if (scope === 'selected') {
|
||||||
|
// 导出选中的设备
|
||||||
|
if (selectedDevices.length === 0) {
|
||||||
message.warning('没有可导出的设备');
|
message.warning('没有可导出的设备');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
requestBody.deviceIds = selectedDevices;
|
||||||
|
} else if (scope === 'currentPage') {
|
||||||
|
// 导出当前页设备
|
||||||
|
const currentPageIds = allDevices.map(device => device.deviceId);
|
||||||
|
if (currentPageIds.length === 0) {
|
||||||
|
message.warning('没有可导出的设备');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
requestBody.deviceIds = currentPageIds;
|
||||||
|
} else if (scope === 'all') {
|
||||||
|
// 按筛选条件导出全部设备(由后端查询,不受分页限制)
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const response = await axios.post('/api/devices/enhanced-export', requestBody, {
|
||||||
deviceIds.forEach(id => params.append('deviceIds', id));
|
|
||||||
params.append('format', format);
|
|
||||||
|
|
||||||
const response = await axios.get(`/api/devices/enhanced-export?${params.toString()}`, {
|
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -494,7 +514,7 @@ function DeviceManagement() {
|
|||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
message.success(`成功导出 ${deviceIds.length} 个设备`);
|
message.success('设备数据导出成功');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImport = async (file, callbacks) => {
|
const handleImport = async (file, callbacks) => {
|
||||||
@@ -1294,9 +1314,9 @@ function DeviceManagement() {
|
|||||||
|
|
||||||
<ExportModal
|
<ExportModal
|
||||||
visible={exportModalVisible}
|
visible={exportModalVisible}
|
||||||
selectedDevices={selectedDevices}
|
selectedCount={selectedDevices.length}
|
||||||
currentPageDevices={allDevices}
|
currentPageCount={allDevices.length}
|
||||||
allDevices={allDevices}
|
totalCount={pagination.total}
|
||||||
onExport={handleEnhancedExport}
|
onExport={handleEnhancedExport}
|
||||||
onCancel={() => setExportModalVisible(false)}
|
onCancel={() => setExportModalVisible(false)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user