feat: 实现安全存储和批量导入功能增强
refactor(前端): 重构认证和存储逻辑使用secureStorage feat(前端): 添加安全存储工具和日志工具 feat(前端): 增强批量导入功能支持服务器和交换机设备 fix(后端): 改进设备端口和网卡批量导入接口 docs: 更新导入模板和操作说明
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import { API_CONFIG } from '../config/api';
|
||||
import secureStorage, { TOKEN_KEY } from '../utils/secureStorage';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_CONFIG.baseURL,
|
||||
@@ -11,7 +12,7 @@ const api = axios.create({
|
||||
|
||||
api.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem('token');
|
||||
const token = secureStorage.get(TOKEN_KEY);
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
@@ -47,8 +48,8 @@ api.interceptors.response.use(
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
if (!currentPath.startsWith('/login')) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
secureStorage.remove(TOKEN_KEY);
|
||||
secureStorage.remove('user');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ import React from 'react';
|
||||
import { Modal, Button, Typography, Divider } from 'antd';
|
||||
import {
|
||||
ImportOutlined,
|
||||
CloudServerOutlined,
|
||||
ApiOutlined,
|
||||
InfoCircleOutlined,
|
||||
CloudServerOutlined as ServerOutlined,
|
||||
SwapOutlined as SwitcherOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { designTokens } from '../config/theme';
|
||||
import CloseButton from './CloseButton';
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
function BatchImportModal({ visible, onClose, onImportNetworkCard, onImportPort }) {
|
||||
return (
|
||||
@@ -49,7 +49,7 @@ function BatchImportModal({ visible, onClose, onImportNetworkCard, onImportPort
|
||||
>
|
||||
<Button
|
||||
size="large"
|
||||
icon={<CloudServerOutlined />}
|
||||
icon={<ServerOutlined />}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
onImportNetworkCard();
|
||||
@@ -65,17 +65,17 @@ function BatchImportModal({ visible, onClose, onImportNetworkCard, onImportPort
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '16px', fontWeight: 600 }}>批量导入网卡</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: 600 }}>服务器批量导入</div>
|
||||
<div
|
||||
style={{ fontSize: '12px', color: designTokens.colors.neutral[500], fontWeight: 400 }}
|
||||
>
|
||||
用于服务器设备
|
||||
同时导入网卡和端口
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="large"
|
||||
icon={<ApiOutlined />}
|
||||
icon={<SwitcherOutlined />}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
onImportPort();
|
||||
@@ -91,11 +91,11 @@ function BatchImportModal({ visible, onClose, onImportNetworkCard, onImportPort
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '16px', fontWeight: 600 }}>批量导入端口</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: 600 }}>交换机设备</div>
|
||||
<div
|
||||
style={{ fontSize: '12px', color: designTokens.colors.neutral[500], fontWeight: 400 }}
|
||||
>
|
||||
用于所有设备
|
||||
直接导入端口
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -127,7 +127,7 @@ function BatchImportModal({ visible, onClose, onImportNetworkCard, onImportPort
|
||||
marginBottom: '8px',
|
||||
}}
|
||||
>
|
||||
交换机与服务器导入说明
|
||||
导入流程说明
|
||||
</div>
|
||||
<Paragraph
|
||||
style={{
|
||||
@@ -138,11 +138,20 @@ function BatchImportModal({ visible, onClose, onImportNetworkCard, onImportPort
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<strong>交换机端口:</strong>可直接批量导入端口,无需先导入网卡
|
||||
<strong>服务器批量导入:</strong>通过一个模板文件同时导入网卡和端口数据。
|
||||
系统会先导入网卡数据,再导入端口数据,自动关联网卡。
|
||||
</div>
|
||||
<div>
|
||||
<strong>服务器端口:</strong>必须先在"网卡管理"中添加网卡,才能导入端口。
|
||||
服务器的网卡和端口是层级关系:设备 → 网卡 → 端口
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<strong>交换机设备:</strong>可直接批量导入端口,无需先导入网卡
|
||||
</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<strong>操作步骤:</strong>
|
||||
</div>
|
||||
<div style={{ paddingLeft: '12px', marginTop: '4px' }}>
|
||||
<div>1. 点击对应设备类型的按钮</div>
|
||||
<div>2. 下载导入模板并填写数据</div>
|
||||
<div>3. 上传模板文件并开始导入</div>
|
||||
<div>4. 查看导入结果</div>
|
||||
</div>
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
@@ -115,13 +115,13 @@ function NetworkCardImportModal({ visible, onClose, onSuccess }) {
|
||||
const rowNum = index + 2;
|
||||
const errors = [];
|
||||
|
||||
if (!row['设备ID']) {
|
||||
if (!row['设备SN']) {
|
||||
errors.push({
|
||||
row: rowNum,
|
||||
field: '设备ID',
|
||||
value: row['设备ID'] || '(空)',
|
||||
field: '设备SN',
|
||||
value: row['设备SN'] || '(空)',
|
||||
error: '缺少必填字段',
|
||||
suggestion: '请填写设备ID,格式如:DEV001',
|
||||
suggestion: '请填写设备SN,格式如:SNAA0001',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ function NetworkCardImportModal({ visible, onClose, onSuccess }) {
|
||||
try {
|
||||
const networkCardsData = importPreview.map((row, index) => ({
|
||||
nicId: generateUniqueNicId(),
|
||||
deviceId: row['设备ID'],
|
||||
deviceSn: row['设备SN'],
|
||||
name: row['网卡名称'],
|
||||
slotNumber: row['插槽编号'] ? parseInt(row['插槽编号']) : null,
|
||||
model: row['网卡型号'] || null,
|
||||
@@ -208,11 +208,28 @@ function NetworkCardImportModal({ visible, onClose, onSuccess }) {
|
||||
}
|
||||
if (failed > 0) {
|
||||
msgContent += `失败 ${failed} 个`;
|
||||
console.error('导入错误:', errors);
|
||||
Modal.error({
|
||||
title: '导入失败详情',
|
||||
content: (
|
||||
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
|
||||
<Table
|
||||
columns={[
|
||||
{ title: '行号', dataIndex: 'index', key: 'index' },
|
||||
{ title: '网卡名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '失败原因', dataIndex: 'error', key: 'error' },
|
||||
]}
|
||||
dataSource={errors}
|
||||
rowKey="index"
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
width: 600,
|
||||
});
|
||||
}
|
||||
|
||||
if (failed > 0 && success === 0 && skipped === 0 && updated === 0) {
|
||||
message.error(`导入失败!${msgContent}`);
|
||||
if (failed > 0) {
|
||||
message.warning(`导入完成,但有 ${failed} 个失败。${msgContent}`);
|
||||
} else {
|
||||
message.success({
|
||||
content: `导入完成!${msgContent}`,
|
||||
@@ -239,7 +256,7 @@ function NetworkCardImportModal({ visible, onClose, onSuccess }) {
|
||||
const handleDownloadTemplate = () => {
|
||||
const templateData = [
|
||||
{
|
||||
设备ID: 'DEV001',
|
||||
设备SN: 'SN-20230001',
|
||||
网卡名称: '网卡1',
|
||||
插槽编号: '1',
|
||||
网卡型号: 'Intel X710',
|
||||
@@ -247,7 +264,7 @@ function NetworkCardImportModal({ visible, onClose, onSuccess }) {
|
||||
描述: '主网卡',
|
||||
},
|
||||
{
|
||||
设备ID: 'DEV001',
|
||||
设备SN: 'SN-20230001',
|
||||
网卡名称: '网卡2',
|
||||
插槽编号: '2',
|
||||
网卡型号: 'Intel X710',
|
||||
@@ -342,7 +359,7 @@ function NetworkCardImportModal({ visible, onClose, onSuccess }) {
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<strong>前置条件:</strong>请先在
|
||||
<span style={{ color: '#1890ff', fontWeight: 600 }}>设备管理</span>
|
||||
中添加目标服务器,确保设备ID已存在
|
||||
中添加目标服务器,确保设备SN已存在
|
||||
</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<strong>操作步骤:</strong>
|
||||
@@ -351,7 +368,7 @@ function NetworkCardImportModal({ visible, onClose, onSuccess }) {
|
||||
<div>1. 点击「下载模板」获取标准Excel/CSV文件</div>
|
||||
<div>
|
||||
2. 按模板格式填写网卡信息,
|
||||
<span style={{ color: '#ff4d4f', fontWeight: 600 }}>设备ID</span>和
|
||||
<span style={{ color: '#ff4d4f', fontWeight: 600 }}>设备SN</span>和
|
||||
<span style={{ color: '#ff4d4f', fontWeight: 600 }}>网卡名称</span>为必填项
|
||||
</div>
|
||||
<div>3. 点击上传区域选择文件,或直接拖拽文件到上传区域</div>
|
||||
@@ -363,7 +380,7 @@ function NetworkCardImportModal({ visible, onClose, onSuccess }) {
|
||||
</div>
|
||||
<div style={{ paddingLeft: '12px', marginTop: '4px' }}>
|
||||
<div>
|
||||
• <strong>设备ID</strong>(必填):服务器的唯一标识,如DEV001
|
||||
• <strong>设备SN</strong>(必填):服务器的唯一序列号,如SN-20230001
|
||||
</div>
|
||||
<div>
|
||||
• <strong>网卡名称</strong>(必填):网卡的名称或标识,如eth0、网卡1
|
||||
@@ -537,7 +554,7 @@ function NetworkCardImportModal({ visible, onClose, onSuccess }) {
|
||||
</div>
|
||||
<Table
|
||||
columns={[
|
||||
{ title: '设备ID', dataIndex: '设备ID', key: 'deviceId', width: 120 },
|
||||
{ title: '设备SN', dataIndex: '设备SN', key: 'deviceSn', width: 120 },
|
||||
{ title: '网卡名称', dataIndex: '网卡名称', key: 'name', width: 120 },
|
||||
{ title: '插槽编号', dataIndex: '插槽编号', key: 'slotNumber', width: 100 },
|
||||
{ title: '网卡型号', dataIndex: '网卡型号', key: 'model', width: 120 },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import { authAPI } from '../api';
|
||||
import secureStorage, { TOKEN_KEY, USER_KEY } from '../utils/secureStorage';
|
||||
|
||||
const AuthContext = createContext(null);
|
||||
|
||||
@@ -14,41 +15,24 @@ export const useAuth = () => {
|
||||
export const AuthProvider = ({ children }) => {
|
||||
console.log('[AuthContext] Initializing...');
|
||||
|
||||
const savedToken = localStorage.getItem('token');
|
||||
const savedUser = localStorage.getItem('user');
|
||||
const savedToken = secureStorage.get(TOKEN_KEY);
|
||||
const savedUser = secureStorage.get(USER_KEY);
|
||||
|
||||
console.log('[AuthContext] Saved token:', savedToken ? 'exists' : 'null');
|
||||
console.log('[AuthContext] Saved user:', savedUser ? 'exists' : 'null');
|
||||
|
||||
const [user, setUser] = useState(() => {
|
||||
try {
|
||||
if (savedUser) {
|
||||
return JSON.parse(savedUser);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[AuthContext] Parse user error:', e);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const [user, setUser] = useState(() => savedUser || null);
|
||||
const [token, setToken] = useState(() => savedToken);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const currentToken = localStorage.getItem('token');
|
||||
const currentUser = localStorage.getItem('user');
|
||||
const currentToken = secureStorage.get(TOKEN_KEY);
|
||||
const currentUser = secureStorage.get(USER_KEY);
|
||||
|
||||
if (currentToken && currentToken === token) {
|
||||
if (currentUser) {
|
||||
try {
|
||||
const parsedUser = JSON.parse(currentUser);
|
||||
if (JSON.stringify(parsedUser) !== JSON.stringify(user)) {
|
||||
setUser(parsedUser);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[AuthContext] Parse user error:', e);
|
||||
}
|
||||
if (currentUser && JSON.stringify(currentUser) !== JSON.stringify(user)) {
|
||||
setUser(currentUser);
|
||||
}
|
||||
setInitialized(true);
|
||||
} else if (!currentToken) {
|
||||
@@ -62,7 +46,7 @@ export const AuthProvider = ({ children }) => {
|
||||
}, []);
|
||||
|
||||
const fetchProfile = useCallback(async () => {
|
||||
const currentToken = localStorage.getItem('token');
|
||||
const currentToken = secureStorage.get(TOKEN_KEY);
|
||||
if (!currentToken) {
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -72,6 +56,7 @@ export const AuthProvider = ({ children }) => {
|
||||
const response = await authAPI.getProfile();
|
||||
if (response.success) {
|
||||
setUser(response.data.user);
|
||||
secureStorage.set(USER_KEY, response.data.user);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
@@ -85,8 +70,8 @@ export const AuthProvider = ({ children }) => {
|
||||
const response = await authAPI.login({ username, password });
|
||||
if (response.success) {
|
||||
const { token: newToken, user: userData } = response.data;
|
||||
localStorage.setItem('token', newToken);
|
||||
localStorage.setItem('user', JSON.stringify(userData));
|
||||
secureStorage.set(TOKEN_KEY, newToken);
|
||||
secureStorage.set(USER_KEY, userData);
|
||||
setToken(newToken);
|
||||
setUser(userData);
|
||||
return { success: true };
|
||||
@@ -103,8 +88,8 @@ export const AuthProvider = ({ children }) => {
|
||||
if (response.success) {
|
||||
const { token: newToken, user: newUser, isFirstUser, pendingApproval } = response.data;
|
||||
if (newToken) {
|
||||
localStorage.setItem('token', newToken);
|
||||
localStorage.setItem('user', JSON.stringify(newUser));
|
||||
secureStorage.set(TOKEN_KEY, newToken);
|
||||
secureStorage.set(USER_KEY, newUser);
|
||||
setToken(newToken);
|
||||
setUser(newUser);
|
||||
}
|
||||
@@ -117,8 +102,8 @@ export const AuthProvider = ({ children }) => {
|
||||
};
|
||||
|
||||
const logout = useCallback(() => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
secureStorage.remove(TOKEN_KEY);
|
||||
secureStorage.remove(USER_KEY);
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
}, []);
|
||||
@@ -126,7 +111,7 @@ export const AuthProvider = ({ children }) => {
|
||||
const updateUser = newUserData => {
|
||||
const updatedUser = { ...user, ...newUserData };
|
||||
setUser(updatedUser);
|
||||
localStorage.setItem('user', JSON.stringify(updatedUser));
|
||||
secureStorage.set(USER_KEY, updatedUser);
|
||||
};
|
||||
|
||||
const hasPermission = permission => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { message, Modal } from 'antd';
|
||||
import secureStorage, { TOKEN_KEY, USER_KEY } from '../utils/secureStorage';
|
||||
|
||||
/**
|
||||
* 用户空闲超时检测 Hook
|
||||
@@ -43,8 +44,8 @@ const useIdleTimeout = ({
|
||||
clearTimers();
|
||||
|
||||
// 清除登录状态
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
secureStorage.remove(TOKEN_KEY);
|
||||
secureStorage.remove(USER_KEY);
|
||||
|
||||
// 执行自定义登出回调
|
||||
if (onLogout) {
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
import api, { backupAPI } from '../api';
|
||||
import CloseButton from '../components/CloseButton';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import secureStorage, { TOKEN_KEY } from '../utils/secureStorage';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
@@ -605,7 +606,7 @@ const BackupManagement = () => {
|
||||
setRestoreProgress(0);
|
||||
setRestoreStatus('正在初始化...');
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
const token = secureStorage.get(TOKEN_KEY);
|
||||
const options = {
|
||||
overwriteExisting: true,
|
||||
skipFiles: false,
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
CheckSquareOutlined,
|
||||
SyncOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import api from '../api';
|
||||
import dayjs from 'dayjs';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { designTokens } from '../config/theme';
|
||||
@@ -44,18 +44,6 @@ import CloseButton from '../components/CloseButton';
|
||||
const { RangePicker } = DatePicker;
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
});
|
||||
|
||||
api.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
const InventoryManagement = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
@@ -34,24 +34,12 @@ import {
|
||||
CloseOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import api from '../api';
|
||||
import dayjs from 'dayjs';
|
||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { designTokens } from '../config/theme';
|
||||
import CloseButton from '../components/CloseButton';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
});
|
||||
|
||||
api.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
const InventoryTaskExecution = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -32,24 +32,12 @@ import {
|
||||
CloudUploadOutlined,
|
||||
CloudServerOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import api from '../api';
|
||||
import dayjs from 'dayjs';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { designTokens } from '../config/theme';
|
||||
import CloseButton from '../components/CloseButton';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
});
|
||||
|
||||
api.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
const PendingDeviceManagement = () => {
|
||||
const navigate = useNavigate();
|
||||
const [pendingDevices, setPendingDevices] = useState([]);
|
||||
|
||||
@@ -135,6 +135,7 @@ function PortManagement() {
|
||||
const [batchImportModalVisible, setBatchImportModalVisible] = useState(false);
|
||||
const [portAddGuideModalVisible, setPortAddGuideModalVisible] = useState(false);
|
||||
const [portExportModalVisible, setPortExportModalVisible] = useState(false);
|
||||
const [importDeviceType, setImportDeviceType] = useState(null);
|
||||
|
||||
// 展开的设备
|
||||
const [expandedKeys, setExpandedKeys] = useState([]);
|
||||
@@ -707,7 +708,7 @@ function PortManagement() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
const handleImport = (deviceType = 'all') => {
|
||||
setImportModalVisible(true);
|
||||
setImportPreview([]);
|
||||
setImportProgress({ current: 0, total: 0 });
|
||||
@@ -720,12 +721,24 @@ function PortManagement() {
|
||||
try {
|
||||
const data = e.target.result;
|
||||
let parsedData = [];
|
||||
let networkCardData = [];
|
||||
|
||||
if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) {
|
||||
const workbook = XLSX.read(data, { type: 'binary' });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
parsedData = XLSX.utils.sheet_to_json(worksheet);
|
||||
|
||||
if (importDeviceType === 'server' && workbook.SheetNames.length >= 2) {
|
||||
// 服务器设备:解析两张表格
|
||||
const networkCardSheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||
const portSheet = workbook.Sheets[workbook.SheetNames[1]];
|
||||
|
||||
networkCardData = XLSX.utils.sheet_to_json(networkCardSheet);
|
||||
parsedData = XLSX.utils.sheet_to_json(portSheet);
|
||||
} else {
|
||||
// 其他设备:只解析第一张表格
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
parsedData = XLSX.utils.sheet_to_json(worksheet);
|
||||
}
|
||||
} else if (file.name.endsWith('.csv')) {
|
||||
parsedData = Papa.parse(data, {
|
||||
header: true,
|
||||
@@ -741,13 +754,81 @@ function PortManagement() {
|
||||
return;
|
||||
}
|
||||
|
||||
const validatedResult = await validateImportData(parsedData);
|
||||
// 保存网卡数据,供后续导入使用
|
||||
if (networkCardData.length > 0) {
|
||||
// 验证网卡数据
|
||||
const validatedNicData = [];
|
||||
const nicErrors = [];
|
||||
|
||||
for (let i = 0; i < networkCardData.length; i++) {
|
||||
const row = networkCardData[i];
|
||||
const rowNum = i + 2;
|
||||
const errors = [];
|
||||
|
||||
if (!row['设备SN']) {
|
||||
errors.push({
|
||||
row: rowNum,
|
||||
field: '设备SN',
|
||||
value: row['设备SN'] || '(空)',
|
||||
error: '缺少必填字段',
|
||||
suggestion: '请填写设备SN,格式如:SNAA0001',
|
||||
});
|
||||
}
|
||||
|
||||
if (!row['网卡名称']) {
|
||||
errors.push({
|
||||
row: rowNum,
|
||||
field: '网卡名称',
|
||||
value: row['网卡名称'] || '(空)',
|
||||
error: '缺少必填字段',
|
||||
suggestion: '请填写网卡名称,格式如:网卡1、eth0',
|
||||
});
|
||||
}
|
||||
|
||||
if (row['插槽编号']) {
|
||||
const validSlotValues = ['LOM', 'OCP', '1', '2', '3', '4'];
|
||||
const slotValue = row['插槽编号'].toString().trim();
|
||||
if (!validSlotValues.includes(slotValue) && !/^\d+$/.test(slotValue)) {
|
||||
errors.push({
|
||||
row: rowNum,
|
||||
field: '插槽编号',
|
||||
value: row['插槽编号'],
|
||||
error: '插槽编号格式错误',
|
||||
suggestion: '插槽编号必须为数字或预选项(LOM、OCP、1-4)',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
nicErrors.push(...errors.map(err => ({ ...err, originalRow: row })));
|
||||
} else {
|
||||
validatedNicData.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
if (nicErrors.length > 0) {
|
||||
message.warning({
|
||||
content: `发现 ${nicErrors.length} 个网卡数据错误,请查看错误详情并修正后重新导入`,
|
||||
duration: 5,
|
||||
});
|
||||
setImportErrors(nicErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.setItem('networkCardImportData', JSON.stringify(validatedNicData));
|
||||
}
|
||||
|
||||
// 验证并处理数据
|
||||
// 对于服务器设备,跳过网卡存在性验证,因为网卡还没有被导入
|
||||
const validatedResult = await validateImportData(parsedData, importDeviceType === 'server');
|
||||
console.log('文件解析结果:', {
|
||||
总行数: parsedData.length,
|
||||
有效数据: validatedResult.validData.length,
|
||||
错误数据: validatedResult.errors.length,
|
||||
第一行数据: parsedData[0],
|
||||
网卡数据行数: networkCardData.length,
|
||||
});
|
||||
|
||||
setImportPreview(validatedResult.validData);
|
||||
setImportErrors(validatedResult.errors);
|
||||
setImportProgress({ current: 0, total: validatedResult.validData.length });
|
||||
@@ -765,13 +846,13 @@ function PortManagement() {
|
||||
}
|
||||
};
|
||||
|
||||
const validateImportData = async data => {
|
||||
const validateImportData = async (data, skipNicValidation = false) => {
|
||||
const validData = [];
|
||||
const allErrors = [];
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const row = data[i];
|
||||
const result = await validatePortRow(row, i);
|
||||
const result = await validatePortRow(row, i, skipNicValidation);
|
||||
|
||||
if (result.valid) {
|
||||
validData.push(row);
|
||||
@@ -790,17 +871,17 @@ function PortManagement() {
|
||||
return { validData, errors: allErrors };
|
||||
};
|
||||
|
||||
const validatePortRow = async (row, index) => {
|
||||
const validatePortRow = async (row, index, skipNicValidation = false) => {
|
||||
const rowNum = index + 2;
|
||||
const errors = [];
|
||||
|
||||
if (!row['设备ID']) {
|
||||
if (!row['设备SN']) {
|
||||
errors.push({
|
||||
row: rowNum,
|
||||
field: '设备ID',
|
||||
value: row['设备ID'] || '(空)',
|
||||
field: '设备SN',
|
||||
value: row['设备SN'] || '(空)',
|
||||
error: '缺少必填字段',
|
||||
suggestion: '请填写设备ID,格式如:DEV001',
|
||||
suggestion: '请填写设备SN,格式如:SNAA0001',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -814,15 +895,15 @@ function PortManagement() {
|
||||
});
|
||||
}
|
||||
|
||||
if (row['设备ID']) {
|
||||
const device = devices.find(d => d.deviceId === row['设备ID']);
|
||||
if (row['设备SN']) {
|
||||
const device = devices.find(d => d.serialNumber === row['设备SN']);
|
||||
if (!device) {
|
||||
errors.push({
|
||||
row: rowNum,
|
||||
field: '设备ID',
|
||||
value: row['设备ID'],
|
||||
field: '设备SN',
|
||||
value: row['设备SN'],
|
||||
error: '设备不存在',
|
||||
suggestion: `设备ID "${row['设备ID']}" 未在系统中注册,请先在设备管理中添加该设备`,
|
||||
suggestion: `设备SN "${row['设备SN']}" 未在系统中注册,请先在设备管理中添加该设备`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -884,10 +965,10 @@ function PortManagement() {
|
||||
}
|
||||
}
|
||||
|
||||
if (row['网卡名称'] && row['设备ID']) {
|
||||
if (!skipNicValidation && row['网卡名称'] && row['设备SN']) {
|
||||
try {
|
||||
const nicResponse = await api.get('/network-cards/find', {
|
||||
params: { deviceId: row['设备ID'], name: row['网卡名称'] },
|
||||
params: { deviceSn: row['设备SN'], name: row['网卡名称'] },
|
||||
});
|
||||
row._nicId = nicResponse.nicId || null;
|
||||
} catch (error) {
|
||||
@@ -895,8 +976,8 @@ function PortManagement() {
|
||||
}
|
||||
}
|
||||
|
||||
if (row['设备ID']) {
|
||||
const device = devices.find(d => d.deviceId === row['设备ID']);
|
||||
if (row['设备SN']) {
|
||||
const device = devices.find(d => d.serialNumber === row['设备SN']) || devices.find(d => d.sn === row['设备SN']);
|
||||
if (device) {
|
||||
const isServer = device.type && device.type.toLowerCase().includes('server');
|
||||
if (isServer) {
|
||||
@@ -908,7 +989,7 @@ function PortManagement() {
|
||||
error: '服务器端口必须关联网卡',
|
||||
suggestion: `服务器 "${device.name}" 的端口必须关联到网卡,请先在网卡管理中添加网卡,或在导入模板中填写网卡名称`,
|
||||
});
|
||||
} else if (row['网卡名称'] && !row._nicId) {
|
||||
} else if (!skipNicValidation && row['网卡名称'] && !row._nicId) {
|
||||
errors.push({
|
||||
row: rowNum,
|
||||
field: '网卡名称',
|
||||
@@ -940,6 +1021,44 @@ function PortManagement() {
|
||||
let currentProgress = 0;
|
||||
|
||||
try {
|
||||
// 检查是否有网卡数据需要导入
|
||||
let networkCardImportResult = null;
|
||||
const networkCardDataStr = sessionStorage.getItem('networkCardImportData');
|
||||
|
||||
if (networkCardDataStr) {
|
||||
const networkCardData = JSON.parse(networkCardDataStr);
|
||||
if (networkCardData.length > 0) {
|
||||
// 先导入网卡数据
|
||||
console.log('开始导入网卡数据:', networkCardData.length, '条');
|
||||
|
||||
// 生成网卡ID
|
||||
const networkCardsData = networkCardData.map((card, index) => ({
|
||||
nicId: `NIC-${Date.now()}-${index}`,
|
||||
deviceSn: card['设备SN'],
|
||||
name: card['网卡名称'],
|
||||
slotNumber: card['插槽编号'] ? parseInt(card['插槽编号']) : null,
|
||||
model: card['网卡型号'] || null,
|
||||
manufacturer: card['制造商'] || null,
|
||||
description: card['描述'] || null,
|
||||
}));
|
||||
|
||||
const nicResponse = await api.post('/network-cards/batch', {
|
||||
networkCards: networkCardsData,
|
||||
skipExisting: true,
|
||||
updateExisting: false,
|
||||
});
|
||||
|
||||
networkCardImportResult = nicResponse;
|
||||
console.log('网卡导入结果:', nicResponse);
|
||||
|
||||
// 重新获取设备列表,确保包含最新数据
|
||||
await fetchDevices();
|
||||
}
|
||||
|
||||
// 清除保存的网卡数据
|
||||
sessionStorage.removeItem('networkCardImportData');
|
||||
}
|
||||
|
||||
const statusMap = {
|
||||
空闲: 'free',
|
||||
占用: 'occupied',
|
||||
@@ -948,8 +1067,9 @@ function PortManagement() {
|
||||
|
||||
const portsData = importPreview.map((row, index) => ({
|
||||
portId: generateUniquePortId(),
|
||||
deviceId: row['设备ID'],
|
||||
deviceSn: row['设备SN'],
|
||||
nicId: row._nicId || null,
|
||||
'网卡名称': row['网卡名称'],
|
||||
portName: row['端口名称'],
|
||||
portType: row['端口类型'],
|
||||
portSpeed: row['端口速率'],
|
||||
@@ -977,17 +1097,26 @@ function PortManagement() {
|
||||
setImportProgress({ current: importPreview.length, total: importPreview.length });
|
||||
|
||||
let msgContent = '';
|
||||
if (networkCardImportResult) {
|
||||
const { success: nicSuccess, failed: nicFailed } = networkCardImportResult;
|
||||
if (nicSuccess > 0) {
|
||||
msgContent += `网卡导入成功 ${nicSuccess} 个,`;
|
||||
}
|
||||
if (nicFailed > 0) {
|
||||
msgContent += `网卡导入失败 ${nicFailed} 个,`;
|
||||
}
|
||||
}
|
||||
if (updated > 0) {
|
||||
msgContent += `更新 ${updated} 个,`;
|
||||
msgContent += `端口更新 ${updated} 个,`;
|
||||
}
|
||||
if (skipped > 0) {
|
||||
msgContent += `跳过 ${skipped} 个,`;
|
||||
msgContent += `端口跳过 ${skipped} 个,`;
|
||||
}
|
||||
if (success > 0) {
|
||||
msgContent += `新增 ${success - updated} 个,`;
|
||||
msgContent += `端口新增 ${success - updated} 个,`;
|
||||
}
|
||||
if (failed > 0) {
|
||||
msgContent += `失败 ${failed} 个`;
|
||||
msgContent += `端口失败 ${failed} 个`;
|
||||
console.error('导入错误:', errors);
|
||||
}
|
||||
|
||||
@@ -1010,27 +1139,107 @@ function PortManagement() {
|
||||
} finally {
|
||||
clearInterval(progressInterval);
|
||||
setImporting(false);
|
||||
// 确保清除保存的网卡数据
|
||||
sessionStorage.removeItem('networkCardImportData');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadTemplate = () => {
|
||||
const templateData = [
|
||||
{
|
||||
设备ID: 'DEV001',
|
||||
端口名称: 'eth0/1',
|
||||
网卡名称: '',
|
||||
端口类型: 'RJ45',
|
||||
端口速率: '1G',
|
||||
状态: '空闲',
|
||||
'VLAN ID': '100',
|
||||
描述: '示例端口',
|
||||
},
|
||||
];
|
||||
|
||||
const worksheet = XLSX.utils.json_to_sheet(templateData);
|
||||
let fileName = '端口导入模板.xlsx';
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, '端口数据');
|
||||
XLSX.writeFile(workbook, '端口导入模板.xlsx');
|
||||
|
||||
if (importDeviceType === 'server') {
|
||||
// 网卡数据表格
|
||||
const networkCardTemplateData = [
|
||||
{
|
||||
'设备SN': 'SN-20230001',
|
||||
'网卡名称': '网卡1',
|
||||
'插槽编号': 'LOM',
|
||||
'网卡型号': 'Intel X710',
|
||||
'制造商': 'Intel',
|
||||
'描述': '板载网卡',
|
||||
},
|
||||
{
|
||||
'设备SN': 'SN-20230001',
|
||||
'网卡名称': '网卡2',
|
||||
'插槽编号': 'OCP',
|
||||
'网卡型号': 'Intel X710',
|
||||
'制造商': 'Intel',
|
||||
'描述': 'OCP网卡',
|
||||
},
|
||||
{
|
||||
'设备SN': 'SN-20230001',
|
||||
'网卡名称': '网卡3',
|
||||
'插槽编号': '1',
|
||||
'网卡型号': 'Intel X710',
|
||||
'制造商': 'Intel',
|
||||
'描述': 'PCIe插槽1',
|
||||
},
|
||||
];
|
||||
|
||||
// 端口数据表格
|
||||
const portTemplateData = [
|
||||
{
|
||||
'设备SN': 'SN-20230001',
|
||||
'网卡名称': '网卡1',
|
||||
'端口名称': 'eth0/1',
|
||||
'端口类型': 'RJ45',
|
||||
'端口速率': '1G',
|
||||
'状态': '空闲',
|
||||
'VLAN ID': '100',
|
||||
'描述': '服务器端口',
|
||||
},
|
||||
{
|
||||
'设备SN': 'SN-20230001',
|
||||
'网卡名称': '网卡2',
|
||||
'端口名称': 'eth0/2',
|
||||
'端口类型': 'RJ45',
|
||||
'端口速率': '1G',
|
||||
'状态': '空闲',
|
||||
'VLAN ID': '200',
|
||||
'描述': '服务器端口',
|
||||
},
|
||||
];
|
||||
|
||||
const networkCardWorksheet = XLSX.utils.json_to_sheet(networkCardTemplateData);
|
||||
const portWorksheet = XLSX.utils.json_to_sheet(portTemplateData);
|
||||
|
||||
XLSX.utils.book_append_sheet(workbook, networkCardWorksheet, '网卡数据');
|
||||
XLSX.utils.book_append_sheet(workbook, portWorksheet, '端口数据');
|
||||
fileName = '服务器设备导入模板.xlsx';
|
||||
} else if (importDeviceType === 'switch') {
|
||||
const templateData = [
|
||||
{
|
||||
'设备SN': 'SW-20230001',
|
||||
'端口名称': 'GigabitEthernet0/0/1',
|
||||
'端口类型': 'RJ45',
|
||||
'端口速率': '1G',
|
||||
'状态': '空闲',
|
||||
'VLAN ID': '100',
|
||||
'描述': '交换机端口',
|
||||
},
|
||||
];
|
||||
const worksheet = XLSX.utils.json_to_sheet(templateData);
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, '端口数据');
|
||||
fileName = '交换机端口导入模板.xlsx';
|
||||
} else {
|
||||
const templateData = [
|
||||
{
|
||||
'设备SN': 'DEV-20230001',
|
||||
'端口名称': 'eth0/1',
|
||||
'网卡名称': '',
|
||||
'端口类型': 'RJ45',
|
||||
'端口速率': '1G',
|
||||
'状态': '空闲',
|
||||
'VLAN ID': '100',
|
||||
'描述': '示例端口',
|
||||
},
|
||||
];
|
||||
const worksheet = XLSX.utils.json_to_sheet(templateData);
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, '端口数据');
|
||||
}
|
||||
|
||||
XLSX.writeFile(workbook, fileName);
|
||||
};
|
||||
|
||||
const handleExport = async ({ scope, format }) => {
|
||||
@@ -2412,7 +2621,9 @@ function PortManagement() {
|
||||
>
|
||||
<ImportOutlined />
|
||||
</div>
|
||||
<span style={{ fontSize: '18px', fontWeight: 600 }}>批量导入端口</span>
|
||||
<span style={{ fontSize: '18px', fontWeight: 600 }}>
|
||||
{importDeviceType === 'server' ? '服务器批量导入' : '交换机批量导入'}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
open={importModalVisible}
|
||||
@@ -2459,61 +2670,78 @@ function PortManagement() {
|
||||
>
|
||||
<div style={{ padding: '16px 0' }}>
|
||||
<Alert
|
||||
message="操作说明"
|
||||
message="导入说明"
|
||||
description={
|
||||
<div style={{ fontSize: '12px', lineHeight: '1.8' }}>
|
||||
<div>
|
||||
<strong>适用范围:</strong>批量导入端口适用于
|
||||
<span style={{ color: '#1890ff', fontWeight: 600 }}>所有设备类型</span>
|
||||
(交换机、服务器、路由器、存储等)
|
||||
</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<strong>前置条件:</strong>
|
||||
</div>
|
||||
<div style={{ paddingLeft: '12px', marginTop: '4px' }}>
|
||||
<div>
|
||||
• <strong>交换机端口:</strong>可直接导入,无需前置操作
|
||||
</div>
|
||||
<div>
|
||||
• <strong>服务器端口:</strong>必须先在
|
||||
<span style={{ color: '#1890ff', fontWeight: 600 }}>网卡管理</span>
|
||||
中添加网卡,服务器端口与网卡是
|
||||
<span style={{ color: '#ff4d4f', fontWeight: 600 }}>层级关系</span>:设备 → 网卡
|
||||
→ 端口
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<strong>操作步骤:</strong>
|
||||
</div>
|
||||
<div style={{ paddingLeft: '12px', marginTop: '4px' }}>
|
||||
<div>1. 点击「下载模板」获取标准Excel/CSV文件</div>
|
||||
<div>1. 点击「下载模板」获取{importDeviceType === 'server' ? '服务器批量导入' : '交换机批量导入'}模板</div>
|
||||
<div>
|
||||
2. 按模板格式填写端口信息,
|
||||
<span style={{ color: '#ff4d4f', fontWeight: 600 }}>设备ID</span>和
|
||||
<span style={{ color: '#ff4d4f', fontWeight: 600 }}>端口名称</span>为必填项
|
||||
2. 按模板格式填写数据:
|
||||
</div>
|
||||
<div>
|
||||
3.
|
||||
服务器端口可在「网卡名称」列填写对应的网卡名称(如eth0),不填则归入「未分组端口」
|
||||
<div style={{ paddingLeft: '12px', marginTop: '4px' }}>
|
||||
{importDeviceType === 'server' ? (
|
||||
<>
|
||||
<div>
|
||||
- <strong>第一张表格(网卡数据):</strong>填写服务器的网卡信息
|
||||
</div>
|
||||
<div>
|
||||
- <strong>第二张表格(端口数据):</strong>填写网卡对应的端口信息
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
- <strong>端口数据:</strong>填写交换机的端口信息
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>4. 点击上传区域选择文件,或直接拖拽文件到上传区域</div>
|
||||
<div>5. 系统自动校验数据,可预览前10条数据及错误详情</div>
|
||||
<div>6. 选择导入策略(跳过/更新已存在),点击「开始导入」</div>
|
||||
<div>3. 点击上传区域选择文件,或直接拖拽文件到上传区域</div>
|
||||
<div>4. 系统自动校验数据,可预览前10条数据及错误详情</div>
|
||||
<div>5. 选择导入策略(跳过/更新已存在),点击「开始导入」</div>
|
||||
</div>
|
||||
{importDeviceType === 'server' && (
|
||||
<>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<strong>网卡数据字段说明:</strong>
|
||||
</div>
|
||||
<div style={{ paddingLeft: '12px', marginTop: '4px' }}>
|
||||
<div>
|
||||
• <strong>设备SN</strong>(必填):服务器的唯一序列号,如SN-20230001
|
||||
</div>
|
||||
<div>
|
||||
• <strong>网卡名称</strong>(必填):网卡的名称或标识,如网卡1、eth0
|
||||
</div>
|
||||
<div>
|
||||
• <strong>插槽编号</strong>(选填):网卡所在的插槽位置,可选择预选项(LOM、OCP、1-4)或填写其他数字
|
||||
</div>
|
||||
<div>
|
||||
• <strong>网卡型号</strong>(选填):如Intel X710、BCM57414
|
||||
</div>
|
||||
<div>
|
||||
• <strong>制造商</strong>(选填):如Intel、Mellanox
|
||||
</div>
|
||||
<div>
|
||||
• <strong>描述</strong>(选填):备注信息
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<strong>字段说明:</strong>
|
||||
<strong>端口数据字段说明:</strong>
|
||||
</div>
|
||||
<div style={{ paddingLeft: '12px', marginTop: '4px' }}>
|
||||
<div>
|
||||
• <strong>设备ID</strong>(必填):设备的唯一标识,如DEV001
|
||||
• <strong>设备SN</strong>(必填):{importDeviceType === 'server' ? '服务器' : '交换机'}的唯一序列号,如{importDeviceType === 'server' ? 'SN-20230001' : 'SW-20230001'}
|
||||
</div>
|
||||
{importDeviceType === 'server' && (
|
||||
<div>
|
||||
• <strong>网卡名称</strong>(必填):端口所属的网卡名称,必须与网卡数据中的网卡名称对应
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
• <strong>端口名称</strong>
|
||||
(必填):端口的名称或标识,如eth0/1、GigabitEthernet0/0/1
|
||||
</div>
|
||||
<div>
|
||||
• <strong>网卡名称</strong>
|
||||
(选填):服务器端口所属的网卡名称,如eth0;交换机端口留空
|
||||
• <strong>端口名称</strong>(必填):端口的名称或标识,如{importDeviceType === 'server' ? 'eth0/1' : 'GigabitEthernet0/0/1'}
|
||||
</div>
|
||||
<div>
|
||||
• <strong>端口类型</strong>(选填):如RJ45、LC、SC、FC、SFP+、QSFP28等
|
||||
@@ -2535,9 +2763,15 @@ function PortManagement() {
|
||||
<strong>注意事项:</strong>
|
||||
</div>
|
||||
<div style={{ paddingLeft: '12px', marginTop: '4px', color: '#faad14' }}>
|
||||
{importDeviceType === 'server' && (
|
||||
<div>• 同一设备下网卡名称不可重复</div>
|
||||
)}
|
||||
<div>• 同一设备下端口名称不可重复</div>
|
||||
<div>• 批量导入支持最多50000条记录</div>
|
||||
<div>• 状态字段请使用:空闲/占用/故障,勿使用其他值</div>
|
||||
{importDeviceType === 'server' && (
|
||||
<div>• 端口数据的网卡名称必须与网卡数据中的网卡名称对应</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -2697,7 +2931,7 @@ function PortManagement() {
|
||||
</div>
|
||||
<Table
|
||||
columns={[
|
||||
{ title: '设备ID', dataIndex: '设备ID', key: 'deviceId', width: 120 },
|
||||
{ title: '设备SN', dataIndex: '设备SN', key: 'deviceSn', width: 120 },
|
||||
{ title: '端口名称', dataIndex: '端口名称', key: 'portName', width: 120 },
|
||||
{
|
||||
title: '端口类型',
|
||||
@@ -3499,8 +3733,12 @@ function PortManagement() {
|
||||
<BatchImportModal
|
||||
visible={batchImportModalVisible}
|
||||
onClose={() => setBatchImportModalVisible(false)}
|
||||
onImportNetworkCard={() => setNetworkCardImportModalVisible(true)}
|
||||
onImportNetworkCard={() => {
|
||||
setImportDeviceType('server');
|
||||
handleImport();
|
||||
}}
|
||||
onImportPort={() => {
|
||||
setImportDeviceType('switch');
|
||||
handleImport();
|
||||
}}
|
||||
/>
|
||||
@@ -3509,12 +3747,21 @@ function PortManagement() {
|
||||
<NetworkCardImportModal
|
||||
visible={networkCardImportModalVisible}
|
||||
onClose={() => setNetworkCardImportModalVisible(false)}
|
||||
onSuccess={() => {
|
||||
onSuccess={async () => {
|
||||
message.success({
|
||||
content: '网卡导入成功',
|
||||
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
||||
});
|
||||
setRefreshTrigger(prev => prev + 1);
|
||||
// 重新获取设备列表,确保包含最新数据
|
||||
await fetchDevices();
|
||||
// 服务器设备导入网卡后自动进入端口导入
|
||||
if (importDeviceType === 'server') {
|
||||
setTimeout(() => {
|
||||
setNetworkCardImportModalVisible(false);
|
||||
handleImport();
|
||||
}, 1000);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ import CloseButton from '../components/CloseButton';
|
||||
import dayjs from 'dayjs';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { debounce, getUserFromStorage } from '../utils/common';
|
||||
import secureStorage, { TICKET_COLUMNS_KEY } from '../utils/secureStorage';
|
||||
|
||||
const { Option } = Select;
|
||||
const { RangePicker } = DatePicker;
|
||||
@@ -715,7 +716,7 @@ function TicketManagement() {
|
||||
message.success('工单更新成功');
|
||||
} else {
|
||||
const user = getUserFromStorage();
|
||||
ticketData.reporterId = user.userId || localStorage.getItem('userId') || 'USER001';
|
||||
ticketData.reporterId = user.userId || 'USER001';
|
||||
ticketData.reporterName = user.username || '系统用户';
|
||||
await axios.post('/api/tickets', ticketData);
|
||||
message.success('工单创建成功');
|
||||
@@ -767,7 +768,7 @@ function TicketManagement() {
|
||||
const user = getUserFromStorage();
|
||||
await axios.put(`/api/tickets/${selectedTicket.ticketId}/process`, {
|
||||
...values,
|
||||
operatorId: localStorage.getItem('userId'),
|
||||
operatorId: user.userId,
|
||||
operatorName: user.username,
|
||||
});
|
||||
message.success('工单处理完成');
|
||||
@@ -787,7 +788,7 @@ function TicketManagement() {
|
||||
const user = getUserFromStorage();
|
||||
await axios.put(`/api/tickets/${ticketId}/status`, {
|
||||
status: newStatus,
|
||||
operatorId: localStorage.getItem('userId'),
|
||||
operatorId: user.userId,
|
||||
operatorName: user.username,
|
||||
});
|
||||
message.success('状态更新成功');
|
||||
@@ -1128,7 +1129,7 @@ function TicketManagement() {
|
||||
scroll={{ x: 1200 }}
|
||||
columnsState={{
|
||||
onChange: ({ visibleColumns }) => {
|
||||
localStorage.setItem('ticketVisibleColumns', JSON.stringify(visibleColumns));
|
||||
secureStorage.set(TICKET_COLUMNS_KEY, visibleColumns);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import secureStorage, { USER_KEY } from './secureStorage';
|
||||
|
||||
export const debounce = (func, wait) => {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
@@ -23,8 +25,8 @@ export const throttle = (func, limit) => {
|
||||
|
||||
export const getUserFromStorage = () => {
|
||||
try {
|
||||
const userStr = localStorage.getItem('user');
|
||||
return userStr ? JSON.parse(userStr) : {};
|
||||
const user = secureStorage.get(USER_KEY);
|
||||
return user || {};
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
const isDev = import.meta.env.DEV;
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
export const logger = {
|
||||
log: isDev ? console.log.bind(console) : noop,
|
||||
warn: isDev ? console.warn.bind(console) : noop,
|
||||
error: console.error.bind(console),
|
||||
info: isDev ? console.info.bind(console) : noop,
|
||||
debug: isDev ? console.debug.bind(console) : noop,
|
||||
|
||||
group: isDev ? console.group.bind(console) : noop,
|
||||
groupEnd: isDev ? console.groupEnd.bind(console) : noop,
|
||||
groupCollapsed: isDev ? console.groupCollapsed.bind(console) : noop,
|
||||
|
||||
table: isDev ? console.table.bind(console) : noop,
|
||||
trace: isDev ? console.trace.bind(console) : noop,
|
||||
time: isDev ? console.time.bind(console) : noop,
|
||||
timeEnd: isDev ? console.timeEnd.bind(console) : noop,
|
||||
timeLog: isDev ? console.timeLog.bind(console) : noop,
|
||||
|
||||
clear: isDev ? console.clear.bind(console) : noop,
|
||||
};
|
||||
|
||||
export default logger;
|
||||
@@ -0,0 +1,111 @@
|
||||
const STORAGE_PREFIX = 'idc_';
|
||||
const ENCRYPTION_ENABLED = import.meta.env.PROD;
|
||||
|
||||
const simpleEncrypt = (text) => {
|
||||
if (!ENCRYPTION_ENABLED) return text;
|
||||
|
||||
const key = import.meta.env.VITE_STORAGE_KEY || 'idc_default_secret_key_2024';
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result += String.fromCharCode(text.charCodeAt(i) ^ key.charCodeAt(i % key.length));
|
||||
}
|
||||
return btoa(encodeURIComponent(result));
|
||||
};
|
||||
|
||||
const simpleDecrypt = (encrypted) => {
|
||||
if (!ENCRYPTION_ENABLED) return encrypted;
|
||||
|
||||
try {
|
||||
const key = import.meta.env.VITE_STORAGE_KEY || 'idc_default_secret_key_2024';
|
||||
const decoded = decodeURIComponent(atob(encrypted));
|
||||
let result = '';
|
||||
for (let i = 0; i < decoded.length; i++) {
|
||||
result += String.fromCharCode(decoded.charCodeAt(i) ^ key.charCodeAt(i % key.length));
|
||||
}
|
||||
return result;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const secureStorage = {
|
||||
set: (key, value, options = {}) => {
|
||||
try {
|
||||
const storageKey = `${STORAGE_PREFIX}${key}`;
|
||||
const data = {
|
||||
value,
|
||||
timestamp: Date.now(),
|
||||
expiry: options.expiry || null,
|
||||
};
|
||||
const serialized = JSON.stringify(data);
|
||||
const encrypted = simpleEncrypt(serialized);
|
||||
localStorage.setItem(storageKey, encrypted);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[SecureStorage] Set error:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
get: (key) => {
|
||||
try {
|
||||
const storageKey = `${STORAGE_PREFIX}${key}`;
|
||||
const encrypted = localStorage.getItem(storageKey);
|
||||
|
||||
if (!encrypted) return null;
|
||||
|
||||
const decrypted = simpleDecrypt(encrypted);
|
||||
if (!decrypted) {
|
||||
localStorage.removeItem(storageKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = JSON.parse(decrypted);
|
||||
|
||||
if (data.expiry && Date.now() > data.expiry) {
|
||||
localStorage.removeItem(storageKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.value;
|
||||
} catch (error) {
|
||||
console.error('[SecureStorage] Get error:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
remove: (key) => {
|
||||
try {
|
||||
const storageKey = `${STORAGE_PREFIX}${key}`;
|
||||
localStorage.removeItem(storageKey);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[SecureStorage] Remove error:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
try {
|
||||
const keys = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && key.startsWith(STORAGE_PREFIX)) {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
keys.forEach((key) => localStorage.removeItem(key));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[SecureStorage] Clear error:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const TOKEN_KEY = 'token';
|
||||
export const USER_KEY = 'user';
|
||||
export const USER_ID_KEY = 'userId';
|
||||
export const TICKET_COLUMNS_KEY = 'ticketVisibleColumns';
|
||||
|
||||
export default secureStorage;
|
||||
Reference in New Issue
Block a user