From a6a6198eaa5b6e239fcb8ba728ef2ad8c87ce9cc Mon Sep 17 00:00:00 2001 From: wang_yp <357754663@qq.com> Date: Wed, 26 Aug 2026 14:01:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E4=BC=98=E5=8C=96)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/router/routes/modules/ops.ts | 13 + .../datacenter/three-machine-room/index.vue | 205 ++++- .../three-machine-room/scene/ThreeMap.js | 422 ++++++++-- .../ops/pages/hikvision-video-test/index.vue | 718 ++++++++++++++++++ 4 files changed, 1307 insertions(+), 51 deletions(-) create mode 100644 src/views/ops/pages/hikvision-video-test/index.vue diff --git a/src/router/routes/modules/ops.ts b/src/router/routes/modules/ops.ts index 449c25f..419bc6e 100644 --- a/src/router/routes/modules/ops.ts +++ b/src/router/routes/modules/ops.ts @@ -100,6 +100,19 @@ const OPS: AppRouteRecordRaw = { roles: ['*'], }, }, + { + path: 'hikvision-video-test', + alias: ['/hikvision-video-test'], + name: 'HikvisionVideoTest', + component: () => import('@/views/ops/pages/hikvision-video-test/index.vue'), + meta: { + locale: '海康视频拉流测试', + requiresAuth: true, + roles: ['*'], + hideInMenu: true, + ignoreCache: true, + }, + }, { path: 'datacenter/three-machine-room/:room_id?', alias: ['/datacenter/three-machine-room/:room_id?'], diff --git a/src/views/ops/pages/datacenter/three-machine-room/index.vue b/src/views/ops/pages/datacenter/three-machine-room/index.vue index 8b3c1c2..b1a959f 100644 --- a/src/views/ops/pages/datacenter/three-machine-room/index.vue +++ b/src/views/ops/pages/datacenter/three-machine-room/index.vue @@ -22,13 +22,13 @@ {{ exteriorDoorsOpen ? '关闭外墙门' : '打开外墙门' }} - {{ cabinetDoorsOpen ? '关闭全部机柜门' : '打开全部机柜门' }} + {{ cabinetDoorsOpen ? '关闭当前机柜门' : '打开机柜并查看设备' }} {{ serversVisible ? '隐藏服务器设备' : '显示服务器设备' }} - {{ insideAisle ? '返回总览' : '进入右侧过道' }} + {{ insideAisle ? '返回总览' : '进入机柜过道' }} @@ -45,7 +45,7 @@ 活动告警 {{ signalSummary.activeAlertCount }} - {{ insideAisle ? '滚轮前后移动,拖拽转向;点击设备查看实时信息' : '点击机柜上架设备;双击门板开关' }} + {{ insideAisle ? '当前斜向查看机柜正面;滚轮移动,拖拽转向' : '双击机柜门可开门聚焦,并查看柜内设备信息' }} @@ -58,6 +58,40 @@ + + (null) const selectedRack = ref(null) const selectedDevice = ref(null) +const activeRack = ref(null) const rackPlacementVisible = ref(false) const rackLayoutVisible = ref(false) const sceneConfigVisible = ref(false) const roomDevicePlacementVisible = ref(false) -const cameraPreviewVisible = ref(true) +const cameraPreviewVisible = ref(false) const deviceManagementVisible = ref(false) const exportLoading = ref(false) @@ -184,6 +219,7 @@ function resolveRoomId(): number { const roomId = resolveRoomId() const roomName = computed(() => roomScene.value?.room?.name || `机房 ${roomId}`) const layoutVersion = computed(() => Number(roomScene.value?.room?.layout_version) || 0) +const activeRackDevices = computed(() => (Array.isArray(activeRack.value?.devices) ? activeRack.value.devices : [])) const loadingText = computed(() => { if (modelLoading.value) { return modelProgress.value > 0 ? `3D 模型加载中 ${modelProgress.value}%` : '3D 模型加载中' @@ -199,6 +235,20 @@ function getErrorMessage(error: unknown, fallback: string): string { return error instanceof Error && error.message ? error.message : fallback } +/** 格式化机柜设备占用的 U 位范围。 */ +function formatDeviceUnits(device: ThreeMachineRoomDevice): string { + const start = Number(device.unit_start) + if (!Number.isInteger(start) || start <= 0) return 'U位未设置' + const end = Number(device.unit_end) || start + Math.max(1, Number(device.occupied_units) || 1) - 1 + return end > start ? `U${start}-U${end}` : `U${start}` +} + +/** 收起机柜设备信息面板。 */ +function closeActiveRackPanel(): void { + activeRack.value = null + machineRoomMap?.clearActiveCabinetSelection() +} + /** 停止设备状态轮询。 */ function stopSignalPolling(): void { if (signalTimer !== undefined) { @@ -410,6 +460,9 @@ async function initializeMachineRoom(): Promise { onRoomSceneApplied: (summary: SceneSummary) => Object.assign(sceneSummary, summary), onDeviceSelected: (device: ThreeMachineRoomDevice) => handleDeviceSelected(device), onRackSelected: (rack: ThreeMachineRoomRack) => handleRackSelected(rack), + onCabinetDoorChanged: (rack: ThreeMachineRoomRack | null) => { + activeRack.value = rack + }, onModelLoading: (loading: boolean) => { modelLoading.value = loading }, @@ -454,7 +507,7 @@ function toggleExteriorDoors(): void { if (machineRoomMap) exteriorDoorsOpen.value = machineRoomMap.toggleExteriorDoors() } -/** 切换全部机柜门。 */ +/** 打开有设备的机柜并聚焦,或关闭当前机柜门。 */ function toggleCabinetDoors(): void { if (machineRoomMap) cabinetDoorsOpen.value = machineRoomMap.toggleCabinetDoors() } @@ -467,7 +520,7 @@ function toggleServers(): void { /** 切换总览与过道视角。 */ function toggleAisleView(): void { if (!machineRoomMap) return - const changed = insideAisle.value ? machineRoomMap.setOverviewView() : machineRoomMap.setAisleView() + const changed = insideAisle.value ? machineRoomMap.setOverviewView() : machineRoomMap.setAisleView('right') if (changed) { insideAisle.value = !insideAisle.value } else { @@ -614,6 +667,146 @@ export default { width: 280px; } } + + .rack-equipment-panel { + position: absolute; + z-index: 9; + right: 22px; + bottom: 22px; + width: min(360px, calc(100vw - 44px)); + max-height: calc(100vh - 150px); + overflow: hidden; + border: 1px solid rgba(51, 211, 255, 0.5); + border-radius: 8px; + background: linear-gradient(150deg, rgba(3, 52, 79, 0.97), rgba(3, 17, 42, 0.97)); + box-shadow: + 0 16px 42px rgba(0, 5, 24, 0.65), + 0 0 26px rgba(28, 199, 255, 0.14); + backdrop-filter: blur(10px); + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px 12px; + border-bottom: 1px solid rgba(56, 211, 255, 0.18); + + div { + display: flex; + flex-direction: column; + gap: 3px; + } + + strong { + color: #e7faff; + font-size: 16px; + } + + span { + color: #7fc7df; + font-size: 11px; + } + + button { + width: 28px; + height: 28px; + border: 0; + border-radius: 50%; + color: #a5d9e9; + background: rgba(55, 171, 207, 0.12); + font-size: 20px; + line-height: 26px; + cursor: pointer; + } + } + + &__summary { + display: flex; + justify-content: space-between; + padding: 9px 16px; + color: #8fcde1; + background: rgba(28, 157, 196, 0.08); + font-size: 12px; + } + + &__list { + max-height: min(390px, calc(100vh - 300px)); + padding: 8px; + overflow-y: auto; + } + + &__device { + display: flex; + width: 100%; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 11px; + border: 1px solid transparent; + border-radius: 5px; + color: inherit; + background: transparent; + text-align: left; + cursor: pointer; + + &:hover, + &:focus-visible { + border-color: rgba(43, 211, 255, 0.36); + background: rgba(22, 164, 205, 0.12); + outline: none; + } + } + + &__device-main, + &__device-meta { + display: flex; + min-width: 0; + flex-direction: column; + gap: 4px; + } + + &__device-main { + strong { + overflow: hidden; + color: #e4f8ff; + font-size: 13px; + text-overflow: ellipsis; + white-space: nowrap; + } + + small { + color: #779fb0; + } + } + + &__device-meta { + flex: 0 0 auto; + align-items: flex-end; + + b { + color: #40ddff; + font-size: 12px; + } + + small { + color: #94adba; + } + } + + &__empty { + padding: 28px 16px; + color: #789cac; + text-align: center; + } + + &__tip { + margin: 0; + padding: 9px 16px 11px; + border-top: 1px solid rgba(56, 211, 255, 0.12); + color: #6fa7ba; + font-size: 11px; + } + } } .rack-device-callout-layer { diff --git a/src/views/ops/pages/datacenter/three-machine-room/scene/ThreeMap.js b/src/views/ops/pages/datacenter/three-machine-room/scene/ThreeMap.js index b9e963b..c10f27e 100644 --- a/src/views/ops/pages/datacenter/three-machine-room/scene/ThreeMap.js +++ b/src/views/ops/pages/datacenter/three-machine-room/scene/ThreeMap.js @@ -52,6 +52,9 @@ export default class ThreeMap { this.outerWallMeshes = [] this.exteriorDoorPanels = [] this.cabinetDoorPanels = [] + this.cabinetDisplayPanels = [] + this.cabinetRowPairs = [] + this.cabinetShellMeshes = [] this.roomSceneData = null this.roomSignalData = null this.rackPanelMap = new Map() @@ -64,6 +67,7 @@ export default class ThreeMap { this.deviceCallouts = new Map() this.hoveredEquipment = null this.selectedEquipment = null + this.activeRackId = null this._singleClickTimer = null this._dbClickResetTimer = null @@ -388,6 +392,10 @@ export default class ThreeMap { this.deviceSignals.clear() this.deviceObservability.clear() this.rackPanelMap.clear() + this.cabinetDisplayPanels = [] + this.cabinetRowPairs = [] + this.cabinetShellMeshes = [] + this.activeRackId = null this._onMouseDown = null this._onMouseMove = null } @@ -738,6 +746,56 @@ export default class ThreeMap { return panel } + /** 为外墙门克隆独立材质,避免开门效果污染墙体和其他门板。 */ + prepareExteriorDoorMaterials(panel) { + panel.materialStates = [] + panel.meshes.forEach((mesh) => { + const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material] + const clonedMaterials = materials.map((material) => material.clone()) + mesh.material = Array.isArray(mesh.material) ? clonedMaterials : clonedMaterials[0] + panel.materialStates.push( + clonedMaterials.map((material) => ({ + color: material.color ? material.color.clone() : null, + emissive: material.emissive ? material.emissive.clone() : null, + emissiveIntensity: material.emissiveIntensity, + opacity: material.opacity, + transparent: material.transparent, + depthWrite: material.depthWrite, + side: material.side, + })) + ) + }) + } + + /** 打开大门时使用半透明蓝灰内侧,关闭后恢复模型原材质。 */ + setExteriorDoorAppearance(panel, open) { + if (!panel || !panel.materialStates) { + return + } + panel.meshes.forEach((mesh, meshIndex) => { + const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material] + const states = panel.materialStates[meshIndex] || [] + materials.forEach((material, materialIndex) => { + const state = states[materialIndex] + if (!state) { + return + } + if (material.color && state.color) { + material.color.copy(open ? new THREE.Color(0x4f7f92) : state.color) + } + if (material.emissive && state.emissive) { + material.emissive.copy(open ? new THREE.Color(0x0a2533) : state.emissive) + material.emissiveIntensity = open ? 0.16 : state.emissiveIntensity + } + material.transparent = open ? true : state.transparent + material.opacity = open ? 0.68 : state.opacity + material.depthWrite = open ? false : state.depthWrite + material.side = open ? THREE.DoubleSide : state.side + material.needsUpdate = true + }) + }) + } + createExteriorDoorPanels(object, meshEntries) { const meshByName = {} meshEntries.forEach((entry) => { @@ -782,6 +840,7 @@ export default class ThreeMap { 'exterior' ) if (panel) { + this.prepareExteriorDoorMaterials(panel) panels.push(panel) } }) @@ -790,9 +849,7 @@ export default class ThreeMap { createCabinetDoorPanels(object, meshEntries, modelBox) { const modelSize = new THREE.Vector3() - const modelCenter = new THREE.Vector3() modelBox.getSize(modelSize) - modelBox.getCenter(modelCenter) const candidates = meshEntries.filter((entry) => { const size = new THREE.Vector3() entry.box.getSize(size) @@ -820,7 +877,7 @@ export default class ThreeMap { const center = new THREE.Vector3() entry.box.getCenter(center) let cluster = clusters.find( - (item) => Math.abs(item.centerX - center.x) < modelSize.x * 0.009 && Math.abs(item.centerZ - center.z) < modelSize.z * 0.022 + (item) => Math.abs(item.centerX - center.x) < modelSize.x * 0.014 && Math.abs(item.centerZ - center.z) < modelSize.z * 0.022 ) if (!cluster) { cluster = { centerX: center.x, centerZ: center.z, entries: [] } @@ -832,14 +889,13 @@ export default class ThreeMap { return clusters .map((cluster, index) => { const box = this.getCombinedBox(cluster.entries) - const openAngle = cluster.centerX < modelCenter.x ? -0.5 * Math.PI : 0.5 * Math.PI return this.createImportedDoorPanel( object, 'cabinet_door_' + (index + 1), cluster.entries, (box.min.x + box.max.x) / 2, box.min.z, - openAngle, + 0.5 * Math.PI, 'cabinet' ) }) @@ -865,18 +921,139 @@ export default class ThreeMap { }) xLevels.sort((a, b) => a.centerX - b.centerX) - xLevels.forEach((level, index) => { - let direction - if (index % 2 === 0 && index + 1 < xLevels.length) { - direction = 1 - } else if (index % 2 === 1) { - direction = -1 - } else { - direction = level.centerX < modelCenter.x ? 1 : -1 + this.cabinetRowPairs = [] + this.cabinetDisplayPanels = [] + + for (let index = 0; index < xLevels.length; index += 2) { + const leftLevel = xLevels[index] + const rightLevel = xLevels[index + 1] + if (!rightLevel) { + const fallbackDirection = leftLevel.centerX < modelCenter.x ? 1 : -1 + leftLevel.panels.forEach((panel) => { + panel.interiorDirection = fallbackDirection + panel.openAngle = -fallbackDirection * Math.PI * 0.58 + this.cabinetDisplayPanels.push(panel) + }) + continue } - level.panels.forEach((panel) => { - panel.interiorDirection = direction + + const rowPair = { + index: this.cabinetRowPairs.length, + leftLevel: leftLevel, + rightLevel: rightLevel, + } + this.cabinetRowPairs.push(rowPair) + + leftLevel.panels.forEach((panel) => { + panel.interiorDirection = 1 + panel.openAngle = -Math.PI * 0.58 + panel.cabinetRowIndex = rowPair.index + panel.cabinetSide = 'left' }) + rightLevel.panels.forEach((panel) => { + panel.interiorDirection = -1 + panel.openAngle = Math.PI * 0.58 + panel.cabinetRowIndex = rowPair.index + panel.cabinetSide = 'right' + }) + + leftLevel.panels.forEach((leftPanel) => { + const leftZ = leftPanel.box.getCenter(new THREE.Vector3()).z + const mate = rightLevel.panels.find((rightPanel) => { + const rightZ = rightPanel.box.getCenter(new THREE.Vector3()).z + return Math.abs(rightZ - leftZ) < modelSize.z * 0.035 + }) + if (mate) { + leftPanel.cabinetMate = mate + mate.cabinetMate = leftPanel + } + }) + } + + this.cabinetRowPairs.forEach((rowPair, index) => { + const previousRow = this.cabinetRowPairs[index - 1] + const nextRow = this.cabinetRowPairs[index + 1] + const leftGap = previousRow ? rowPair.leftLevel.centerX - previousRow.rightLevel.centerX : -1 + const rightGap = nextRow ? nextRow.leftLevel.centerX - rowPair.rightLevel.centerX : -1 + const displayLevel = rightGap >= leftGap ? rowPair.rightLevel : rowPair.leftLevel + displayLevel.panels.forEach((panel) => { + panel.isDisplayPanel = true + this.cabinetDisplayPanels.push(panel) + }) + }) + } + + /** 将每扇机柜门关联到对应柜体,开门时淡化封闭外壳以露出内部设备。 */ + assignCabinetShells(panels, meshEntries, modelBox) { + const modelSize = modelBox.getSize(new THREE.Vector3()) + const shellEntries = meshEntries.filter((entry) => { + const size = entry.box.getSize(new THREE.Vector3()) + return ( + entry.mesh.name.indexOf('Obj3d66_11807706_1509_') === 0 && + size.x > modelSize.x * 0.04 && + size.x < modelSize.x * 0.14 && + size.y > modelSize.y * 0.42 && + size.z > modelSize.z * 0.025 && + size.z < modelSize.z * 0.12 + ) + }) + + this.cabinetShellMeshes = shellEntries.map((entry) => { + const mesh = entry.mesh + const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material] + const clonedMaterials = materials.map((material) => material.clone()) + mesh.material = Array.isArray(mesh.material) ? clonedMaterials : clonedMaterials[0] + mesh.userData.cabinetShellMaterialState = clonedMaterials.map((material) => ({ + opacity: material.opacity, + transparent: material.transparent, + depthWrite: material.depthWrite, + })) + mesh.userData.openCabinetDoorCount = 0 + return mesh + }) + + panels.forEach((panel) => { + const center = panel.box.getCenter(new THREE.Vector3()) + const matchingEntry = shellEntries + .filter( + (entry) => + center.x >= entry.box.min.x - modelSize.x * 0.01 && + center.x <= entry.box.max.x + modelSize.x * 0.01 && + center.z >= entry.box.min.z - modelSize.z * 0.01 && + center.z <= entry.box.max.z + modelSize.z * 0.01 + ) + .sort((a, b) => { + const aCenter = a.box.getCenter(new THREE.Vector3()) + const bCenter = b.box.getCenter(new THREE.Vector3()) + return ( + Math.abs(aCenter.x - center.x) + + Math.abs(aCenter.z - center.z) - + Math.abs(bCenter.x - center.x) - + Math.abs(bCenter.z - center.z) + ) + })[0] + panel.cabinetShell = matchingEntry ? matchingEntry.mesh : null + }) + } + + /** 根据机柜门状态隐藏或恢复柜体外壳。 */ + setCabinetShellOpen(panel, open) { + const shell = panel && panel.cabinetShell + if (!shell) { + return + } + const currentCount = Number(shell.userData.openCabinetDoorCount) || 0 + shell.userData.openCabinetDoorCount = Math.max(0, currentCount + (open ? 1 : -1)) + const shouldFade = shell.userData.openCabinetDoorCount > 0 + shell.visible = !shouldFade + const materials = Array.isArray(shell.material) ? shell.material : [shell.material] + const states = shell.userData.cabinetShellMaterialState || [] + materials.forEach((material, index) => { + const state = states[index] || { opacity: 1, transparent: false, depthWrite: true } + material.transparent = shouldFade ? true : state.transparent + material.opacity = shouldFade ? 0.035 : state.opacity + material.depthWrite = shouldFade ? false : state.depthWrite + material.needsUpdate = true }) } @@ -897,23 +1074,29 @@ export default class ThreeMap { sortSceneRacks(racks) { return (racks || []).slice().sort((a, b) => { - const rowDiff = (a.row || 0) - (b.row || 0) - if (rowDiff !== 0) { - return rowDiff - } - const columnDiff = (a.column || 0) - (b.column || 0) - if (columnDiff !== 0) { - return columnDiff + const aHasLayout = Number(a.row) > 0 || Number(a.column) > 0 + const bHasLayout = Number(b.row) > 0 || Number(b.column) > 0 + if (aHasLayout && bHasLayout) { + const rowDiff = (a.row || 0) - (b.row || 0) + if (rowDiff !== 0) { + return rowDiff + } + const columnDiff = (a.column || 0) - (b.column || 0) + if (columnDiff !== 0) { + return columnDiff + } } + const codeDiff = String(a.code || a.name || '').localeCompare(String(b.code || b.name || ''), 'zh-CN', { numeric: true }) + if (codeDiff !== 0) return codeDiff return (a.id || 0) - (b.id || 0) }) } bindSceneRacksToPanels(panels, racks) { - const sortedPanels = this.sortCabinetPanels(panels) + const sortedPanels = this.sortCabinetPanels(this.cabinetDisplayPanels.length ? this.cabinetDisplayPanels : panels) const sortedRacks = this.sortSceneRacks(racks) this.rackPanelMap.clear() - sortedPanels.forEach((panel) => { + panels.forEach((panel) => { panel.rackData = null panel.rackId = null panel.meshes.forEach((mesh) => { @@ -1388,8 +1571,8 @@ export default class ThreeMap { const rackBottom = panel.box.min.y + panelSize.y * 0.09 const serverDepth = Math.max(modelSize.x * 0.004, panelSize.x * 2) const serverWidth = panelSize.z * 0.78 - const interiorOffset = serverDepth * 0.35 - const centerX = panelCenter.x + panel.interiorDirection * interiorOffset + const exteriorOffset = serverDepth * 0.62 + const centerX = panelCenter.x - panel.interiorDirection * exteriorOffset rack.devices.forEach((device, deviceIndex) => { const startUnit = Math.max(1, Number(device.unit_start) || 1) @@ -1421,11 +1604,17 @@ export default class ThreeMap { equipmentBodyMaterial, ] const server = new THREE.Mesh(geometry, materials) + const outline = new THREE.LineSegments( + new THREE.EdgesGeometry(geometry), + new THREE.LineBasicMaterial({ color: 0x35d9ff, transparent: true, opacity: 0.82 }) + ) + outline.raycast = function () {} + server.add(outline) server.name = 'rack_device_' + device.asset_id server.position.set(centerX, centerY, panelCenter.z) server.castShadow = true server.receiveShadow = true - server.visible = this.serverEquipmentVisible + server.visible = this.serverEquipmentVisible && panel.isOpen server.userData.equipmentType = appearance.type server.userData.cabinetDoor = panel.name server.userData.rackId = rack.id @@ -1519,6 +1708,15 @@ export default class ThreeMap { if (equipment.geometry) { geometries.add(equipment.geometry) } + equipment.children.forEach((child) => { + if (child.geometry) { + geometries.add(child.geometry) + } + const childMaterials = Array.isArray(child.material) ? child.material : [child.material] + childMaterials.forEach((material) => { + if (material) materials.add(material) + }) + }) const equipmentMaterials = Array.isArray(equipment.material) ? equipment.material : [equipment.material] equipmentMaterials.forEach((material) => { if (material) { @@ -1614,6 +1812,7 @@ export default class ThreeMap { this.exteriorDoorPanels = this.createExteriorDoorPanels(object, modelInfo.meshes) this.cabinetDoorPanels = this.createCabinetDoorPanels(object, modelInfo.meshes, modelInfo.box) this.assignCabinetInteriorDirections(this.cabinetDoorPanels, modelInfo.box) + this.assignCabinetShells(this.cabinetDoorPanels, modelInfo.meshes, modelInfo.box) this.serverEquipment = [] const doorMeshes = new Set() this.exteriorDoorPanels.concat(this.cabinetDoorPanels).forEach((panel) => { @@ -1654,11 +1853,71 @@ export default class ThreeMap { setServerEquipmentVisible(visible) { this.serverEquipmentVisible = visible !== false this.serverEquipment.forEach((server) => { - server.visible = this.serverEquipmentVisible + const panel = this.cabinetDoorPanels.find((item) => item.name === server.userData.cabinetDoor) + server.visible = this.serverEquipmentVisible && (!panel || panel.isOpen) }) return this.serverEquipmentVisible } + /** 机柜门开关后同步柜内设备的可见状态。 */ + updateCabinetEquipmentVisibility(panel) { + if (!panel) return + this.serverEquipment.forEach((server) => { + if (server.userData.cabinetDoor === panel.name) { + server.visible = this.serverEquipmentVisible && panel.isOpen + } + }) + } + + /** 将模型局部坐标转换为场景世界坐标。 */ + getImportedModelWorldPoint(point) { + this.importedModel.updateMatrixWorld(true) + return this.importedModel.localToWorld(point.clone()) + } + + /** 通知页面展示或收起当前机柜的设备清单。 */ + setActiveCabinetPanel(panel) { + const rack = panel && panel.rackData ? panel.rackData : null + this.activeRackId = rack && rack.id !== undefined ? String(rack.id) : null + if (typeof this.props.onCabinetDoorChanged === 'function') { + this.props.onCabinetDoorChanged(rack) + } + } + + /** 由页面主动收起机柜设备清单。 */ + clearActiveCabinetSelection() { + this.setActiveCabinetPanel(null) + } + + /** 将相机移动到门板正前方,清楚展示机柜内部。 */ + focusCabinetPanel(panel) { + if (!panel || !this.importedModel || !this.camera || !this.camera.isPerspectiveCamera || !this.controls) { + return false + } + this.importedModel.updateMatrixWorld(true) + const panelSize = panel.box.getSize(new THREE.Vector3()) + const panelCenter = panel.box.getCenter(new THREE.Vector3()) + const worldScale = this.importedModel.getWorldScale(new THREE.Vector3()) + const rotationMatrix = new THREE.Matrix4().extractRotation(this.importedModel.matrixWorld) + const exteriorDirection = new THREE.Vector3(-panel.interiorDirection, 0, 0).applyMatrix4(rotationMatrix).normalize() + const interiorDirection = exteriorDirection.clone().multiplyScalar(-1) + const worldCenter = this.getImportedModelWorldPoint(panelCenter) + const worldHeight = panelSize.y * Math.abs(worldScale.y) + const position = worldCenter.clone().add(exteriorDirection.multiplyScalar(worldHeight * 1.52)) + position.y += worldHeight * 0.06 + const target = worldCenter.clone().add(interiorDirection.multiplyScalar(worldHeight * 0.035)) + + this.camera.fov = 46 + this.camera.near = Math.max(0.1, worldHeight / 1000) + this.camera.far = Math.max(this.camera.far, worldHeight * 30) + this.camera.updateProjectionMatrix() + this.controls.minDistance = Math.max(1, worldHeight * 0.08) + this.controls.maxDistance = worldHeight * 5 + this.moveCameraTo(position, target, 720) + this.viewMode = 'cabinet' + return true + } + setImportedDoorState(panel, open) { if (!panel) { return false @@ -1670,7 +1929,14 @@ export default class ThreeMap { if (panel.tween) { panel.tween.stop() } + const wasOpen = panel.isOpen panel.isOpen = shouldOpen + if (panel.type === 'exterior') { + this.setExteriorDoorAppearance(panel, shouldOpen) + } else if (panel.type === 'cabinet' && wasOpen !== shouldOpen) { + this.setCabinetShellOpen(panel, shouldOpen) + this.updateCabinetEquipmentVisibility(panel) + } panel.tween = new TWEEN.Tween(panel.pivot.rotation) .to({ y: shouldOpen ? panel.openAngle : 0 }, 480) .easing(TWEEN.Easing.Quadratic.Out) @@ -1682,7 +1948,16 @@ export default class ThreeMap { } toggleImportedDoor(panel) { - return this.setImportedDoorState(panel, !panel.isOpen) + const shouldOpen = this.setImportedDoorState(panel, !panel.isOpen) + if (panel && panel.type === 'cabinet') { + if (shouldOpen) { + this.setActiveCabinetPanel(panel) + this.focusCabinetPanel(panel) + } else if (this.activeRackId && panel.rackId !== undefined && String(panel.rackId) === this.activeRackId) { + this.setActiveCabinetPanel(null) + } + } + return shouldOpen } toggleExteriorDoors(forceOpen) { @@ -1698,8 +1973,26 @@ export default class ThreeMap { if (this.cabinetDoorPanels.length === 0) { return false } - const shouldOpen = typeof forceOpen === 'boolean' ? forceOpen : !this.cabinetDoorPanels.every((panel) => panel.isOpen) - this.cabinetDoorPanels.forEach((panel) => this.setImportedDoorState(panel, shouldOpen)) + const shouldOpen = typeof forceOpen === 'boolean' ? forceOpen : !this.cabinetDoorPanels.some((panel) => panel.isOpen) + if (shouldOpen) { + const modelCenter = this.importedModelBox ? this.importedModelBox.getCenter(new THREE.Vector3()) : new THREE.Vector3() + const equipmentPanel = + this.cabinetDisplayPanels + .filter((panel) => panel.rackData && Array.isArray(panel.rackData.devices) && panel.rackData.devices.length > 0) + .sort((a, b) => { + const aCenter = this.getImportedModelWorldPoint(a.box.getCenter(new THREE.Vector3())) + const bCenter = this.getImportedModelWorldPoint(b.box.getCenter(new THREE.Vector3())) + return Math.abs(aCenter.z - modelCenter.z) - Math.abs(bCenter.z - modelCenter.z) + })[0] || this.cabinetDisplayPanels.find((panel) => panel.rackData) + if (equipmentPanel) { + this.cabinetDoorPanels.forEach((panel) => this.setImportedDoorState(panel, panel === equipmentPanel)) + this.setActiveCabinetPanel(equipmentPanel) + this.focusCabinetPanel(equipmentPanel) + } + } else { + this.cabinetDoorPanels.forEach((panel) => this.setImportedDoorState(panel, false)) + this.setActiveCabinetPanel(null) + } return shouldOpen } @@ -1866,36 +2159,75 @@ export default class ThreeMap { this.controls.minDistance = this.overviewView.minDistance this.controls.maxDistance = this.overviewView.maxDistance this.moveCameraTo(this.overviewView.position, this.overviewView.target, 760) + this.setActiveCabinetPanel(null) this.viewMode = 'overview' return true } - setAisleView() { + /** 根据机柜排之间的实际空隙选择最宽的可行走过道。 */ + getPreferredCabinetAisle() { + if (this.cabinetRowPairs.length < 2) { + return null + } + const aisleList = [] + for (let index = 0; index < this.cabinetRowPairs.length - 1; index += 1) { + const leftRow = this.cabinetRowPairs[index] + const rightRow = this.cabinetRowPairs[index + 1] + const leftX = leftRow.rightLevel.centerX + const rightX = rightRow.leftLevel.centerX + aisleList.push({ + leftX: leftX, + rightX: rightX, + width: rightX - leftX, + panels: leftRow.rightLevel.panels.concat(rightRow.leftLevel.panels), + }) + } + return aisleList.sort((a, b) => b.width - a.width)[0] || null + } + + /** 切换到最宽过道,并斜向观察指定一侧的机柜门。 */ + setAisleView(viewSide) { if (!this.importedModel || !this.camera || !this.camera.isPerspectiveCamera) { return false } - var box = this.importedModelBox ? this.importedModelBox.clone() : new THREE.Box3().setFromObject(this.importedModel) + var box = this.getMeshListAndBox(this.importedModel).box var size = new THREE.Vector3() var center = new THREE.Vector3() box.getSize(size) box.getCenter(center) var aisleOptions = (this.importedModelOptions && this.importedModelOptions.aisleView) || {} - var xRatio = aisleOptions.xRatio === undefined ? 0.18 : aisleOptions.xRatio - var startZRatio = aisleOptions.startZRatio === undefined ? 0.82 : aisleOptions.startZRatio - var targetZRatio = aisleOptions.targetZRatio === undefined ? 0.28 : aisleOptions.targetZRatio - var eyeHeightRatio = aisleOptions.eyeHeightRatio === undefined ? 0.46 : aisleOptions.eyeHeightRatio + var preferredAisle = this.getPreferredCabinetAisle() + var eyeHeightRatio = aisleOptions.eyeHeightRatio === undefined ? 0.43 : aisleOptions.eyeHeightRatio var eyeY = box.min.y + size.y * eyeHeightRatio - var aisleX = center.x + size.x * xRatio - var position = new THREE.Vector3(aisleX, eyeY, box.min.z + size.z * startZRatio) - var target = new THREE.Vector3(aisleX, eyeY, box.min.z + size.z * targetZRatio) + var aisleX = preferredAisle ? (preferredAisle.leftX + preferredAisle.rightX) / 2 : center.x + var targetX = aisleX + var startZ = box.min.z + size.z * 0.82 + var targetZ = box.min.z + size.z * 0.34 - this.camera.fov = aisleOptions.fov || 58 - this.camera.near = Math.max(0.1, size.y / 2000) - this.camera.far = Math.max(size.x, size.y, size.z) * 12 + if (preferredAisle && preferredAisle.panels.length > 0) { + const doorBox = new THREE.Box3().makeEmpty() + preferredAisle.panels.forEach((panel) => { + doorBox.expandByPoint(panel.box.min) + doorBox.expandByPoint(panel.box.max) + }) + const doorSize = doorBox.getSize(new THREE.Vector3()) + startZ = Math.min(box.max.z - size.z * 0.04, doorBox.max.z + Math.max(doorSize.z / preferredAisle.panels.length, size.z * 0.06)) + targetZ = doorBox.min.z + doorSize.z * 0.44 + targetX = viewSide === 'left' ? preferredAisle.leftX : preferredAisle.rightX + } + + var position = this.getImportedModelWorldPoint(new THREE.Vector3(aisleX, eyeY, startZ)) + var target = this.getImportedModelWorldPoint(new THREE.Vector3(targetX, eyeY - size.y * 0.035, targetZ)) + const worldSize = this.importedModel.getWorldScale(new THREE.Vector3()).multiply(size) + + this.camera.fov = aisleOptions.fov || 54 + this.camera.near = Math.max(0.1, worldSize.y / 2000) + this.camera.far = Math.max(worldSize.x, worldSize.y, worldSize.z) * 12 this.camera.updateProjectionMatrix() - this.controls.minDistance = Math.max(1, size.y * 0.01) - this.controls.maxDistance = Math.max(size.x, size.z) * 2 + this.controls.minDistance = Math.max(1, worldSize.y * 0.01) + this.controls.maxDistance = Math.max(worldSize.x, worldSize.z) * 2 this.moveCameraTo(position, target, 820) + this.setActiveCabinetPanel(null) this.viewMode = 'aisle' return true } diff --git a/src/views/ops/pages/hikvision-video-test/index.vue b/src/views/ops/pages/hikvision-video-test/index.vue new file mode 100644 index 0000000..595c314 --- /dev/null +++ b/src/views/ops/pages/hikvision-video-test/index.vue @@ -0,0 +1,718 @@ + + + + + + +