Merge branch 'main' of https://git.apinb.com/ops/front
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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
|
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('演示数据不支持资源绑定和位置管理')
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user