feat: 实现安全存储和批量导入功能增强

refactor(前端): 重构认证和存储逻辑使用secureStorage
feat(前端): 添加安全存储工具和日志工具
feat(前端): 增强批量导入功能支持服务器和交换机设备
fix(后端): 改进设备端口和网卡批量导入接口
docs: 更新导入模板和操作说明
This commit is contained in:
zhangbing
2026-03-29 10:06:32 +08:00
parent 63f0cb570e
commit 1d815b812e
22 changed files with 1326 additions and 214 deletions
+10 -4
View File
@@ -38,14 +38,20 @@ lerna-debug.log*
*.db
*.sqlite
# Temporary files
*.tmp
*.temp
# Backup files (generated by maintenance scripts)
backend/backups/
*.backup.json
# Uploads directory (user-uploaded files)
backend/uploads/*
!backend/uploads/.gitkeep
# Temporary files
backend/temp/*
!backend/temp/.gitkeep
*.tmp
*.temp
#数据文件
*.xlsx
*.csv
Binary file not shown.
+13 -4
View File
@@ -165,13 +165,22 @@ router.post('/batch', async (req, res) => {
const portData = ports[i];
try {
if (!portData.portId || !portData.deviceId || !portData.portName) {
if (!portData.portId || !(portData.deviceId || portData.deviceSn) || !portData.portName) {
throw new Error('缺少必填字段');
}
const device = await Device.findByPk(portData.deviceId, { transaction });
if (!device) {
throw new Error(`设备 ${portData.deviceId} 不存在`);
let device;
if (portData.deviceSn) {
device = await Device.findOne({ where: { serialNumber: portData.deviceSn }, transaction });
if (!device) {
throw new Error(`设备SN ${portData.deviceSn} 不存在`);
}
portData.deviceId = device.deviceId;
} else {
device = await Device.findByPk(portData.deviceId, { transaction });
if (!device) {
throw new Error(`设备ID ${portData.deviceId} 不存在`);
}
}
const isServer = device.type && device.type.toLowerCase().includes('server');
+26 -9
View File
@@ -172,14 +172,22 @@ router.get('/:nicId/ports', async (req, res) => {
router.get('/find', async (req, res) => {
try {
const { deviceId, name } = req.query;
const { deviceId, deviceSn, name } = req.query;
if (!deviceId || !name) {
return res.status(400).json({ error: '缺少设备ID或网卡名称' });
if ((!deviceId && !deviceSn) || !name) {
return res.status(400).json({ error: '缺少设备ID/SN或网卡名称' });
}
let device;
if (deviceSn) {
device = await Device.findOne({ where: { serialNumber: deviceSn } });
if (!device) {
return res.json({ nicId: null });
}
}
const networkCard = await NetworkCard.findOne({
where: { deviceId, name },
where: { deviceId: device ? device.deviceId : deviceId, name },
});
if (!networkCard) {
@@ -265,13 +273,22 @@ router.post('/batch', async (req, res) => {
const cardData = networkCards[i];
try {
if (!cardData.deviceId || !cardData.name) {
throw new Error('缺少必填字段:设备ID和网卡名称');
if (!(cardData.deviceId || cardData.deviceSn) || !cardData.name) {
throw new Error('缺少必填字段:设备ID/SN和网卡名称');
}
const device = await Device.findByPk(cardData.deviceId, { transaction });
if (!device) {
throw new Error(`设备 ${cardData.deviceId} 不存在`);
let device;
if (cardData.deviceSn) {
device = await Device.findOne({ where: { serialNumber: cardData.deviceSn }, transaction });
if (!device) {
throw new Error(`设备SN ${cardData.deviceSn} 不存在`);
}
cardData.deviceId = device.deviceId;
} else {
device = await Device.findByPk(cardData.deviceId, { transaction });
if (!device) {
throw new Error(`设备ID ${cardData.deviceId} 不存在`);
}
}
const isServer = device.type && device.type.toLowerCase().includes('server');
+2
View File
@@ -0,0 +1,2 @@
# This file ensures the temp directory is tracked by Git
# All temporary files in this directory are ignored
+2
View File
@@ -0,0 +1,2 @@
# This file ensures the uploads directory is tracked by Git
# All user-uploaded files in this directory are ignored
+710
View File
@@ -15,11 +15,13 @@
"axios": "^1.13.6",
"dayjs": "^1.11.19",
"framer-motion": "^12.34.0",
"html2canvas": "^1.4.1",
"papaparse": "^5.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.15.0",
"react-transition-group": "^4.4.5",
"reactflow": "^11.11.4",
"styled-components": "^6.3.9",
"swr": "^2.4.0",
"three": "^0.183.2",
@@ -1806,6 +1808,276 @@
}
}
},
"node_modules/@reactflow/background": {
"version": "11.3.14",
"resolved": "https://registry.npmmirror.com/@reactflow/background/-/background-11.3.14.tgz",
"integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==",
"license": "MIT",
"dependencies": {
"@reactflow/core": "11.11.4",
"classcat": "^5.0.3",
"zustand": "^4.4.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@reactflow/background/node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/controls": {
"version": "11.2.14",
"resolved": "https://registry.npmmirror.com/@reactflow/controls/-/controls-11.2.14.tgz",
"integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==",
"license": "MIT",
"dependencies": {
"@reactflow/core": "11.11.4",
"classcat": "^5.0.3",
"zustand": "^4.4.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@reactflow/controls/node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/core": {
"version": "11.11.4",
"resolved": "https://registry.npmmirror.com/@reactflow/core/-/core-11.11.4.tgz",
"integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==",
"license": "MIT",
"dependencies": {
"@types/d3": "^7.4.0",
"@types/d3-drag": "^3.0.1",
"@types/d3-selection": "^3.0.3",
"@types/d3-zoom": "^3.0.1",
"classcat": "^5.0.3",
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0",
"zustand": "^4.4.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@reactflow/core/node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/minimap": {
"version": "11.7.14",
"resolved": "https://registry.npmmirror.com/@reactflow/minimap/-/minimap-11.7.14.tgz",
"integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==",
"license": "MIT",
"dependencies": {
"@reactflow/core": "11.11.4",
"@types/d3-selection": "^3.0.3",
"@types/d3-zoom": "^3.0.1",
"classcat": "^5.0.3",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0",
"zustand": "^4.4.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@reactflow/minimap/node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/node-resizer": {
"version": "2.2.14",
"resolved": "https://registry.npmmirror.com/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
"integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==",
"license": "MIT",
"dependencies": {
"@reactflow/core": "11.11.4",
"classcat": "^5.0.4",
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0",
"zustand": "^4.4.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@reactflow/node-resizer/node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/node-toolbar": {
"version": "1.3.14",
"resolved": "https://registry.npmmirror.com/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
"integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==",
"license": "MIT",
"dependencies": {
"@reactflow/core": "11.11.4",
"classcat": "^5.0.3",
"zustand": "^4.4.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@reactflow/node-toolbar/node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@remix-run/router": {
"version": "1.23.1",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz",
@@ -2296,6 +2568,259 @@
"assertion-error": "^2.0.1"
}
},
"node_modules/@types/d3": {
"version": "7.4.3",
"resolved": "https://registry.npmmirror.com/@types/d3/-/d3-7.4.3.tgz",
"integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
"license": "MIT",
"dependencies": {
"@types/d3-array": "*",
"@types/d3-axis": "*",
"@types/d3-brush": "*",
"@types/d3-chord": "*",
"@types/d3-color": "*",
"@types/d3-contour": "*",
"@types/d3-delaunay": "*",
"@types/d3-dispatch": "*",
"@types/d3-drag": "*",
"@types/d3-dsv": "*",
"@types/d3-ease": "*",
"@types/d3-fetch": "*",
"@types/d3-force": "*",
"@types/d3-format": "*",
"@types/d3-geo": "*",
"@types/d3-hierarchy": "*",
"@types/d3-interpolate": "*",
"@types/d3-path": "*",
"@types/d3-polygon": "*",
"@types/d3-quadtree": "*",
"@types/d3-random": "*",
"@types/d3-scale": "*",
"@types/d3-scale-chromatic": "*",
"@types/d3-selection": "*",
"@types/d3-shape": "*",
"@types/d3-time": "*",
"@types/d3-time-format": "*",
"@types/d3-timer": "*",
"@types/d3-transition": "*",
"@types/d3-zoom": "*"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz",
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
"license": "MIT"
},
"node_modules/@types/d3-axis": {
"version": "3.0.6",
"resolved": "https://registry.npmmirror.com/@types/d3-axis/-/d3-axis-3.0.6.tgz",
"integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-brush": {
"version": "3.0.6",
"resolved": "https://registry.npmmirror.com/@types/d3-brush/-/d3-brush-3.0.6.tgz",
"integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-chord": {
"version": "3.0.6",
"resolved": "https://registry.npmmirror.com/@types/d3-chord/-/d3-chord-3.0.6.tgz",
"integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
"license": "MIT"
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-contour": {
"version": "3.0.6",
"resolved": "https://registry.npmmirror.com/@types/d3-contour/-/d3-contour-3.0.6.tgz",
"integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
"license": "MIT",
"dependencies": {
"@types/d3-array": "*",
"@types/geojson": "*"
}
},
"node_modules/@types/d3-delaunay": {
"version": "6.0.4",
"resolved": "https://registry.npmmirror.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
"integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
"license": "MIT"
},
"node_modules/@types/d3-dispatch": {
"version": "3.0.7",
"resolved": "https://registry.npmmirror.com/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
"integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
"license": "MIT"
},
"node_modules/@types/d3-drag": {
"version": "3.0.7",
"resolved": "https://registry.npmmirror.com/@types/d3-drag/-/d3-drag-3.0.7.tgz",
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-dsv": {
"version": "3.0.7",
"resolved": "https://registry.npmmirror.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
"integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
"license": "MIT"
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-fetch": {
"version": "3.0.7",
"resolved": "https://registry.npmmirror.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
"integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
"license": "MIT",
"dependencies": {
"@types/d3-dsv": "*"
}
},
"node_modules/@types/d3-force": {
"version": "3.0.10",
"resolved": "https://registry.npmmirror.com/@types/d3-force/-/d3-force-3.0.10.tgz",
"integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
"license": "MIT"
},
"node_modules/@types/d3-format": {
"version": "3.0.4",
"resolved": "https://registry.npmmirror.com/@types/d3-format/-/d3-format-3.0.4.tgz",
"integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
"license": "MIT"
},
"node_modules/@types/d3-geo": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/@types/d3-geo/-/d3-geo-3.1.0.tgz",
"integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/d3-hierarchy": {
"version": "3.1.7",
"resolved": "https://registry.npmmirror.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
"integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
"license": "MIT"
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"license": "MIT"
},
"node_modules/@types/d3-polygon": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
"integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
"license": "MIT"
},
"node_modules/@types/d3-quadtree": {
"version": "3.0.6",
"resolved": "https://registry.npmmirror.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
"integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
"license": "MIT"
},
"node_modules/@types/d3-random": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/@types/d3-random/-/d3-random-3.0.3.tgz",
"integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-scale-chromatic": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
"license": "MIT"
},
"node_modules/@types/d3-selection": {
"version": "3.0.11",
"resolved": "https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.11.tgz",
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
"license": "MIT"
},
"node_modules/@types/d3-shape": {
"version": "3.1.8",
"resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz",
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"license": "MIT"
},
"node_modules/@types/d3-time-format": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
"integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
"license": "MIT"
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/d3-transition": {
"version": "3.0.9",
"resolved": "https://registry.npmmirror.com/@types/d3-transition/-/d3-transition-3.0.9.tgz",
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-zoom": {
"version": "3.0.8",
"resolved": "https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
"license": "MIT",
"dependencies": {
"@types/d3-interpolate": "*",
"@types/d3-selection": "*"
}
},
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
@@ -2316,6 +2841,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -3034,6 +3565,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -3304,6 +3844,12 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/classcat": {
"version": "5.0.5",
"resolved": "https://registry.npmmirror.com/classcat/-/classcat-5.0.5.tgz",
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
"license": "MIT"
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
@@ -3456,6 +4002,15 @@
"node": ">=4"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css-to-react-native": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
@@ -3509,6 +4064,112 @@
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-dispatch": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-drag": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz",
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-selection": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-selection": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-transition": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz",
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3",
"d3-dispatch": "1 - 3",
"d3-ease": "1 - 3",
"d3-interpolate": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"d3-selection": "2 - 3"
}
},
"node_modules/d3-zoom": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz",
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-drag": "2 - 3",
"d3-interpolate": "1 - 3",
"d3-selection": "2 - 3",
"d3-transition": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/data-urls": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz",
@@ -4792,6 +5453,19 @@
"node": ">=18"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
@@ -7017,6 +7691,24 @@
}
}
},
"node_modules/reactflow": {
"version": "11.11.4",
"resolved": "https://registry.npmmirror.com/reactflow/-/reactflow-11.11.4.tgz",
"integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==",
"license": "MIT",
"dependencies": {
"@reactflow/background": "11.3.14",
"@reactflow/controls": "11.2.14",
"@reactflow/core": "11.11.4",
"@reactflow/minimap": "11.7.14",
"@reactflow/node-resizer": "2.2.14",
"@reactflow/node-toolbar": "1.3.14"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
@@ -7828,6 +8520,15 @@
"node": ">=10"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/three": {
"version": "0.183.2",
"resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
@@ -8397,6 +9098,15 @@
"node": ">= 4"
}
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/vite": {
"version": "4.5.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz",
+2
View File
@@ -20,11 +20,13 @@
"axios": "^1.13.6",
"dayjs": "^1.11.19",
"framer-motion": "^12.34.0",
"html2canvas": "^1.4.1",
"papaparse": "^5.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.15.0",
"react-transition-group": "^4.4.5",
"reactflow": "^11.11.4",
"styled-components": "^6.3.9",
"swr": "^2.4.0",
"three": "^0.183.2",
+4 -3
View File
@@ -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';
}
}
+23 -14
View File
@@ -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 },
+17 -32
View File
@@ -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 => {
+3 -2
View File
@@ -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) {
+2 -1
View File
@@ -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,
+1 -13
View File
@@ -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();
+1 -13
View File
@@ -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();
+1 -13
View File
@@ -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([]);
+333 -86
View File
@@ -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>必填网卡的名称或标识如网卡1eth0
</div>
<div>
<strong>插槽编号</strong>选填网卡所在的插槽位置可选择预选项LOMOCP1-4或填写其他数字
</div>
<div>
<strong>网卡型号</strong>选填如Intel X710BCM57414
</div>
<div>
<strong>制造商</strong>选填如IntelMellanox
</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/1GigabitEthernet0/0/1
</div>
<div>
<strong>网卡名称</strong>
选填服务器端口所属的网卡名称如eth0交换机端口留空
<strong>端口名称</strong>必填端口的名称或标识{importDeviceType === 'server' ? 'eth0/1' : 'GigabitEthernet0/0/1'}
</div>
<div>
<strong>端口类型</strong>选填如RJ45LCSCFCSFP+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);
}
}}
/>
+5 -4
View File
@@ -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);
},
}}
/>
+4 -2
View File
@@ -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 {};
}
+25
View File
@@ -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;
+111
View File
@@ -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;