diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0e75af1..80a8769 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,10 +14,12 @@ "antd": "^5.8.6", "axios": "^1.5.0", "dayjs": "^1.11.19", + "framer-motion": "^12.34.0", "papaparse": "^5.5.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.15.0", + "styled-components": "^6.3.9", "three": "^0.160.1", "xlsx": "^0.18.5" }, @@ -663,6 +665,22 @@ "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", "license": "MIT" }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, "node_modules/@emotion/unitless": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", @@ -2298,6 +2316,12 @@ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", "license": "MIT" }, + "node_modules/@types/stylis": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.7.tgz", + "integrity": "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==", + "license": "MIT" + }, "node_modules/@types/three": { "version": "0.182.0", "resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz", @@ -3010,6 +3034,15 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/camera-controls": { "version": "2.10.1", "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.10.1.tgz", @@ -3216,6 +3249,26 @@ "node": ">= 8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "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", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", @@ -4180,6 +4233,33 @@ "node": ">=0.8" } }, + "node_modules/framer-motion": { + "version": "12.34.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.0.tgz", + "integrity": "sha512-+/H49owhzkzQyxtn7nZeF4kdH++I2FWrESQ184Zbcw5cEqNHYkE5yxWxcTLSj5lNx3NWdbIRy5FHqUvetD8FWg==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.34.0", + "motion-utils": "^12.29.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5395,6 +5475,21 @@ "node": "*" } }, + "node_modules/motion-dom": { + "version": "12.34.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.0.tgz", + "integrity": "sha512-Lql3NuEcScRDxTAO6GgUsRHBZOWI/3fnMlkMcH5NftzcN37zJta+bpbMAV9px4Nj057TuvRooMK7QrzMCgtz6Q==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.29.2" + } + }, + "node_modules/motion-utils": { + "version": "12.29.2", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz", + "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5406,7 +5501,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -5690,7 +5784,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -5746,6 +5839,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/potpack": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", @@ -6873,6 +6972,12 @@ "node": ">= 0.4" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -6991,7 +7096,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -7204,6 +7308,73 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/styled-components": { + "version": "6.3.9", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.3.9.tgz", + "integrity": "sha512-J72R4ltw0UBVUlEjTzI0gg2STOqlI9JBhQOL4Dxt7aJOnnSesy0qJDn4PYfMCafk9cWOaVg129Pesl5o+DIh0Q==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.4.0", + "@emotion/unitless": "0.10.0", + "@types/stylis": "4.2.7", + "css-to-react-native": "3.2.0", + "csstype": "3.2.3", + "postcss": "8.4.49", + "shallowequal": "1.1.0", + "stylis": "4.3.6", + "tslib": "2.8.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/styled-components/node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/styled-components/node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/stylis": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", @@ -7463,6 +7634,12 @@ "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", "license": "MIT" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tunnel-rat": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 4f998df..f34bc83 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,10 +19,12 @@ "antd": "^5.8.6", "axios": "^1.5.0", "dayjs": "^1.11.19", + "framer-motion": "^12.34.0", "papaparse": "^5.5.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.15.0", + "styled-components": "^6.3.9", "three": "^0.160.1", "xlsx": "^0.18.5" }, diff --git a/frontend/src/pages/CableManagement.jsx b/frontend/src/pages/CableManagement.jsx index 16d4d8e..b646f03 100644 --- a/frontend/src/pages/CableManagement.jsx +++ b/frontend/src/pages/CableManagement.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Table, Button, @@ -18,6 +18,14 @@ import { Upload, Progress, Checkbox, + Statistic, + Row, + Col, + Badge, + Divider, + Typography, + Alert, + Skeleton, } from 'antd'; import { PlusOutlined, @@ -29,48 +37,134 @@ import { ImportOutlined, DownloadOutlined, UploadOutlined as UploadIcon, + AppstoreOutlined, + SwapOutlined, + CheckCircleOutlined, + ExclamationCircleOutlined, + DisconnectOutlined, + LinkOutlined, + FilterOutlined, + ClearOutlined, + MoreOutlined, } from '@ant-design/icons'; import axios from 'axios'; import * as XLSX from 'xlsx'; import Papa from 'papaparse'; +import { motion, AnimatePresence } from 'framer-motion'; const { Option } = Select; const { Panel } = Collapse; +const { Text, Title } = Typography; +const { TextArea } = Input; +// 设计令牌 - 现代化配色方案 const designTokens = { colors: { primary: { - main: '#667eea', - gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', - light: '#8b9ff0', - dark: '#4f5db8', + main: '#6366f1', + gradient: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', + light: '#818cf8', + dark: '#4f46e5', + bg: '#eef2ff', }, success: { main: '#10b981', gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', light: '#34d399', dark: '#047857', + bg: '#ecfdf5', }, warning: { main: '#f59e0b', gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', light: '#fbbf24', dark: '#b45309', + bg: '#fffbeb', }, error: { main: '#ef4444', gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)', light: '#f87171', dark: '#b91c1c', + bg: '#fef2f2', + }, + info: { + main: '#3b82f6', + bg: '#eff6ff', + }, + neutral: { + 50: '#f8fafc', + 100: '#f1f5f9', + 200: '#e2e8f0', + 300: '#cbd5e1', + 400: '#94a3b8', + 500: '#64748b', + 600: '#475569', + 700: '#334155', + 800: '#1e293b', + 900: '#0f172a', }, }, borderRadius: { - small: '6px', - medium: '10px', - large: '16px', + sm: '6px', + md: '10px', + lg: '16px', + xl: '20px', }, shadows: { - medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)', + sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', + md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)', + lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)', + xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)', + }, +}; + +// 动画配置 +const animations = { + container: { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.05, + delayChildren: 0.1, + }, + }, + }, + item: { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.3, + ease: [0.25, 0.46, 0.45, 0.94], + }, + }, + }, + card: { + hidden: { opacity: 0, scale: 0.95 }, + visible: { + opacity: 1, + scale: 1, + transition: { + duration: 0.3, + ease: [0.25, 0.46, 0.45, 0.94], + }, + }, + hover: { + scale: 1.01, + boxShadow: designTokens.shadows.lg, + transition: { duration: 0.2 }, + }, + }, + table: { + hidden: { opacity: 0, y: 10 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.25 }, + }, }, }; @@ -85,6 +179,7 @@ function CableManagement() { switchDeviceId: '', status: 'all', cableType: 'all', + searchText: '', }); const [modalVisible, setModalVisible] = useState(false); const [editingCable, setEditingCable] = useState(null); @@ -98,6 +193,12 @@ function CableManagement() { const [skipExisting, setSkipExisting] = useState(false); const [updateExisting, setUpdateExisting] = useState(false); + const [conflictModalVisible, setConflictModalVisible] = useState(false); + const [conflictInfo, setConflictInfo] = useState(null); + const [pendingSubmitValues, setPendingSubmitValues] = useState(null); + + const [expandedKeys, setExpandedKeys] = useState([]); + const fetchCables = useCallback(async () => { try { setLoading(true); @@ -109,10 +210,24 @@ function CableManagement() { const response = await axios.get('/api/cables', { params }); const cablesData = response.data.cables || []; - setCables(cablesData); + + // 搜索过滤 + let filteredCables = cablesData; + if (filters.searchText) { + const searchLower = filters.searchText.toLowerCase(); + filteredCables = cablesData.filter(cable => + cable.sourceDevice?.name?.toLowerCase().includes(searchLower) || + cable.targetDevice?.name?.toLowerCase().includes(searchLower) || + cable.sourcePort?.toLowerCase().includes(searchLower) || + cable.targetPort?.toLowerCase().includes(searchLower) || + cable.cableId?.toLowerCase().includes(searchLower) + ); + } + + setCables(filteredCables); const grouped = {}; - cablesData.forEach(cable => { + filteredCables.forEach(cable => { const switchId = cable.sourceDeviceId; if (!grouped[switchId]) { grouped[switchId] = { @@ -124,8 +239,11 @@ function CableManagement() { }); setGroupedCables(grouped); - // 自动为每个交换机加载端口数据 + // 自动展开前5个 const switchIds = Object.keys(grouped); + setExpandedKeys(switchIds.slice(0, 5)); + + // 自动为每个交换机加载端口数据 for (const switchId of switchIds) { if (!devicePorts[switchId]) { try { @@ -186,6 +304,7 @@ function CableManagement() { switchDeviceId: '', status: 'all', cableType: 'all', + searchText: '', }); }; @@ -213,7 +332,10 @@ function CableManagement() { const handleDelete = async cableId => { try { await axios.delete(`/api/cables/${cableId}`); - message.success('删除成功'); + message.success({ + content: '删除成功', + icon: , + }); fetchCables(); } catch (error) { message.error('删除失败'); @@ -224,7 +346,10 @@ function CableManagement() { const handleDeleteSwitch = async switchId => { try { await axios.delete(`/api/devices/${switchId}`); - message.success('删除设备成功'); + message.success({ + content: '删除设备成功', + icon: , + }); fetchDevices(); fetchCables(); } catch (error) { @@ -233,10 +358,6 @@ function CableManagement() { } }; - const [conflictModalVisible, setConflictModalVisible] = useState(false); - const [conflictInfo, setConflictInfo] = useState(null); - const [pendingSubmitValues, setPendingSubmitValues] = useState(null); - const handleSubmit = async () => { try { const values = await form.validateFields(); @@ -244,7 +365,10 @@ function CableManagement() { // 如果是编辑模式,直接提交 if (editingCable) { await axios.put(`/api/cables/${editingCable.cableId}`, values); - message.success('更新成功'); + message.success({ + content: '更新成功', + icon: , + }); setModalVisible(false); form.resetFields(); fetchCables(); @@ -269,7 +393,10 @@ function CableManagement() { // 无冲突,直接创建 await axios.post('/api/cables', values); - message.success('创建成功'); + message.success({ + content: '创建成功', + icon: , + }); setModalVisible(false); form.resetFields(); fetchCables(); @@ -303,7 +430,10 @@ function CableManagement() { force: true, }); - message.success('接线已强制接管并创建成功'); + message.success({ + content: '接线已强制接管并创建成功', + icon: , + }); setConflictModalVisible(false); setModalVisible(false); form.resetFields(); @@ -463,7 +593,10 @@ function CableManagement() { console.error('导入错误:', errors); message.warning(`导入完成!成功 ${success} 条,失败 ${failed} 条`); } else { - message.success(`导入完成!成功 ${success} 条`); + message.success({ + content: `导入完成!成功 ${success} 条`, + icon: , + }); } fetchCables(); @@ -499,22 +632,60 @@ function CableManagement() { const getStatusTag = status => { const statusMap = { - normal: { color: 'success', text: '正常' }, - fault: { color: 'error', text: '故障' }, - disconnected: { color: 'default', text: '未连接' }, + normal: { + color: 'success', + text: '正常', + icon: , + bg: designTokens.colors.success.bg, + }, + fault: { + color: 'error', + text: '故障', + icon: , + bg: designTokens.colors.error.bg, + }, + disconnected: { + color: 'default', + text: '未连接', + icon: , + bg: designTokens.colors.neutral[100], + }, }; - const config = statusMap[status] || { color: 'default', text: status }; - return {config.text}; + const config = statusMap[status] || { color: 'default', text: status, icon: null, bg: designTokens.colors.neutral[100] }; + return ( + + {config.text} + + ); }; const getCableTypeTag = type => { const typeMap = { - 网线: { color: 'blue', text: '网线' }, - 光纤: { color: 'green', text: '光纤' }, - 铜缆: { color: 'orange', text: '铜缆' }, + ethernet: { color: 'blue', text: '网线', icon: '🔌' }, + fiber: { color: 'cyan', text: '光纤', icon: '🔷' }, + copper: { color: 'orange', text: '铜缆', icon: '⚡' }, }; - const config = typeMap[type] || { color: 'default', text: type }; - return {config.text}; + const config = typeMap[type] || { color: 'default', text: type, icon: '' }; + return ( + + {config.text} + + ); }; const getPortConnectionStatus = (portName, switchData) => { @@ -529,12 +700,13 @@ function CableManagement() { }; }; - const portColumns = [ + const portColumns = useMemo(() => [ { title: '端口名称', dataIndex: 'portName', key: 'portName', width: 120, + render: text => {text}, }, { title: '端口类型', @@ -551,7 +723,7 @@ function CableManagement() { QSFP28: { color: 'red', text: 'QSFP28' }, }; const config = typeMap[type] || { color: 'default', text: type }; - return {config.text}; + return {config.text}; }, }, { @@ -559,6 +731,7 @@ function CableManagement() { dataIndex: 'portSpeed', key: 'portSpeed', width: 100, + render: text => {text}, }, { title: '连接状态', @@ -567,7 +740,12 @@ function CableManagement() { width: 100, render: (_, record) => { const status = getPortConnectionStatus(record.portName, record.switchData); - return {status.text}; + return ( + + ); }, }, { @@ -577,11 +755,11 @@ function CableManagement() { width: 200, render: (_, record) => { const cable = record.switchData.cables.find(c => c.sourcePort === record.portName); - if (!cable) return '-'; + if (!cable) return -; return (
-
{cable.targetDevice?.name || '-'}
-
{cable.targetPort}
+
{cable.targetDevice?.name || '-'}
+
{cable.targetPort}
); }, @@ -593,25 +771,25 @@ function CableManagement() { width: 100, render: (_, record) => { const cable = record.switchData.cables.find(c => c.sourcePort === record.portName); - if (!cable) return '-'; + if (!cable) return -; return getCableTypeTag(cable.cableType); }, }, { - title: '长度(米)', + title: '长度', dataIndex: 'cableLength', key: 'cableLength', - width: 100, + width: 80, render: (_, record) => { const cable = record.switchData.cables.find(c => c.sourcePort === record.portName); - if (!cable) return '-'; - return cable.cableLength ? `${cable.cableLength}m` : '-'; + if (!cable) return -; + return cable.cableLength ? {cable.cableLength}m : -; }, }, { title: '操作', key: 'action', - width: 150, + width: 120, fixed: 'right', render: (_, record) => { const cable = record.switchData.cables.find(c => c.sourcePort === record.portName); @@ -619,408 +797,697 @@ function CableManagement() { {cable && ( <> - - handleDelete(cable.cableId)} - okText="确定" - cancelText="取消" - > - - + + - - - - - -
- - - - - - - -
- - {loading ? ( -
- -
- ) : Object.keys(groupedCables).length === 0 ? ( -
- -
- ) : ( - + {/* 页面标题 */} + +
+
- {Object.entries(groupedCables).map(([switchId, switchData]) => { - const switchDevice = switchData.switch; - const switchPorts = devicePorts[switchId] || []; - const connectedCount = switchData.cables.filter(c => c.status === 'normal').length; - const disconnectedCount = switchData.cables.filter( - c => c.status === 'disconnected' - ).length; - const faultCount = switchData.cables.filter(c => c.status === 'fault').length; + +
+
+ 接线管理 + 管理设备间的网络连接关系 +
+
+
- return ( - + + {/* 过滤器区域 */} +
+ + + +
+ 交换机 +
+ + + + +
+ 线缆类型 +
+ + + + +
+ 连接状态 +
+ + + + +
+ 搜索 +
+ setFilters(prev => ({ ...prev, searchText: e.target.value }))} + onPressEnter={handleSearch} + prefix={} + allowClear + /> + + + +
操作
+ + + +
+ + {/* 操作按钮区域 */} +
+ + + + + + + +
+ + {/* 数据展示区域 */} + {loading ? ( +
+ +
+ ) : Object.keys(groupedCables).length === 0 ? ( + + +
+ 暂无接线数据
- } - extra={ - - +
+
+ ) : ( + + + {Object.entries(groupedCables).map(([switchId, switchData], index) => { + const switchDevice = switchData.switch; + const switchPorts = devicePorts[switchId] || []; + const connectedCount = switchData.cables.filter(c => c.status === 'normal').length; + const disconnectedCount = switchData.cables.filter(c => c.status === 'disconnected').length; + const faultCount = switchData.cables.filter(c => c.status === 'fault').length; + + return ( + + +
+
+ +
+
+
+ {switchDevice?.name || '未知设备'} +
+
+ {switchDevice?.deviceId || '-'} · {switchDevice?.model || '交换机'} +
+
+
+ + + } + > + {connectedCount} + + + + } + > + {disconnectedCount} + + + {faultCount > 0 && ( + + } + > + {faultCount} + + + )} + + 总计: {switchData.cables.length} + + + + } + extra={ + e.stopPropagation()}> + + - handleDeleteSwitch(switchId)} - okText="确定" - cancelText="取消" - > - - - - } - > - ({ - ...port, - switchData: switchData, - }))} - rowKey="portId" - pagination={false} - size="small" - scroll={{ x: 1200 }} - /> - - ); - })} - - )} - + +
({ + ...port, + switchData: switchData, + }))} + rowKey="portId" + pagination={false} + size="middle" + scroll={{ x: 1100 }} + style={{ + borderRadius: designTokens.borderRadius.md, + overflow: 'hidden', + }} + /> + + + + ); + })} + + + )} + + + {/* 新增/编辑接线弹窗 */} +
+ +
+ + {editingCable ? '编辑接线' : '新增接线'} + + + } open={modalVisible} onOk={handleSubmit} onCancel={() => { setModalVisible(false); form.resetFields(); }} - width={600} + width={700} okText="确定" cancelText="取消" + okButtonProps={{ + style: { + background: designTokens.colors.primary.gradient, + border: 'none', + borderRadius: designTokens.borderRadius.sm, + }, + }} > -
- - - + + +
+ 源设备 (起点)} + size="small" + style={{ + background: designTokens.colors.primary.bg, + border: `1px solid ${designTokens.colors.primary.light}30`, + borderRadius: designTokens.borderRadius.md, + }} + headStyle={{ background: 'transparent', borderBottom: 'none' }} + > + + + - - - + + + + + - - - + + 目标设备 (终点)} + size="small" + style={{ + background: designTokens.colors.success.bg, + border: `1px solid ${designTokens.colors.success.light}30`, + borderRadius: designTokens.borderRadius.md, + }} + headStyle={{ background: 'transparent', borderBottom: 'none' }} + > + + + - - - + + + + + + - - - + + + + + + - - - + + + + + - - - + + + + + + - +