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 && (
<>
- }
- onClick={() => handleEdit(cable)}
- >
- 编辑
-
- handleDelete(cable.cableId)}
- okText="确定"
- cancelText="取消"
- >
- }>
- 删除
-
-
+
+ }
+ onClick={() => handleEdit(cable)}
+ style={{ color: designTokens.colors.primary.main }}
+ />
+
+
+ handleDelete(cable.cableId)}
+ okText="确定"
+ cancelText="取消"
+ >
+ } />
+
+
>
)}
);
},
},
- ];
+ ], []);
return (
-
-
-
-
-
-
-
-
-
-
- }
- onClick={handleSearch}
- style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
- >
- 搜索
-
-
- } onClick={handleReset}>
- 重置
-
-
-
-
-
-
- }
- onClick={handleAdd}
- style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
- >
- 新增接线
-
-
- }
- onClick={handleImport}
- style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
- >
- 批量导入
-
-
- }>导出
-
-
-
- {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
+ />
+
+
+
+ 操作
+
+ }
+ onClick={handleSearch}
+ style={{
+ background: designTokens.colors.primary.gradient,
+ border: 'none',
+ borderRadius: designTokens.borderRadius.sm,
}}
>
-
-
- 🔀
-
-
-
- {switchDevice?.name || '未知设备'}
-
-
- {switchDevice?.deviceId || '-'}
-
-
-
-
- 已连接: {connectedCount}
- 未连接: {disconnectedCount}
- {faultCount > 0 && 故障: {faultCount}}
- 总计: {switchData.cables.length}
-
+ 筛选
+
+
+ }
+ onClick={handleReset}
+ style={{ borderRadius: designTokens.borderRadius.sm }}
+ />
+
+
+
+
+
+
+
+ {/* 操作按钮区域 */}
+
+
+ }
+ onClick={handleAdd}
+ size="large"
+ style={{
+ background: designTokens.colors.primary.gradient,
+ border: 'none',
+ borderRadius: designTokens.borderRadius.sm,
+ boxShadow: designTokens.shadows.md,
+ }}
+ >
+ 新增接线
+
+ }
+ onClick={handleImport}
+ size="large"
+ style={{ borderRadius: designTokens.borderRadius.sm }}
+ >
+ 批量导入
+
+ }
+ size="large"
+ style={{ borderRadius: designTokens.borderRadius.sm }}
+ >
+ 导出
+
+
+
+
+ }
+ onClick={fetchCables}
+ loading={loading}
+ style={{ borderRadius: designTokens.borderRadius.sm }}
+ />
+
+
+
+
+ {/* 数据展示区域 */}
+ {loading ? (
+
+
+
+ ) : Object.keys(groupedCables).length === 0 ? (
+
+
+
+ 暂无接线数据
- }
- extra={
-
- }
- onClick={() => {
- setEditingCable(null);
- form.setFieldsValue({ sourceDeviceId: switchId });
- setModalVisible(true);
+
+ 点击"新增接线"按钮创建第一条接线记录
+
+
+ }
+ >
+ }
+ onClick={handleAdd}
+ style={{
+ background: designTokens.colors.primary.gradient,
+ border: 'none',
+ borderRadius: designTokens.borderRadius.sm,
+ }}
+ >
+ 新增接线
+
+
+
+ ) : (
+
+
+ {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()}>
+
+ }
+ onClick={() => {
+ setEditingCable(null);
+ form.setFieldsValue({ sourceDeviceId: switchId });
+ setModalVisible(true);
+ }}
+ style={{ color: designTokens.colors.primary.main }}
+ />
+
+
+ handleDeleteSwitch(switchId)}
+ okText="确定"
+ cancelText="取消"
+ >
+ } />
+
+
+
+ }
+ style={{
+ background: '#fff',
+ borderRadius: designTokens.borderRadius.lg,
+ marginBottom: '12px',
+ border: `1px solid ${designTokens.colors.neutral[200]}`,
+ overflow: 'hidden',
}}
>
- 添加接线
-
- 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,
+ },
+ }}
>
-
-
-
+
-
-
+
+
+
+
+
+
-
-
-
+
+
+
+
+
-
-
-
+
+
+
+
+
+
-
+
+ {/* 批量导入弹窗 */}
+
+
+
+ 批量导入接线
+
+ }
open={importModalVisible}
onCancel={() => {
setImportModalVisible(false);
setImportPreview([]);
setImportProgress({ current: 0, total: 0 });
}}
- width={800}
+ width={900}
footer={[
- ,
开始导入
,
]}
>
-
-
-
false}
- customRequest={({ file, onSuccess }) => {
- handleFileUpload({ file, onSuccess });
- }}
- >
-
-
-
- 点击或拖拽文件到此处上传
- 支持 .xlsx, .xls, .csv 格式
-
-
+
+
false}
+ customRequest={({ file, onSuccess }) => {
+ handleFileUpload({ file, onSuccess });
+ }}
+ style={{
+ borderRadius: designTokens.borderRadius.lg,
+ border: `2px dashed ${designTokens.colors.primary.light}`,
+ background: designTokens.colors.primary.bg,
+ }}
+ >
+
+
+
+
+ 点击或拖拽文件到此处上传
+
+
+ 支持 .xlsx, .xls, .csv 格式文件
+
+
-
+
setSkipExisting(e.target.checked)}>
跳过已存在的接线
@@ -1064,88 +1542,54 @@ function CableManagement() {
{importPreview.length > 0 && (
- <>
-
-
- 数据预览(前10条)
- } onClick={handleDownloadTemplate}>
- 下载模板
-
-
-
getCableTypeTag(type),
- },
- {
- title: '状态',
- dataIndex: '状态',
- key: 'status',
- width: 100,
- render: status => getStatusTag(status),
- },
- {
- title: '描述',
- dataIndex: '描述',
- key: 'description',
- ellipsis: true,
- },
- ]}
- dataSource={importPreview.slice(0, 10)}
- rowKey={(record, index) => index}
- pagination={false}
- size="small"
- scroll={{ x: 1000 }}
- />
+
+
+
+ 数据预览(前10条)
-
+ getCableTypeTag(type) },
+ { title: '状态', dataIndex: '状态', key: 'status', width: 90, render: status => getStatusTag(status) },
+ { title: '描述', dataIndex: '描述', key: 'description', ellipsis: true },
+ ]}
+ dataSource={importPreview.slice(0, 10)}
+ rowKey={(record, index) => index}
+ pagination={false}
+ size="small"
+ scroll={{ x: 800 }}
+ style={{ borderRadius: designTokens.borderRadius.md }}
+ />
{importPreview.length > 10 && (
-
-
仅显示前10条数据,共 {importPreview.length} 条
+
+ 仅显示前10条数据,共 {importPreview.length} 条
)}
- >
+
)}
{importing && (
-
+
-
+
-
-
- 正在导入 {importProgress.current} / {importProgress.total} 条数据...
-
- {importProgress.current > 0 && (
-
- 预计剩余时间:{Math.ceil((importProgress.total - importProgress.current) / 5)}{' '}
- 秒
-
- )}
+
+ 正在导入 {importProgress.current} / {importProgress.total} 条数据...
-
+
)}
{/* 冲突提示弹窗 */}
+
+ 端口冲突警告
+
+ }
open={conflictModalVisible}
onCancel={() => {
setConflictModalVisible(false);
@@ -1188,79 +1630,88 @@ function CableManagement() {
setConflictInfo(null);
setPendingSubmitValues(null);
}}
+ style={{ borderRadius: designTokens.borderRadius.sm }}
>
取消
,
-
+
强制接管
,
]}
width={600}
>
{conflictInfo && (
-
-
- ⚠️
- 检测到端口冲突,以下端口已被占用:
-
- {conflictInfo.map((conflict, index) => (
-
-
-
- {conflict.type === 'source'
- ? '源端口'
- : conflict.type === 'target'
- ? '目标端口'
- : '端口'}
-
- {conflict.port}
-
- {conflict.existingCable && (
-
-
当前连接:
-
-
- 源设备:
- {conflict.existingCable.sourceDevice?.name ||
- conflict.existingCable.sourceDeviceId}
- ({conflict.existingCable.sourcePort})
-
-
- 目标设备:
- {conflict.existingCable.targetDevice?.name ||
- conflict.existingCable.targetDeviceId}
- ({conflict.existingCable.targetPort})
-
-
- 线缆类型:{getCableTypeTag(conflict.existingCable.cableType)}
+
+
+
+ {conflictInfo.map((conflict, index) => (
+
+
+
+ {conflict.type === 'source' ? '源端口' : conflict.type === 'target' ? '目标端口' : '端口'}
+
+
+ {conflict.port}
+
+
+ {conflict.existingCable && (
+
+
当前连接:
+
+
+ 源设备:
+
+ {conflict.existingCable.sourceDevice?.name || conflict.existingCable.sourceDeviceId}
+
+ {conflict.existingCable.sourcePort}
+
+
+ 目标设备:
+
+ {conflict.existingCable.targetDevice?.name || conflict.existingCable.targetDeviceId}
+
+ {conflict.existingCable.targetPort}
+
+
+ 线缆类型:
+ {getCableTypeTag(conflict.existingCable.cableType)}
+
-
- )}
-
- ))}
-
- 💡
-
- 点击"强制接管"将断开原有连接并创建新接线。此操作不可恢复!
-
+ )}
+
+ ))}
+
)}
-
+
);
}
diff --git a/frontend/src/pages/ConsumableManagement.jsx b/frontend/src/pages/ConsumableManagement.jsx
index 62781dc..2b1d162 100644
--- a/frontend/src/pages/ConsumableManagement.jsx
+++ b/frontend/src/pages/ConsumableManagement.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
+import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useDebounce } from '../hooks/useDebounce';
import {
Table,
@@ -13,9 +13,17 @@ import {
Space,
Popconfirm,
Upload,
- Table as AntTable,
Progress,
Checkbox,
+ Row,
+ Col,
+ Badge,
+ Tag,
+ Tooltip,
+ Empty,
+ Skeleton,
+ Alert,
+ Typography,
} from 'antd';
import {
PlusOutlined,
@@ -26,11 +34,108 @@ import {
ImportOutlined,
UploadOutlined,
FileExcelOutlined,
- InboxOutlined,
+ ShoppingOutlined,
+ FilterOutlined,
+ ClearOutlined,
+ ReloadOutlined,
+ CheckCircleOutlined,
+ ExclamationCircleOutlined,
+ ArrowUpOutlined,
+ ArrowDownOutlined,
} from '@ant-design/icons';
import axios from 'axios';
+import { motion, AnimatePresence } from 'framer-motion';
const { Option } = Select;
+const { Text, Title } = Typography;
+const { TextArea } = Input;
+
+// 设计令牌 - 与接线/端口管理页面保持一致
+const designTokens = {
+ colors: {
+ primary: {
+ 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: {
+ sm: '6px',
+ md: '10px',
+ lg: '16px',
+ xl: '20px',
+ },
+ shadows: {
+ 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],
+ },
+ },
+ },
+};
function ConsumableManagement() {
const [consumables, setConsumables] = useState([]);
@@ -69,7 +174,8 @@ function ConsumableManagement() {
const response = await axios.get('/api/consumables', {
params: { page, pageSize, keyword: debouncedKeyword, category, status },
});
- setConsumables(response.data.consumables);
+ const data = response.data.consumables || [];
+ setConsumables(data);
setPagination(prev => ({ ...prev, current: page, pageSize, total: response.data.total }));
} catch (error) {
message.error('获取耗材列表失败');
@@ -84,7 +190,7 @@ function ConsumableManagement() {
const fetchCategories = useCallback(async () => {
try {
const response = await axios.get('/api/consumable-categories/list');
- setCategories(response.data);
+ setCategories(response.data || []);
} catch (error) {
console.error('获取分类列表失败:', error);
}
@@ -139,13 +245,19 @@ function ConsumableManagement() {
};
if (editingConsumable) {
await axios.put(`/api/consumables/${editingConsumable.consumableId}`, submitData);
- message.success('耗材更新成功');
+ message.success({
+ content: '耗材更新成功',
+ icon:
,
+ });
} else {
await axios.post('/api/consumables', {
...submitData,
consumableId: `CON${Date.now()}`,
});
- message.success('耗材创建成功');
+ message.success({
+ content: '耗材创建成功',
+ icon:
,
+ });
}
setModalVisible(false);
fetchConsumables();
@@ -162,7 +274,10 @@ function ConsumableManagement() {
async consumableId => {
try {
await axios.delete(`/api/consumables/${consumableId}`);
- message.success('删除成功');
+ message.success({
+ content: '删除成功',
+ icon:
,
+ });
fetchConsumables();
} catch (error) {
message.error('删除失败');
@@ -176,6 +291,12 @@ function ConsumableManagement() {
setKeyword(value);
}, []);
+ const handleReset = () => {
+ setKeyword('');
+ setCategory('all');
+ setStatus('all');
+ };
+
const exportToCSV = (data, filename) => {
const headers = [
'耗材ID',
@@ -241,7 +362,10 @@ function ConsumableManagement() {
});
const consumables = response.data.consumables;
exportToCSV(consumables, `consumables_${new Date().toISOString().split('T')[0]}.csv`);
- message.success('导出成功');
+ message.success({
+ content: '导出成功',
+ icon:
,
+ });
} catch (error) {
message.error('导出失败');
console.error('导出失败:', error);
@@ -331,7 +455,6 @@ function ConsumableManagement() {
reader.onload = async e => {
const text = e.target.result;
setImportProgress(10);
- setImportPhase('正在读取文件...');
setTimeout(() => {
setImportProgress(20);
@@ -374,22 +497,18 @@ function ConsumableManagement() {
if (results.failed > 0) {
message.warning(`导入完成,成功 ${results.success} 条,失败 ${results.failed} 条`);
} else {
- message.success(response.data.message || `成功导入 ${results.success} 条记录`);
+ message.success({
+ content: response.data.message || `成功导入 ${results.success} 条记录`,
+ icon:
,
+ });
}
+ fetchConsumables();
}, 300);
};
reader.onerror = () => {
setImporting(false);
setImportProgress(0);
setImportPhase('文件读取失败');
- setImportResult({
- success: false,
- total: 0,
- imported: 0,
- failed: 0,
- errors: [{ row: '-', error: '文件读取失败,请检查文件是否损坏' }],
- message: '文件读取失败',
- });
message.error('文件读取失败');
};
reader.readAsText(importFile);
@@ -397,43 +516,7 @@ function ConsumableManagement() {
setImporting(false);
setImportProgress(0);
setImportPhase('导入失败');
-
- let errorMessage = '导入失败,请检查网络连接或服务器状态';
- let errorDetails = [];
-
- if (error.response && error.response.data) {
- const { data } = error.response;
- if (data.errors && Array.isArray(data.errors) && data.errors.length > 0) {
- errorDetails = data.errors.map((err, index) => ({
- row: err.row || index + 1,
- error: err.error || err.message || '数据格式错误',
- }));
- errorMessage = `导入失败,共发现 ${errorDetails.length} 处数据错误`;
- } else if (data.message) {
- errorMessage = data.message;
- } else if (data.error) {
- errorMessage = data.error;
- }
- } else if (error.message) {
- if (error.message.includes('Network Error') || error.message.includes('network')) {
- errorMessage = '网络连接失败,请检查服务器是否运行';
- } else if (error.message.includes('timeout')) {
- errorMessage = '请求超时,请稍后重试';
- } else {
- errorMessage = error.message;
- }
- }
-
- setImportResult({
- success: false,
- total: 0,
- imported: 0,
- failed: 0,
- errors: errorDetails,
- message: errorMessage,
- });
-
- message.error(errorMessage);
+ message.error('导入失败,请检查网络连接或服务器状态');
console.error('导入耗材失败:', error);
}
};
@@ -476,7 +559,7 @@ function ConsumableManagement() {
const handleStockSubmit = useCallback(
async values => {
try {
- const response = await axios.post('/api/consumables/quick-inout', {
+ await axios.post('/api/consumables/quick-inout', {
consumableId: stockRecord.consumableId,
type: stockType,
quantity: values.quantity,
@@ -484,7 +567,10 @@ function ConsumableManagement() {
reason: values.reason,
notes: values.notes,
});
- message.success(`${stockType === 'in' ? '入库' : '出库'}操作成功`);
+ message.success({
+ content: `${stockType === 'in' ? '入库' : '出库'}操作成功`,
+ icon:
,
+ });
setStockModalVisible(false);
fetchConsumables();
} catch (error) {
@@ -504,18 +590,21 @@ function ConsumableManagement() {
dataIndex: 'name',
key: 'name',
width: 150,
+ render: text =>
{text},
},
{
title: '分类',
dataIndex: 'category',
key: 'category',
width: 120,
+ render: text =>
{text},
},
{
title: '单位',
dataIndex: 'unit',
key: 'unit',
width: 80,
+ render: text =>
{text},
},
{
title: '当前库存',
@@ -525,9 +614,17 @@ function ConsumableManagement() {
render: (value, record) => {
const isLow = value <= record.minStock;
return (
-
- {value}
-
+
+ {value}
+
+ }
+ />
);
},
},
@@ -536,42 +633,51 @@ function ConsumableManagement() {
dataIndex: 'minStock',
key: 'minStock',
width: 100,
+ render: text => {text},
},
{
title: '最大库存',
dataIndex: 'maxStock',
key: 'maxStock',
width: 100,
- render: value => (value === 0 || value === null || value === undefined ? '无限制' : value),
+ render: value => (
+
+ {value === 0 || value === null || value === undefined ? '无限制' : value}
+
+ ),
},
{
- title: '单价(元)',
+ title: '单价',
dataIndex: 'unitPrice',
key: 'unitPrice',
width: 100,
- render: value => `¥${parseFloat(value || 0).toFixed(2)}`,
+ render: value => (
+
+ ¥{parseFloat(value || 0).toFixed(2)}
+
+ ),
},
{
title: '供应商',
dataIndex: 'supplier',
key: 'supplier',
width: 150,
- render: value => value || '-',
+ render: value => value || -,
},
{
title: '位置',
dataIndex: 'location',
key: 'location',
width: 120,
- render: value => value || '-',
+ render: value => value || -,
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
width: 200,
- render: value => value || '-',
ellipsis: true,
+ render: value => value || -,
},
{
title: '状态',
@@ -579,48 +685,56 @@ function ConsumableManagement() {
key: 'status',
width: 100,
render: value => (
-
+ : }
+ >
{value === 'active' ? '启用' : '停用'}
-
+
),
},
{
title: '操作',
key: 'action',
width: 200,
+ fixed: 'right',
render: (_, record) => (
-
- }
- size="small"
- onClick={() => showModal(record)}
- >
- 编辑
-
- }
- size="small"
- style={{ background: '#f6ffed', borderColor: '#b7eb8f', color: '#52c41a' }}
- onClick={() => showStockModal(record, 'in')}
- >
- 入库
-
- }
- size="small"
- style={{ background: '#fff2f0', borderColor: '#ffccc7', color: '#ff4d4f' }}
- onClick={() => showStockModal(record, 'out')}
- >
- 出库
-
- handleDelete(record.consumableId)}>
- } size="small">
- 删除
-
-
+
+
+ }
+ onClick={() => showModal(record)}
+ style={{ color: designTokens.colors.primary.main }}
+ />
+
+
+ }
+ onClick={() => showStockModal(record, 'in')}
+ style={{ color: designTokens.colors.success.main }}
+ />
+
+
+ }
+ onClick={() => showStockModal(record, 'out')}
+ style={{ color: designTokens.colors.error.main }}
+ />
+
+
+ handleDelete(record.consumableId)}
+ okText="确定"
+ cancelText="取消"
+ >
+ } />
+
+
),
},
@@ -637,157 +751,412 @@ function ConsumableManagement() {
];
return (
-
-
- } onClick={() => showModal()}>
- 添加耗材
-
- } onClick={showImportModal}>
- 导入
-
- } onClick={handleExport}>
- 导出
-
-
- }
- >
-
-
-
-
-
-
+
+ {/* 页面标题 */}
+
+
+
+
+
+
+
耗材管理
+ 管理机房耗材的库存、入库和出库
+
+
+
+
+ {/* 主内容区 */}
+
+
+ {/* 过滤器区域 */}
+
+
+
+
+
+ 搜索
+
+ handleSearch(e.target.value)}
+ onPressEnter={() => fetchConsumables()}
+ prefix={}
+ allowClear
+ />
+
+
+
+
+ 分类
+
+
+
+
+
+
+ 状态
+
+
+
+
+
+ 操作
+
+ }
+ onClick={() => fetchConsumables()}
+ style={{
+ background: designTokens.colors.primary.gradient,
+ border: 'none',
+ borderRadius: designTokens.borderRadius.sm,
+ }}
+ >
+ 筛选
+
+
+ }
+ onClick={handleReset}
+ style={{ borderRadius: designTokens.borderRadius.sm }}
+ />
+
+
+
+
+
+
+
+ {/* 操作按钮区域 */}
+
+
+ }
+ onClick={() => showModal()}
+ size="large"
+ style={{
+ background: designTokens.colors.primary.gradient,
+ border: 'none',
+ borderRadius: designTokens.borderRadius.sm,
+ boxShadow: designTokens.shadows.md,
+ }}
+ >
+ 添加耗材
+
+ }
+ onClick={showImportModal}
+ size="large"
+ style={{ borderRadius: designTokens.borderRadius.sm }}
+ >
+ 批量导入
+
+ }
+ onClick={handleExport}
+ size="large"
+ style={{ borderRadius: designTokens.borderRadius.sm }}
+ >
+ 导出
+
+
+
+
+
+ }
+ onClick={() => fetchConsumables()}
+ loading={loading}
+ style={{ borderRadius: designTokens.borderRadius.sm }}
+ />
+
+
+
+
+ {/* 数据表格 */}
+ {loading ? (
+
+
+
+ ) : consumables.length === 0 ? (
+
+
+
+ 暂无耗材数据
+
+
+ 点击"添加耗材"按钮创建第一个耗材
+
+
+ }
+ >
+ }
+ onClick={() => showModal()}
+ style={{
+ background: designTokens.colors.primary.gradient,
+ border: 'none',
+ borderRadius: designTokens.borderRadius.sm,
+ }}
+ >
+ 添加耗材
+
+
+
+ ) : (
+
+ fetchConsumables(pagination.current, pagination.pageSize)}
+ scroll={{ x: 1400 }}
+ style={{
+ borderRadius: designTokens.borderRadius.md,
+ overflow: 'hidden',
+ }}
+ />
+
+ )}
+
- fetchConsumables(pagination.current, pagination.pageSize)}
- scroll={{ x: 1300 }}
- />
-
-
+ {/* 添加/编辑耗材弹窗 */}
+
+
+
+
+ {editingConsumable ? '编辑耗材' : '添加耗材'}
+
+
+ }
open={modalVisible}
onCancel={handleCancel}
footer={null}
- width={600}
+ width={700}
>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- setMaxStockUnlimited(e.target.checked);
- if (e.target.checked) {
- form.setFieldsValue({ maxStock: undefined });
- }
- }}
- >
- 无限制
-
- {!maxStockUnlimited && (
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ setMaxStockUnlimited(e.target.checked);
+ if (e.target.checked) {
+ form.setFieldsValue({ maxStock: undefined });
+ }
+ }}
>
-
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
+ 无限制
+
+ {!maxStockUnlimited && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
-
+
-
+
{editingConsumable ? '更新' : '创建'}
取消
@@ -796,30 +1165,85 @@ function ConsumableManagement() {
+ {/* 导入耗材弹窗 */}
+
+
+
+ 批量导入耗材
+
+ }
open={importModalVisible}
onCancel={handleImportCancel}
footer={null}
width={700}
>
-
-
+
+
+
+
- } onClick={downloadTemplate}>
+ }
+ onClick={downloadTemplate}
+ style={{ borderRadius: designTokens.borderRadius.sm }}
+ >
下载模板
- 请下载模板后填写数据再导入
+
+ 请下载模板后填写数据再导入
+
-
false} onChange={handleFileChange}>
- }>选择CSV文件
+ false}
+ onChange={handleFileChange}
+ >
+ }
+ style={{ borderRadius: designTokens.borderRadius.sm }}
+ >
+ 选择CSV文件
+
{importPreview.length > 0 && (
-
-
预览 (前10条):
+
+
+ 数据预览(前10条)
+
-
+
)}
-
+ {importing && (
+
+
+
+ {importPhase}
+
+
+ )}
+
+
取消
开始导入
-
+
+ {/* 入库/出库弹窗 */}
+
+ {stockType === 'in' ?
:
}
+
+
+ {stockType === 'in' ? '耗材入库' : '耗材出库'}
+
+
+ }
open={stockModalVisible}
onCancel={handleStockCancel}
footer={null}
width={500}
>
-
@@ -871,15 +1343,23 @@ function ConsumableManagement() {
-
-
+
+
-
+
-
+
{stockType === 'in' ? '确认入库' : '确认出库'}
取消
@@ -887,7 +1367,7 @@ function ConsumableManagement() {
-
+
);
}
diff --git a/frontend/src/pages/ConsumableStatistics.jsx b/frontend/src/pages/ConsumableStatistics.jsx
index ff88455..ee9f5d6 100644
--- a/frontend/src/pages/ConsumableStatistics.jsx
+++ b/frontend/src/pages/ConsumableStatistics.jsx
@@ -6,208 +6,454 @@ import {
Statistic,
Table,
Tag,
- DatePicker,
- Space,
- Select,
Progress,
- message,
+ DatePicker,
+ Select,
Button,
+ Empty,
+ Tooltip,
+ Badge,
+ Divider,
+ Typography,
+ Space,
+ Avatar,
} from 'antd';
import {
- InboxOutlined,
- ExportOutlined,
- WarningOutlined,
- DollarOutlined,
- ShoppingCartOutlined,
- ExclamationCircleOutlined,
- PlusOutlined,
+ PieChartOutlined,
BarChartOutlined,
- DownloadOutlined,
+ WarningOutlined,
+ HistoryOutlined,
+ ReloadOutlined,
+ CalendarOutlined,
+ AppstoreOutlined,
+ ArrowUpOutlined,
+ ArrowDownOutlined,
+ BoxPlotOutlined,
+ ExclamationCircleOutlined,
+ ShoppingCartOutlined,
+ DatabaseOutlined,
} from '@ant-design/icons';
-import axios from 'axios';
+import { motion, AnimatePresence } from 'framer-motion';
+import styled from 'styled-components';
import dayjs from 'dayjs';
+import api from '../api';
+import { message } from 'antd';
+const { Title, Text } = Typography;
const { RangePicker } = DatePicker;
const { Option } = Select;
+// 设计令牌 - 与其他页面统一
const designTokens = {
colors: {
primary: {
- main: '#667eea',
- gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
- light: '#8b9ff0',
+ main: '#6366f1',
+ light: '#818cf8',
+ dark: '#4f46e5',
+ gradient: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
+ },
+ secondary: {
+ main: '#ec4899',
+ light: '#f472b6',
+ gradient: 'linear-gradient(135deg, #ec4899 0%, #f97316 100%)',
},
success: {
main: '#10b981',
+ light: '#34d399',
gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
},
warning: {
main: '#f59e0b',
+ light: '#fbbf24',
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
},
error: {
main: '#ef4444',
+ light: '#f87171',
gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
},
+ info: {
+ main: '#3b82f6',
+ light: '#60a5fa',
+ },
+ background: {
+ main: '#f8fafc',
+ card: '#ffffff',
+ elevated: '#ffffff',
+ },
text: {
primary: '#1e293b',
secondary: '#64748b',
- tertiary: '#94a3b8',
- inverse: '#ffffff',
- },
- background: {
- primary: '#ffffff',
- secondary: '#f8fafc',
- tertiary: '#f1f5f9',
- },
- border: {
- light: '#e2e8f0',
- medium: '#cbd5e1',
- dark: '#94a3b8',
+ muted: '#94a3b8',
},
+ border: '#e2e8f0',
},
shadows: {
small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
- medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
- large: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
+ medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
+ large: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
+ glow: '0 0 20px rgba(99, 102, 241, 0.15)',
},
borderRadius: {
- small: '6px',
- medium: '10px',
+ small: '8px',
+ medium: '12px',
large: '16px',
- },
- spacing: {
- xs: '4px',
- sm: '8px',
- md: '16px',
- lg: '24px',
- xl: '32px',
+ xl: '20px',
+ full: '9999px',
},
};
-const pageContainerStyle = {
- minHeight: '100vh',
- background: designTokens.colors.background.secondary,
- padding: designTokens.spacing.lg,
+// 动画配置
+const containerVariants = {
+ hidden: { opacity: 0 },
+ visible: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.08,
+ delayChildren: 0.1,
+ },
+ },
};
-const headerStyle = {
- marginBottom: designTokens.spacing.lg,
- padding: `${designTokens.spacing.lg}px ${designTokens.spacing.xl}px`,
- background: designTokens.colors.background.primary,
- borderRadius: designTokens.borderRadius.large,
- boxShadow: designTokens.shadows.small,
- border: `1px solid ${designTokens.colors.border.light}`,
+const itemVariants = {
+ hidden: { opacity: 0, y: 20 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ type: 'spring',
+ stiffness: 100,
+ damping: 15,
+ },
+ },
};
-const titleRowStyle = {
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'space-between',
- marginBottom: designTokens.spacing.md,
- flexWrap: 'wrap',
- gap: designTokens.spacing.md,
+const cardHoverVariants = {
+ rest: { scale: 1 },
+ hover: {
+ scale: 1.02,
+ transition: {
+ type: 'spring',
+ stiffness: 400,
+ damping: 25,
+ },
+ },
};
-const titleStyle = {
- display: 'flex',
- alignItems: 'center',
- gap: designTokens.spacing.sm,
- fontSize: '20px',
- fontWeight: '600',
- color: designTokens.colors.text.primary,
-};
+// 样式组件
+const PageContainer = styled.div`
+ padding: 24px;
+ background: ${designTokens.colors.background.main};
+ min-height: 100vh;
-const statsRowStyle = {
- display: 'flex',
- gap: designTokens.spacing.md,
- flexWrap: 'wrap',
-};
+ @media (max-width: 768px) {
+ padding: 16px;
+ }
+`;
-const statCardStyle = {
- background: designTokens.colors.background.primary,
- borderRadius: designTokens.borderRadius.medium,
- padding: `${designTokens.spacing.md}px ${designTokens.spacing.lg}px`,
- boxShadow: designTokens.shadows.small,
- border: `1px solid ${designTokens.colors.border.light}`,
- minWidth: '180px',
- flex: 1,
-};
+const PageHeader = styled(motion.div)`
+ margin-bottom: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 16px;
+`;
-const statCardTextStyle = {
- fontSize: '13px',
- color: designTokens.colors.text.secondary,
- marginBottom: designTokens.spacing.xs,
-};
+const TitleSection = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 16px;
-const statCardValueStyle = {
- fontSize: '28px',
- fontWeight: '600',
- color: designTokens.colors.text.primary,
-};
+ .icon-wrapper {
+ width: 56px;
+ height: 56px;
+ background: ${designTokens.colors.primary.gradient};
+ border-radius: ${designTokens.borderRadius.large};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: ${designTokens.shadows.glow};
-const statCardIconStyle = color => ({
- fontSize: '28px',
- color: color,
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- width: '48px',
- height: '48px',
- borderRadius: designTokens.borderRadius.medium,
- background: `${color}12`,
-});
+ .anticon {
+ font-size: 28px;
+ color: white;
+ }
+ }
-const panelStyle = {
- background: designTokens.colors.background.primary,
- borderRadius: designTokens.borderRadius.large,
- boxShadow: designTokens.shadows.small,
- border: `1px solid ${designTokens.colors.border.light}`,
- overflow: 'hidden',
-};
+ .title-content {
+ h1 {
+ margin: 0;
+ font-size: 28px;
+ font-weight: 700;
+ background: ${designTokens.colors.primary.gradient};
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ }
-const panelHeaderStyle = {
- padding: `${designTokens.spacing.md}px ${designTokens.spacing.lg}px`,
- borderBottom: `1px solid ${designTokens.colors.border.light}`,
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'space-between',
-};
+ .subtitle {
+ color: ${designTokens.colors.text.secondary};
+ font-size: 14px;
+ margin-top: 4px;
+ }
+ }
+`;
-const panelTitleStyle = {
- fontSize: '15px',
- fontWeight: '600',
- color: designTokens.colors.text.primary,
- display: 'flex',
- alignItems: 'center',
- gap: designTokens.spacing.sm,
-};
+const FilterSection = styled(motion.div)`
+ background: ${designTokens.colors.background.card};
+ padding: 20px 24px;
+ border-radius: ${designTokens.borderRadius.large};
+ box-shadow: ${designTokens.shadows.small};
+ margin-bottom: 24px;
+ border: 1px solid ${designTokens.colors.border};
+`;
-const panelBodyStyle = {
- padding: designTokens.spacing.lg,
-};
+const StatsCard = styled(motion.div)`
+ background: ${designTokens.colors.background.card};
+ border-radius: ${designTokens.borderRadius.large};
+ padding: 24px;
+ box-shadow: ${designTokens.shadows.small};
+ border: 1px solid ${designTokens.colors.border};
+ transition: all 0.3s ease;
+ height: 100%;
+ position: relative;
+ overflow: hidden;
-const actionButtonStyle = {
- height: '36px',
- padding: `0 ${designTokens.spacing.md}px`,
- borderRadius: designTokens.borderRadius.small,
- fontSize: '13px',
- display: 'flex',
- alignItems: 'center',
- gap: designTokens.spacing.xs,
-};
+ &:hover {
+ box-shadow: ${designTokens.shadows.large};
+ transform: translateY(-2px);
+ }
-const primaryActionStyle = {
- ...actionButtonStyle,
- background: designTokens.colors.primary.gradient,
- border: 'none',
- color: '#ffffff',
- boxShadow: designTokens.shadows.small,
-};
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 4px;
+ background: ${props => props.accent || designTokens.colors.primary.gradient};
+ }
-function ConsumableStatistics() {
- const [summary, setSummary] = useState({ total: 0, lowStock: 0, totalValue: 0, byCategory: [] });
- const [lowStockItems, setLowStockItems] = useState([]);
+ .card-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 16px;
+
+ .icon-box {
+ width: 48px;
+ height: 48px;
+ border-radius: ${designTokens.borderRadius.medium};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 24px;
+ background: ${props => props.iconBg || 'rgba(99, 102, 241, 0.1)'};
+ color: ${props => props.iconColor || designTokens.colors.primary.main};
+ }
+
+ .card-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: ${designTokens.colors.text.primary};
+ }
+ }
+
+ .stat-value {
+ font-size: 32px;
+ font-weight: 700;
+ color: ${designTokens.colors.text.primary};
+ margin-bottom: 8px;
+ background: ${props => props.valueGradient || 'none'};
+ -webkit-background-clip: ${props => props.valueGradient ? 'text' : 'unset'};
+ -webkit-text-fill-color: ${props => props.valueGradient ? 'transparent' : 'inherit'};
+ background-clip: ${props => props.valueGradient ? 'text' : 'unset'};
+ }
+
+ .stat-label {
+ font-size: 13px;
+ color: ${designTokens.colors.text.secondary};
+ }
+`;
+
+const ContentCard = styled(motion.div)`
+ background: ${designTokens.colors.background.card};
+ border-radius: ${designTokens.borderRadius.large};
+ box-shadow: ${designTokens.shadows.small};
+ border: 1px solid ${designTokens.colors.border};
+ overflow: hidden;
+ height: 100%;
+ transition: all 0.3s ease;
+
+ &:hover {
+ box-shadow: ${designTokens.shadows.medium};
+ }
+
+ .card-header {
+ padding: 20px 24px;
+ border-bottom: 1px solid ${designTokens.colors.border};
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
+
+ .header-left {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+
+ .header-icon {
+ width: 40px;
+ height: 40px;
+ border-radius: ${designTokens.borderRadius.medium};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 20px;
+ background: ${props => props.iconBg || designTokens.colors.primary.gradient};
+ color: white;
+ }
+
+ .header-title {
+ font-size: 17px;
+ font-weight: 600;
+ color: ${designTokens.colors.text.primary};
+ }
+ }
+
+ .header-extra {
+ color: ${designTokens.colors.text.secondary};
+ font-size: 13px;
+ }
+ }
+
+ .card-body {
+ padding: 24px;
+ }
+`;
+
+const StyledTable = styled(Table)`
+ .ant-table {
+ background: transparent;
+ }
+
+ .ant-table-thead > tr > th {
+ background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
+ font-weight: 600;
+ color: ${designTokens.colors.text.primary};
+ border-bottom: 2px solid ${designTokens.colors.border};
+ padding: 16px;
+ }
+
+ .ant-table-tbody > tr > td {
+ padding: 16px;
+ border-bottom: 1px solid ${designTokens.colors.border};
+ }
+
+ .ant-table-tbody > tr:hover > td {
+ background: rgba(99, 102, 241, 0.04);
+ }
+
+ .ant-table-row {
+ transition: all 0.2s ease;
+ }
+`;
+
+const ProgressBar = styled.div`
+ .progress-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+
+ .ant-progress {
+ flex: 1;
+ margin: 0;
+ }
+
+ .progress-text {
+ font-size: 13px;
+ font-weight: 600;
+ color: ${props => props.color || designTokens.colors.text.primary};
+ min-width: 45px;
+ text-align: right;
+ }
+ }
+`;
+
+const CategoryTag = styled(Tag)`
+ padding: 6px 14px;
+ border-radius: ${designTokens.borderRadius.full};
+ font-size: 13px;
+ font-weight: 500;
+ border: none;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+
+ .dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: currentColor;
+ }
+`;
+
+const StatItem = styled.div`
+ text-align: center;
+ padding: 16px;
+ background: ${props => props.bg || 'transparent'};
+ border-radius: ${designTokens.borderRadius.medium};
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: ${designTokens.shadows.small};
+ }
+
+ .stat-icon {
+ font-size: 24px;
+ margin-bottom: 8px;
+ color: ${props => props.iconColor || designTokens.colors.primary.main};
+ }
+
+ .stat-number {
+ font-size: 24px;
+ font-weight: 700;
+ color: ${designTokens.colors.text.primary};
+ margin-bottom: 4px;
+ }
+
+ .stat-label {
+ font-size: 12px;
+ color: ${designTokens.colors.text.secondary};
+ }
+`;
+
+const EmptyState = styled.div`
+ text-align: center;
+ padding: 48px 24px;
+ color: ${designTokens.colors.text.secondary};
+
+ .empty-icon {
+ font-size: 64px;
+ margin-bottom: 16px;
+ opacity: 0.5;
+ }
+
+ .empty-text {
+ font-size: 16px;
+ margin-bottom: 8px;
+ }
+
+ .empty-subtext {
+ font-size: 13px;
+ opacity: 0.7;
+ }
+`;
+
+const ConsumableStatistics = () => {
+ const [loading, setLoading] = useState(true);
const [stats, setStats] = useState({
inCount: 0,
outCount: 0,
@@ -215,143 +461,158 @@ function ConsumableStatistics() {
outQuantity: 0,
recentRecords: [],
});
- const [loading, setLoading] = useState(true);
- const [dateRange, setDateRange] = useState([]);
- const [categoryFilter, setCategoryFilter] = useState(null);
+ const [summary, setSummary] = useState({
+ total: 0,
+ lowStock: 0,
+ totalValue: 0,
+ byCategory: [],
+ });
+ const [lowStockItems, setLowStockItems] = useState([]);
+ const [dateRange, setDateRange] = useState([dayjs().subtract(30, 'days'), dayjs()]);
+ const [categoryFilter, setCategoryFilter] = useState('all');
- const fetchSummary = async () => {
+ // 加载统计数据
+ const loadStatistics = async () => {
try {
- const response = await axios.get('/api/consumables/statistics/summary');
- setSummary(response.data);
+ setLoading(true);
+ const params = {
+ startDate: dateRange[0]?.format('YYYY-MM-DD'),
+ endDate: dateRange[1]?.format('YYYY-MM-DD'),
+ };
+
+ // 加载记录统计
+ const statsResponse = await api.get('/consumable-records/statistics', { params });
+ setStats(statsResponse || {
+ inCount: 0,
+ outCount: 0,
+ inQuantity: 0,
+ outQuantity: 0,
+ recentRecords: [],
+ });
+
+ // 加载汇总数据
+ const summaryResponse = await api.get('/consumables/statistics/summary');
+ setSummary(summaryResponse || {
+ total: 0,
+ lowStock: 0,
+ totalValue: 0,
+ byCategory: [],
+ });
} catch (error) {
- message.error('获取统计信息失败');
+ const errorMsg = error?.message || error || '未知错误';
+ message.error('加载统计数据失败: ' + errorMsg);
+ console.error('加载统计数据失败:', error);
+ // 使用默认数据
+ setStats({
+ inCount: 0,
+ outCount: 0,
+ inQuantity: 0,
+ outQuantity: 0,
+ recentRecords: [],
+ });
+ setSummary({
+ total: 0,
+ lowStock: 0,
+ totalValue: 0,
+ byCategory: [],
+ });
+ } finally {
+ setLoading(false);
}
};
- const fetchLowStock = async () => {
+ // 加载低库存预警
+ const loadLowStockItems = async () => {
try {
- const response = await axios.get('/api/consumables/low-stock');
- setLowStockItems(response.data);
+ const response = await api.get('/consumables/low-stock');
+ setLowStockItems(response || []);
} catch (error) {
- message.error('获取低库存信息失败');
- }
- };
-
- const fetchInOutStats = async () => {
- try {
- const params = {};
- if (dateRange && dateRange.length === 2) {
- params.startDate = dateRange[0].toISOString();
- params.endDate = dateRange[1].toISOString();
- }
- if (categoryFilter) {
- params.category = categoryFilter;
- }
- const response = await axios.get('/api/consumable-records/statistics', { params });
- setStats(response.data);
- } catch (error) {
- message.error('获取出入库统计失败');
+ console.error('加载低库存预警失败:', error?.message || error);
+ setLowStockItems([]);
}
};
useEffect(() => {
- const loadData = async () => {
- setLoading(true);
- await Promise.all([fetchSummary(), fetchLowStock(), fetchInOutStats()]);
- setLoading(false);
- };
- loadData();
+ loadStatistics();
+ loadLowStockItems();
}, []);
- useEffect(() => {
- fetchInOutStats();
- }, [dateRange, categoryFilter]);
-
+ // 低库存表格列
const lowStockColumns = [
{
title: '耗材名称',
dataIndex: 'name',
key: 'name',
- width: 150,
- render: text => (
- {text}
- ),
- },
- {
- title: '分类',
- dataIndex: 'category',
- key: 'category',
- width: 120,
- render: category => (
-
- {category}
-
+ render: (text, record) => (
+
+
+
+
+
+
+ {text}
+
+
+ {record.specification}
+
+
+
),
},
{
title: '当前库存',
dataIndex: 'currentStock',
key: 'currentStock',
- width: 100,
- render: value => (
-
- {value}
-
+ align: 'center',
+ render: (currentStock, record) => (
+
+ {currentStock} {record.unit}
+
),
},
{
title: '最小库存',
dataIndex: 'minStock',
key: 'minStock',
- width: 100,
- render: value => {value},
- },
- {
- title: '单位',
- dataIndex: 'unit',
- key: 'unit',
- width: 80,
- render: value => {value},
+ align: 'center',
+ render: (minStock, record) => (
+
+ {minStock} {record.unit}
+
+ ),
},
{
title: '充足率',
key: 'rate',
- width: 140,
+ align: 'center',
render: (_, record) => {
- const rate = Math.min(
- 100,
- Math.round((record.currentStock / (record.maxStock || 100)) * 100)
- );
+ const rate = Math.min(100, Math.round((record.currentStock / (record.maxStock || 100)) * 100));
const status = rate < 30 ? 'exception' : rate < 60 ? 'active' : 'success';
+ const color = rate < 30 ? designTokens.colors.error.main :
+ rate < 60 ? designTokens.colors.warning.main :
+ designTokens.colors.success.main;
+
return (
-
+
+
+
);
},
},
@@ -359,439 +620,504 @@ function ConsumableStatistics() {
title: '供应商',
dataIndex: 'supplier',
key: 'supplier',
- width: 120,
- render: value => (
- {value || '-'}
+ render: (value) => (
+ {value || '-'}
),
},
];
+ // 最近记录表格列
const recentColumns = [
{
- title: '时间',
- dataIndex: 'createdAt',
- key: 'createdAt',
- width: 170,
- render: date => (
-
- {dayjs(date).format('YYYY-MM-DD HH:mm')}
-
- ),
- },
- {
- title: '耗材名称',
- dataIndex: ['Consumable', 'name'],
- key: 'consumableName',
- width: 140,
- render: text => {text},
- },
- {
- title: '类型',
+ title: '操作类型',
dataIndex: 'type',
key: 'type',
- width: 90,
- render: type => (
+ render: (type) => (
: }
+ color={type === 'in' ? 'success' : 'processing'}
style={{
- border: 'none',
- borderRadius: designTokens.borderRadius.small,
- background:
- type === 'in'
- ? `${designTokens.colors.success.main}15`
- : `${designTokens.colors.error.main}15`,
- color:
- type === 'in' ? designTokens.colors.success.main : designTokens.colors.error.main,
- fontWeight: '500',
+ padding: '4px 12px',
+ borderRadius: designTokens.borderRadius.full,
+ fontWeight: 500,
}}
>
{type === 'in' ? '入库' : '出库'}
),
},
+ {
+ title: '耗材名称',
+ dataIndex: ['Consumable', 'name'],
+ key: 'consumableName',
+ render: (text) => (
+
+ {text}
+
+ ),
+ },
{
title: '数量',
dataIndex: 'quantity',
key: 'quantity',
- width: 100,
- render: (value, record) => (
- (
+
- {record.type === 'in' ? '+' : '-'}
- {value}
-
+ {record.type === 'in' ? '+' : '-'}{quantity}
+
),
},
{
title: '操作人',
dataIndex: 'operator',
key: 'operator',
- width: 100,
- render: value => (
- {value || '-'}
+ render: (operator) => (
+
+
+ {operator?.charAt(0) || '?'}
+
+ {operator}
+
),
},
{
- title: '原因',
- dataIndex: 'reason',
- key: 'reason',
- width: 150,
- render: value => (
- {value || '-'}
+ title: '操作时间',
+ dataIndex: 'createdAt',
+ key: 'createdAt',
+ render: (date) => (
+
+
+ {dayjs(date).format('YYYY-MM-DD HH:mm')}
+
),
},
];
- const categories = summary.byCategory?.map(item => item.category) || [];
+ // 获取类别颜色
+ const getCategoryColor = (category) => {
+ const colors = {
+ '网络设备': '#6366f1',
+ '线缆': '#10b981',
+ '配件': '#f59e0b',
+ '工具': '#ec4899',
+ '其他': '#6b7280',
+ };
+ return colors[category] || '#6366f1';
+ };
- const netQuantity = stats.inQuantity - stats.outQuantity;
+ // 刷新数据
+ const handleRefresh = () => {
+ loadStatistics();
+ loadLowStockItems();
+ message.success('数据已刷新');
+ };
return (
-
-
-
-
-
- 耗材统计
+
+ {/* 页面标题 */}
+
+
+
+
+
+
-
-
- {categories.map(cat => (
-
- ))}
-
+
+
+ }
+ onClick={handleRefresh}
+ loading={loading}
+ style={{
+ height: 40,
+ borderRadius: designTokens.borderRadius.medium,
+ borderColor: designTokens.colors.border,
+ }}
+ >
+ 刷新数据
+
+
+
+
+ {/* 筛选区域 */}
+
+
+
+
+
+ 时间范围
+
- } style={actionButtonStyle}>
- 导出报表
+
+
+
+
+ 耗材类别
+
+
+
+
+
+
+
+
+
+
+
+
+ 应用筛选
-
-
+
+
+
-
-
-
-
+ {/* 统计卡片 */}
+
+
+
+
+
+
+ {summary?.total || 0}
+
+ 种不同类型耗材
+
+
+
+
+
+
+
+ {summary?.lowStock || 0}
+
+ 低于安全库存
+
+
+
+
+
+
+
+ {stats?.inCount || 0}
+
+ 笔入库记录
+
+
+
+
+
+
+
+ {stats?.outCount || 0}
+
+ 笔出库记录
+
+
+
+
+
+ {/* 入库/出库统计 */}
+
+
+
+
+
-
-
耗材种类
-
{summary.total}
-
+
入库/出库统计
+
+
+ {dateRange[0]?.format('YYYY-MM-DD')} 至 {dateRange[1]?.format('YYYY-MM-DD')}
-
-
-
-
-
-
-
-
低库存预警
-
- {summary.lowStock}
-
-
-
-
-
-
-
-
-
-
-
-
库存总价值
-
- ¥{parseFloat(summary.totalValue || 0).toLocaleString()}
-
-
-
-
-
- = 0 ? designTokens.colors.success.main : designTokens.colors.error.main}`,
- }}
- >
-
-
= 0
- ? designTokens.colors.success.main
- : designTokens.colors.error.main
- )}
- >
-
-
-
-
净入库量
-
= 0
- ? designTokens.colors.success.main
- : designTokens.colors.error.main,
- }}
- >
- {netQuantity >= 0 ? '+' : ''}
- {netQuantity}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 入库次数
-
-
- {stats.inCount}
-
-
- +{stats.inQuantity}
-
+
+
+
+
+
-
-
-
-
- 出库次数
-
-
- {stats.outCount}
-
-
- -{stats.outQuantity}
-
+
+ {stats?.inCount || 0}
+
+
入库次数
+
+
+
+
+
-
-
+
+ {stats?.outCount || 0}
+
+ 出库次数
+
+
+
+
+
+
+
+
+ {stats?.inQuantity || 0}
+
+ 入库数量
+
+
+
+
+
+
+
+
+ {stats?.outQuantity || 0}
+
+ 出库数量
+
+
+
+
+
+
+
+ {/* 类别统计 */}
+
+
+
-
-
-
-
-
-
-
- 分类统计
-
-
{categories.length}类
-
-
-
- {summary.byCategory?.map((item, index) => {
- const colors = [
- designTokens.colors.primary.main,
- designTokens.colors.success.main,
- designTokens.colors.warning.main,
- '#8b5cf6',
- '#06b6d4',
- '#ec4899',
- ];
- const color = colors[index % colors.length];
- return (
-
+ {summary?.byCategory?.length > 0 ? (
+
+ {summary.byCategory.map((item, index) => (
+
+
-
-
+ >
+
{item.category}
-
-
+
+
{item.count}
-
-
- );
- })}
- {(!summary.byCategory || summary.byCategory.length === 0) && (
-
- 暂无分类数据
+
+
+ 种耗材
+
+
+
+ 总库存: {item.totalQuantity || 0}
+
+
+
+ ))}
+
+ ) : (
+
+
+ 暂无类别统计数据
+ 添加耗材后将自动统计
+
+ )}
+
+
+
+
+ {/* 低库存预警和最近记录 */}
+
+
+
+
+
-
-
-
-
+
+
+
+ 库存充足
+ 所有耗材库存均在安全范围内
+
+ ),
+ }}
+ />
+
+
+
-
-
-
-
-
-
- 低库存预警
+
+
+
-
- {lowStockItems.length}项
-
-
-
-
-
-
-
-
-
-
-
- 最近出入库记录
+
+
+
+ 暂无记录
+ 出入库操作后将显示在这里
+
+ ),
+ }}
+ />
-
最近
-
-
-
-
-
-
+
+
+
+
+
);
-}
+};
export default ConsumableStatistics;
diff --git a/frontend/src/pages/PortManagement.jsx b/frontend/src/pages/PortManagement.jsx
index 989a5f0..9295515 100644
--- a/frontend/src/pages/PortManagement.jsx
+++ b/frontend/src/pages/PortManagement.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
+import React, { useState, useEffect, useCallback, useMemo } from 'react';
import {
Table,
Button,
@@ -19,9 +19,11 @@ import {
Upload,
Progress,
Checkbox,
- Tabs,
Badge,
- List,
+ Row,
+ Col,
+ Skeleton,
+ Alert,
Typography,
} from 'antd';
import {
@@ -37,56 +39,110 @@ import {
AppstoreOutlined,
UnorderedListOutlined,
FilterOutlined,
- EyeOutlined,
- CompressOutlined,
+ ClearOutlined,
CloudServerOutlined,
+ ApiOutlined,
+ CheckCircleOutlined,
+ ExclamationCircleOutlined,
+ DisconnectOutlined,
} from '@ant-design/icons';
import axios from 'axios';
import * as XLSX from 'xlsx';
import Papa from 'papaparse';
-import PortPanel from '../components/PortPanel';
+import { motion, AnimatePresence } from 'framer-motion';
import VirtualDeviceList from '../components/VirtualDeviceList';
import NetworkCardPanel from '../components/NetworkCardPanel';
import NetworkCardCreateModal from '../components/NetworkCardCreateModal';
-import PortCreateModal from '../components/PortCreateModal';
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],
+ },
+ },
},
};
@@ -101,13 +157,13 @@ function PortManagement() {
status: 'all',
portType: 'all',
portSpeed: 'all',
+ searchText: '',
});
const [modalVisible, setModalVisible] = useState(false);
const [editingPort, setEditingPort] = useState(null);
const [form] = Form.useForm();
const [importModalVisible, setImportModalVisible] = useState(false);
- const [importFileList, setImportFileList] = useState([]);
const [importPreview, setImportPreview] = useState([]);
const [importProgress, setImportProgress] = useState({ current: 0, total: 0 });
const [importing, setImporting] = useState(false);
@@ -117,26 +173,20 @@ function PortManagement() {
// 视图模式:list 或 panel
const [viewMode, setViewMode] = useState('list');
- // 面板视图优化状态
- const [panelFilters, setPanelFilters] = useState({
- deviceType: 'all',
- searchText: '',
- showOnlyOccupied: false,
- });
- const [visibleDeviceCount, setVisibleDeviceCount] = useState(10);
- const [expandedDevices, setExpandedDevices] = useState({});
-
// 网卡管理相关状态
const [networkCardModalVisible, setNetworkCardModalVisible] = useState(false);
const [portCreateModalVisible, setPortCreateModalVisible] = useState(false);
const [selectedDeviceForNic, setSelectedDeviceForNic] = useState(null);
const [refreshTrigger, setRefreshTrigger] = useState(0);
+ // 展开的设备
+ const [expandedKeys, setExpandedKeys] = useState([]);
+
const fetchPorts = useCallback(async () => {
try {
setLoading(true);
const params = {
- pageSize: 1000, // 获取所有端口,不分页
+ pageSize: 1000,
};
if (filters.deviceId) params.deviceId = filters.deviceId;
@@ -145,7 +195,20 @@ function PortManagement() {
if (filters.portSpeed !== 'all') params.portSpeed = filters.portSpeed;
const response = await axios.get('/api/device-ports', { params });
- setPorts(response.data.ports || response.data || []);
+ const portsData = response.data.ports || response.data || [];
+
+ // 搜索过滤
+ let filteredPorts = portsData;
+ if (filters.searchText) {
+ const searchLower = filters.searchText.toLowerCase();
+ filteredPorts = portsData.filter(port =>
+ port.portName?.toLowerCase().includes(searchLower) ||
+ port.portType?.toLowerCase().includes(searchLower) ||
+ port.description?.toLowerCase().includes(searchLower)
+ );
+ }
+
+ setPorts(filteredPorts);
} catch (error) {
message.error('获取端口列表失败');
console.error('获取端口列表失败:', error);
@@ -211,6 +274,10 @@ function PortManagement() {
});
setGroupedPorts(grouped);
+
+ // 自动展开前5个
+ const deviceIds = Object.keys(grouped);
+ setExpandedKeys(deviceIds.slice(0, 5));
}, [ports, devices]);
const handleSearch = () => {
@@ -223,6 +290,7 @@ function PortManagement() {
status: 'all',
portType: 'all',
portSpeed: 'all',
+ searchText: '',
});
};
@@ -235,34 +303,22 @@ function PortManagement() {
const handleAddPortForDevice = device => {
setEditingPort(null);
form.resetFields();
- // 自动选中当前设备
form.setFieldsValue({
deviceId: device.deviceId,
});
setModalVisible(true);
};
- // 打开网卡管理模态框
const handleManageNetworkCards = device => {
setSelectedDeviceForNic(device);
setNetworkCardModalVisible(true);
};
- // 打开添加网卡模态框
- const handleAddNetworkCard = device => {
- setSelectedDeviceForNic(device);
- setPortCreateModalVisible(true);
- };
-
- // 网卡/端口创建成功回调
const handleNicSuccess = () => {
- message.success('操作成功');
- setRefreshTrigger(prev => prev + 1);
- fetchPorts();
- };
-
- const handlePortSuccess = () => {
- message.success('端口添加成功');
+ message.success({
+ content: '操作成功',
+ icon:
,
+ });
setRefreshTrigger(prev => prev + 1);
fetchPorts();
};
@@ -285,7 +341,10 @@ function PortManagement() {
const handleDelete = async portId => {
try {
await axios.delete(`/api/device-ports/${portId}`);
- message.success('删除成功');
+ message.success({
+ content: '删除成功',
+ icon:
,
+ });
fetchPorts();
} catch (error) {
message.error('删除失败');
@@ -293,7 +352,6 @@ function PortManagement() {
}
};
- // 解析端口名称范围,例如 "1/0/1-1/0/48" -> ["1/0/1", "1/0/2", ..., "1/0/48"]
const parsePortRange = portName => {
const rangeMatch = portName.match(/^(.*?)\/(\d+)-\1\/(\d+)$/);
if (rangeMatch) {
@@ -302,11 +360,10 @@ function PortManagement() {
const end = parseInt(rangeMatch[3]);
if (start <= end && end - start < 100) {
- // 限制最多100个端口
return Array.from({ length: end - start + 1 }, (_, i) => `${prefix}/${start + i}`);
}
}
- return [portName]; // 如果不是范围格式,返回原名称
+ return [portName];
};
const handleSubmit = async () => {
@@ -315,13 +372,14 @@ function PortManagement() {
if (editingPort) {
await axios.put(`/api/device-ports/${editingPort.portId}`, values);
- message.success('更新成功');
+ message.success({
+ content: '更新成功',
+ icon:
,
+ });
} else {
- // 解析端口名称范围
const portNames = parsePortRange(values.portName);
if (portNames.length > 1) {
- // 批量创建端口
const portsData = portNames.map((name, index) => ({
portId: `PORT-${Date.now()}-${index}`,
deviceId: values.deviceId,
@@ -339,12 +397,17 @@ function PortManagement() {
if (failed > 0) {
message.warning(`批量创建完成!成功 ${success} 个,失败 ${failed} 个`);
} else {
- message.success(`成功创建 ${success} 个端口`);
+ message.success({
+ content: `成功创建 ${success} 个端口`,
+ icon:
,
+ });
}
} else {
- // 单个创建
await axios.post('/api/device-ports', values);
- message.success('创建成功');
+ message.success({
+ content: '创建成功',
+ icon:
,
+ });
}
}
@@ -365,7 +428,6 @@ function PortManagement() {
const handleFileUpload = info => {
const { file } = info;
- setImportFileList([file]);
const reader = new FileReader();
reader.onload = async e => {
@@ -427,8 +489,6 @@ function PortManagement() {
};
const validatePortRow = async (row, index) => {
- const errors = [];
-
if (!row['设备ID'] || !row['端口名称']) {
return { valid: false, error: `第 ${index + 1} 行:缺少必填字段(设备ID或端口名称)` };
}
@@ -453,10 +513,6 @@ function PortManagement() {
return { valid: false, error: `第 ${index + 1} 行:无效的状态` };
}
- if (errors.length > 0) {
- return { valid: false, error: errors.join('; ') };
- }
-
return { valid: true };
};
@@ -488,7 +544,6 @@ function PortManagement() {
}));
const response = await axios.post('/api/device-ports/batch', { ports: portsData });
-
const { total, success, failed, errors } = response.data;
setImportProgress({ current: total, total: total });
@@ -497,7 +552,10 @@ function PortManagement() {
console.error('导入错误:', errors);
message.warning(`导入完成!成功 ${success} 条,失败 ${failed} 条`);
} else {
- message.success(`导入完成!成功 ${success} 条`);
+ message.success({
+ content: `导入完成!成功 ${success} 条`,
+ icon:
,
+ });
}
fetchPorts();
@@ -532,15 +590,23 @@ function PortManagement() {
const getStatusTag = status => {
const statusMap = {
- free: { color: 'success', text: '空闲' },
- occupied: { color: 'processing', text: '占用' },
- fault: { color: 'error', text: '故障' },
- 空闲: { color: 'success', text: '空闲' },
- 占用: { color: 'processing', text: '占用' },
- 故障: { color: 'error', text: '故障' },
+ free: { color: 'success', text: '空闲', icon:
},
+ occupied: { color: 'processing', text: '占用', icon:
},
+ fault: { color: 'error', text: '故障', icon:
},
+ 空闲: { color: 'success', text: '空闲', icon:
},
+ 占用: { color: 'processing', text: '占用', icon:
},
+ 故障: { color: 'error', text: '故障', icon:
},
};
- const config = statusMap[status] || { color: 'default', text: status };
- return
{config.text};
+ const config = statusMap[status] || { color: 'default', text: status, icon: null };
+ return (
+
+ {config.text}
+
+ );
};
const getPortTypeTag = type => {
@@ -553,15 +619,20 @@ function PortManagement() {
QSFP28: { color: 'red', text: 'QSFP28' },
};
const config = typeMap[type] || { color: 'default', text: type };
- return
{config.text};
+ return (
+
+ {config.text}
+
+ );
};
- const portColumns = [
+ const portColumns = useMemo(() => [
{
title: '端口名称',
dataIndex: 'portName',
key: 'portName',
width: 120,
+ render: text =>
{text},
},
{
title: '端口类型',
@@ -575,6 +646,7 @@ function PortManagement() {
dataIndex: 'portSpeed',
key: 'portSpeed',
width: 100,
+ render: text =>
{text},
},
{
title: '状态',
@@ -588,316 +660,544 @@ function PortManagement() {
dataIndex: 'vlanId',
key: 'vlanId',
width: 100,
- render: vlanId => vlanId || '-',
+ render: vlanId => vlanId ||
-,
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true,
- render: text => (
-
- {text || '-'}
-
- ),
+ render: text => text ||
-,
},
{
title: '操作',
key: 'action',
- width: 150,
+ width: 120,
fixed: 'right',
render: (_, record) => (
- }
- onClick={() => handleEdit(record)}
- >
- 编辑
-
- handleDelete(record.portId)}
- okText="确定"
- cancelText="取消"
- >
- }>
- 删除
-
-
+
+ }
+ onClick={() => handleEdit(record)}
+ style={{ color: designTokens.colors.primary.main }}
+ />
+
+
+ handleDelete(record.portId)}
+ okText="确定"
+ cancelText="取消"
+ >
+ } />
+
+
),
},
- ];
+ ], []);
+
+ // 获取设备图标
+ const getDeviceIcon = device => {
+ if (!device?.type) return
;
+ const type = device.type.toLowerCase();
+ if (type.includes('server')) return
;
+ if (type.includes('switch')) return
;
+ if (type.includes('router')) return
;
+ return
;
+ };
return (
-
-
-
-
- setFilters(prev => ({ ...prev, deviceId: value }))}
- allowClear
- showSearch
- filterOption={(input, option) => {
- const device = devices.find(d => d.deviceId === option.value);
- if (!device) return false;
- const searchText = `${device.name} ${device.deviceId}`.toLowerCase();
- return searchText.indexOf(input.toLowerCase()) >= 0;
- }}
- >
- {devices.map(device => (
-
- ))}
-
-
- setFilters(prev => ({ ...prev, portType: value }))}
- >
-
-
-
-
-
-
-
-
-
- setFilters(prev => ({ ...prev, portSpeed: value }))}
- >
-
-
-
-
-
-
-
-
-
- setFilters(prev => ({ ...prev, status: value }))}
- >
-
-
-
-
-
-
- }
- onClick={handleSearch}
- style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
- >
- 搜索
-
-
- } onClick={handleReset}>
- 重置
-
-
-
-
-
-
- }
- onClick={handleAdd}
- style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
- >
- 新增端口
-
-
- }
- onClick={handleImport}
- style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
- >
- 批量导入
-
-
- }>导出
-
-
-
-
- }
- onClick={() => setViewMode('list')}
- >
- 列表
-
- }
- onClick={() => setViewMode('panel')}
- >
- 面板
-
-
-
-
-
- {loading ? (
-
-
-
- ) : Object.keys(groupedPorts).length === 0 ? (
-
-
-
- ) : viewMode === 'panel' ? (
- // 面板视图 - 使用虚拟滚动优化
- g.device)
- .filter(Boolean)}
- groupedPorts={groupedPorts}
- cables={cables}
- allDevices={devices}
- onPortClick={port => handleEdit(port)}
- onAddPort={device => handleAddPortForDevice(device)}
- onManageNetworkCards={device => handleManageNetworkCards(device)}
- initialVisibleCount={5}
- loadMoreCount={5}
- />
- ) : (
- // 列表视图
-
+ {/* 页面标题 */}
+
+
+
- {Object.entries(groupedPorts).map(([deviceId, data]) => {
- const device = data.device;
- const devicePorts = data.ports || [];
- const freeCount = devicePorts.filter(p => p.status === 'free').length;
- const occupiedCount = devicePorts.filter(p => p.status === 'occupied').length;
- const faultCount = devicePorts.filter(p => p.status === 'fault').length;
+
+
+
+
端口管理
+ 管理设备的网络端口信息
+
+
+
- return (
-
+
+ {/* 过滤器区域 */}
+
+
+
+
+
+ 设备
+
+ setFilters(prev => ({ ...prev, deviceId: value }))}
+ allowClear
+ showSearch
+ filterOption={(input, option) => {
+ const device = devices.find(d => d.deviceId === option.value);
+ if (!device) return false;
+ const searchText = `${device.name} ${device.deviceId}`.toLowerCase();
+ return searchText.indexOf(input.toLowerCase()) >= 0;
+ }}
+ >
+ {devices.map(device => (
+
+ ))}
+
+
+
+
+
+ 端口类型
+
+ setFilters(prev => ({ ...prev, portType: value }))}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ 端口速率
+
+ setFilters(prev => ({ ...prev, portSpeed: value }))}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ 状态
+
+ setFilters(prev => ({ ...prev, status: value }))}
+ >
+
+
+
+
+
+
+
+
+
+ 搜索
+
+ setFilters(prev => ({ ...prev, searchText: e.target.value }))}
+ onPressEnter={handleSearch}
+ prefix={}
+ allowClear
+ />
+
+
+
+ 操作
+
+ }
+ onClick={handleSearch}
+ style={{
+ background: designTokens.colors.primary.gradient,
+ border: 'none',
+ borderRadius: designTokens.borderRadius.sm,
}}
>
-
-
- {device?.type?.toLowerCase()?.includes('server')
- ? '🖥️'
- : device?.type?.toLowerCase()?.includes('switch')
- ? '🔀'
- : device?.type?.toLowerCase()?.includes('router')
- ? '🌐'
- : '📦'}
-
-
-
- {device?.name || '未知设备'}
-
-
- {device?.deviceId || '-'}
-
-
-
-
- 空闲: {freeCount}
- 占用: {occupiedCount}
- 故障: {faultCount}
- 总计: {devicePorts.length}
- {/* 网卡管理按钮 - 只有服务器显示 */}
- {device?.type?.toLowerCase()?.includes('server') && (
- }
- onClick={e => {
- e.stopPropagation();
- handleManageNetworkCards(device);
- }}
+ 筛选
+
+
+ }
+ onClick={handleReset}
+ style={{ borderRadius: designTokens.borderRadius.sm }}
+ />
+
+
+
+
+
+
+
+ {/* 操作按钮区域 */}
+
+
+ }
+ onClick={handleAdd}
+ size="large"
+ style={{
+ background: designTokens.colors.primary.gradient,
+ border: 'none',
+ borderRadius: designTokens.borderRadius.sm,
+ boxShadow: designTokens.shadows.md,
+ }}
+ >
+ 新增端口
+
+ }
+ onClick={handleImport}
+ size="large"
+ style={{ borderRadius: designTokens.borderRadius.sm }}
+ >
+ 批量导入
+
+ }
+ size="large"
+ style={{ borderRadius: designTokens.borderRadius.sm }}
+ >
+ 导出
+
+
+
+
+
+ }
+ onClick={() => setViewMode('list')}
+ style={viewMode === 'list' ? {
+ background: designTokens.colors.primary.gradient,
+ border: 'none',
+ } : {}}
+ >
+ 列表
+
+ }
+ onClick={() => setViewMode('panel')}
+ style={viewMode === 'panel' ? {
+ background: designTokens.colors.primary.gradient,
+ border: 'none',
+ } : {}}
+ >
+ 面板
+
+
+
+ }
+ onClick={fetchPorts}
+ loading={loading}
+ style={{ borderRadius: designTokens.borderRadius.sm }}
+ />
+
+
+
+
+ {/* 数据展示区域 */}
+ {loading ? (
+
+
+
+ ) : Object.keys(groupedPorts).length === 0 ? (
+
+
+
+ 暂无端口数据
+
+
+ 点击"新增端口"按钮创建第一个端口
+
+
+ }
+ >
+
}
+ onClick={handleAdd}
+ style={{
+ background: designTokens.colors.primary.gradient,
+ border: 'none',
+ borderRadius: designTokens.borderRadius.sm,
+ }}
+ >
+ 新增端口
+
+
+
+ ) : viewMode === 'panel' ? (
+
g.device)
+ .filter(Boolean)}
+ groupedPorts={groupedPorts}
+ cables={cables}
+ allDevices={devices}
+ onPortClick={port => handleEdit(port)}
+ onAddPort={device => handleAddPortForDevice(device)}
+ onManageNetworkCards={device => handleManageNetworkCards(device)}
+ initialVisibleCount={5}
+ loadMoreCount={5}
+ />
+ ) : (
+
+
+ {Object.entries(groupedPorts).map(([deviceId, data], index) => {
+ const device = data.device;
+ const devicePorts = data.ports || [];
+ const freeCount = devicePorts.filter(p => p.status === 'free').length;
+ const occupiedCount = devicePorts.filter(p => p.status === 'occupied').length;
+ const faultCount = devicePorts.filter(p => p.status === 'fault').length;
+
+ return (
+
+
- 网卡管理
-
- )}
-
-
- }
- >
- `共 ${total} 个端口`,
- pageSizeOptions: ['10', '20', '50', '100'],
- }}
- size="small"
- scroll={{ x: 1000 }}
- />
-
- );
- })}
-
- )}
-
+
+
+ {getDeviceIcon(device)}
+
+
+
+ {device?.name || '未知设备'}
+
+
+ {device?.deviceId || '-'} · {device?.model || device?.type || '设备'}
+
+
+
+
+
+ }
+ >
+ {freeCount}
+
+
+
+ }
+ >
+ {occupiedCount}
+
+
+ {faultCount > 0 && (
+
+ }
+ >
+ {faultCount}
+
+
+ )}
+
+ 总计: {devicePorts.length}
+
+
+
+ }
+ extra={
+ e.stopPropagation()}>
+
+ }
+ onClick={() => handleAddPortForDevice(device)}
+ style={{ color: designTokens.colors.primary.main }}
+ />
+
+ {device?.type?.toLowerCase()?.includes('server') && (
+
+ }
+ onClick={() => handleManageNetworkCards(device)}
+ style={{ color: designTokens.colors.primary.main }}
+ />
+
+ )}
+
+ }
+ style={{
+ background: '#fff',
+ borderRadius: designTokens.borderRadius.lg,
+ marginBottom: '12px',
+ border: `1px solid ${designTokens.colors.neutral[200]}`,
+ overflow: 'hidden',
+ }}
+ >
+
+ `共 ${total} 个端口`,
+ pageSizeOptions: ['10', '20', '50', '100'],
+ }}
+ size="middle"
+ scroll={{ x: 1000 }}
+ style={{
+ borderRadius: designTokens.borderRadius.md,
+ overflow: 'hidden',
+ }}
+ />
+
+
+
+ );
+ })}
+
+
+ )}
+
+
+ {/* 新增/编辑端口弹窗 */}
+
+
+ {editingPort ? '编辑端口' : '新增端口'}
+
+
+ }
open={modalVisible}
onOk={handleSubmit}
onCancel={() => {
@@ -907,8 +1207,15 @@ function PortManagement() {
width={600}
okText="确定"
cancelText="取消"
+ okButtonProps={{
+ style: {
+ background: designTokens.colors.primary.gradient,
+ border: 'none',
+ borderRadius: designTokens.borderRadius.sm,
+ },
+ }}
>
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+ {/* 批量导入弹窗 */}
+
+
+
+ 批量导入端口
+
+ }
open={importModalVisible}
onCancel={() => {
setImportModalVisible(false);
@@ -1006,10 +1342,19 @@ function PortManagement() {
}}
width={900}
footer={[
- setImportModalVisible(false)}>
+ setImportModalVisible(false)}
+ style={{ borderRadius: designTokens.borderRadius.sm }}
+ >
取消
,
- } onClick={handleDownloadTemplate}>
+ }
+ onClick={handleDownloadTemplate}
+ style={{ borderRadius: designTokens.borderRadius.sm }}
+ >
下载模板
,
开始导入
,
]}
>
-
-
-
false}
- customRequest={({ file, onSuccess }) => {
- handleFileUpload({ file, onSuccess });
- }}
- >
-
-
-
- 点击或拖拽文件到此处上传
- 支持 .xlsx, .xls, .csv 格式
-
-
+
+
false}
+ customRequest={({ file, onSuccess }) => {
+ handleFileUpload({ file, onSuccess });
+ }}
+ style={{
+ borderRadius: designTokens.borderRadius.lg,
+ border: `2px dashed ${designTokens.colors.primary.light}`,
+ background: designTokens.colors.primary.bg,
+ }}
+ >
+
+
+
+
+ 点击或拖拽文件到此处上传
+
+
+ 支持 .xlsx, .xls, .csv 格式文件
+
+
-
+
setSkipExisting(e.target.checked)}>
跳过已存在的端口
@@ -1053,94 +1409,54 @@ function PortManagement() {
{importPreview.length > 0 && (
- <>
-
-
- 数据预览(前10条)
- } onClick={handleDownloadTemplate}>
- 下载模板
-
-
-
getPortTypeTag(type),
- },
- {
- title: '端口速率',
- dataIndex: '端口速率',
- key: 'portSpeed',
- width: 100,
- },
- {
- title: '状态',
- dataIndex: '状态',
- key: 'status',
- width: 100,
- render: status => getStatusTag(status),
- },
- {
- title: 'VLAN ID',
- dataIndex: 'VLAN ID',
- key: 'vlanId',
- width: 100,
- render: vlanId => vlanId || '-',
- },
- {
- title: '描述',
- dataIndex: '描述',
- key: 'description',
- ellipsis: true,
- render: text => (
-
- {text || '-'}
-
- ),
- },
- ]}
- dataSource={importPreview.slice(0, 10)}
- rowKey={(record, index) => index}
- pagination={false}
- size="small"
- scroll={{ x: 1000 }}
- />
+
+
+
+ 数据预览(前10条)
-
+ getPortTypeTag(type) },
+ { title: '端口速率', dataIndex: '端口速率', key: 'portSpeed', width: 100 },
+ { title: '状态', dataIndex: '状态', key: 'status', width: 100, render: status => getStatusTag(status) },
+ { title: 'VLAN ID', dataIndex: 'VLAN ID', key: 'vlanId', width: 100, render: vlanId => vlanId || '-' },
+ { title: '描述', dataIndex: '描述', key: 'description', ellipsis: true },
+ ]}
+ dataSource={importPreview.slice(0, 10)}
+ rowKey={(record, index) => index}
+ pagination={false}
+ size="small"
+ scroll={{ x: 800 }}
+ style={{ borderRadius: designTokens.borderRadius.md }}
+ />
{importPreview.length > 10 && (
-
-
仅显示前10条数据,共 {importPreview.length} 条
+
+ 仅显示前10条数据,共 {importPreview.length} 条
)}
- >
+
)}
{importing && (
-
+
-
+
-
-
- 正在导入 {importProgress.current} / {importProgress.total} 条数据...
-
- {importProgress.current > 0 && (
-
- 预计剩余时间:{Math.ceil((importProgress.total - importProgress.current) / 5)}{' '}
- 秒
-
- )}
+
+ 正在导入 {importProgress.current} / {importProgress.total} 条数据...
-
+
)}
@@ -1169,9 +1478,24 @@ function PortManagement() {
{/* 网卡管理模态框 */}
-
- 网卡管理 - {selectedDeviceForNic?.name}
+
+
+
+
+
+ 网卡管理 - {selectedDeviceForNic?.name}
+
}
open={networkCardModalVisible}
@@ -1203,7 +1527,7 @@ function PortManagement() {
}}
onSuccess={handleNicSuccess}
/>
-
+
);
}