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: , }); setRefreshTrigger(prev => prev + 1); - fetchPorts(1, false); + fetchPorts(1); }; const handleEdit = port => { @@ -579,7 +511,7 @@ function PortManagement() { content: '删除成功', icon: , }); - fetchPorts(1, false); + fetchPorts(1); } catch (error) { const errorMsg = error.response?.data?.error || ''; if (errorMsg.includes('关联的接线记录')) { @@ -615,7 +547,7 @@ function PortManagement() { icon: , }); setSelectedRowKeys([]); - fetchPorts(1, false); + fetchPorts(1); } catch (error) { const errorMsg = error.response?.data?.error || ''; if (errorMsg.includes('关联的接线记录')) { @@ -861,7 +793,7 @@ function PortManagement() { setParseResult(null); setPreviewPorts([]); setShowPreview(false); - fetchPorts(1, false); + fetchPorts(1); } catch (error) { message.error(error.response?.data?.error || editingPort ? '更新失败' : '创建失败'); console.error('提交失败:', error); @@ -1289,7 +1221,7 @@ function PortManagement() { }); } - fetchPorts(1, false); + fetchPorts(1); setImportModalVisible(false); setImportPreview([]); } catch (error) { @@ -1871,7 +1803,7 @@ function PortManagement() {
- ) : Object.keys(groupedPorts).length === 0 ? ( + ) : groupedPorts.length === 0 ? ( ) : (
- {Object.entries(groupedPorts) - .slice( - (deviceCardPage - 1) * deviceCardPageSize, - deviceCardPage * deviceCardPageSize - ) - .map(([deviceId, data], index) => { + {groupedPorts.map((data, index) => { const device = data.device; + const deviceId = device?.deviceId; 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; - const isExpanded = expandedKeys.includes(deviceId); + const isExpanded = deviceId ? expandedKeys.includes(deviceId) : false; return ( `共 ${total} 个端口`, pageSizeOptions: ['10', '20', '50', '100'], @@ -2164,8 +2092,8 @@ function PortManagement() { pageSize={deviceCardPageSize} total={deviceCardTotal} onChange={page => { - setDeviceCardPage(page); setExpandedKeys([]); + fetchPorts(page); }} showSizeChanger={false} showTotal={(total, range) => @@ -2174,14 +2102,9 @@ function PortManagement() { />
)} - {hasMoreRef.current && ( -
- {portLoadingMore && } -
- )} - {!hasMoreRef.current && ports.length > 0 && ( + {ports.length > 0 && (
- 已加载全部 {portTotal} 个端口 + 共 {deviceCardTotal} 个设备,{portTotal} 个端口
)} diff --git a/package.json b/package.json index d12a910..f45c762 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "idc-device-management", - "version": "2.1.0", + "version": "2.1.1", "description": "IDC设备管理系统", "scripts": { "start": "concurrently \"npm run start:backend\" \"npm run start:frontend\"",