feat(系统设置): 新增系统设置模块
- 添加SystemSetting模型用于存储系统配置 - 实现系统设置API路由,支持CRUD操作 - 新增系统设置前端页面,包含全局配置、外观设置、数据备份和关于页面 - 设备批量操作增强,支持批量移动、状态变更和导出 - 工单模型允许deviceId为空
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
|
const SystemSetting = sequelize.define('SystemSetting', {
|
||||||
|
settingKey: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false,
|
||||||
|
comment: '设置键名'
|
||||||
|
},
|
||||||
|
settingValue: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
comment: '设置值(JSON格式)'
|
||||||
|
},
|
||||||
|
settingType: {
|
||||||
|
type: DataTypes.STRING(20),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'string',
|
||||||
|
comment: '设置类型: string, number, boolean, json, array'
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: DataTypes.STRING(50),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 'general',
|
||||||
|
comment: '设置分类: general, appearance, backup, about'
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.STRING(255),
|
||||||
|
allowNull: true,
|
||||||
|
comment: '设置描述'
|
||||||
|
},
|
||||||
|
isEditable: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: true,
|
||||||
|
comment: '是否可编辑'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'system_settings',
|
||||||
|
timestamps: true,
|
||||||
|
indexes: [
|
||||||
|
{ fields: ['category'] }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = SystemSetting;
|
||||||
@@ -17,7 +17,7 @@ const Ticket = sequelize.define('Ticket', {
|
|||||||
},
|
},
|
||||||
deviceId: {
|
deviceId: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: true,
|
||||||
comment: '关联设备ID'
|
comment: '关联设备ID'
|
||||||
},
|
},
|
||||||
deviceName: {
|
deviceName: {
|
||||||
|
|||||||
+341
-10
@@ -10,6 +10,7 @@ const Device = require('../models/Device');
|
|||||||
const Rack = require('../models/Rack');
|
const Rack = require('../models/Rack');
|
||||||
const Room = require('../models/Room');
|
const Room = require('../models/Room');
|
||||||
const DeviceField = require('../models/DeviceField');
|
const DeviceField = require('../models/DeviceField');
|
||||||
|
const Ticket = require('../models/Ticket');
|
||||||
|
|
||||||
// 获取所有设备(支持搜索和筛选)
|
// 获取所有设备(支持搜索和筛选)
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
@@ -653,16 +654,23 @@ router.delete('/batch-delete', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { deviceIds } = req.body;
|
const { deviceIds } = req.body;
|
||||||
|
|
||||||
if (!deviceIds || !Array.isArray(deviceIds)) {
|
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
|
||||||
return res.status(400).json({ error: '无效的设备ID列表' });
|
return res.status(400).json({ error: '请提供有效的设备ID列表' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先获取所有设备的功率信息
|
|
||||||
const devices = await Device.findAll({
|
const devices = await Device.findAll({
|
||||||
where: { deviceId: deviceIds }
|
where: { deviceId: { [Op.in]: deviceIds } }
|
||||||
|
});
|
||||||
|
|
||||||
|
await Ticket.update(
|
||||||
|
{ deviceId: null },
|
||||||
|
{ where: { deviceId: { [Op.in]: deviceIds } } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const deletedCount = await Device.destroy({
|
||||||
|
where: { deviceId: { [Op.in]: deviceIds } }
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新机柜功率
|
|
||||||
for (const device of devices) {
|
for (const device of devices) {
|
||||||
const rack = await Rack.findByPk(device.rackId);
|
const rack = await Rack.findByPk(device.rackId);
|
||||||
if (rack) {
|
if (rack) {
|
||||||
@@ -672,12 +680,10 @@ router.delete('/batch-delete', async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除设备
|
res.json({
|
||||||
const deleted = await Device.destroy({
|
message: `批量删除成功,已删除 ${deletedCount} 个设备`,
|
||||||
where: { deviceId: deviceIds }
|
deletedCount
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ message: `成功删除 ${deleted} 个设备` });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
@@ -714,4 +720,329 @@ router.delete('/:deviceId', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 批量上线设备
|
||||||
|
router.put('/batch-online', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { deviceIds } = req.body;
|
||||||
|
|
||||||
|
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
|
||||||
|
return res.status(400).json({ error: '请提供有效的设备ID列表' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新设备状态为运行中
|
||||||
|
const [affectedCount] = await Device.update(
|
||||||
|
{ status: 'running' },
|
||||||
|
{ where: { deviceId: { [Op.in]: deviceIds } } }
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: `批量上线成功,已更新 ${affectedCount} 个设备`,
|
||||||
|
affectedCount
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 批量下线设备
|
||||||
|
router.put('/batch-offline', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { deviceIds } = req.body;
|
||||||
|
|
||||||
|
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
|
||||||
|
return res.status(400).json({ error: '请提供有效的设备ID列表' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新设备状态为离线
|
||||||
|
const [affectedCount] = await Device.update(
|
||||||
|
{ status: 'offline' },
|
||||||
|
{ where: { deviceId: { [Op.in]: deviceIds } } }
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: `批量下线成功,已更新 ${affectedCount} 个设备`,
|
||||||
|
affectedCount
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 批量变更设备状态
|
||||||
|
router.put('/batch-status', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { deviceIds, status } = req.body;
|
||||||
|
|
||||||
|
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
|
||||||
|
return res.status(400).json({ error: '请提供有效的设备ID列表' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const validStatus = ['running', 'maintenance', 'offline', 'fault'];
|
||||||
|
if (!validStatus.includes(status)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: `状态值无效,有效值为:${validStatus.join('、')}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态映射
|
||||||
|
const statusText = {
|
||||||
|
running: '运行中',
|
||||||
|
maintenance: '维护中',
|
||||||
|
offline: '离线',
|
||||||
|
fault: '故障'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新设备状态
|
||||||
|
const [affectedCount] = await Device.update(
|
||||||
|
{ status },
|
||||||
|
{ where: { deviceId: { [Op.in]: deviceIds } } }
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: `批量状态变更成功,已将 ${affectedCount} 个设备状态变更为"${statusText[status]}"`,
|
||||||
|
affectedCount,
|
||||||
|
newStatus: status
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 批量移动设备
|
||||||
|
router.put('/batch-move', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { deviceIds, targetRackId, startPosition } = req.body;
|
||||||
|
|
||||||
|
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
|
||||||
|
return res.status(400).json({ error: '请提供有效的设备ID列表' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetRackId) {
|
||||||
|
return res.status(400).json({ error: '请提供目标机柜ID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startPosition === undefined || startPosition === null) {
|
||||||
|
return res.status(400).json({ error: '请提供起始U位' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证目标机柜是否存在
|
||||||
|
const targetRack = await Rack.findByPk(targetRackId);
|
||||||
|
if (!targetRack) {
|
||||||
|
return res.status(404).json({ error: '目标机柜不存在' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取要移动的设备
|
||||||
|
const devices = await Device.findAll({
|
||||||
|
where: { deviceId: { [Op.in]: deviceIds } }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (devices.length === 0) {
|
||||||
|
return res.status(404).json({ error: '未找到指定的设备' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const movedDevices = [];
|
||||||
|
let currentPosition = parseInt(startPosition);
|
||||||
|
|
||||||
|
// 按原位置排序设备
|
||||||
|
devices.sort((a, b) => a.position - b.position);
|
||||||
|
|
||||||
|
for (const device of devices) {
|
||||||
|
// 计算新位置
|
||||||
|
const newPosition = currentPosition;
|
||||||
|
|
||||||
|
// 验证位置是否在机柜范围内
|
||||||
|
if (newPosition < 1 || newPosition > targetRack.height) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: `设备 ${device.name} 的位置 ${newPosition} 超出机柜高度范围(1-${targetRack.height})`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查目标位置是否被其他设备占用(排除自身)
|
||||||
|
const existingDevice = await Device.findOne({
|
||||||
|
where: {
|
||||||
|
rackId: targetRackId,
|
||||||
|
position: newPosition,
|
||||||
|
deviceId: { [Op.ne]: device.deviceId }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingDevice) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: `机柜 ${targetRack.name} 的位置 ${newPosition} 已被设备 ${existingDevice.name} 占用`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算功率变化
|
||||||
|
const oldPower = device.powerConsumption;
|
||||||
|
|
||||||
|
// 更新设备位置
|
||||||
|
await device.update({
|
||||||
|
rackId: targetRackId,
|
||||||
|
position: newPosition
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新原机柜和新机柜的功率
|
||||||
|
const oldRack = await Rack.findByPk(device.rackId);
|
||||||
|
if (oldRack) {
|
||||||
|
await oldRack.update({
|
||||||
|
currentPower: Math.max(0, oldRack.currentPower - oldPower)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await targetRack.update({
|
||||||
|
currentPower: targetRack.currentPower + oldPower
|
||||||
|
});
|
||||||
|
|
||||||
|
movedDevices.push({
|
||||||
|
deviceId: device.deviceId,
|
||||||
|
name: device.name,
|
||||||
|
oldRackId: device.rackId,
|
||||||
|
newRackId: targetRackId,
|
||||||
|
oldPosition: device.position,
|
||||||
|
newPosition: newPosition
|
||||||
|
});
|
||||||
|
|
||||||
|
// 下一个设备的起始位置 = 当前设备位置 + 当前设备高度
|
||||||
|
currentPosition += device.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: `批量移动成功,已移动 ${movedDevices.length} 个设备`,
|
||||||
|
movedDevices
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 增强导出设备数据(支持自定义字段选择和格式选择)
|
||||||
|
router.get('/enhanced-export', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { deviceIds, format = 'csv', fields, fieldLabels } = req.query;
|
||||||
|
|
||||||
|
// 解析字段列表
|
||||||
|
let selectedFields = [];
|
||||||
|
try {
|
||||||
|
selectedFields = fields ? JSON.parse(fields) : [];
|
||||||
|
} catch (e) {
|
||||||
|
selectedFields = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析字段标签
|
||||||
|
let fieldLabelMap = {};
|
||||||
|
try {
|
||||||
|
fieldLabelMap = fieldLabels ? JSON.parse(fieldLabels) : {};
|
||||||
|
} catch (e) {
|
||||||
|
fieldLabelMap = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
const where = {};
|
||||||
|
if (deviceIds) {
|
||||||
|
const ids = Array.isArray(deviceIds) ? deviceIds : [deviceIds];
|
||||||
|
where.deviceId = { [Op.in]: ids };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询设备数据
|
||||||
|
const devices = await Device.findAll({
|
||||||
|
where,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Rack,
|
||||||
|
include: [
|
||||||
|
{ model: Room }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (devices.length === 0) {
|
||||||
|
return res.status(404).json({ error: '未找到指定的设备' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备导出数据
|
||||||
|
const exportData = devices.map(device => {
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
selectedFields.forEach(fieldName => {
|
||||||
|
// 映射字段名到中文标签
|
||||||
|
const label = fieldLabelMap[fieldName] || fieldName;
|
||||||
|
|
||||||
|
// 根据字段名获取值
|
||||||
|
if (fieldName === 'rackName') {
|
||||||
|
data[label] = device.Rack?.name || '';
|
||||||
|
} else if (fieldName === 'roomName') {
|
||||||
|
data[label] = device.Rack?.Room?.name || '';
|
||||||
|
} else if (fieldName === 'status') {
|
||||||
|
const statusMap = {
|
||||||
|
running: '运行中',
|
||||||
|
maintenance: '维护中',
|
||||||
|
offline: '离线',
|
||||||
|
fault: '故障'
|
||||||
|
};
|
||||||
|
data[label] = statusMap[device.status] || device.status;
|
||||||
|
} else if (fieldName === 'type') {
|
||||||
|
const typeMap = {
|
||||||
|
server: '服务器',
|
||||||
|
switch: '交换机',
|
||||||
|
router: '路由器',
|
||||||
|
storage: '存储设备',
|
||||||
|
other: '其他设备'
|
||||||
|
};
|
||||||
|
data[label] = typeMap[device.type] || device.type;
|
||||||
|
} else if (fieldName === 'purchaseDate' || fieldName === 'warrantyExpiry') {
|
||||||
|
data[label] = device[fieldName] ? new Date(device[fieldName]).toLocaleDateString('zh-CN') : '';
|
||||||
|
} else if (fieldName === 'customFields' && device.customFields) {
|
||||||
|
// 如果选择导出自定义字段,展开为单独的列
|
||||||
|
Object.entries(device.customFields).forEach(([key, value]) => {
|
||||||
|
data[key] = value;
|
||||||
|
});
|
||||||
|
} else if (device[fieldName] !== undefined) {
|
||||||
|
data[label] = device[fieldName];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (format === 'json') {
|
||||||
|
// JSON格式导出
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename=devices.json');
|
||||||
|
res.json({
|
||||||
|
exportTime: new Date().toISOString(),
|
||||||
|
totalCount: devices.length,
|
||||||
|
devices: exportData
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// CSV格式导出
|
||||||
|
const csvWriter = createObjectCsvWriter({
|
||||||
|
path: path.join(__dirname, '../temp/enhanced_export.csv'),
|
||||||
|
header: Object.keys(exportData[0] || {}).map(key => ({ id: key, title: key })),
|
||||||
|
encoding: 'utf8'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 确保temp目录存在
|
||||||
|
if (!fs.existsSync(path.join(__dirname, '../temp'))) {
|
||||||
|
fs.mkdirSync(path.join(__dirname, '../temp'));
|
||||||
|
}
|
||||||
|
|
||||||
|
await csvWriter.writeRecords(exportData);
|
||||||
|
|
||||||
|
const csvContent = fs.readFileSync(path.join(__dirname, '../temp/enhanced_export.csv'), 'utf8');
|
||||||
|
const gbkContent = iconv.encode(csvContent, 'gbk');
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/csv');
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename=devices.csv');
|
||||||
|
|
||||||
|
res.send(gbkContent);
|
||||||
|
|
||||||
|
fs.unlinkSync(path.join(__dirname, '../temp/enhanced_export.csv'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('增强导出失败:', error);
|
||||||
|
res.status(500).json({ error: '增强导出失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -0,0 +1,505 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { Op } = require('sequelize');
|
||||||
|
const SystemSetting = require('../models/SystemSetting');
|
||||||
|
|
||||||
|
// 初始化默认系统设置
|
||||||
|
const initDefaultSettings = async () => {
|
||||||
|
const defaultSettings = [
|
||||||
|
// 全局配置
|
||||||
|
{ settingKey: 'site_name', settingValue: JSON.stringify('机柜管理系统'), settingType: 'string', category: 'general', description: '网站名称', isEditable: true },
|
||||||
|
{ settingKey: 'site_logo', settingValue: JSON.stringify(''), settingType: 'string', category: 'general', description: '网站Logo URL', isEditable: true },
|
||||||
|
{ settingKey: 'timezone', settingValue: JSON.stringify('Asia/Shanghai'), settingType: 'string', category: 'general', description: '时区设置', isEditable: true },
|
||||||
|
{ settingKey: 'date_format', settingValue: JSON.stringify('YYYY-MM-DD'), settingType: 'string', category: 'general', description: '日期格式', isEditable: true },
|
||||||
|
{ settingKey: 'language', settingValue: JSON.stringify('zh-CN'), settingType: 'string', category: 'general', description: '语言设置', isEditable: true },
|
||||||
|
{ settingKey: 'session_timeout', settingValue: JSON.stringify(30), settingType: 'number', category: 'general', description: '会话超时时间(分钟)', isEditable: true },
|
||||||
|
{ settingKey: 'max_login_attempts', settingValue: JSON.stringify(5), settingType: 'number', category: 'general', description: '最大登录尝试次数', isEditable: true },
|
||||||
|
{ settingKey: 'maintenance_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'general', description: '维护模式', isEditable: true },
|
||||||
|
|
||||||
|
// 外观设置
|
||||||
|
{ settingKey: 'primary_color', settingValue: JSON.stringify('#667eea'), settingType: 'string', category: 'appearance', description: '主题主色调', isEditable: true },
|
||||||
|
{ settingKey: 'secondary_color', settingValue: JSON.stringify('#764ba2'), settingType: 'string', category: 'appearance', description: '主题辅助色调', isEditable: true },
|
||||||
|
{ settingKey: 'dark_mode', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'appearance', description: '深色模式', isEditable: true },
|
||||||
|
{ settingKey: 'compact_mode', settingValue: JSON.stringify(false), type: 'boolean', category: 'appearance', description: '紧凑模式', isEditable: true },
|
||||||
|
{ settingKey: 'sidebar_collapsed', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'appearance', description: '侧边栏默认折叠', isEditable: true },
|
||||||
|
{ settingKey: 'table_row_height', settingValue: JSON.stringify('default'), settingType: 'string', category: 'appearance', description: '表格行高: small/default/middle/large', isEditable: true },
|
||||||
|
{ settingKey: 'animation_enabled', settingValue: JSON.stringify(true), settingType: 'boolean', category: 'appearance', description: '启用动画效果', isEditable: true },
|
||||||
|
|
||||||
|
// 数据备份设置
|
||||||
|
{ settingKey: 'auto_backup_enabled', settingValue: JSON.stringify(false), settingType: 'boolean', category: 'backup', description: '启用自动备份', isEditable: true },
|
||||||
|
{ settingKey: 'backup_interval', settingValue: JSON.stringify(24), settingType: 'number', category: 'backup', description: '备份间隔(小时)', isEditable: true },
|
||||||
|
{ settingKey: 'backup_retention', settingValue: JSON.stringify(7), settingType: 'number', category: 'backup', description: '备份保留天数', isEditable: true },
|
||||||
|
{ settingKey: 'backup_path', settingValue: JSON.stringify('./backups'), settingType: 'string', category: 'backup', description: '备份存储路径', isEditable: true },
|
||||||
|
{ settingKey: 'last_backup_time', settingValue: JSON.stringify(null), settingType: 'string', category: 'backup', description: '上次备份时间', isEditable: false },
|
||||||
|
{ settingKey: 'backup_count', settingValue: JSON.stringify(0), settingType: 'number', category: 'backup', description: '备份文件数量', isEditable: false },
|
||||||
|
|
||||||
|
// 关于页面
|
||||||
|
{ settingKey: 'app_version', settingValue: JSON.stringify('1.0.0'), settingType: 'string', category: 'about', description: '应用版本', isEditable: false },
|
||||||
|
{ settingKey: 'company_name', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '公司/组织名称', isEditable: true },
|
||||||
|
{ settingKey: 'contact_email', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '联系邮箱', isEditable: true },
|
||||||
|
{ settingKey: 'contact_phone', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '联系电话', isEditable: true },
|
||||||
|
{ settingKey: 'company_address', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '公司地址', isEditable: true },
|
||||||
|
{ settingKey: 'system_description', settingValue: JSON.stringify('机柜管理系统 - 专业的数据中心设备管理解决方案'), settingType: 'string', category: 'about', description: '系统描述', isEditable: true },
|
||||||
|
{ settingKey: 'privacy_policy', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '隐私政策URL', isEditable: true },
|
||||||
|
{ settingKey: 'terms_of_service', settingValue: JSON.stringify(''), settingType: 'string', category: 'about', description: '服务条款URL', isEditable: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const setting of defaultSettings) {
|
||||||
|
try {
|
||||||
|
const existing = await SystemSetting.findByPk(setting.settingKey);
|
||||||
|
if (!existing) {
|
||||||
|
await SystemSetting.create(setting);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`初始化设置 ${setting.settingKey} 失败:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化默认设置
|
||||||
|
initDefaultSettings();
|
||||||
|
|
||||||
|
// 获取所有设置
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { category } = req.query;
|
||||||
|
const where = {};
|
||||||
|
if (category) {
|
||||||
|
where.category = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = await SystemSetting.findAll({
|
||||||
|
where,
|
||||||
|
order: [['category', 'ASC'], ['settingKey', 'ASC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 格式化返回数据
|
||||||
|
const formattedSettings = {};
|
||||||
|
settings.forEach(setting => {
|
||||||
|
formattedSettings[setting.settingKey] = {
|
||||||
|
value: JSON.parse(setting.settingValue),
|
||||||
|
type: setting.settingType,
|
||||||
|
category: setting.category,
|
||||||
|
description: setting.description,
|
||||||
|
isEditable: setting.isEditable,
|
||||||
|
updatedAt: setting.updatedAt
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(formattedSettings);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取单个设置
|
||||||
|
router.get('/:key', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { key } = req.params;
|
||||||
|
const setting = await SystemSetting.findByPk(key);
|
||||||
|
|
||||||
|
if (!setting) {
|
||||||
|
return res.status(404).json({ error: '设置不存在' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
key: setting.settingKey,
|
||||||
|
value: JSON.parse(setting.settingValue),
|
||||||
|
type: setting.settingType,
|
||||||
|
category: setting.category,
|
||||||
|
description: setting.description,
|
||||||
|
isEditable: setting.isEditable,
|
||||||
|
updatedAt: setting.updatedAt
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新设置
|
||||||
|
router.put('/:key', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { key } = req.params;
|
||||||
|
const { value } = req.body;
|
||||||
|
|
||||||
|
const setting = await SystemSetting.findByPk(key);
|
||||||
|
|
||||||
|
if (!setting) {
|
||||||
|
return res.status(404).json({ error: '设置不存在' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!setting.isEditable) {
|
||||||
|
return res.status(403).json({ error: '该设置不可编辑' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证值类型
|
||||||
|
let parsedValue = value;
|
||||||
|
if (setting.settingType === 'number') {
|
||||||
|
parsedValue = Number(value);
|
||||||
|
if (isNaN(parsedValue)) {
|
||||||
|
return res.status(400).json({ error: '值必须是有效的数字' });
|
||||||
|
}
|
||||||
|
} else if (setting.settingType === 'boolean') {
|
||||||
|
parsedValue = Boolean(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
await setting.update({
|
||||||
|
settingValue: JSON.stringify(parsedValue)
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: '设置更新成功',
|
||||||
|
setting: {
|
||||||
|
key: setting.settingKey,
|
||||||
|
value: parsedValue,
|
||||||
|
updatedAt: setting.updatedAt
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 批量更新设置
|
||||||
|
router.put('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { settings } = req.body;
|
||||||
|
|
||||||
|
if (!settings || typeof settings !== 'object') {
|
||||||
|
return res.status(400).json({ error: '请提供有效的设置对象' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSettings = [];
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(settings)) {
|
||||||
|
try {
|
||||||
|
const setting = await SystemSetting.findByPk(key);
|
||||||
|
|
||||||
|
if (!setting) {
|
||||||
|
errors.push({ key, error: '设置不存在' });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!setting.isEditable) {
|
||||||
|
errors.push({ key, error: '该设置不可编辑' });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedValue = value;
|
||||||
|
if (setting.settingType === 'number') {
|
||||||
|
parsedValue = Number(value);
|
||||||
|
if (isNaN(parsedValue)) {
|
||||||
|
errors.push({ key, error: '值必须是有效的数字' });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (setting.settingType === 'boolean') {
|
||||||
|
parsedValue = Boolean(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
await setting.update({
|
||||||
|
settingValue: JSON.stringify(parsedValue)
|
||||||
|
});
|
||||||
|
|
||||||
|
updatedSettings.push({ key, value: parsedValue });
|
||||||
|
} catch (error) {
|
||||||
|
errors.push({ key, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: `成功更新 ${updatedSettings.length} 个设置`,
|
||||||
|
updatedSettings,
|
||||||
|
errors: errors.length > 0 ? errors : undefined
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重置设置为默认值
|
||||||
|
router.post('/reset/:key', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { key } = req.params;
|
||||||
|
const setting = await SystemSetting.findByPk(key);
|
||||||
|
|
||||||
|
if (!setting) {
|
||||||
|
return res.status(404).json({ error: '设置不存在' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
site_name: '机柜管理系统',
|
||||||
|
site_logo: '',
|
||||||
|
timezone: 'Asia/Shanghai',
|
||||||
|
date_format: 'YYYY-MM-DD',
|
||||||
|
language: 'zh-CN',
|
||||||
|
session_timeout: 30,
|
||||||
|
max_login_attempts: 5,
|
||||||
|
maintenance_mode: false,
|
||||||
|
primary_color: '#667eea',
|
||||||
|
secondary_color: '#764ba2',
|
||||||
|
dark_mode: false,
|
||||||
|
compact_mode: false,
|
||||||
|
sidebar_collapsed: false,
|
||||||
|
table_row_height: 'default',
|
||||||
|
animation_enabled: true,
|
||||||
|
auto_backup_enabled: false,
|
||||||
|
backup_interval: 24,
|
||||||
|
backup_retention: 7,
|
||||||
|
backup_path: './backups',
|
||||||
|
company_name: '',
|
||||||
|
contact_email: '',
|
||||||
|
contact_phone: '',
|
||||||
|
company_address: '',
|
||||||
|
system_description: '机柜管理系统 - 专业的数据中心设备管理解决方案',
|
||||||
|
privacy_policy: '',
|
||||||
|
terms_of_service: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultValue = defaultValues[key];
|
||||||
|
if (defaultValue === undefined) {
|
||||||
|
return res.status(400).json({ error: '该设置没有默认值' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await setting.update({
|
||||||
|
settingValue: JSON.stringify(defaultValue)
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: '设置已重置为默认值',
|
||||||
|
key,
|
||||||
|
value: defaultValue
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 手动执行备份
|
||||||
|
router.post('/backup', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const backupDir = path.join(__dirname, '../backups');
|
||||||
|
|
||||||
|
// 确保备份目录存在
|
||||||
|
if (!fs.existsSync(backupDir)) {
|
||||||
|
fs.mkdirSync(backupDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const backupFile = path.join(backupDir, `backup_${timestamp}.json`);
|
||||||
|
|
||||||
|
// 获取所有数据库数据
|
||||||
|
const Device = require('../models/Device');
|
||||||
|
const Rack = require('../models/Rack');
|
||||||
|
const Room = require('../models/Room');
|
||||||
|
const Consumable = require('../models/Consumable');
|
||||||
|
const User = require('../models/User');
|
||||||
|
|
||||||
|
const backupData = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
version: '1.0.0',
|
||||||
|
data: {
|
||||||
|
devices: await Device.findAll({ raw: true }),
|
||||||
|
racks: await Rack.findAll({ raw: true }),
|
||||||
|
rooms: await Room.findAll({ raw: true }),
|
||||||
|
consumables: await Consumable.findAll({ raw: true }),
|
||||||
|
// 不包含敏感用户信息
|
||||||
|
users: await User.findAll({
|
||||||
|
attributes: ['userId', 'username', 'role', 'createdAt', 'updatedAt'],
|
||||||
|
raw: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 写入备份文件
|
||||||
|
fs.writeFileSync(backupFile, JSON.stringify(backupData, null, 2));
|
||||||
|
|
||||||
|
// 更新最后备份时间
|
||||||
|
const lastBackupSetting = await SystemSetting.findByPk('last_backup_time');
|
||||||
|
if (lastBackupSetting) {
|
||||||
|
await lastBackupSetting.update({
|
||||||
|
settingValue: JSON.stringify(new Date().toISOString())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计备份文件数量
|
||||||
|
const backupFiles = fs.readdirSync(backupDir).filter(f => f.startsWith('backup_'));
|
||||||
|
const countSetting = await SystemSetting.findByPk('backup_count');
|
||||||
|
if (countSetting) {
|
||||||
|
await countSetting.update({
|
||||||
|
settingValue: JSON.stringify(backupFiles.length)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: '备份成功',
|
||||||
|
backupFile: `backups/backup_${timestamp}.json`,
|
||||||
|
fileSize: fs.statSync(backupFile).size,
|
||||||
|
backupCount: backupFiles.length
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('备份失败:', error);
|
||||||
|
res.status(500).json({ error: '备份失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取备份列表
|
||||||
|
router.get('/backup/list', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const backupDir = path.join(__dirname, '../backups');
|
||||||
|
|
||||||
|
if (!fs.existsSync(backupDir)) {
|
||||||
|
return res.json({ backups: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync(backupDir)
|
||||||
|
.filter(f => f.startsWith('backup_') && f.endsWith('.json'))
|
||||||
|
.map(f => {
|
||||||
|
const filePath = path.join(backupDir, f);
|
||||||
|
const stats = fs.statSync(filePath);
|
||||||
|
return {
|
||||||
|
filename: f,
|
||||||
|
path: `backups/${f}`,
|
||||||
|
size: stats.size,
|
||||||
|
createdAt: stats.birthtime,
|
||||||
|
modifiedAt: stats.mtime
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||||
|
|
||||||
|
res.json({ backups: files });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 恢复备份
|
||||||
|
router.post('/backup/restore', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { filename } = req.body;
|
||||||
|
|
||||||
|
if (!filename) {
|
||||||
|
return res.status(400).json({ error: '请提供备份文件名' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupFile = path.join(__dirname, '../backups', filename);
|
||||||
|
|
||||||
|
if (!fs.existsSync(backupFile)) {
|
||||||
|
return res.status(404).json({ error: '备份文件不存在' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupData = JSON.parse(fs.readFileSync(backupFile, 'utf8'));
|
||||||
|
|
||||||
|
// 恢复数据
|
||||||
|
const { Device, Rack, Room, Consumable, User } = require('../models');
|
||||||
|
|
||||||
|
if (backupData.data.devices) {
|
||||||
|
for (const device of backupData.data.devices) {
|
||||||
|
await Device.upsert(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backupData.data.racks) {
|
||||||
|
for (const rack of backupData.data.racks) {
|
||||||
|
await Rack.upsert(rack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backupData.data.rooms) {
|
||||||
|
for (const room of backupData.data.rooms) {
|
||||||
|
await Room.upsert(room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backupData.data.consumables) {
|
||||||
|
for (const consumable of backupData.data.consumables) {
|
||||||
|
await Consumable.upsert(consumable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: '恢复成功',
|
||||||
|
restoredAt: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('恢复备份失败:', error);
|
||||||
|
res.status(500).json({ error: '恢复备份失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 删除备份
|
||||||
|
router.delete('/backup/:filename', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { filename } = req.params;
|
||||||
|
const backupFile = path.join(__dirname, '../backups', filename);
|
||||||
|
|
||||||
|
if (!fs.existsSync(backupFile)) {
|
||||||
|
return res.status(404).json({ error: '备份文件不存在' });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.unlinkSync(backupFile);
|
||||||
|
|
||||||
|
res.json({ message: '删除成功', filename });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 下载备份文件
|
||||||
|
router.get('/backup/download/:filename', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { filename } = req.params;
|
||||||
|
const backupFile = path.join(__dirname, '../backups', filename);
|
||||||
|
|
||||||
|
if (!fs.existsSync(backupFile)) {
|
||||||
|
return res.status(404).json({ error: '备份文件不存在' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.download(backupFile, filename);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取系统信息
|
||||||
|
router.get('/system/info', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const Device = require('../models/Device');
|
||||||
|
const Rack = require('../models/Rack');
|
||||||
|
const Room = require('../models/Room');
|
||||||
|
const User = require('../models/User');
|
||||||
|
|
||||||
|
const [deviceCount, rackCount, roomCount, userCount] = await Promise.all([
|
||||||
|
Device.count(),
|
||||||
|
Rack.count(),
|
||||||
|
Room.count(),
|
||||||
|
User.count()
|
||||||
|
]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
system: {
|
||||||
|
name: '机柜管理系统',
|
||||||
|
version: '1.0.0',
|
||||||
|
uptime: process.uptime(),
|
||||||
|
nodeVersion: process.version,
|
||||||
|
platform: process.platform,
|
||||||
|
arch: process.arch,
|
||||||
|
memoryUsage: process.memoryUsage(),
|
||||||
|
pid: process.pid
|
||||||
|
},
|
||||||
|
statistics: {
|
||||||
|
devices: deviceCount,
|
||||||
|
racks: rackCount,
|
||||||
|
rooms: roomCount,
|
||||||
|
users: userCount
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -39,6 +39,11 @@ sequelize.authenticate()
|
|||||||
// 初始化工单模型关联
|
// 初始化工单模型关联
|
||||||
return require('./models/ticketIndex').initializeModels();
|
return require('./models/ticketIndex').initializeModels();
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
// 同步系统设置模型
|
||||||
|
const SystemSetting = require('./models/SystemSetting');
|
||||||
|
return SystemSetting.sync();
|
||||||
|
})
|
||||||
.catch(err => console.error('数据库操作失败:', err));
|
.catch(err => console.error('数据库操作失败:', err));
|
||||||
|
|
||||||
// 导入路由
|
// 导入路由
|
||||||
@@ -58,6 +63,7 @@ const operationLogsRoutes = require('./routes/operationLogs');
|
|||||||
const ticketRoutes = require('./routes/tickets');
|
const ticketRoutes = require('./routes/tickets');
|
||||||
const ticketCategoryRoutes = require('./routes/ticketCategories');
|
const ticketCategoryRoutes = require('./routes/ticketCategories');
|
||||||
const ticketFieldRoutes = require('./routes/ticketFields');
|
const ticketFieldRoutes = require('./routes/ticketFields');
|
||||||
|
const systemSettingsRoutes = require('./routes/systemSettings');
|
||||||
|
|
||||||
// 使用路由
|
// 使用路由
|
||||||
app.use('/api/devices', deviceRoutes);
|
app.use('/api/devices', deviceRoutes);
|
||||||
@@ -76,6 +82,7 @@ app.use('/api/operation-logs', operationLogsRoutes);
|
|||||||
app.use('/api/tickets', ticketRoutes);
|
app.use('/api/tickets', ticketRoutes);
|
||||||
app.use('/api/ticket-categories', ticketCategoryRoutes);
|
app.use('/api/ticket-categories', ticketCategoryRoutes);
|
||||||
app.use('/api/ticket-fields', ticketFieldRoutes);
|
app.use('/api/ticket-fields', ticketFieldRoutes);
|
||||||
|
app.use('/api/system-settings', systemSettingsRoutes);
|
||||||
|
|
||||||
// 静态文件服务
|
// 静态文件服务
|
||||||
app.use('/uploads', express.static('uploads'));
|
app.use('/uploads', express.static('uploads'));
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const content = fs.readFileSync('e:/IDC/jigui/frontend/src/pages/DeviceManagement.jsx', 'utf8');
|
||||||
|
|
||||||
|
const startMarker = '// 可调整列宽的表头组件';
|
||||||
|
const endMarker = '// 防抖 Hook';
|
||||||
|
|
||||||
|
const startIndex = content.indexOf(startMarker);
|
||||||
|
const endIndex = content.indexOf(endMarker, startIndex);
|
||||||
|
|
||||||
|
console.log('startMarker index:', startIndex);
|
||||||
|
console.log('endMarker index:', endIndex);
|
||||||
|
|
||||||
|
if (startIndex >= 0 && endIndex >= 0) {
|
||||||
|
console.log('Content to remove:');
|
||||||
|
console.log(content.substring(startIndex, endIndex + endMarker.length).substring(0, 500));
|
||||||
|
}
|
||||||
+15
-1
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, Suspense, lazy } from 'react';
|
import React, { useState, Suspense, lazy } from 'react';
|
||||||
import { Layout, Menu, theme, Button, Dropdown, Avatar, message, Space, Divider } from 'antd';
|
import { Layout, Menu, theme, Button, Dropdown, Avatar, message, Space, Divider } from 'antd';
|
||||||
import { BarChartOutlined, DatabaseOutlined, CloudServerOutlined, MenuUnfoldOutlined, MenuFoldOutlined, EyeOutlined, BuildOutlined, HomeOutlined, ShoppingCartOutlined, InboxOutlined, ImportOutlined, FileTextOutlined, UserOutlined, LogoutOutlined, HistoryOutlined, AuditOutlined, ToolOutlined, ScheduleOutlined } from '@ant-design/icons';
|
import { BarChartOutlined, DatabaseOutlined, CloudServerOutlined, MenuUnfoldOutlined, MenuFoldOutlined, EyeOutlined, BuildOutlined, HomeOutlined, ShoppingCartOutlined, InboxOutlined, ImportOutlined, FileTextOutlined, UserOutlined, LogoutOutlined, HistoryOutlined, AuditOutlined, ToolOutlined, ScheduleOutlined, SettingOutlined } from '@ant-design/icons';
|
||||||
import { BrowserRouter as Router, Routes, Route, Link, Navigate, useLocation, useNavigate } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Link, Navigate, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from './context/AuthContext';
|
import { useAuth } from './context/AuthContext';
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
@@ -23,6 +23,7 @@ const TicketManagement = lazy(() => import('./pages/TicketManagement'));
|
|||||||
const TicketCategoryManagement = lazy(() => import('./pages/TicketCategoryManagement'));
|
const TicketCategoryManagement = lazy(() => import('./pages/TicketCategoryManagement'));
|
||||||
const TicketStatistics = lazy(() => import('./pages/TicketStatistics'));
|
const TicketStatistics = lazy(() => import('./pages/TicketStatistics'));
|
||||||
const TicketFieldManagement = lazy(() => import('./pages/TicketFieldManagement'));
|
const TicketFieldManagement = lazy(() => import('./pages/TicketFieldManagement'));
|
||||||
|
const SystemSettings = lazy(() => import('./pages/SystemSettings'));
|
||||||
|
|
||||||
const { Header, Content, Sider } = Layout;
|
const { Header, Content, Sider } = Layout;
|
||||||
|
|
||||||
@@ -226,6 +227,11 @@ const AppLayout = ({ children }) => {
|
|||||||
icon: <AuditOutlined />,
|
icon: <AuditOutlined />,
|
||||||
label: <Link to="/operation-logs">操作日志</Link>,
|
label: <Link to="/operation-logs">操作日志</Link>,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'system-settings',
|
||||||
|
icon: <SettingOutlined />,
|
||||||
|
label: <Link to="/settings">系统设置</Link>,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -463,6 +469,14 @@ function App() {
|
|||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/settings"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<SystemSettings />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||||
import { Table, Button, Modal, Form, Input, Select, DatePicker, message, Card, Space, InputNumber, Switch, Upload, Progress, Checkbox, Spin } from 'antd';
|
import { Table, Button, Modal, Form, Input, Select, DatePicker, message, Card, Space, InputNumber, Switch, Upload, Progress, Checkbox, Spin, Dropdown, Tooltip } from 'antd';
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, UploadOutlined, DownloadOutlined, SettingOutlined, UndoOutlined, CloudServerOutlined, SwapOutlined, SafetyOutlined, DatabaseOutlined, AppstoreOutlined } from '@ant-design/icons';
|
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, UploadOutlined, DownloadOutlined, SettingOutlined, UndoOutlined, CloudServerOutlined, SwapOutlined, SafetyOutlined, DatabaseOutlined, AppstoreOutlined, MoreOutlined, ReloadOutlined, ExportOutlined, DragOutlined, FileExcelOutlined } from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
@@ -97,22 +97,24 @@ const defaultDeviceFields = [
|
|||||||
{ fieldName: 'description', displayName: '描述', fieldType: 'textarea', required: false, order: 14, visible: true }
|
{ fieldName: 'description', displayName: '描述', fieldType: 'textarea', required: false, order: 14, visible: true }
|
||||||
];
|
];
|
||||||
|
|
||||||
// 可调整列宽的表头组件
|
// 简单的可调整列宽的表头组件
|
||||||
const ResizeableTitle = (props) => {
|
const ResizableTitle = (props) => {
|
||||||
const { onResize, width, ...restProps } = props;
|
const { children, onResize, width, ...restProps } = props;
|
||||||
|
|
||||||
if (!width) {
|
|
||||||
return <th {...restProps} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseDown = (e) => {
|
const handleMouseDown = (e) => {
|
||||||
if (!onResize) return;
|
if (!onResize) return;
|
||||||
|
|
||||||
const startX = e.pageX;
|
e.preventDefault();
|
||||||
const startWidth = width;
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const th = e.currentTarget.closest('th');
|
||||||
|
if (!th) return;
|
||||||
|
|
||||||
|
const startWidth = th.offsetWidth;
|
||||||
|
const startX = e.clientX;
|
||||||
|
|
||||||
const handleMouseMove = (moveEvent) => {
|
const handleMouseMove = (moveEvent) => {
|
||||||
const diff = moveEvent.pageX - startX;
|
const diff = moveEvent.clientX - startX;
|
||||||
const newWidth = Math.max(50, startWidth + diff);
|
const newWidth = Math.max(50, startWidth + diff);
|
||||||
onResize(newWidth);
|
onResize(newWidth);
|
||||||
};
|
};
|
||||||
@@ -127,34 +129,37 @@ const ResizeableTitle = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<th
|
<th {...restProps} style={{ position: 'relative' }}>
|
||||||
{...restProps}
|
<div style={{
|
||||||
style={{
|
display: 'flex',
|
||||||
position: 'relative',
|
alignItems: 'center',
|
||||||
width: width,
|
justifyContent: 'space-between',
|
||||||
maxWidth: width,
|
width: '100%'
|
||||||
minWidth: width,
|
}}>
|
||||||
...restProps.style,
|
<span style={{
|
||||||
}}
|
flex: 1,
|
||||||
>
|
overflow: 'hidden',
|
||||||
{restProps.children}
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}>{children}</span>
|
||||||
|
{onResize && (
|
||||||
<div
|
<div
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
right: '-3px',
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
width: '6px',
|
|
||||||
cursor: 'col-resize',
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
zIndex: 10,
|
|
||||||
}}
|
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
title="拖拽调整列宽"
|
style={{
|
||||||
|
width: '8px',
|
||||||
|
height: '20px',
|
||||||
|
backgroundColor: '#e0e0e0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'col-resize',
|
||||||
|
marginLeft: '8px',
|
||||||
|
flexShrink: 0
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function DeviceManagement() {
|
function DeviceManagement() {
|
||||||
const [devices, setDevices] = useState([]);
|
const [devices, setDevices] = useState([]);
|
||||||
@@ -200,6 +205,27 @@ function DeviceManagement() {
|
|||||||
// 字段配置模态框
|
// 字段配置模态框
|
||||||
const [fieldConfigModalVisible, setFieldConfigModalVisible] = useState(false);
|
const [fieldConfigModalVisible, setFieldConfigModalVisible] = useState(false);
|
||||||
|
|
||||||
|
// 批量状态变更模态框
|
||||||
|
const [batchStatusModalVisible, setBatchStatusModalVisible] = useState(false);
|
||||||
|
const [batchStatusLoading, setBatchStatusLoading] = useState(false);
|
||||||
|
const [batchStatusForm] = Form.useForm();
|
||||||
|
|
||||||
|
// 批量移动模态框
|
||||||
|
const [batchMoveModalVisible, setBatchMoveModalVisible] = useState(false);
|
||||||
|
const [batchMoveLoading, setBatchMoveLoading] = useState(false);
|
||||||
|
const [batchMoveForm] = Form.useForm();
|
||||||
|
|
||||||
|
// 导出选项模态框
|
||||||
|
const [exportModalVisible, setExportModalVisible] = useState(false);
|
||||||
|
const [exportFormat, setExportFormat] = useState('csv');
|
||||||
|
const [exportScope, setExportScope] = useState('selected');
|
||||||
|
const [exportFields, setExportFields] = useState([]);
|
||||||
|
const [exportLoading, setExportLoading] = useState(false);
|
||||||
|
const [currentPageDevices, setCurrentPageDevices] = useState([]);
|
||||||
|
|
||||||
|
// 全选状态
|
||||||
|
const [selectAll, setSelectAll] = useState(false);
|
||||||
|
|
||||||
// 列宽状态
|
// 列宽状态
|
||||||
const [columnWidths, setColumnWidths] = useState({});
|
const [columnWidths, setColumnWidths] = useState({});
|
||||||
|
|
||||||
@@ -429,6 +455,18 @@ function DeviceManagement() {
|
|||||||
fetchDeviceFields();
|
fetchDeviceFields();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 同步当前页设备数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (filteredDevicesMemo.length > 0) {
|
||||||
|
const start = (pagination.current - 1) * pagination.pageSize;
|
||||||
|
const end = start + pagination.pageSize;
|
||||||
|
const currentPageData = filteredDevicesMemo.slice(start, end);
|
||||||
|
setCurrentPageDevices(currentPageData);
|
||||||
|
} else {
|
||||||
|
setCurrentPageDevices([]);
|
||||||
|
}
|
||||||
|
}, [filteredDevicesMemo, pagination.current, pagination.pageSize]);
|
||||||
|
|
||||||
// 打开模态框
|
// 打开模态框
|
||||||
const showModal = (device = null) => {
|
const showModal = (device = null) => {
|
||||||
setEditingDevice(device);
|
setEditingDevice(device);
|
||||||
@@ -565,9 +603,15 @@ function DeviceManagement() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 表格分页变化处理
|
// 表格分页变化处理
|
||||||
const handleTableChange = (pagination) => {
|
const handleTableChange = (newPagination) => {
|
||||||
setPagination(pagination);
|
setPagination(newPagination);
|
||||||
fetchDevices(pagination.current, pagination.pageSize);
|
|
||||||
|
const start = (newPagination.current - 1) * newPagination.pageSize;
|
||||||
|
const end = start + newPagination.pageSize;
|
||||||
|
const currentPageData = filteredDevicesMemo.slice(start, end);
|
||||||
|
setCurrentPageDevices(currentPageData);
|
||||||
|
|
||||||
|
fetchDevices(newPagination.current, newPagination.pageSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 批量下线设备
|
// 批量下线设备
|
||||||
@@ -647,39 +691,177 @@ function DeviceManagement() {
|
|||||||
setDetailModalVisible(true);
|
setDetailModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 导出设备数据
|
// 打开批量状态变更模态框
|
||||||
const handleExport = async () => {
|
const showBatchStatusModal = () => {
|
||||||
|
if (selectedDevices.length === 0) {
|
||||||
|
message.warning('请先选择要操作的设备');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
batchStatusForm.resetFields();
|
||||||
|
setBatchStatusModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 执行批量状态变更
|
||||||
|
const handleBatchStatusChange = async () => {
|
||||||
try {
|
try {
|
||||||
|
const values = await batchStatusForm.validateFields();
|
||||||
|
setBatchStatusLoading(true);
|
||||||
|
|
||||||
|
const response = await axios.put('/api/devices/batch-status', {
|
||||||
|
deviceIds: selectedDevices,
|
||||||
|
status: values.status
|
||||||
|
});
|
||||||
|
|
||||||
|
message.success(response.data.message || '批量状态变更成功');
|
||||||
|
setBatchStatusModalVisible(false);
|
||||||
|
setSelectedDevices([]);
|
||||||
|
setSelectAll(false);
|
||||||
|
fetchDevices();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.errorFields) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
message.error('批量状态变更失败');
|
||||||
|
console.error('批量状态变更失败:', error);
|
||||||
|
} finally {
|
||||||
|
setBatchStatusLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开批量移动模态框
|
||||||
|
const showBatchMoveModal = () => {
|
||||||
|
if (selectedDevices.length === 0) {
|
||||||
|
message.warning('请先选择要移动的设备');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
batchMoveForm.resetFields();
|
||||||
|
setBatchMoveModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 执行批量移动
|
||||||
|
const handleBatchMove = async () => {
|
||||||
|
try {
|
||||||
|
const values = await batchMoveForm.validateFields();
|
||||||
|
setBatchMoveLoading(true);
|
||||||
|
|
||||||
|
const response = await axios.put('/api/devices/batch-move', {
|
||||||
|
deviceIds: selectedDevices,
|
||||||
|
targetRackId: values.targetRackId,
|
||||||
|
startPosition: values.startPosition
|
||||||
|
});
|
||||||
|
|
||||||
|
message.success(response.data.message || '批量移动成功');
|
||||||
|
setBatchMoveModalVisible(false);
|
||||||
|
setSelectedDevices([]);
|
||||||
|
setSelectAll(false);
|
||||||
|
fetchDevices();
|
||||||
|
fetchRacks();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.errorFields) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
message.error('批量移动失败');
|
||||||
|
console.error('批量移动失败:', error);
|
||||||
|
} finally {
|
||||||
|
setBatchMoveLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开导出选项模态框
|
||||||
|
const showExportModal = () => {
|
||||||
if (selectedDevices.length === 0) {
|
if (selectedDevices.length === 0) {
|
||||||
message.warning('请先选择要导出的设备');
|
message.warning('请先选择要导出的设备');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setExportFormat('csv');
|
||||||
|
setExportFields(deviceFields.filter(f => f.visible && f.fieldName !== 'rackId').map(f => f.fieldName));
|
||||||
|
setExportModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 执行增强导出
|
||||||
|
const handleEnhancedExport = async () => {
|
||||||
|
try {
|
||||||
|
setExportLoading(true);
|
||||||
|
|
||||||
|
const fieldLabels = {};
|
||||||
|
deviceFields.forEach(field => {
|
||||||
|
fieldLabels[field.fieldName] = field.displayName;
|
||||||
|
});
|
||||||
|
|
||||||
|
let deviceIds = [];
|
||||||
|
if (exportScope === 'selected') {
|
||||||
|
deviceIds = selectedDevices;
|
||||||
|
} else if (exportScope === 'currentPage') {
|
||||||
|
deviceIds = filteredDevicesMemo.map(device => device.deviceId);
|
||||||
|
} else if (exportScope === 'all') {
|
||||||
|
deviceIds = allDevices.map(device => device.deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceIds.length === 0) {
|
||||||
|
message.warning('没有可导出的设备');
|
||||||
|
setExportLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
selectedDevices.forEach(id => params.append('deviceIds', id));
|
deviceIds.forEach(id => params.append('deviceIds', id));
|
||||||
|
params.append('format', exportFormat);
|
||||||
|
params.append('fields', JSON.stringify(exportFields));
|
||||||
|
params.append('fieldLabels', JSON.stringify(fieldLabels));
|
||||||
|
|
||||||
const response = await axios.get(`/api/devices/export?${params.toString()}`, { responseType: 'blob' });
|
const response = await axios.get(`/api/devices/enhanced-export?${params.toString()}`, { responseType: 'blob' });
|
||||||
const blob = new Blob([response.data], { type: 'text/csv; charset=gbk' });
|
|
||||||
|
const contentType = exportFormat === 'csv' ? 'text/csv; charset=gbk' : 'application/json';
|
||||||
|
const blob = new Blob([response.data], { type: contentType });
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = `devices_export_${new Date().toISOString().split('T')[0]}.csv`;
|
link.download = `devices_export_${new Date().toISOString().split('T')[0]}.${exportFormat}`;
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
message.success('导出成功');
|
|
||||||
|
message.success(`成功导出 ${deviceIds.length} 个设备`);
|
||||||
|
setExportModalVisible(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('导出失败');
|
message.error('导出失败');
|
||||||
console.error('导出设备失败:', error);
|
console.error('增强导出失败:', error);
|
||||||
|
} finally {
|
||||||
|
setExportLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 切换选择全部设备
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
if (selectAll) {
|
||||||
|
setSelectedDevices([]);
|
||||||
|
setSelectAll(false);
|
||||||
|
} else {
|
||||||
|
const allIds = filteredDevicesMemo.map(device => device.deviceId);
|
||||||
|
setSelectedDevices(allIds);
|
||||||
|
setSelectAll(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理选择变化
|
||||||
|
const handleSelectionChange = (selectedRowKeys) => {
|
||||||
|
setSelectedDevices(selectedRowKeys);
|
||||||
|
setSelectAll(selectedRowKeys.length === filteredDevicesMemo.length && filteredDevicesMemo.length > 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 全选复选框的处理函数
|
||||||
|
const handleSelectAllCheckbox = (e) => {
|
||||||
// 导入设备数据
|
const checked = e.target.checked;
|
||||||
|
if (checked) {
|
||||||
|
const allIds = filteredDevicesMemo.map(device => device.deviceId);
|
||||||
|
setSelectedDevices(allIds);
|
||||||
|
setSelectAll(true);
|
||||||
|
} else {
|
||||||
|
setSelectedDevices([]);
|
||||||
|
setSelectAll(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
const handleImport = async (file) => {
|
const handleImport = async (file) => {
|
||||||
try {
|
try {
|
||||||
setIsImporting(true);
|
setIsImporting(true);
|
||||||
@@ -810,10 +992,12 @@ function DeviceManagement() {
|
|||||||
message.success('列宽已重置为默认值');
|
message.success('列宽已重置为默认值');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理表头单元格拖拽
|
// 处理表头单元格拖拽 - 自定义实现
|
||||||
const handleHeaderCellResize = (key) => (column) => ({
|
const handleHeaderCellResize = (key) => (column) => ({
|
||||||
width: column.width,
|
width: column.width,
|
||||||
onResize: (width) => handleColumnResize(key, width),
|
onResize: (width) => {
|
||||||
|
setColumnWidths(prev => ({ ...prev, [key]: width }));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -942,7 +1126,10 @@ function DeviceManagement() {
|
|||||||
dataIndex: field.fieldName,
|
dataIndex: field.fieldName,
|
||||||
key: field.fieldName,
|
key: field.fieldName,
|
||||||
width: columnWidths[field.fieldName] || defaultWidth,
|
width: columnWidths[field.fieldName] || defaultWidth,
|
||||||
|
minWidth: 80,
|
||||||
|
maxWidth: field.fieldType === 'textarea' ? 300 : 200,
|
||||||
onHeaderCell: handleHeaderCellResize(field.fieldName),
|
onHeaderCell: handleHeaderCellResize(field.fieldName),
|
||||||
|
ellipsis: field.fieldType !== 'textarea',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设备名称和ID列添加点击效果
|
// 设备名称和ID列添加点击效果
|
||||||
@@ -953,7 +1140,11 @@ function DeviceManagement() {
|
|||||||
style={{
|
style={{
|
||||||
color: '#1890ff',
|
color: '#1890ff',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
cursor: 'pointer'
|
cursor: 'pointer',
|
||||||
|
display: 'block',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => e.target.style.textDecoration = 'underline'}
|
onMouseEnter={(e) => e.target.style.textDecoration = 'underline'}
|
||||||
onMouseLeave={(e) => e.target.style.textDecoration = 'none'}
|
onMouseLeave={(e) => e.target.style.textDecoration = 'none'}
|
||||||
@@ -971,17 +1162,32 @@ function DeviceManagement() {
|
|||||||
generatedColumns.push({
|
generatedColumns.push({
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
width: columnWidths.action || 120,
|
width: columnWidths.action || 80,
|
||||||
|
minWidth: 60,
|
||||||
|
maxWidth: 100,
|
||||||
onHeaderCell: handleHeaderCellResize('action'),
|
onHeaderCell: handleHeaderCellResize('action'),
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Space size="middle">
|
<div style={{ display: 'flex', gap: '4px' }}>
|
||||||
<Button type="primary" icon={<EditOutlined />} onClick={() => showModal(record)} size="small">
|
<Tooltip title="编辑">
|
||||||
编辑
|
<Button
|
||||||
</Button>
|
type="text"
|
||||||
<Button danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.deviceId)} size="small">
|
icon={<EditOutlined />}
|
||||||
删除
|
onClick={() => showModal(record)}
|
||||||
</Button>
|
size="small"
|
||||||
</Space>
|
style={{ color: '#1890ff', padding: '4px 8px' }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => handleDelete(record.deviceId)}
|
||||||
|
size="small"
|
||||||
|
style={{ padding: '4px 8px' }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1023,7 +1229,7 @@ function DeviceManagement() {
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
gap: '16px'
|
gap: '12px'
|
||||||
};
|
};
|
||||||
|
|
||||||
const titleStyle = {
|
const titleStyle = {
|
||||||
@@ -1077,12 +1283,114 @@ function DeviceManagement() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '24px' }}>
|
<div style={{ padding: '24px' }}>
|
||||||
|
<style>{`
|
||||||
|
/* 表格自适应换行样式 */
|
||||||
|
.device-table-wrapper .ant-table {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-table-wrapper .ant-table-container {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-table-wrapper .ant-table-content {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-table-wrapper .ant-table-thead > tr > th {
|
||||||
|
white-space: normal !important;
|
||||||
|
word-break: break-word !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
line-height: 1.4 !important;
|
||||||
|
padding: 12px 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-table-wrapper .ant-table-tbody > tr > td {
|
||||||
|
white-space: normal !important;
|
||||||
|
word-break: break-word !important;
|
||||||
|
line-height: 1.6 !important;
|
||||||
|
max-width: 250px !important;
|
||||||
|
padding: 12px 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-table-wrapper .ant-table-tbody > tr > td .ant-typography,
|
||||||
|
.device-table-wrapper .ant-table-tbody > tr > td .ant-typography-expand,
|
||||||
|
.device-table-wrapper .ant-table-tbody > tr > td span {
|
||||||
|
white-space: normal !important;
|
||||||
|
word-break: break-word !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-table-wrapper .ant-table-cell {
|
||||||
|
word-break: break-word !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 斑马纹样式 */
|
||||||
|
.device-table-wrapper .ant-table-row-even {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-table-wrapper .ant-table-row-odd {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-table-wrapper .ant-table-row-selected {
|
||||||
|
background-color: #e6f7ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-table-wrapper .ant-table-row-selected:hover > td {
|
||||||
|
background-color: #bae7ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格行悬停效果 */
|
||||||
|
.device-table-wrapper .ant-table-tbody > tr:hover > td {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 复选框列固定 */
|
||||||
|
.device-table-wrapper .ant-table-selection-column {
|
||||||
|
position: sticky !important;
|
||||||
|
left: 0 !important;
|
||||||
|
z-index: 2 !important;
|
||||||
|
background: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作列样式 */
|
||||||
|
.device-table-wrapper .ant-table-tbody > tr > td:last-child {
|
||||||
|
min-width: 120px !important;
|
||||||
|
max-width: 150px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页器样式 */
|
||||||
|
.device-table-wrapper .ant-pagination {
|
||||||
|
margin: 16px 0 !important;
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.device-table-wrapper .ant-table-tbody > tr > td {
|
||||||
|
max-width: 150px !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-table-wrapper .ant-table-thead > tr > th {
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
<div style={pageHeaderStyle}>
|
<div style={pageHeaderStyle}>
|
||||||
<h1 style={titleStyle}>
|
<h1 style={titleStyle}>
|
||||||
<CloudServerOutlined style={{ marginRight: '12px' }} />
|
<CloudServerOutlined style={{ marginRight: '12px' }} />
|
||||||
设备管理
|
设备管理
|
||||||
</h1>
|
</h1>
|
||||||
<Space size={12}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||||
<Button
|
<Button
|
||||||
style={primaryButtonStyle}
|
style={primaryButtonStyle}
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
@@ -1090,13 +1398,6 @@ function DeviceManagement() {
|
|||||||
>
|
>
|
||||||
添加设备
|
添加设备
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
style={secondaryButtonStyle}
|
|
||||||
icon={<DownloadOutlined />}
|
|
||||||
onClick={handleExport}
|
|
||||||
>
|
|
||||||
导出设备
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
style={secondaryButtonStyle}
|
style={secondaryButtonStyle}
|
||||||
icon={<UploadOutlined />}
|
icon={<UploadOutlined />}
|
||||||
@@ -1122,14 +1423,26 @@ function DeviceManagement() {
|
|||||||
<Button
|
<Button
|
||||||
style={{
|
style={{
|
||||||
...secondaryButtonStyle,
|
...secondaryButtonStyle,
|
||||||
color: selectedDevices.length > 0 ? '#1890ff' : undefined,
|
color: selectedDevices.length > 0 ? '#52c41a' : undefined,
|
||||||
borderColor: selectedDevices.length > 0 ? '#1890ff' : undefined
|
borderColor: selectedDevices.length > 0 ? '#52c41a' : undefined
|
||||||
}}
|
}}
|
||||||
icon={<SwapOutlined />}
|
icon={<ReloadOutlined />}
|
||||||
disabled={selectedDevices.length === 0}
|
disabled={selectedDevices.length === 0}
|
||||||
onClick={handleBatchOffline}
|
onClick={showBatchStatusModal}
|
||||||
>
|
>
|
||||||
一键下线 ({selectedDevices.length})
|
状态变更 ({selectedDevices.length})
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
...secondaryButtonStyle,
|
||||||
|
color: selectedDevices.length > 0 ? '#722ed1' : undefined,
|
||||||
|
borderColor: selectedDevices.length > 0 ? '#722ed1' : undefined
|
||||||
|
}}
|
||||||
|
icon={<DragOutlined />}
|
||||||
|
disabled={selectedDevices.length === 0}
|
||||||
|
onClick={showBatchMoveModal}
|
||||||
|
>
|
||||||
|
批量移动 ({selectedDevices.length})
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
style={{
|
style={{
|
||||||
@@ -1142,17 +1455,30 @@ function DeviceManagement() {
|
|||||||
disabled={selectedDevices.length === 0}
|
disabled={selectedDevices.length === 0}
|
||||||
onClick={handleBatchDelete}
|
onClick={handleBatchDelete}
|
||||||
>
|
>
|
||||||
一键删除 ({selectedDevices.length})
|
批量删除 ({selectedDevices.length})
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
<Button
|
||||||
|
style={{
|
||||||
|
...secondaryButtonStyle,
|
||||||
|
color: selectedDevices.length > 0 ? '#ff4d4f' : undefined,
|
||||||
|
borderColor: selectedDevices.length > 0 ? '#ff4d4f' : undefined
|
||||||
|
}}
|
||||||
|
danger
|
||||||
|
icon={<SwapOutlined />}
|
||||||
|
disabled={selectedDevices.length === 0}
|
||||||
|
onClick={handleBatchOffline}
|
||||||
|
>
|
||||||
|
批量下线 ({selectedDevices.length})
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card size="small" style={searchCardStyle} styles={{ body: { padding: '16px 20px' } }}>
|
<Card size="small" style={searchCardStyle} styles={{ body: { padding: '16px 20px', display: 'flex', alignItems: 'center', gap: '16px' } }}>
|
||||||
<Form
|
<Form
|
||||||
form={searchForm}
|
form={searchForm}
|
||||||
layout="inline"
|
layout="inline"
|
||||||
onFinish={handleSearch}
|
onFinish={handleSearch}
|
||||||
style={{ width: '100%' }}
|
style={{ flex: 1 }}
|
||||||
>
|
>
|
||||||
<Form.Item name="keyword">
|
<Form.Item name="keyword">
|
||||||
<Input
|
<Input
|
||||||
@@ -1216,6 +1542,18 @@ function DeviceManagement() {
|
|||||||
</Space>
|
</Space>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
...secondaryButtonStyle,
|
||||||
|
color: '#fa8c16',
|
||||||
|
borderColor: '#fa8c16',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}
|
||||||
|
icon={<ExportOutlined />}
|
||||||
|
onClick={showExportModal}
|
||||||
|
>
|
||||||
|
增强导出
|
||||||
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card style={cardStyle}>
|
<Card style={cardStyle}>
|
||||||
@@ -1230,25 +1568,63 @@ function DeviceManagement() {
|
|||||||
<p>暂无设备数据</p>
|
<p>暂无设备数据</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="device-table-wrapper">
|
||||||
<Table
|
<Table
|
||||||
components={{
|
|
||||||
header: {
|
|
||||||
cell: ResizeableTitle,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={filteredDevicesMemo}
|
dataSource={filteredDevicesMemo}
|
||||||
rowKey="deviceId"
|
rowKey="deviceId"
|
||||||
loading={loading || searching}
|
loading={loading || searching}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
scroll={{ y: 600, x: 'max-content' }}
|
scroll={{ y: 'calc(100vh - 380px)', scrollToFirstRowOnChange: true }}
|
||||||
virtual
|
virtual
|
||||||
|
components={{
|
||||||
|
header: {
|
||||||
|
cell: ResizableTitle,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
style={{ width: '100%', maxWidth: '100%' }}
|
||||||
|
size="middle"
|
||||||
rowSelection={{
|
rowSelection={{
|
||||||
selectedRowKeys: selectedDevices,
|
selectedRowKeys: selectedDevices,
|
||||||
onChange: setSelectedDevices,
|
onChange: handleSelectionChange,
|
||||||
|
columnWidth: 48,
|
||||||
|
fixed: 'left',
|
||||||
|
type: 'checkbox',
|
||||||
|
crossPageSelect: true,
|
||||||
|
selections: [
|
||||||
|
{ key: 'all', text: '全选', onSelect: () => {
|
||||||
|
const allIds = filteredDevicesMemo.map(device => device.deviceId);
|
||||||
|
setSelectedDevices(allIds);
|
||||||
|
setSelectAll(true);
|
||||||
|
}},
|
||||||
|
{ key: 'invert', text: '反选', onSelect: () => {
|
||||||
|
const visibleIds = filteredDevicesMemo.map(device => device.deviceId);
|
||||||
|
const newSelected = visibleIds.filter(id => !selectedDevices.includes(id));
|
||||||
|
setSelectedDevices(newSelected);
|
||||||
|
setSelectAll(newSelected.length === filteredDevicesMemo.length);
|
||||||
|
}},
|
||||||
|
{ key: 'none', text: '清除选择', onSelect: () => {
|
||||||
|
setSelectedDevices([]);
|
||||||
|
setSelectAll(false);
|
||||||
|
}},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
onRow={(record) => ({
|
||||||
|
onClick: () => handleSelectionChange(
|
||||||
|
selectedDevices.includes(record.deviceId)
|
||||||
|
? selectedDevices.filter(id => id !== record.deviceId)
|
||||||
|
: [...selectedDevices, record.deviceId]
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
rowClassName={(record, index) => {
|
||||||
|
if (selectedDevices.includes(record.deviceId)) {
|
||||||
|
return 'ant-table-row-selected';
|
||||||
|
}
|
||||||
|
return index % 2 === 0 ? 'ant-table-row-even' : 'ant-table-row-odd';
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
@@ -1669,6 +2045,154 @@ function DeviceManagement() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<div style={modalHeaderStyle}>
|
||||||
|
<ReloadOutlined style={{ color: '#52c41a' }} />
|
||||||
|
批量状态变更
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
open={batchStatusModalVisible}
|
||||||
|
onCancel={() => setBatchStatusModalVisible(false)}
|
||||||
|
footer={[
|
||||||
|
<Button key="cancel" onClick={() => setBatchStatusModalVisible(false)} style={secondaryButtonStyle}>
|
||||||
|
取消
|
||||||
|
</Button>,
|
||||||
|
<Button key="submit" type="primary" loading={batchStatusLoading} onClick={handleBatchStatusChange} style={primaryButtonStyle}>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
destroyOnHidden
|
||||||
|
styles={{ header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, body: { padding: '24px' } }}
|
||||||
|
>
|
||||||
|
<Form form={batchStatusForm} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
name="status"
|
||||||
|
label="选择新状态"
|
||||||
|
rules={[{ required: true, message: '请选择设备状态' }]}
|
||||||
|
>
|
||||||
|
<Select placeholder="请选择设备状态" style={{ width: '100%' }}>
|
||||||
|
<Option value="running">运行中</Option>
|
||||||
|
<Option value="maintenance">维护中</Option>
|
||||||
|
<Option value="offline">离线</Option>
|
||||||
|
<Option value="fault">故障</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<div style={{ color: '#666', fontSize: '13px' }}>
|
||||||
|
已选择 <span style={{ color: '#1890ff', fontWeight: 600 }}>{selectedDevices.length}</span> 个设备
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<div style={modalHeaderStyle}>
|
||||||
|
<DragOutlined style={{ color: '#722ed1' }} />
|
||||||
|
批量移动设备
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
open={batchMoveModalVisible}
|
||||||
|
onCancel={() => setBatchMoveModalVisible(false)}
|
||||||
|
footer={[
|
||||||
|
<Button key="cancel" onClick={() => setBatchMoveModalVisible(false)} style={secondaryButtonStyle}>
|
||||||
|
取消
|
||||||
|
</Button>,
|
||||||
|
<Button key="submit" type="primary" loading={batchMoveLoading} onClick={handleBatchMove} style={primaryButtonStyle}>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
destroyOnHidden
|
||||||
|
styles={{ header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, body: { padding: '24px' } }}
|
||||||
|
>
|
||||||
|
<Form form={batchMoveForm} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
name="targetRackId"
|
||||||
|
label="目标机柜"
|
||||||
|
rules={[{ required: true, message: '请选择目标机柜' }]}
|
||||||
|
>
|
||||||
|
<Select placeholder="请选择目标机柜" style={{ width: '100%' }}>
|
||||||
|
{racks.map(rack => (
|
||||||
|
<Option key={rack.rackId} value={rack.rackId}>
|
||||||
|
{rack.name} {rack.Room ? `(${rack.Room.name})` : ''}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="startPosition"
|
||||||
|
label="起始U位"
|
||||||
|
rules={[{ required: true, message: '请输入起始U位' }]}
|
||||||
|
>
|
||||||
|
<InputNumber min={1} placeholder="输入起始U位" style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
<div style={{ color: '#666', fontSize: '13px' }}>
|
||||||
|
已选择 <span style={{ color: '#1890ff', fontWeight: 600 }}>{selectedDevices.length}</span> 个设备
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<div style={modalHeaderStyle}>
|
||||||
|
<ExportOutlined style={{ color: '#fa8c16' }} />
|
||||||
|
导出设备数据
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
open={exportModalVisible}
|
||||||
|
onCancel={() => setExportModalVisible(false)}
|
||||||
|
footer={[
|
||||||
|
<Button key="cancel" onClick={() => setExportModalVisible(false)} style={secondaryButtonStyle}>
|
||||||
|
取消
|
||||||
|
</Button>,
|
||||||
|
<Button key="submit" type="primary" loading={exportLoading} onClick={handleEnhancedExport} style={primaryButtonStyle}>
|
||||||
|
导出
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
destroyOnHidden
|
||||||
|
styles={{ header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }, body: { padding: '24px' } }}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
<Form layout="vertical">
|
||||||
|
<Form.Item label="导出格式">
|
||||||
|
<Select value={exportFormat} onChange={setExportFormat} style={{ width: '100%' }}>
|
||||||
|
<Option value="csv">CSV 格式</Option>
|
||||||
|
<Option value="json">JSON 格式</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="导出范围">
|
||||||
|
<Select value={exportScope} onChange={setExportScope} style={{ width: '100%' }}>
|
||||||
|
<Option value="selected">选择的行 ({selectedDevices.length} 个)</Option>
|
||||||
|
<Option value="currentPage">当前页 ({currentPageDevices.length} 个)</Option>
|
||||||
|
<Option value="all">全部设备 ({allDevices.length} 个)</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="选择导出字段">
|
||||||
|
<div style={{ maxHeight: '300px', overflow: 'auto', border: '1px solid #f0f0f0', borderRadius: '8px', padding: '12px' }}>
|
||||||
|
{deviceFields.filter(f => f.visible && f.fieldName !== 'rackId').map(field => (
|
||||||
|
<div key={field.fieldName} style={{ marginBottom: '8px' }}>
|
||||||
|
<Checkbox
|
||||||
|
checked={exportFields.includes(field.fieldName)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setExportFields([...exportFields, field.fieldName]);
|
||||||
|
} else {
|
||||||
|
setExportFields(exportFields.filter(f => f !== field.fieldName));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{field.displayName}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
<div style={{ color: '#666', fontSize: '13px' }}>
|
||||||
|
已选择 <span style={{ color: '#1890ff', fontWeight: 600 }}>{selectedDevices.length}</span> 个设备,
|
||||||
|
将导出 <span style={{ color: '#52c41a', fontWeight: 600 }}>{exportFields.length}</span> 个字段
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,489 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Tabs, Form, Input, Switch, Select, Button, Card, Space, message, Modal, Table, Tag, Progress, Divider, Descriptions, Alert } from 'antd';
|
||||||
|
import { SettingOutlined, GlobalOutlined, BgColorsOutlined, DatabaseOutlined, InfoCircleOutlined, CloudUploadOutlined, DeleteOutlined, ReloadOutlined, DownloadOutlined, SyncOutlined, CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
const { TabPane } = Tabs;
|
||||||
|
|
||||||
|
const SystemSettings = () => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [settings, setSettings] = useState({});
|
||||||
|
const [activeTab, setActiveTab] = useState('general');
|
||||||
|
const [backupList, setBackupList] = useState([]);
|
||||||
|
const [backupLoading, setBackupLoading] = useState(false);
|
||||||
|
const [systemInfo, setSystemInfo] = useState(null);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchSettings();
|
||||||
|
fetchBackupList();
|
||||||
|
fetchSystemInfo();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchSettings = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/system-settings');
|
||||||
|
setSettings(response.data);
|
||||||
|
|
||||||
|
const formValues = {};
|
||||||
|
Object.entries(response.data).forEach(([key, data]) => {
|
||||||
|
formValues[key] = data.value;
|
||||||
|
});
|
||||||
|
form.setFieldsValue(formValues);
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取设置失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchBackupList = async () => {
|
||||||
|
setBackupLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/system-settings/backup/list');
|
||||||
|
setBackupList(response.data.backups || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取备份列表失败');
|
||||||
|
} finally {
|
||||||
|
setBackupLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchSystemInfo = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/system-settings/system/info');
|
||||||
|
setSystemInfo(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取系统信息失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveSettings = async (values) => {
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const updates = {};
|
||||||
|
Object.entries(values).forEach(([key, value]) => {
|
||||||
|
if (settings[key] && settings[key].isEditable) {
|
||||||
|
updates[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await axios.put('/api/system-settings', { settings: updates });
|
||||||
|
message.success('设置保存成功');
|
||||||
|
fetchSettings();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('保存设置失败');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateBackup = async () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认创建备份',
|
||||||
|
icon: <ExclamationCircleOutlined />,
|
||||||
|
content: '确定要创建系统备份吗?这将导出所有设备、机柜、机房和耗材数据。',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
message.loading('正在创建备份...', 0);
|
||||||
|
const response = await axios.post('/api/system-settings/backup');
|
||||||
|
message.destroy();
|
||||||
|
message.success('备份创建成功');
|
||||||
|
fetchBackupList();
|
||||||
|
} catch (error) {
|
||||||
|
message.destroy();
|
||||||
|
message.error('备份创建失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRestoreBackup = (filename) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认恢复备份',
|
||||||
|
icon: <ExclamationCircleOutlined />,
|
||||||
|
content: `确定要恢复备份 "${filename}" 吗?当前数据将被覆盖,且此操作不可撤销。`,
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
message.loading('正在恢复备份...', 0);
|
||||||
|
await axios.post('/api/system-settings/backup/restore', { filename });
|
||||||
|
message.destroy();
|
||||||
|
message.success('恢复成功,请刷新页面查看最新数据');
|
||||||
|
} catch (error) {
|
||||||
|
message.destroy();
|
||||||
|
message.error('恢复备份失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteBackup = (filename) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除备份',
|
||||||
|
icon: <ExclamationCircleOutlined />,
|
||||||
|
content: `确定要删除备份 "${filename}" 吗?`,
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await axios.delete(`/api/system-settings/backup/${filename}`);
|
||||||
|
message.success('删除成功');
|
||||||
|
fetchBackupList();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadBackup = (filename) => {
|
||||||
|
window.open(`/api/system-settings/backup/download/${filename}`, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetSetting = (key) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认重置',
|
||||||
|
icon: <ExclamationCircleOutlined />,
|
||||||
|
content: `确定要将 "${settings[key]?.description || key}" 重置为默认值吗?`,
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await axios.post(`/api/system-settings/reset/${key}`);
|
||||||
|
message.success('重置成功');
|
||||||
|
fetchSettings();
|
||||||
|
} catch (error) {
|
||||||
|
message.error('重置失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFormItem = (key, data) => {
|
||||||
|
if (!data.isEditable) {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
key={key}
|
||||||
|
label={data.description || key}
|
||||||
|
name={key}
|
||||||
|
>
|
||||||
|
<Input disabled suffix={<LockOutlined />} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (data.type) {
|
||||||
|
case 'boolean':
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
key={key}
|
||||||
|
label={data.description || key}
|
||||||
|
name={key}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
case 'number':
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
key={key}
|
||||||
|
label={data.description || key}
|
||||||
|
name={key}
|
||||||
|
rules={[{ required: false, message: `请输入${data.description || key}` }]}
|
||||||
|
>
|
||||||
|
<Input type="number" style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
case 'select':
|
||||||
|
const options = getSelectOptions(key);
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
key={key}
|
||||||
|
label={data.description || key}
|
||||||
|
name={key}
|
||||||
|
>
|
||||||
|
<Select>
|
||||||
|
{options.map(opt => (
|
||||||
|
<Option key={opt.value} value={opt.value}>{opt.label}</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
key={key}
|
||||||
|
label={data.description || key}
|
||||||
|
name={key}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectOptions = (key) => {
|
||||||
|
const optionsMap = {
|
||||||
|
timezone: [
|
||||||
|
{ value: 'Asia/Shanghai', label: '亚洲/上海 (UTC+8)' },
|
||||||
|
{ value: 'Asia/Beijing', label: '亚洲/北京 (UTC+8)' },
|
||||||
|
{ value: 'America/New_York', label: '美洲/纽约 (UTC-5)' },
|
||||||
|
{ value: 'Europe/London', label: '欧洲/伦敦 (UTC+0)' },
|
||||||
|
{ value: 'UTC', label: 'UTC (UTC+0)' }
|
||||||
|
],
|
||||||
|
date_format: [
|
||||||
|
{ value: 'YYYY-MM-DD', label: '2024-01-01' },
|
||||||
|
{ value: 'YYYY/MM/DD', label: '2024/01/01' },
|
||||||
|
{ value: 'DD/MM/YYYY', label: '01/01/2024' },
|
||||||
|
{ value: 'MM/DD/YYYY', label: '01/01/2024' }
|
||||||
|
],
|
||||||
|
language: [
|
||||||
|
{ value: 'zh-CN', label: '简体中文' },
|
||||||
|
{ value: 'zh-TW', label: '繁體中文' },
|
||||||
|
{ value: 'en-US', label: 'English' }
|
||||||
|
],
|
||||||
|
table_row_height: [
|
||||||
|
{ value: 'small', label: '紧凑 (Small)' },
|
||||||
|
{ value: 'default', label: '默认 (Default)' },
|
||||||
|
{ value: 'middle', label: '中等 (Middle)' },
|
||||||
|
{ value: 'large', label: '宽松 (Large)' }
|
||||||
|
],
|
||||||
|
dark_mode: [
|
||||||
|
{ value: 'false', label: '关闭' },
|
||||||
|
{ value: 'true', label: '开启' }
|
||||||
|
],
|
||||||
|
compact_mode: [
|
||||||
|
{ value: 'false', label: '关闭' },
|
||||||
|
{ value: 'true', label: '开启' }
|
||||||
|
],
|
||||||
|
animation_enabled: [
|
||||||
|
{ value: 'false', label: '关闭' },
|
||||||
|
{ value: 'true', label: '开启' }
|
||||||
|
],
|
||||||
|
sidebar_collapsed: [
|
||||||
|
{ value: 'false', label: '展开' },
|
||||||
|
{ value: 'true', label: '折叠' }
|
||||||
|
],
|
||||||
|
auto_backup_enabled: [
|
||||||
|
{ value: 'false', label: '关闭' },
|
||||||
|
{ value: 'true', label: '开启' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
return optionsMap[key] || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderGeneralSettings = () => {
|
||||||
|
const generalKeys = ['site_name', 'site_logo', 'timezone', 'date_format', 'language', 'session_timeout', 'max_login_attempts', 'maintenance_mode'];
|
||||||
|
return (
|
||||||
|
<Card title="全局配置" bordered={false}>
|
||||||
|
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
|
||||||
|
{generalKeys.map(key => settings[key] && renderFormItem(key, settings[key]))}
|
||||||
|
<Form.Item>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" htmlType="submit" loading={saving}>保存设置</Button>
|
||||||
|
<Button onClick={() => fetchSettings()}>重置表单</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderAppearanceSettings = () => {
|
||||||
|
const appearanceKeys = ['primary_color', 'secondary_color', 'dark_mode', 'compact_mode', 'sidebar_collapsed', 'table_row_height', 'animation_enabled'];
|
||||||
|
return (
|
||||||
|
<Card title="外观设置" bordered={false}>
|
||||||
|
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
|
||||||
|
<Alert
|
||||||
|
message="主题颜色"
|
||||||
|
description="修改主题颜色后需要刷新页面才能生效。深色模式可以减少眼睛疲劳。"
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
{appearanceKeys.map(key => settings[key] && renderFormItem(key, settings[key]))}
|
||||||
|
<Form.Item>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" htmlType="submit" loading={saving}>保存设置</Button>
|
||||||
|
<Button onClick={() => fetchSettings()}>重置表单</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBackupSettings = () => {
|
||||||
|
const backupInfo = settings.backup_retention ? {
|
||||||
|
auto_backup_enabled: settings.auto_backup_enabled,
|
||||||
|
backup_interval: settings.backup_interval,
|
||||||
|
backup_retention: settings.backup_retention,
|
||||||
|
last_backup_time: settings.last_backup_time,
|
||||||
|
backup_count: settings.backup_count
|
||||||
|
} : null;
|
||||||
|
|
||||||
|
const backupColumns = [
|
||||||
|
{
|
||||||
|
title: '文件名',
|
||||||
|
dataIndex: 'filename',
|
||||||
|
key: 'filename',
|
||||||
|
render: (text) => <code>{text}</code>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '大小',
|
||||||
|
dataIndex: 'size',
|
||||||
|
key: 'size',
|
||||||
|
render: (size) => {
|
||||||
|
const kb = size / 1024;
|
||||||
|
return kb < 1024 ? `${kb.toFixed(2)} KB` : `${(kb / 1024).toFixed(2)} MB`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
key: 'createdAt',
|
||||||
|
render: (date) => new Date(date).toLocaleString('zh-CN')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space size="small">
|
||||||
|
<Button size="small" icon={<ReloadOutlined />} onClick={() => handleRestoreBackup(record.filename)}>恢复</Button>
|
||||||
|
<Button size="small" icon={<DownloadOutlined />} onClick={() => handleDownloadBackup(record.filename)}>下载</Button>
|
||||||
|
<Button size="small" danger icon={<DeleteOutlined />} onClick={() => handleDeleteBackup(record.filename)}>删除</Button>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Card title="自动备份设置" bordered={false} style={{ marginBottom: 16 }}>
|
||||||
|
<Form form={form} layout="vertical" onFinish={handleSaveSettings}>
|
||||||
|
{backupInfo && Object.entries(backupInfo).map(([key, data]) => (
|
||||||
|
data && typeof data === 'object' ? renderFormItem(key, data) : null
|
||||||
|
))}
|
||||||
|
<Form.Item>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" htmlType="submit" loading={saving}>保存设置</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="手动备份管理" bordered={false}>
|
||||||
|
<Alert
|
||||||
|
message="数据安全提示"
|
||||||
|
description="建议定期创建备份,并将备份文件保存到安全的位置。恢复备份前请确保已创建当前数据的备份。"
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
<Space style={{ marginBottom: 16 }}>
|
||||||
|
<Button type="primary" icon={<CloudUploadOutlined />} onClick={handleCreateBackup}>立即备份</Button>
|
||||||
|
<Button icon={<SyncOutlined />} onClick={fetchBackupList}>刷新列表</Button>
|
||||||
|
</Space>
|
||||||
|
<Table
|
||||||
|
dataSource={backupList}
|
||||||
|
columns={backupColumns}
|
||||||
|
rowKey="filename"
|
||||||
|
loading={backupLoading}
|
||||||
|
pagination={{ pageSize: 5 }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderAboutPage = () => {
|
||||||
|
const aboutKeys = ['app_version', 'company_name', 'contact_email', 'contact_phone', 'company_address', 'system_description', 'privacy_policy', 'terms_of_service'];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Card title="关于系统" bordered={false} style={{ marginBottom: 16 }}>
|
||||||
|
<Descriptions column={{ xs: 1, sm: 2, md: 3 }} bordered>
|
||||||
|
<Descriptions.Item label="系统名称">机柜管理系统</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="版本号">{settings.app_version?.value || '1.0.0'}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="系统状态">
|
||||||
|
<Tag color="success">运行正常</Tag>
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="公司信息" bordered={false} style={{ marginBottom: 16 }}>
|
||||||
|
<Form layout="vertical">
|
||||||
|
{aboutKeys.slice(1).map(key => settings[key] && renderFormItem(key, settings[key]))}
|
||||||
|
<Form.Item>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" htmlType="submit" loading={saving} onClick={() => form.submit()}>保存信息</Button>
|
||||||
|
<Button onClick={() => fetchSettings()}>重置</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{systemInfo && (
|
||||||
|
<Card title="系统统计信息" bordered={false}>
|
||||||
|
<Descriptions column={{ xs: 1, sm: 2, md: 4 }} bordered size="small">
|
||||||
|
<Descriptions.Item label="设备总数">{systemInfo.statistics?.devices || 0}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="机柜总数">{systemInfo.statistics?.racks || 0}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="机房总数">{systemInfo.statistics?.rooms || 0}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="用户总数">{systemInfo.statistics?.users || 0}</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
<Divider />
|
||||||
|
<Descriptions column={{ xs: 1, sm: 2 }} bordered size="small">
|
||||||
|
<Descriptions.Item label="Node.js 版本">{systemInfo.system?.nodeVersion}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="运行平台">{systemInfo.system?.platform} ({systemInfo.system?.arch})</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="进程 ID">{systemInfo.system?.pid}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="运行时间">
|
||||||
|
{systemInfo.system?.uptime ? `${Math.floor(systemInfo.system.uptime / 3600)}小时${Math.floor((systemInfo.system.uptime % 3600) / 60)}分钟` : '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="内存使用">
|
||||||
|
{systemInfo.system?.memoryUsage ? `${(systemInfo.system.memoryUsage.heapUsed / 1024 / 1024).toFixed(2)} MB` : '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="系统时间">
|
||||||
|
{systemInfo.timestamp ? new Date(systemInfo.timestamp).toLocaleString('zh-CN') : '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 24 }}>
|
||||||
|
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||||
|
<TabPane
|
||||||
|
tab={<span><GlobalOutlined /> 全局配置</span>}
|
||||||
|
key="general"
|
||||||
|
>
|
||||||
|
{renderGeneralSettings()}
|
||||||
|
</TabPane>
|
||||||
|
<TabPane
|
||||||
|
tab={<span><BgColorsOutlined /> 外观设置</span>}
|
||||||
|
key="appearance"
|
||||||
|
>
|
||||||
|
{renderAppearanceSettings()}
|
||||||
|
</TabPane>
|
||||||
|
<TabPane
|
||||||
|
tab={<span><DatabaseOutlined /> 数据备份</span>}
|
||||||
|
key="backup"
|
||||||
|
>
|
||||||
|
{renderBackupSettings()}
|
||||||
|
</TabPane>
|
||||||
|
<TabPane
|
||||||
|
tab={<span><InfoCircleOutlined /> 关于</span>}
|
||||||
|
key="about"
|
||||||
|
>
|
||||||
|
{renderAboutPage()}
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
import { LockOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
export default SystemSettings;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const content = fs.readFileSync('e:/IDC/jigui/frontend/src/pages/DeviceManagement.jsx', 'utf8');
|
||||||
|
|
||||||
|
const startMarker = '];\n\n// 可调整列宽的表头组件';
|
||||||
|
const endMarker = '\n\n// 防抖 Hook';
|
||||||
|
|
||||||
|
const startIndex = content.indexOf(startMarker);
|
||||||
|
const endIndex = content.indexOf(endMarker, startIndex);
|
||||||
|
|
||||||
|
if (startIndex >= 0 && endIndex >= 0) {
|
||||||
|
const newContent = content.substring(0, startIndex + 3) + '\n\n// 防抖 Hook' + content.substring(endIndex + endMarker.length);
|
||||||
|
fs.writeFileSync('e:/IDC/jigui/frontend/src/pages/DeviceManagement.jsx', newContent, 'utf8');
|
||||||
|
console.log('Removed ResizeableTitle component');
|
||||||
|
} else {
|
||||||
|
console.log('Could not find markers');
|
||||||
|
console.log('startMarker found:', startIndex >= 0);
|
||||||
|
console.log('endMarker found:', endIndex >= 0);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user