This commit is contained in:
zxr
2026-09-16 13:59:24 +08:00
4 changed files with 368 additions and 154 deletions

View File

@@ -81,7 +81,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue' import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue' import { Message } from '@arco-design/web-vue'
import { fetchAssetDetail } from '@/api/ops/asset'
import type { ThreeMachineRoomDevice } from '@/api/ops/three-machine-room' import type { ThreeMachineRoomDevice } from '@/api/ops/three-machine-room'
import { import {
destroyHikvisionPlayer, destroyHikvisionPlayer,
@@ -91,14 +90,19 @@ import {
stopHikvisionPreview, stopHikvisionPreview,
type HikvisionPreviewConfig, type HikvisionPreviewConfig,
} from '@/services/hikvisionWebSdk' } from '@/services/hikvisionWebSdk'
import {
findThreeMachineRoomCameraPointByText,
getPlayableThreeMachineRoomCameraPoints,
getThreeMachineRoomCameraPointByIndex,
type ThreeMachineRoomCameraPoint,
} from '../config/camera-points'
/** 摄像头预览浮层属性。 */ /** 摄像头预览浮层属性。 */
interface Props { interface Props {
visible: boolean visible: boolean
device: ThreeMachineRoomDevice | null device: ThreeMachineRoomDevice | null
devices?: ThreeMachineRoomDevice[]
cameraIndex?: number cameraIndex?: number
cameraCount?: number cameraPoint?: ThreeMachineRoomCameraPoint | null
} }
/** 摄像头预览浮层事件。 */ /** 摄像头预览浮层事件。 */
@@ -123,7 +127,7 @@ interface CameraConnectionForm {
} }
const PLAYER_CONTAINER_ID = 'hikvision-camera-preview-player' const PLAYER_CONTAINER_ID = 'hikvision-camera-preview-player'
/** 3D 预览使用已验证通过的固定 WebSDK 参数,设备身份信息由资产详情提供。 */ /** 3D 预览使用已验证通过的固定 WebSDK 参数,设备身份信息来自固定点位表。 */
const FIXED_CAMERA_CONFIG: Omit<CameraConnectionForm, 'host' | 'username' | 'password'> = { const FIXED_CAMERA_CONFIG: Omit<CameraConnectionForm, 'host' | 'username' | 'password'> = {
protocol: 2, protocol: 2,
httpPort: 443, httpPort: 443,
@@ -161,22 +165,27 @@ let dragPointerId: number | null = null
let dragOffsetX = 0 let dragOffsetX = 0
let dragOffsetY = 0 let dragOffsetY = 0
const selectedCameraSlot = computed(() => { const selectedFixedCameraPoint = computed(
if (props.cameraIndex) return props.cameraIndex () => props.cameraPoint || getThreeMachineRoomCameraPointByIndex(props.cameraIndex) || getCameraPointByDevice(props.device)
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 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(() => { const splitName = computed(() => {
if (previewCount.value <= 1) return '单画面' if (previewCount.value <= 1) return '单画面'
if (previewCount.value <= 4) return '四宫格' if (previewCount.value <= 4) return '四宫格'
if (previewCount.value <= 9) return '九宫格' if (previewCount.value <= 9) return '九宫格'
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 floatingLayerDimensions = computed(() => {
const preferred = const preferred =
previewCount.value <= 1 previewCount.value <= 1
@@ -202,22 +211,6 @@ const developmentProxyTip = computed(() => {
return '开发环境尚未配置海康代理;请配置 VITE_HIKVISION_PROXY_TARGET,或切换为直连。' 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 { function matchesDevelopmentProxyTarget(connection: CameraConnectionForm): boolean {
if (!import.meta.env.DEV || !connection.proxyEnabled || !developmentProxyConfigured) return true 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> { function getCameraPointByDevice(device: ThreeMachineRoomDevice | null): ThreeMachineRoomCameraPoint | null {
if (typeof sourceAddress !== 'string' || !sourceAddress.trim()) return {} if (!device) return null
const source = sourceAddress.trim() return findThreeMachineRoomCameraPointByText(device.asset_name, device.asset_code, device.category_name, device.category_code)
if (source.startsWith('{')) {
try {
return asRecord(JSON.parse(source)) || {}
} catch (_error) {
return { host: source }
}
} }
try { /** 将固定摄像头点位转换为海康 WebSDK 连接参数。 */
const sourceUrl = new URL(source.includes('://') ? source : `http://${source}`) function createCameraConnection(point: ThreeMachineRoomCameraPoint): CameraConnectionForm {
return { return {
host: sourceUrl.hostname, host: point.host,
username: sourceUrl.username ? decodeURIComponent(sourceUrl.username) : undefined, username: point.username,
password: sourceUrl.password ? decodeURIComponent(sourceUrl.password) : undefined, password: point.password,
}
} catch (_error) {
return { host: source }
}
}
/** 只将资产详情中的 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))
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']) || ''),
...FIXED_CAMERA_CONFIG, ...FIXED_CAMERA_CONFIG,
} }
} }
/** 获取单个资产详情中的摄像头连接配置。 */ /** 加载当前可播放点位的摄像头身份信息;不再依赖接口返回的 IP。 */
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 参数不依赖接口返回。 */
async function loadPreviewConnections(): Promise<CameraConnectionForm[]> { async function loadPreviewConnections(): Promise<CameraConnectionForm[]> {
let failureCount = 0 return previewCameraPoints.value.map((point) => createCameraConnection(point))
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))
} }
/** 将视频浮层限制在浏览器可视区域内。 */ /** 将视频浮层限制在浏览器可视区域内。 */
@@ -392,14 +323,11 @@ async function openPreview(): Promise<void> {
async function startPreview(): Promise<void> { async function startPreview(): Promise<void> {
const sequence = ++playbackSequence const sequence = ++playbackSequence
errorMessage.value = '' errorMessage.value = ''
const selectedIndex = Math.min(connections.value.length - 1, Math.max(0, selectedCameraSlot.value - 1)) const previewConnections = connections.value.map((connection) => ({ ...connection }))
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 }))
if (!previewConnections.length) { if (!previewConnections.length) {
errorMessage.value = '没有可播放的摄像头' const selectedPoint = selectedFixedCameraPoint.value
errorMessage.value =
selectedPoint && selectedPoint.brand === 'huawei' ? '华为摄像头暂未接入,当前仅支持海康视频查看' : '没有可播放的海康摄像头'
return return
} }
if (previewConnections.some((connection) => !connection.host || !connection.username || !connection.password)) { if (previewConnections.some((connection) => !connection.host || !connection.username || !connection.password)) {
@@ -505,9 +433,10 @@ watch(
[ [
() => props.visible, () => props.visible,
() => props.device?.asset_id, () => props.device?.asset_id,
() => props.device?.asset_name,
() => props.device?.asset_code,
() => props.cameraIndex, () => props.cameraIndex,
() => props.cameraCount, () => props.cameraPoint?.index,
() => (props.devices || []).map((device) => device.asset_id).join(','),
], ],
([visible]) => { ([visible]) => {
if (visible) void openPreview() if (visible) void openPreview()

View File

@@ -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
)
}

View File

@@ -55,7 +55,7 @@
{{ {{
insideAisle insideAisle
? '当前为机柜过道第一视角;点击设备可近距离查看,双击柜内设备可抽出或归位' ? '当前为机柜过道第一视角;点击设备可近距离查看,双击柜内设备可抽出或归位'
: '双击门板可开合;机柜开门后双击设备可抽出或归位,点击摄像头播放监控、长按可拖动' : '双击门板可开合;机柜开门后双击设备可抽出或归位,点击海康摄像头播放监控、长按可拖动'
}} }}
</span> </span>
</div> </div>
@@ -128,9 +128,8 @@
<camera-preview-dialog <camera-preview-dialog
v-model:visible="cameraPreviewVisible" v-model:visible="cameraPreviewVisible"
:device="selectedDevice" :device="selectedDevice"
:devices="sceneCameraDevices"
:camera-index="selectedCameraIndex" :camera-index="selectedCameraIndex"
:camera-count="sceneSummary.importedCameraCount" :camera-point="selectedCameraPoint"
@manage="handleOpenDeviceManagement" @manage="handleOpenDeviceManagement"
/> />
<device-management-dialog <device-management-dialog
@@ -171,6 +170,11 @@ import {
import { getMockDeviceObservability, MOCK_ROOM_SCENE, MOCK_ROOM_SIGNALS } from './scene/MockThreeMachineRoom' import { getMockDeviceObservability, MOCK_ROOM_SCENE, MOCK_ROOM_SIGNALS } from './scene/MockThreeMachineRoom'
import ThreeMap from './scene/ThreeMap' import ThreeMap from './scene/ThreeMap'
import { ThreeData } from './scene/ThreeData' import { ThreeData } from './scene/ThreeData'
import {
findThreeMachineRoomCameraPointByText,
getThreeMachineRoomCameraPointByIndex,
type ThreeMachineRoomCameraPoint,
} from './config/camera-points'
/** 场景渲染摘要。 */ /** 场景渲染摘要。 */
interface SceneSummary { interface SceneSummary {
@@ -200,6 +204,10 @@ interface DeviceAlertCalloutContext {
interface ImportedCameraContext { interface ImportedCameraContext {
index: number index: number
name: string name: string
shortName?: string
brand?: string
playable?: boolean
host?: string
} }
const route = useRoute() const route = useRoute()
@@ -226,6 +234,7 @@ const roomScene = ref<ThreeMachineRoomScene | null>(null)
const selectedRack = ref<ThreeMachineRoomRack | null>(null) const selectedRack = ref<ThreeMachineRoomRack | null>(null)
const selectedDevice = ref<ThreeMachineRoomDevice | null>(null) const selectedDevice = ref<ThreeMachineRoomDevice | null>(null)
const selectedCameraIndex = ref<number>() const selectedCameraIndex = ref<number>()
const selectedCameraPoint = ref<ThreeMachineRoomCameraPoint | null>(null)
const activeRack = ref<ThreeMachineRoomRack | null>(null) const activeRack = ref<ThreeMachineRoomRack | null>(null)
const rackPlacementVisible = ref(false) const rackPlacementVisible = ref(false)
const rackLayoutVisible = ref(false) const rackLayoutVisible = ref(false)
@@ -342,11 +351,6 @@ async function loadRoomScene(): Promise<boolean> {
usingMockData.value = false usingMockData.value = false
roomScene.value = scene roomScene.value = scene
machineRoomMap?.setRoomScene(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) await loadRoomSignals(true)
return true return true
} catch (error) { } 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)) 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 { function handleImportedCameraSelected(camera: ImportedCameraContext): void {
const cameraIndex = Number(camera.index) const cameraIndex = Number(camera.index)
if (!Number.isInteger(cameraIndex) || cameraIndex <= 0) return if (!Number.isInteger(cameraIndex) || cameraIndex <= 0) return
const cameraPoint = getThreeMachineRoomCameraPointByIndex(cameraIndex)
selectedCameraIndex.value = 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 cameraPreviewVisible.value = true
} }
@@ -468,10 +496,19 @@ function handleImportedCameraSelected(camera: ImportedCameraContext): void {
function handleDeviceSelected(device: ThreeMachineRoomDevice): void { function handleDeviceSelected(device: ThreeMachineRoomDevice): void {
selectedDevice.value = device selectedDevice.value = device
if (isCameraDevice(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 cameraPreviewVisible.value = true
return return
} }
selectedCameraIndex.value = undefined
selectedCameraPoint.value = null
if (usingMockData.value) { if (usingMockData.value) {
void loadDeviceObservability(device) void loadDeviceObservability(device)
Message.warning('演示数据不支持资源绑定和位置管理') Message.warning('演示数据不支持资源绑定和位置管理')

View File

@@ -11,10 +11,10 @@ import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
import { TGALoader } from 'three/examples/jsm/loaders/TGALoader' import { TGALoader } from 'three/examples/jsm/loaders/TGALoader'
import TWEEN from '@tweenjs/tween.js' import TWEEN from '@tweenjs/tween.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' //r100及以上 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_LONG_PRESS_DURATION = 300
const ROOM_DEVICE_DRAG_MOVE_THRESHOLD = 6 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_OCCLUSION_UPDATE_INTERVAL = 60
const VIEW_OCCLUDER_OPACITY = 0.055 const VIEW_OCCLUDER_OPACITY = 0.055
const RACK_EQUIPMENT_PULL_OUT_RATIO = 0.58 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 const isClick = pressDuration < ROOM_DEVICE_LONG_PRESS_DURATION && drag.maxDistance < ROOM_DEVICE_DRAG_MOVE_THRESHOLD
if (isClick) { if (isClick) {
if (drag.equipment.userData.importedCamera) { if (drag.equipment.userData.importedCamera) {
const cameraPoint = drag.equipment.userData.importedCameraPoint || null
if (typeof this.props.onImportedCameraSelected === 'function') { if (typeof this.props.onImportedCameraSelected === 'function') {
this.props.onImportedCameraSelected({ this.props.onImportedCameraSelected({
index: drag.equipment.userData.importedCameraIndex, 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 { } else {
@@ -2197,10 +2202,14 @@ export default class ThreeMap {
const equipment = drag.equipment const equipment = drag.equipment
if (equipment.userData.importedCamera) { if (equipment.userData.importedCamera) {
const cameraPoint = equipment.userData.importedCameraPoint || null
if (typeof this.props.onImportedCameraMoved === 'function') { if (typeof this.props.onImportedCameraMoved === 'function') {
this.props.onImportedCameraMoved({ this.props.onImportedCameraMoved({
index: equipment.userData.importedCameraIndex, 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: { position: {
x: equipment.position.x, x: equipment.position.x,
y: equipment.position.y, y: equipment.position.y,
@@ -2363,30 +2372,40 @@ export default class ThreeMap {
}) })
} }
/** 将模型内置的 6 个摄像头网格转换为可独立拖动的对象。 */ /** 标记一个导入摄像头网格,并补充点击热区和固定点位信息。 */
prepareImportedCameras(meshEntries, modelBox) { prepareImportedCameraMesh(mesh, point, modelBox, geometryBox) {
this.importedCameras = meshEntries if (!mesh || !point) {
.filter((entry) => IMPORTED_CAMERA_NAME_PATTERN.test(entry.mesh.name)) return null
.sort((a, b) => a.mesh.name.localeCompare(b.mesh.name)) }
.map((entry, index) => { if (geometryBox) {
const mesh = entry.mesh const geometryCenter = geometryBox.getCenter(new THREE.Vector3())
const geometryCenter = entry.box.getCenter(new THREE.Vector3())
const geometry = mesh.geometry.clone() const geometry = mesh.geometry.clone()
geometry.translate(-geometryCenter.x, -geometryCenter.y, -geometryCenter.z) geometry.translate(-geometryCenter.x, -geometryCenter.y, -geometryCenter.z)
geometry.computeBoundingBox() geometry.computeBoundingBox()
geometry.computeBoundingSphere() geometry.computeBoundingSphere()
mesh.geometry = geometry mesh.geometry = geometry
mesh.position.add(geometryCenter) 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 hitRadius = Math.max(cameraSize.x, cameraSize.y, cameraSize.z) * 0.85
const hitArea = new THREE.Mesh( const hitArea = new THREE.Mesh(
new THREE.SphereGeometry(hitRadius, 12, 8), new THREE.SphereGeometry(hitRadius, 12, 8),
new THREE.MeshBasicMaterial({ transparent: true, opacity: 0, depthWrite: false }) 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.add(hitArea)
mesh.userData.importedCamera = true 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.placementType = 'imported-camera'
mesh.userData.draggable = true mesh.userData.draggable = true
mesh.userData.roomPlacement = { mesh.userData.roomPlacement = {
@@ -2394,7 +2413,48 @@ export default class ThreeMap {
} }
mesh.userData.originalModelPosition = mesh.position.clone() mesh.userData.originalModelPosition = mesh.position.clone()
return mesh 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 return this.importedCameras
} }