diff --git a/.gitignore b/.gitignore
index b5ca3db..c0ff40a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -119,3 +119,5 @@ docs/install-split-plan.md
# Local-only references (本地参考文档)
# ==============================================
deploy/nginx-troubleshooting.md
+docs/trae-creativity-contest-proposal.html
+TRAE-创意提案-IDC设备资产管理系统.html
diff --git a/backend/package.json b/backend/package.json
index ee07a02..d6a8a40 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "idc-backend",
- "version": "2.1.0",
+ "version": "2.1.1",
"description": "IDC设备管理系统后端",
"scripts": {
"start": "node server.js",
diff --git a/backend/routes/devicePorts.js b/backend/routes/devicePorts.js
index 234ebda..de7650f 100644
--- a/backend/routes/devicePorts.js
+++ b/backend/routes/devicePorts.js
@@ -1,7 +1,7 @@
const logger = require('../utils/logger').module('DevicePortsRoute');
const express = require('express');
const router = express.Router();
-const { Op } = require('sequelize');
+const { Op, Sequelize } = require('sequelize');
const DevicePort = require('../models/DevicePort');
const Device = require('../models/Device');
const NetworkCard = require('../models/NetworkCard');
@@ -489,6 +489,107 @@ router.get('/export/all', async (req, res) => {
});
// 获取单个端口
+// 按设备分组返回端口,支持设备维度分页
+// 解决端口数超过 pageSize 时部分设备不显示的问题
+router.get('/grouped', async (req, res) => {
+ try {
+ const { deviceId, page = 1, pageSize = 10 } = req.query;
+ const pageNum = Math.max(1, parseInt(page) || 1);
+ const pageSizeNum = Math.max(1, parseInt(pageSize) || 10);
+ const offset = (pageNum - 1) * pageSizeNum;
+
+ // 设备筛选条件
+ const deviceWhere = {};
+ if (deviceId) {
+ deviceWhere.deviceId = deviceId;
+ }
+
+ // 端口总数(不受设备分页影响,用于前端统计与导出)
+ const portWhere = {};
+ if (deviceId) {
+ portWhere.deviceId = deviceId;
+ }
+ const totalPorts = await DevicePort.count({ where: portWhere });
+
+ // 查询有端口的设备(用 EXISTS 子查询,避免 hasMany JOIN 导致 limit 作用于端口行)
+ // 若用 include+JOIN,limit 会作用于 JOIN 后的行数,端口多的设备会占满 limit
+ const { count, rows } = await Device.findAndCountAll({
+ attributes: ['deviceId', 'name', 'type', 'rackId'],
+ where: {
+ ...deviceWhere,
+ [Op.and]: [
+ Sequelize.where(
+ Sequelize.literal(
+ `EXISTS (SELECT 1 FROM device_ports WHERE device_ports.deviceId = Device.deviceId)`
+ ),
+ '=',
+ 1
+ ),
+ ],
+ },
+ order: [['name', 'ASC']],
+ offset,
+ limit: pageSizeNum,
+ });
+
+ // 当前页设备ID列表
+ const pagedDeviceIds = rows.map(d => d.deviceId);
+
+ if (pagedDeviceIds.length === 0) {
+ return res.json({
+ total: count,
+ totalPorts,
+ page: pageNum,
+ pageSize: pageSizeNum,
+ groups: [],
+ });
+ }
+
+ // 查询当前页设备的所有端口(含网卡关联)
+ const ports = await DevicePort.findAll({
+ where: { deviceId: { [Op.in]: pagedDeviceIds } },
+ include: [
+ {
+ model: Device,
+ as: 'device',
+ attributes: ['deviceId', 'name', 'type', 'rackId'],
+ },
+ {
+ model: NetworkCard,
+ as: 'networkCard',
+ attributes: ['nicId', 'name'],
+ },
+ ],
+ order: [['createdAt', 'DESC']],
+ });
+
+ // 按设备分组,保持设备排序(与分页查询一致)
+ const deviceMap = new Map();
+ rows.forEach(d => {
+ deviceMap.set(d.deviceId, { device: d, ports: [] });
+ });
+ ports.forEach(port => {
+ const group = deviceMap.get(port.deviceId);
+ if (group) {
+ group.ports.push(port);
+ }
+ });
+
+ const groups = pagedDeviceIds.map(id => deviceMap.get(id));
+
+ res.json({
+ total: count,
+ totalPorts,
+ page: pageNum,
+ pageSize: pageSizeNum,
+ groups,
+ });
+ } catch (error) {
+ logger.error('按设备分组获取端口失败', { error: error.message, stack: error.stack });
+ res.status(500).json({ error: error.message, errorType: error.name });
+ }
+});
+
router.get('/:portId', async (req, res) => {
try {
const port = await DevicePort.findByPk(req.params.portId, {
diff --git a/backend/routes/systemSettings.js b/backend/routes/systemSettings.js
index 4805186..8e5ff70 100644
--- a/backend/routes/systemSettings.js
+++ b/backend/routes/systemSettings.js
@@ -93,7 +93,7 @@ const initDefaultSettings = async () => {
// 关于页面
{
settingKey: 'app_version',
- settingValue: JSON.stringify('1.2.0'),
+ settingValue: JSON.stringify(APP_VERSION),
settingType: 'string',
category: 'about',
description: '应用版本',
diff --git a/docs/docker-deployment-guide.md b/docs/docker-deployment-guide.md
index 7d0bc3a..197b4dc 100644
--- a/docs/docker-deployment-guide.md
+++ b/docs/docker-deployment-guide.md
@@ -200,7 +200,7 @@ EOF
> **重要**:这个文件是后端 Node.js 应用读取的,配置数据库连接、JWT 等业务参数。不要与项目根目录的 `.env` 混淆。
```bash
-cd /opt/idc_assest
+cd /opt/idc_assest/backend
# 先生成 JWT 密钥(确保已执行 4.1 节的命令)
# JWT_SECRET=$(openssl rand -base64 64)
diff --git a/frontend/package.json b/frontend/package.json
index aeafd4a..8ee0ee3 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "idc-frontend",
- "version": "2.1.0",
+ "version": "2.1.1",
"description": "IDC设备管理系统前端",
"scripts": {
"start": "vite",
diff --git a/frontend/src/components/dashboard/SystemInfo.jsx b/frontend/src/components/dashboard/SystemInfo.jsx
index b5af712..3434188 100644
--- a/frontend/src/components/dashboard/SystemInfo.jsx
+++ b/frontend/src/components/dashboard/SystemInfo.jsx
@@ -10,6 +10,7 @@ import {
} from '@ant-design/icons';
import { designTokens } from '../../config/theme';
import { useFetch } from '../../hooks/useSWR';
+import { version as appVersion } from '../../../package.json';
// 格式化运行时间
const formatUptime = (seconds) => {
@@ -38,7 +39,7 @@ const formatUptime = (seconds) => {
const SystemInfo = ({ onRefresh, isRefreshing }) => {
const { data: systemData, isLoading: isVersionLoading } = useFetch('/system-settings/system/info');
- const version = systemData?.system?.version || '1.2.0';
+ const version = systemData?.system?.version || appVersion;
const formattedVersion = `v${version}`;
const currentTime = new Date().toLocaleString('zh-CN');
diff --git a/frontend/src/pages/PortManagement.jsx b/frontend/src/pages/PortManagement.jsx
index 37194b2..513e534 100644
--- a/frontend/src/pages/PortManagement.jsx
+++ b/frontend/src/pages/PortManagement.jsx
@@ -100,11 +100,34 @@ const animations = {
},
};
+/**
+ * 按端口名称中的数字段进行自然排序
+ * @param {Array} ports - 端口数组
+ * @returns {Array} 排序后的新数组
+ */
+function sortPortsByName(ports) {
+ const extractNumbers = str => {
+ const matches = str ? String(str).match(/\d+/g) : null;
+ return matches ? matches.map(Number) : [];
+ };
+ return [...ports].sort((a, b) => {
+ const numsA = extractNumbers(a.portName);
+ const numsB = extractNumbers(b.portName);
+ for (let i = 0; i < Math.min(numsA.length, numsB.length); i++) {
+ if (numsA[i] !== numsB[i]) {
+ return numsA[i] - numsB[i];
+ }
+ }
+ return String(a.portName).localeCompare(String(b.portName));
+ });
+}
+
function PortManagement() {
const [ports, setPorts] = useState([]);
const [devices, setDevices] = useState([]);
const [deviceSearching, setDeviceSearching] = useState(false);
- const [groupedPorts, setGroupedPorts] = useState({});
+ // 按设备分组的端口列表(数组,每项 { device, ports })
+ const [groupedPorts, setGroupedPorts] = useState([]);
const [loading, setLoading] = useState(false);
const [filters, setFilters] = useState({
deviceId: '',
@@ -167,17 +190,13 @@ function PortManagement() {
const hasMoreRef = useRef(true);
const isLoadingRef = useRef(false);
- // 设备卡片分页状态
+ // 设备卡片分页状态(按设备维度分页)
const [deviceCardPage, setDeviceCardPage] = useState(1);
const deviceCardPageSize = 10;
const [deviceCardTotal, setDeviceCardTotal] = useState(0);
- // 分页和懒加载状态
- const [portPage, setPortPage] = useState(1);
- const [portPageSize, setPortPageSize] = useState(50);
+ // 端口总数(所有设备端口总和,用于统计与导出弹窗)
const [portTotal, setPortTotal] = useState(0);
- const [portLoading, setPortLoading] = useState(false);
- const [portLoadingMore, setPortLoadingMore] = useState(false);
// 服务器网卡卡片列表状态
const [serverNicSearchText, setServerNicSearchText] = useState('');
@@ -186,46 +205,42 @@ function PortManagement() {
const [serverNicCardPage, setServerNicCardPage] = useState(1);
const serverNicCardPageSize = 12;
+ // 按设备分组获取端口(设备维度分页,避免端口数过多时部分设备不显示)
const fetchPorts = useCallback(
- async (page = 1, append = false) => {
+ async (page = 1) => {
const validPage = Number.isInteger(page) && page > 0 ? page : 1;
- const validPageSize = Number.isInteger(portPageSize) && portPageSize > 0 ? portPageSize : 50;
try {
- if (append) {
- setPortLoadingMore(true);
- } else {
- setLoading(true);
- }
-
+ setLoading(true);
const params = {
page: validPage,
- pageSize: validPageSize,
+ pageSize: deviceCardPageSize,
};
-
if (filters.deviceId) params.deviceId = filters.deviceId;
- const response = await api.get('/device-ports', { params });
- const portsData = response.ports || [];
- const total = response.total || 0;
- const filteredPorts = portsData;
+ const response = await api.get('/device-ports/grouped', { params });
+ const groups = response.groups || [];
- if (append) {
- setPorts(prev => [...prev, ...filteredPorts]);
- } else {
- setPorts(filteredPorts);
- }
- setPortTotal(total);
- setPortPage(page);
- hasMoreRef.current = page * portPageSize < total;
+ // 对每个设备的端口按名称自然排序,并展开为扁平数组(供统计、导出当前页使用)
+ const flatPorts = [];
+ const sortedGroups = groups.map(g => {
+ const sortedPorts = sortPortsByName(g.ports || []);
+ sortedPorts.forEach(p => flatPorts.push(p));
+ return { device: g.device, ports: sortedPorts };
+ });
+
+ setPorts(flatPorts);
+ setGroupedPorts(sortedGroups);
+ setDeviceCardTotal(response.total || 0);
+ setPortTotal(response.totalPorts || 0);
+ setDeviceCardPage(validPage);
} catch (error) {
message.error('获取端口列表失败');
console.error('获取端口列表失败:', error);
} finally {
setLoading(false);
- setPortLoadingMore(false);
}
},
- [filters, portPageSize]
+ [filters, deviceCardPageSize]
);
const fetchDevices = useCallback(async (keyword = '') => {
@@ -255,104 +270,21 @@ function PortManagement() {
);
useEffect(() => {
- fetchPorts(1, false);
+ fetchPorts(1);
fetchDevices();
}, [fetchPorts, fetchDevices]);
- const handleLoadMore = useCallback(() => {
- if (!hasMoreRef.current || isLoadingRef.current || portLoadingMore) return;
- isLoadingRef.current = true;
- const nextPage = portPage + 1;
- fetchPorts(nextPage, true).then(() => {
- isLoadingRef.current = false;
- });
- }, [portPage, portLoadingMore, fetchPorts]);
-
- useEffect(() => {
- if (!loadMoreRef.current) return;
-
- const observer = new IntersectionObserver(
- entries => {
- if (entries[0].isIntersecting && hasMoreRef.current && !portLoadingMore) {
- handleLoadMore();
- }
- },
- { threshold: 0.1, rootMargin: '100px' }
- );
-
- observer.observe(loadMoreRef.current);
-
- return () => {
- observer.disconnect();
- };
- }, [handleLoadMore, portLoadingMore]);
-
- const handlePageSizeChange = useCallback(
- (newPage, newPageSize) => {
- setPortPageSize(newPageSize);
- setPortPage(1);
- fetchPorts(1, false);
- },
- [fetchPorts]
- );
-
- useEffect(() => {
- const grouped = {};
- ports.forEach(port => {
- const deviceId = port.deviceId;
- if (!grouped[deviceId]) {
- grouped[deviceId] = {
- device: port.device || devices.find(d => d.deviceId === deviceId),
- ports: [],
- };
- }
- grouped[deviceId].ports.push(port);
- });
-
- // 对每个设备的端口按名称升序排序
- Object.keys(grouped).forEach(deviceId => {
- grouped[deviceId].ports.sort((a, b) => {
- const extractNumbers = str => {
- const matches = str.match(/\d+/g);
- return matches ? matches.map(Number) : [];
- };
- const numsA = extractNumbers(a.portName);
- const numsB = extractNumbers(b.portName);
- for (let i = 0; i < Math.min(numsA.length, numsB.length); i++) {
- if (numsA[i] !== numsB[i]) {
- return numsA[i] - numsB[i];
- }
- }
- return a.portName.localeCompare(b.portName);
- });
- });
-
- setGroupedPorts(grouped);
-
- const deviceIds = Object.keys(grouped);
- const newTotal = deviceIds.length;
- setDeviceCardTotal(newTotal);
- if (deviceCardPage > Math.ceil(newTotal / deviceCardPageSize)) {
- setDeviceCardPage(1);
- }
- setExpandedKeys([]);
- }, [ports, devices, deviceCardPage, deviceCardPageSize]);
-
const handleSearch = useCallback(() => {
- setPortPage(1);
setDeviceCardPage(1);
- hasMoreRef.current = true;
- fetchPorts(1, false);
+ fetchPorts(1);
}, [fetchPorts]);
const handleReset = useCallback(() => {
setFilters({
deviceId: '',
});
- setPortPage(1);
setDeviceCardPage(1);
- hasMoreRef.current = true;
- fetchPorts(1, false);
+ fetchPorts(1);
}, [fetchPorts]);
const handleAdd = () => {
@@ -547,7 +479,7 @@ function PortManagement() {
icon: