fix(3d)
This commit is contained in:
@@ -81,7 +81,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { fetchAssetDetail } from '@/api/ops/asset'
|
||||
import type { ThreeMachineRoomDevice } from '@/api/ops/three-machine-room'
|
||||
import {
|
||||
destroyHikvisionPlayer,
|
||||
@@ -91,14 +90,19 @@ import {
|
||||
stopHikvisionPreview,
|
||||
type HikvisionPreviewConfig,
|
||||
} from '@/services/hikvisionWebSdk'
|
||||
import {
|
||||
findThreeMachineRoomCameraPointByText,
|
||||
getPlayableThreeMachineRoomCameraPoints,
|
||||
getThreeMachineRoomCameraPointByIndex,
|
||||
type ThreeMachineRoomCameraPoint,
|
||||
} from '../config/camera-points'
|
||||
|
||||
/** 摄像头预览浮层属性。 */
|
||||
interface Props {
|
||||
visible: boolean
|
||||
device: ThreeMachineRoomDevice | null
|
||||
devices?: ThreeMachineRoomDevice[]
|
||||
cameraIndex?: number
|
||||
cameraCount?: number
|
||||
cameraPoint?: ThreeMachineRoomCameraPoint | null
|
||||
}
|
||||
|
||||
/** 摄像头预览浮层事件。 */
|
||||
@@ -123,7 +127,7 @@ interface CameraConnectionForm {
|
||||
}
|
||||
|
||||
const PLAYER_CONTAINER_ID = 'hikvision-camera-preview-player'
|
||||
/** 3D 预览使用已验证通过的固定 WebSDK 参数,设备身份信息由资产详情提供。 */
|
||||
/** 3D 预览使用已验证通过的固定 WebSDK 参数,设备身份信息来自固定点位表。 */
|
||||
const FIXED_CAMERA_CONFIG: Omit<CameraConnectionForm, 'host' | 'username' | 'password'> = {
|
||||
protocol: 2,
|
||||
httpPort: 443,
|
||||
@@ -161,22 +165,27 @@ let dragPointerId: number | null = null
|
||||
let dragOffsetX = 0
|
||||
let dragOffsetY = 0
|
||||
|
||||
const selectedCameraSlot = computed(() => {
|
||||
if (props.cameraIndex) return props.cameraIndex
|
||||
const selectedAssetId = Number(props.device?.asset_id)
|
||||
const deviceIndex = (props.devices || []).findIndex((device) => Number(device.asset_id) === selectedAssetId)
|
||||
return deviceIndex >= 0 ? deviceIndex + 1 : 1
|
||||
})
|
||||
const previewCount = computed(() =>
|
||||
Math.min(16, Math.max(1, Number(props.cameraCount) || 0, props.devices?.length || 0, selectedCameraSlot.value))
|
||||
const selectedFixedCameraPoint = computed(
|
||||
() => props.cameraPoint || getThreeMachineRoomCameraPointByIndex(props.cameraIndex) || getCameraPointByDevice(props.device)
|
||||
)
|
||||
const previewCameraPoints = computed(() => {
|
||||
const selectedPoint = selectedFixedCameraPoint.value
|
||||
if (selectedPoint) return selectedPoint.playable && selectedPoint.brand === 'hikvision' ? [selectedPoint] : []
|
||||
return getPlayableThreeMachineRoomCameraPoints()
|
||||
})
|
||||
const previewCount = computed(() => Math.min(16, Math.max(1, previewCameraPoints.value.length)))
|
||||
const splitName = computed(() => {
|
||||
if (previewCount.value <= 1) return '单画面'
|
||||
if (previewCount.value <= 4) return '四宫格'
|
||||
if (previewCount.value <= 9) return '九宫格'
|
||||
return '十六宫格'
|
||||
})
|
||||
const floatingTitle = computed(() => `监控预览 · ${splitName.value}(${previewCount.value} 路)· 当前摄像头 ${selectedCameraSlot.value}`)
|
||||
const currentCameraLabel = computed(() => {
|
||||
const selectedPoint = selectedFixedCameraPoint.value
|
||||
if (!selectedPoint) return `海康摄像头(${previewCameraPoints.value.length} 路)`
|
||||
return `${selectedPoint.name} · ${selectedPoint.host}`
|
||||
})
|
||||
const floatingTitle = computed(() => `监控预览 · ${splitName.value} · ${currentCameraLabel.value}`)
|
||||
const floatingLayerDimensions = computed(() => {
|
||||
const preferred =
|
||||
previewCount.value <= 1
|
||||
@@ -202,22 +211,6 @@ const developmentProxyTip = computed(() => {
|
||||
return '开发环境尚未配置海康代理;请配置 VITE_HIKVISION_PROXY_TARGET,或切换为直连。'
|
||||
})
|
||||
|
||||
/** 将未知值转换为对象。 */
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : null
|
||||
}
|
||||
|
||||
/** 从若干配置对象中读取第一个有效字段。 */
|
||||
function readConfigValue(records: Record<string, unknown>[], keys: string[]): unknown {
|
||||
for (const record of records) {
|
||||
for (const key of keys) {
|
||||
const value = record[key]
|
||||
if (value !== undefined && value !== null && value !== '') return value
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 校验浮层设备与固定开发代理是否为同一目标,避免凭据被发往错误设备。 */
|
||||
function matchesDevelopmentProxyTarget(connection: CameraConnectionForm): boolean {
|
||||
if (!import.meta.env.DEV || !connection.proxyEnabled || !developmentProxyConfigured) return true
|
||||
@@ -233,87 +226,25 @@ function matchesDevelopmentProxyTarget(connection: CameraConnectionForm): boolea
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析资产 source_address 中的设备地址与登录凭据。 */
|
||||
function parseSourceAddress(sourceAddress: unknown): Record<string, unknown> {
|
||||
if (typeof sourceAddress !== 'string' || !sourceAddress.trim()) return {}
|
||||
const source = sourceAddress.trim()
|
||||
if (source.startsWith('{')) {
|
||||
try {
|
||||
return asRecord(JSON.parse(source)) || {}
|
||||
} catch (_error) {
|
||||
return { host: source }
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const sourceUrl = new URL(source.includes('://') ? source : `http://${source}`)
|
||||
return {
|
||||
host: sourceUrl.hostname,
|
||||
username: sourceUrl.username ? decodeURIComponent(sourceUrl.username) : undefined,
|
||||
password: sourceUrl.password ? decodeURIComponent(sourceUrl.password) : undefined,
|
||||
}
|
||||
} catch (_error) {
|
||||
return { host: source }
|
||||
}
|
||||
/** 从资产名称或编码匹配固定摄像头点位。 */
|
||||
function getCameraPointByDevice(device: ThreeMachineRoomDevice | null): ThreeMachineRoomCameraPoint | null {
|
||||
if (!device) return null
|
||||
return findThreeMachineRoomCameraPointByText(device.asset_name, device.asset_code, device.category_name, device.category_code)
|
||||
}
|
||||
|
||||
/** 只将资产详情中的 IP、用户名和密码映射到固定 WebSDK 参数。 */
|
||||
function applyAssetCameraConfig(asset: Record<string, unknown>): CameraConnectionForm {
|
||||
const sourceConfig = parseSourceAddress(asset.source_address)
|
||||
const nestedConfig =
|
||||
asRecord(asset.camera_config) || asRecord(asset.video_config) || asRecord(asset.stream_config) || asRecord(asset.hikvision_config)
|
||||
const records = [nestedConfig, asset, sourceConfig].filter((item): item is Record<string, unknown> => Boolean(item))
|
||||
/** 将固定摄像头点位转换为海康 WebSDK 连接参数。 */
|
||||
function createCameraConnection(point: ThreeMachineRoomCameraPoint): CameraConnectionForm {
|
||||
return {
|
||||
host: String(readConfigValue(records, ['camera_host', 'host', 'ip_address', 'ip', 'device_ip', 'device_address']) || '').trim(),
|
||||
username: String(readConfigValue(records, ['camera_username', 'username', 'user', 'login_username', 'login_name']) || '').trim(),
|
||||
password: String(readConfigValue(records, ['camera_password', 'password', 'login_password']) || ''),
|
||||
host: point.host,
|
||||
username: point.username,
|
||||
password: point.password,
|
||||
...FIXED_CAMERA_CONFIG,
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取单个资产详情中的摄像头连接配置。 */
|
||||
async function loadAssetCameraConfig(device: ThreeMachineRoomDevice): Promise<CameraConnectionForm> {
|
||||
if (!device.asset_id) throw new Error('摄像头未关联资产')
|
||||
const response = (await fetchAssetDetail(device.asset_id)) as unknown
|
||||
const responseRecord = asRecord(response)
|
||||
if (!responseRecord || Number(responseRecord.code) !== 0) {
|
||||
throw new Error(String(responseRecord?.message || '摄像头资产详情获取失败'))
|
||||
}
|
||||
const details = asRecord(responseRecord.details)
|
||||
if (!details) throw new Error('摄像头资产详情格式错误')
|
||||
const connection = applyAssetCameraConfig(details)
|
||||
if (!connection.host || !connection.username || !connection.password) throw new Error('摄像头资产详情缺少 IP、用户名或密码')
|
||||
return connection
|
||||
}
|
||||
|
||||
/** 返回指定宫格位置对应的摄像头资产。 */
|
||||
function getCameraDevice(cameraIndex: number): ThreeMachineRoomDevice | null {
|
||||
const listedDevice = props.devices?.[cameraIndex - 1]
|
||||
if (listedDevice) return listedDevice
|
||||
return cameraIndex === selectedCameraSlot.value ? props.device : null
|
||||
}
|
||||
|
||||
/** 并行加载全部宫格的摄像头身份信息;固定的 WebSDK 参数不依赖接口返回。 */
|
||||
/** 加载当前可播放点位的摄像头身份信息;不再依赖接口返回的 IP。 */
|
||||
async function loadPreviewConnections(): Promise<CameraConnectionForm[]> {
|
||||
let failureCount = 0
|
||||
const results = await Promise.all(
|
||||
Array.from({ length: previewCount.value }, async (_item, index) => {
|
||||
const cameraIndex = index + 1
|
||||
const device = getCameraDevice(cameraIndex)
|
||||
if (!device) return null
|
||||
try {
|
||||
return await loadAssetCameraConfig(device)
|
||||
} catch (error) {
|
||||
failureCount += 1
|
||||
console.warn(`摄像头 ${cameraIndex} 资产身份信息获取失败:`, error)
|
||||
return null
|
||||
}
|
||||
})
|
||||
)
|
||||
if (failureCount > 0) {
|
||||
Message.warning(`${failureCount} 路摄像头的 IP、用户名或密码获取失败`)
|
||||
}
|
||||
return results.filter((connection): connection is CameraConnectionForm => Boolean(connection))
|
||||
return previewCameraPoints.value.map((point) => createCameraConnection(point))
|
||||
}
|
||||
|
||||
/** 将视频浮层限制在浏览器可视区域内。 */
|
||||
@@ -392,14 +323,11 @@ async function openPreview(): Promise<void> {
|
||||
async function startPreview(): Promise<void> {
|
||||
const sequence = ++playbackSequence
|
||||
errorMessage.value = ''
|
||||
const selectedIndex = Math.min(connections.value.length - 1, Math.max(0, selectedCameraSlot.value - 1))
|
||||
const orderedConnections = [
|
||||
connections.value[selectedIndex],
|
||||
...connections.value.filter((_connection, index) => index !== selectedIndex),
|
||||
].filter((connection): connection is CameraConnectionForm => Boolean(connection))
|
||||
const previewConnections = orderedConnections.map((connection) => ({ ...connection }))
|
||||
const previewConnections = connections.value.map((connection) => ({ ...connection }))
|
||||
if (!previewConnections.length) {
|
||||
errorMessage.value = '没有可播放的摄像头'
|
||||
const selectedPoint = selectedFixedCameraPoint.value
|
||||
errorMessage.value =
|
||||
selectedPoint && selectedPoint.brand === 'huawei' ? '华为摄像头暂未接入,当前仅支持海康视频查看' : '没有可播放的海康摄像头'
|
||||
return
|
||||
}
|
||||
if (previewConnections.some((connection) => !connection.host || !connection.username || !connection.password)) {
|
||||
@@ -505,9 +433,10 @@ watch(
|
||||
[
|
||||
() => props.visible,
|
||||
() => props.device?.asset_id,
|
||||
() => props.device?.asset_name,
|
||||
() => props.device?.asset_code,
|
||||
() => props.cameraIndex,
|
||||
() => props.cameraCount,
|
||||
() => (props.devices || []).map((device) => device.asset_id).join(','),
|
||||
() => props.cameraPoint?.index,
|
||||
],
|
||||
([visible]) => {
|
||||
if (visible) void openPreview()
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/** 3D 机房摄像头品牌。 */
|
||||
export type ThreeMachineRoomCameraBrand = 'hikvision' | 'huawei'
|
||||
|
||||
/** 3D 机房模型局部坐标。 */
|
||||
export interface ThreeMachineRoomCameraPosition {
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
}
|
||||
|
||||
/** 3D 机房固定摄像头点位。 */
|
||||
export interface ThreeMachineRoomCameraPoint {
|
||||
index: number
|
||||
name: string
|
||||
shortName: string
|
||||
brand: ThreeMachineRoomCameraBrand
|
||||
host: string
|
||||
username: string
|
||||
password: string
|
||||
modelMeshName?: string
|
||||
cloneFromMeshName?: string
|
||||
position?: ThreeMachineRoomCameraPosition
|
||||
playable: boolean
|
||||
}
|
||||
|
||||
const CAMERA_USERNAME = 'admin'
|
||||
const CAMERA_PASSWORD = 'Xzrmyy@12'
|
||||
const CAMERA_LOCAL_Y = 2717.48
|
||||
|
||||
/** 3D 模型固定摄像头点位,顺序与现场摄像头清单保持一致。 */
|
||||
export const THREE_MACHINE_ROOM_CAMERA_POINTS: ThreeMachineRoomCameraPoint[] = [
|
||||
{
|
||||
index: 1,
|
||||
name: '单通道出口',
|
||||
shortName: '单出',
|
||||
brand: 'hikvision',
|
||||
host: '192.168.101.200',
|
||||
username: CAMERA_USERNAME,
|
||||
password: CAMERA_PASSWORD,
|
||||
modelMeshName: 'Obj3d66_8099876_1_913',
|
||||
playable: true,
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
name: '单通道入口',
|
||||
shortName: '单入',
|
||||
brand: 'hikvision',
|
||||
host: '192.168.101.201',
|
||||
username: CAMERA_USERNAME,
|
||||
password: CAMERA_PASSWORD,
|
||||
modelMeshName: 'Obj3d66_8099876_1_916',
|
||||
playable: true,
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
name: '电池间入口',
|
||||
shortName: '电入',
|
||||
brand: 'hikvision',
|
||||
host: '192.168.101.202',
|
||||
username: CAMERA_USERNAME,
|
||||
password: CAMERA_PASSWORD,
|
||||
modelMeshName: 'Obj3d66_8099876_1_918',
|
||||
playable: true,
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
name: '双通道出口',
|
||||
shortName: '双出',
|
||||
brand: 'hikvision',
|
||||
host: '192.168.101.203',
|
||||
username: CAMERA_USERNAME,
|
||||
password: CAMERA_PASSWORD,
|
||||
modelMeshName: 'Obj3d66_8099876_1_914',
|
||||
playable: true,
|
||||
},
|
||||
{
|
||||
index: 5,
|
||||
name: '双通道入口',
|
||||
shortName: '双入',
|
||||
brand: 'hikvision',
|
||||
host: '192.168.101.204',
|
||||
username: CAMERA_USERNAME,
|
||||
password: CAMERA_PASSWORD,
|
||||
modelMeshName: 'Obj3d66_8099876_1_915',
|
||||
playable: true,
|
||||
},
|
||||
{
|
||||
index: 6,
|
||||
name: '电池间内部',
|
||||
shortName: '电内',
|
||||
brand: 'hikvision',
|
||||
host: '192.168.101.205',
|
||||
username: CAMERA_USERNAME,
|
||||
password: CAMERA_PASSWORD,
|
||||
modelMeshName: 'Obj3d66_8099876_1_917',
|
||||
playable: true,
|
||||
},
|
||||
{
|
||||
index: 7,
|
||||
name: '单通道内入口',
|
||||
shortName: '单内入',
|
||||
brand: 'huawei',
|
||||
host: '192.168.101.206',
|
||||
username: CAMERA_USERNAME,
|
||||
password: CAMERA_PASSWORD,
|
||||
cloneFromMeshName: 'Obj3d66_8099876_1_916',
|
||||
position: { x: -25050, y: CAMERA_LOCAL_Y, z: 1200 },
|
||||
playable: false,
|
||||
},
|
||||
{
|
||||
index: 8,
|
||||
name: '单通道内出口',
|
||||
shortName: '单内出',
|
||||
brand: 'huawei',
|
||||
host: '192.168.101.207',
|
||||
username: CAMERA_USERNAME,
|
||||
password: CAMERA_PASSWORD,
|
||||
cloneFromMeshName: 'Obj3d66_8099876_1_913',
|
||||
position: { x: -23650, y: CAMERA_LOCAL_Y, z: -3250 },
|
||||
playable: false,
|
||||
},
|
||||
{
|
||||
index: 9,
|
||||
name: '双通道内入口',
|
||||
shortName: '双内入',
|
||||
brand: 'huawei',
|
||||
host: '192.168.101.208',
|
||||
username: CAMERA_USERNAME,
|
||||
password: CAMERA_PASSWORD,
|
||||
cloneFromMeshName: 'Obj3d66_8099876_1_915',
|
||||
position: { x: -19550, y: CAMERA_LOCAL_Y, z: 1650 },
|
||||
playable: false,
|
||||
},
|
||||
{
|
||||
index: 10,
|
||||
name: '双通道内出口',
|
||||
shortName: '双内出',
|
||||
brand: 'huawei',
|
||||
host: '192.168.101.209',
|
||||
username: CAMERA_USERNAME,
|
||||
password: CAMERA_PASSWORD,
|
||||
cloneFromMeshName: 'Obj3d66_8099876_1_914',
|
||||
position: { x: -21200, y: CAMERA_LOCAL_Y, z: -3000 },
|
||||
playable: false,
|
||||
},
|
||||
]
|
||||
|
||||
const CAMERA_POINTS_BY_INDEX = new Map(THREE_MACHINE_ROOM_CAMERA_POINTS.map((point) => [point.index, point]))
|
||||
const CAMERA_POINTS_BY_MODEL_MESH = new Map(
|
||||
THREE_MACHINE_ROOM_CAMERA_POINTS.filter((point) => point.modelMeshName).map((point) => [point.modelMeshName, point])
|
||||
)
|
||||
|
||||
/** 归一化摄像头名称,用于匹配资产名称和模型点位。 */
|
||||
export function normalizeThreeMachineRoomCameraName(name: unknown): string {
|
||||
return String(name || '')
|
||||
.replace(/\s+/g, '')
|
||||
.replace(/[()()【】\[\]_-]/g, '')
|
||||
.replace(/摄像头|摄像机|监控/g, '')
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
/** 按点位序号获取固定摄像头配置。 */
|
||||
export function getThreeMachineRoomCameraPointByIndex(index: number | undefined): ThreeMachineRoomCameraPoint | null {
|
||||
if (!Number.isInteger(index)) return null
|
||||
return CAMERA_POINTS_BY_INDEX.get(Number(index)) || null
|
||||
}
|
||||
|
||||
/** 按导入模型网格名称获取固定摄像头配置。 */
|
||||
export function getThreeMachineRoomCameraPointByModelMeshName(meshName: string): ThreeMachineRoomCameraPoint | null {
|
||||
return CAMERA_POINTS_BY_MODEL_MESH.get(meshName) || null
|
||||
}
|
||||
|
||||
/** 返回当前阶段可播放的海康摄像头点位。 */
|
||||
export function getPlayableThreeMachineRoomCameraPoints(): ThreeMachineRoomCameraPoint[] {
|
||||
return THREE_MACHINE_ROOM_CAMERA_POINTS.filter((point) => point.playable && point.brand === 'hikvision')
|
||||
}
|
||||
|
||||
/** 按资产名称或编码匹配固定摄像头点位。 */
|
||||
export function findThreeMachineRoomCameraPointByText(...values: unknown[]): ThreeMachineRoomCameraPoint | null {
|
||||
const normalizedValues = values.map((value) => normalizeThreeMachineRoomCameraName(value)).filter(Boolean)
|
||||
if (!normalizedValues.length) return null
|
||||
return (
|
||||
THREE_MACHINE_ROOM_CAMERA_POINTS.find((point) => {
|
||||
const pointNames = [point.name, point.shortName].map((value) => normalizeThreeMachineRoomCameraName(value))
|
||||
return normalizedValues.some((value) => pointNames.some((pointName) => value.includes(pointName) || pointName.includes(value)))
|
||||
}) || null
|
||||
)
|
||||
}
|
||||
@@ -55,7 +55,7 @@
|
||||
{{
|
||||
insideAisle
|
||||
? '当前为机柜过道第一视角;点击设备可近距离查看,双击柜内设备可抽出或归位'
|
||||
: '双击门板可开合;机柜开门后双击设备可抽出或归位,点击摄像头播放监控、长按可拖动'
|
||||
: '双击门板可开合;机柜开门后双击设备可抽出或归位,点击海康摄像头播放监控、长按可拖动'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
@@ -128,9 +128,8 @@
|
||||
<camera-preview-dialog
|
||||
v-model:visible="cameraPreviewVisible"
|
||||
:device="selectedDevice"
|
||||
:devices="sceneCameraDevices"
|
||||
:camera-index="selectedCameraIndex"
|
||||
:camera-count="sceneSummary.importedCameraCount"
|
||||
:camera-point="selectedCameraPoint"
|
||||
@manage="handleOpenDeviceManagement"
|
||||
/>
|
||||
<device-management-dialog
|
||||
@@ -171,6 +170,11 @@ import {
|
||||
import { getMockDeviceObservability, MOCK_ROOM_SCENE, MOCK_ROOM_SIGNALS } from './scene/MockThreeMachineRoom'
|
||||
import ThreeMap from './scene/ThreeMap'
|
||||
import { ThreeData } from './scene/ThreeData'
|
||||
import {
|
||||
findThreeMachineRoomCameraPointByText,
|
||||
getThreeMachineRoomCameraPointByIndex,
|
||||
type ThreeMachineRoomCameraPoint,
|
||||
} from './config/camera-points'
|
||||
|
||||
/** 场景渲染摘要。 */
|
||||
interface SceneSummary {
|
||||
@@ -200,6 +204,10 @@ interface DeviceAlertCalloutContext {
|
||||
interface ImportedCameraContext {
|
||||
index: number
|
||||
name: string
|
||||
shortName?: string
|
||||
brand?: string
|
||||
playable?: boolean
|
||||
host?: string
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
@@ -226,6 +234,7 @@ const roomScene = ref<ThreeMachineRoomScene | null>(null)
|
||||
const selectedRack = ref<ThreeMachineRoomRack | null>(null)
|
||||
const selectedDevice = ref<ThreeMachineRoomDevice | null>(null)
|
||||
const selectedCameraIndex = ref<number>()
|
||||
const selectedCameraPoint = ref<ThreeMachineRoomCameraPoint | null>(null)
|
||||
const activeRack = ref<ThreeMachineRoomRack | null>(null)
|
||||
const rackPlacementVisible = ref(false)
|
||||
const rackLayoutVisible = ref(false)
|
||||
@@ -342,11 +351,6 @@ async function loadRoomScene(): Promise<boolean> {
|
||||
usingMockData.value = false
|
||||
roomScene.value = scene
|
||||
machineRoomMap?.setRoomScene(scene)
|
||||
const roomConfigured = [scene.room.scene_length, scene.room.scene_width, scene.room.scene_height].every((value) => Number(value) > 0)
|
||||
if (!roomConfigured) {
|
||||
sceneConfigVisible.value = true
|
||||
Message.info('该机房尚未完成 3D 初始化,请先配置机房尺寸和机柜位置')
|
||||
}
|
||||
await loadRoomSignals(true)
|
||||
return true
|
||||
} catch (error) {
|
||||
@@ -455,12 +459,36 @@ function getSceneCameraDevices(): ThreeMachineRoomDevice[] {
|
||||
return Array.from(deviceMap.values()).sort((left, right) => Number(left.asset_id) - Number(right.asset_id))
|
||||
}
|
||||
|
||||
/** 点击模型内置摄像头时,按序关联场景摄像头资产并打开对应通道。 */
|
||||
/** 从资产名称或编码匹配固定摄像头点位。 */
|
||||
function getCameraPointByDevice(device: ThreeMachineRoomDevice | null): ThreeMachineRoomCameraPoint | null {
|
||||
if (!device) return null
|
||||
return findThreeMachineRoomCameraPointByText(device.asset_name, device.asset_code, device.category_name, device.category_code)
|
||||
}
|
||||
|
||||
/** 根据固定点位匹配接口中的摄像头资产,用于从视频浮层进入设备管理。 */
|
||||
function getCameraDeviceByPoint(point: ThreeMachineRoomCameraPoint | null): ThreeMachineRoomDevice | null {
|
||||
if (!point) return null
|
||||
return sceneCameraDevices.value.find((device) => getCameraPointByDevice(device)?.index === point.index) || null
|
||||
}
|
||||
|
||||
/** 判断固定点位是否可播放,华为摄像头现阶段只展示点位。 */
|
||||
function isPlayableCameraPoint(point: ThreeMachineRoomCameraPoint | null): boolean {
|
||||
return Boolean(point && point.playable && point.brand === 'hikvision')
|
||||
}
|
||||
|
||||
/** 点击模型内置摄像头时,使用前端固定点位打开对应海康通道。 */
|
||||
function handleImportedCameraSelected(camera: ImportedCameraContext): void {
|
||||
const cameraIndex = Number(camera.index)
|
||||
if (!Number.isInteger(cameraIndex) || cameraIndex <= 0) return
|
||||
const cameraPoint = getThreeMachineRoomCameraPointByIndex(cameraIndex)
|
||||
selectedCameraIndex.value = cameraIndex
|
||||
selectedDevice.value = sceneCameraDevices.value[cameraIndex - 1] || null
|
||||
selectedCameraPoint.value = cameraPoint
|
||||
selectedDevice.value = getCameraDeviceByPoint(cameraPoint)
|
||||
if (!cameraPoint || !isPlayableCameraPoint(cameraPoint)) {
|
||||
cameraPreviewVisible.value = false
|
||||
Message.info(`${cameraPoint?.name || camera.name || '该'}摄像头暂未接入,当前仅支持海康视频查看`)
|
||||
return
|
||||
}
|
||||
cameraPreviewVisible.value = true
|
||||
}
|
||||
|
||||
@@ -468,10 +496,19 @@ function handleImportedCameraSelected(camera: ImportedCameraContext): void {
|
||||
function handleDeviceSelected(device: ThreeMachineRoomDevice): void {
|
||||
selectedDevice.value = device
|
||||
if (isCameraDevice(device)) {
|
||||
selectedCameraIndex.value = undefined
|
||||
const cameraPoint = getCameraPointByDevice(device)
|
||||
selectedCameraIndex.value = cameraPoint?.index
|
||||
selectedCameraPoint.value = cameraPoint
|
||||
if (!cameraPoint || !isPlayableCameraPoint(cameraPoint)) {
|
||||
cameraPreviewVisible.value = false
|
||||
Message.info(`${cameraPoint?.name || device.asset_name || '该'}摄像头暂未接入,当前仅支持海康视频查看`)
|
||||
return
|
||||
}
|
||||
cameraPreviewVisible.value = true
|
||||
return
|
||||
}
|
||||
selectedCameraIndex.value = undefined
|
||||
selectedCameraPoint.value = null
|
||||
if (usingMockData.value) {
|
||||
void loadDeviceObservability(device)
|
||||
Message.warning('演示数据不支持资源绑定和位置管理')
|
||||
|
||||
@@ -11,10 +11,10 @@ import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
|
||||
import { TGALoader } from 'three/examples/jsm/loaders/TGALoader'
|
||||
import TWEEN from '@tweenjs/tween.js'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' //r100及以上
|
||||
import { THREE_MACHINE_ROOM_CAMERA_POINTS, getThreeMachineRoomCameraPointByModelMeshName } from '../config/camera-points'
|
||||
|
||||
const ROOM_DEVICE_LONG_PRESS_DURATION = 300
|
||||
const ROOM_DEVICE_DRAG_MOVE_THRESHOLD = 6
|
||||
const IMPORTED_CAMERA_NAME_PATTERN = /^Obj3d66_8099876_1_91[3-8]$/
|
||||
const VIEW_OCCLUSION_UPDATE_INTERVAL = 60
|
||||
const VIEW_OCCLUDER_OPACITY = 0.055
|
||||
const RACK_EQUIPMENT_PULL_OUT_RATIO = 0.58
|
||||
@@ -2182,10 +2182,15 @@ export default class ThreeMap {
|
||||
const isClick = pressDuration < ROOM_DEVICE_LONG_PRESS_DURATION && drag.maxDistance < ROOM_DEVICE_DRAG_MOVE_THRESHOLD
|
||||
if (isClick) {
|
||||
if (drag.equipment.userData.importedCamera) {
|
||||
const cameraPoint = drag.equipment.userData.importedCameraPoint || null
|
||||
if (typeof this.props.onImportedCameraSelected === 'function') {
|
||||
this.props.onImportedCameraSelected({
|
||||
index: drag.equipment.userData.importedCameraIndex,
|
||||
name: drag.equipment.name,
|
||||
name: cameraPoint ? cameraPoint.name : drag.equipment.name,
|
||||
shortName: cameraPoint ? cameraPoint.shortName : '',
|
||||
brand: cameraPoint ? cameraPoint.brand : '',
|
||||
playable: cameraPoint ? cameraPoint.playable : false,
|
||||
host: cameraPoint ? cameraPoint.host : '',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
@@ -2197,10 +2202,14 @@ export default class ThreeMap {
|
||||
|
||||
const equipment = drag.equipment
|
||||
if (equipment.userData.importedCamera) {
|
||||
const cameraPoint = equipment.userData.importedCameraPoint || null
|
||||
if (typeof this.props.onImportedCameraMoved === 'function') {
|
||||
this.props.onImportedCameraMoved({
|
||||
index: equipment.userData.importedCameraIndex,
|
||||
name: equipment.name,
|
||||
name: cameraPoint ? cameraPoint.name : equipment.name,
|
||||
shortName: cameraPoint ? cameraPoint.shortName : '',
|
||||
brand: cameraPoint ? cameraPoint.brand : '',
|
||||
playable: cameraPoint ? cameraPoint.playable : false,
|
||||
position: {
|
||||
x: equipment.position.x,
|
||||
y: equipment.position.y,
|
||||
@@ -2363,30 +2372,40 @@ export default class ThreeMap {
|
||||
})
|
||||
}
|
||||
|
||||
/** 将模型内置的 6 个摄像头网格转换为可独立拖动的对象。 */
|
||||
prepareImportedCameras(meshEntries, modelBox) {
|
||||
this.importedCameras = meshEntries
|
||||
.filter((entry) => IMPORTED_CAMERA_NAME_PATTERN.test(entry.mesh.name))
|
||||
.sort((a, b) => a.mesh.name.localeCompare(b.mesh.name))
|
||||
.map((entry, index) => {
|
||||
const mesh = entry.mesh
|
||||
const geometryCenter = entry.box.getCenter(new THREE.Vector3())
|
||||
/** 标记一个导入摄像头网格,并补充点击热区和固定点位信息。 */
|
||||
prepareImportedCameraMesh(mesh, point, modelBox, geometryBox) {
|
||||
if (!mesh || !point) {
|
||||
return null
|
||||
}
|
||||
if (geometryBox) {
|
||||
const geometryCenter = geometryBox.getCenter(new THREE.Vector3())
|
||||
const geometry = mesh.geometry.clone()
|
||||
geometry.translate(-geometryCenter.x, -geometryCenter.y, -geometryCenter.z)
|
||||
geometry.computeBoundingBox()
|
||||
geometry.computeBoundingSphere()
|
||||
mesh.geometry = geometry
|
||||
mesh.position.add(geometryCenter)
|
||||
const cameraSize = geometry.boundingBox.getSize(new THREE.Vector3())
|
||||
} else if (mesh.geometry) {
|
||||
if (!mesh.geometry.boundingBox) mesh.geometry.computeBoundingBox()
|
||||
if (!mesh.geometry.boundingSphere) mesh.geometry.computeBoundingSphere()
|
||||
}
|
||||
|
||||
const cameraBox = this.getGeometryBox(mesh)
|
||||
const cameraSize = cameraBox ? cameraBox.getSize(new THREE.Vector3()) : new THREE.Vector3(120, 120, 120)
|
||||
const hitRadius = Math.max(cameraSize.x, cameraSize.y, cameraSize.z) * 0.85
|
||||
const hitArea = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(hitRadius, 12, 8),
|
||||
new THREE.MeshBasicMaterial({ transparent: true, opacity: 0, depthWrite: false })
|
||||
)
|
||||
hitArea.name = 'imported_camera_hit_area_' + (index + 1)
|
||||
hitArea.name = 'imported_camera_hit_area_' + point.index
|
||||
mesh.add(hitArea)
|
||||
mesh.userData.importedCamera = true
|
||||
mesh.userData.importedCameraIndex = index + 1
|
||||
mesh.userData.importedCameraIndex = point.index
|
||||
mesh.userData.importedCameraName = point.name
|
||||
mesh.userData.importedCameraShortName = point.shortName
|
||||
mesh.userData.importedCameraBrand = point.brand
|
||||
mesh.userData.importedCameraPlayable = point.playable
|
||||
mesh.userData.importedCameraPoint = point
|
||||
mesh.userData.placementType = 'imported-camera'
|
||||
mesh.userData.draggable = true
|
||||
mesh.userData.roomPlacement = {
|
||||
@@ -2394,7 +2413,48 @@ export default class ThreeMap {
|
||||
}
|
||||
mesh.userData.originalModelPosition = mesh.position.clone()
|
||||
return mesh
|
||||
}
|
||||
|
||||
/** 克隆现有摄像头网格,补齐模型缺失的通道内摄像头。 */
|
||||
createSupplementalImportedCamera(point, sourceMesh, modelBox) {
|
||||
if (!point || !point.position || !sourceMesh || !sourceMesh.parent) {
|
||||
return null
|
||||
}
|
||||
const mesh = sourceMesh.clone(false)
|
||||
mesh.name = 'supplemental_imported_camera_' + point.index
|
||||
mesh.geometry = sourceMesh.geometry
|
||||
mesh.material = sourceMesh.material
|
||||
mesh.position.set(point.position.x, point.position.y, point.position.z)
|
||||
mesh.rotation.copy(sourceMesh.rotation)
|
||||
mesh.scale.copy(sourceMesh.scale)
|
||||
sourceMesh.parent.add(mesh)
|
||||
return this.prepareImportedCameraMesh(mesh, point, modelBox, null)
|
||||
}
|
||||
|
||||
/** 将固定摄像头清单映射到模型点位,并补齐 4 个通道内摄像头。 */
|
||||
prepareImportedCameras(meshEntries, modelBox) {
|
||||
const sourceMeshes = new Map()
|
||||
const importedCameras = meshEntries
|
||||
.map((entry) => {
|
||||
const point = getThreeMachineRoomCameraPointByModelMeshName(entry.mesh.name)
|
||||
return point ? { entry: entry, point: point } : null
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.point.index - b.point.index)
|
||||
.map(({ entry, point }) => {
|
||||
const mesh = this.prepareImportedCameraMesh(entry.mesh, point, modelBox, entry.box)
|
||||
if (mesh) sourceMeshes.set(entry.mesh.name, mesh)
|
||||
return mesh
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
THREE_MACHINE_ROOM_CAMERA_POINTS.filter((point) => point.position && point.cloneFromMeshName).forEach((point) => {
|
||||
const sourceMesh = sourceMeshes.get(point.cloneFromMeshName) || importedCameras[0]
|
||||
const mesh = this.createSupplementalImportedCamera(point, sourceMesh, modelBox)
|
||||
if (mesh) importedCameras.push(mesh)
|
||||
})
|
||||
|
||||
this.importedCameras = importedCameras.sort((a, b) => a.userData.importedCameraIndex - b.userData.importedCameraIndex)
|
||||
return this.importedCameras
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user