|
|
<template>
|
|
|
<div class="scene-wrapper">
|
|
|
<div ref="containerRef" class="container" />
|
|
|
|
|
|
<!-- 固定位置的控制按钮 -->
|
|
|
<div class="control-buttons">
|
|
|
<el-button
|
|
|
type="warning"
|
|
|
@click="tranlate(new Vector3(0, 14, 13.5), new Vector3(0, 0, 0))"
|
|
|
:icon="Refresh"
|
|
|
/>
|
|
|
<!-- <el-button type="primary" @click="displayModel('area_oneFloor')" :icon="Bell" />
|
|
|
<el-button type="warning" @click="displayModel('smokeDetector')" :icon="Compass" />
|
|
|
<el-button type="warning" @click="displayModel('acoustoOptic')" :icon="MagicStick" />
|
|
|
<el-button type="warning" @click="displayModel('manual')" :icon="Star" /> -->
|
|
|
<!-- 1:左右 2:上下 3:前后 -->
|
|
|
<!-- <el-button type="warning" @click="displayModel('refresh')" :icon="Refresh" />
|
|
|
<el-button
|
|
|
type="warning"
|
|
|
@click="meshControllerArr[0].controller.startMalfunction()"
|
|
|
:icon="Refresh"
|
|
|
/>
|
|
|
<el-button
|
|
|
type="warning"
|
|
|
@click="meshControllerArr[0].controller.restore()"
|
|
|
:icon="Refresh"
|
|
|
/> -->
|
|
|
</div>
|
|
|
|
|
|
<div class="control-panel" v-show="cardisOpen">
|
|
|
<el-card class="card" v-show="cardisOpen" shadow="hover" :body-style="{ padding: '10px' }">
|
|
|
<h2 class="cardHead">{{ getModelDroupName(moduleType) }}</h2>
|
|
|
|
|
|
<div class="cardBody">
|
|
|
<!-- <el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">厂家</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">海湾公司</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">投运日期</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">2014-08-08</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">电源电压</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">220V AC</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">布设位置</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">35kV高压室</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">供电电压</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">18-30V DC</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">输出电流</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">5A</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">IP等级</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">IP30</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">相对湿度</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">5% ~ 95%</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">接线方式</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">两线制(L+、L-)</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">尺寸</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">350mm x 225mm x 125mm</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">报警灵敏度</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">0.005% 至 20%obs/m</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">运行条件</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag ref="conditionTagRef" type="primary"> 探测器环境(-5°C至45°C) </el-tag>
|
|
|
</el-col>
|
|
|
</el-row> -->
|
|
|
|
|
|
<!-- bindingInfo -->
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">绑定设备</el-col>
|
|
|
<el-col :span="14" :offset="1"
|
|
|
><el-button
|
|
|
type="primary"
|
|
|
class="operation-btn"
|
|
|
size="small"
|
|
|
@click="dialogTableVisible = true"
|
|
|
>绑定</el-button
|
|
|
></el-col
|
|
|
>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row" v-if="bindingInfo.id">
|
|
|
<el-col :span="8">设备ID</el-col>
|
|
|
<el-col :span="14" :offset="1">
|
|
|
<el-tag type="primary" size="small">{{ bindingInfo.id }}</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row" v-if="bindingInfo.id">
|
|
|
<el-col :span="8">设备名称</el-col>
|
|
|
<el-col :span="14" :offset="1">
|
|
|
<el-tag type="primary" size="small">{{ bindingInfo.name }}</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row" v-if="bindingInfo.id">
|
|
|
<el-col :span="8">设备状态</el-col>
|
|
|
<el-col :span="14" :offset="1">
|
|
|
<el-tag size="small" :type="bindingInfo.eState === 0 ? 'success' : 'warning'">
|
|
|
{{ bindingInfo.eState === 0 ? '正常' : '故障' }}
|
|
|
</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row" v-if="bindingInfo.id">
|
|
|
<el-col :span="8">当前状态</el-col>
|
|
|
<el-col :span="14" :offset="1">
|
|
|
<el-tag size="small" v-if="bindingInfo.currentState === 0" type="success"
|
|
|
>运行</el-tag
|
|
|
>
|
|
|
<el-tag size="small" v-else-if="bindingInfo.currentState === 1" type="info"
|
|
|
>停止</el-tag
|
|
|
>
|
|
|
<el-tag size="small" v-else type="error">报警</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row" v-if="bindingInfo.id">
|
|
|
<el-col :span="8">详细信息</el-col>
|
|
|
<el-col :span="14" :offset="1">
|
|
|
<el-button
|
|
|
type="primary"
|
|
|
class="operation-btn"
|
|
|
size="small"
|
|
|
@click="dialogFacilityInfoVisible = true"
|
|
|
>查看</el-button
|
|
|
></el-col
|
|
|
>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row" v-if="bindingInfo.id">
|
|
|
<el-col :span="6">
|
|
|
<el-button type="primary" size="small" @click="operationController('restore')"
|
|
|
>还原</el-button
|
|
|
></el-col
|
|
|
>
|
|
|
<el-col :span="6">
|
|
|
<el-button type="warning" size="small" @click="operationController('alarm')"
|
|
|
>告警</el-button
|
|
|
></el-col
|
|
|
>
|
|
|
<el-col :span="6">
|
|
|
<el-button @click="operationController('fault')" type="danger" size="small"
|
|
|
>故障</el-button
|
|
|
></el-col
|
|
|
>
|
|
|
<el-col :span="6">
|
|
|
<el-button @click="operationController('stop')" type="info" size="small"
|
|
|
>停止</el-button
|
|
|
></el-col
|
|
|
>
|
|
|
</el-row>
|
|
|
<el-row :gutter="0" class="equipment-row">
|
|
|
<el-col :span="24">
|
|
|
<div class="equipment-wrapper">
|
|
|
<vueThreeEquipment :moduleType="moduleType" />
|
|
|
</div>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
</div>
|
|
|
</el-card>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<el-dialog
|
|
|
v-model="dialogFacilityInfoVisible"
|
|
|
title="设备详细信息"
|
|
|
width="800"
|
|
|
top="3vh"
|
|
|
append-to-body
|
|
|
>
|
|
|
<el-card class="card" v-show="cardisOpen" shadow="hover" :body-style="{ padding: '10px' }">
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">厂家</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">海湾公司</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">投运日期</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">2014-08-08</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">电源电压</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">220V AC</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">布设位置</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">35kV高压室</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">供电电压</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">18-30V DC</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">输出电流</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">5A</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">IP等级</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">IP30</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">相对湿度</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">5% ~ 95%</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">接线方式</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">两线制(L+、L-)</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">尺寸</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">350mm x 225mm x 125mm</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">报警灵敏度</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag type="primary">0.005% 至 20%obs/m</el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="0" class="info-row">
|
|
|
<el-col :span="8">运行条件</el-col>
|
|
|
<el-col :span="16">
|
|
|
<el-tag ref="conditionTagRef" type="primary"> 探测器环境(-5°C至45°C) </el-tag>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
</el-card>
|
|
|
</el-dialog>
|
|
|
|
|
|
<el-dialog v-model="dialogTableVisible" title="设备绑定" width="850" top="3vh" append-to-body>
|
|
|
<el-table :data="arr" style="width: 100%" max-height="500" class="full-height-table">
|
|
|
<el-table-column prop="floorName" label="房间" />
|
|
|
<el-table-column prop="id" label="设备ID" />
|
|
|
<el-table-column prop="name" label="设备名称" />
|
|
|
|
|
|
<el-table-column prop="isBinding" label="是否绑定">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="row.isBinding ? 'primary' : 'info'">
|
|
|
{{ row.isBinding ? '绑定' : '待绑' }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="设备状态">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="row.eState === 0 ? 'success' : 'warning'">
|
|
|
{{ row.eState === 0 ? '正常' : '故障' }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="当前状态">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag v-if="row.currentState === 0" type="success">运行</el-tag>
|
|
|
<el-tag v-else-if="row.currentState === 1" type="info">停止</el-tag>
|
|
|
<el-tag v-else type="error">报警</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="操作">
|
|
|
<template #default="{ row }">
|
|
|
<el-button type="primary" class="operation-btn" @click="bindDevice(row)">
|
|
|
绑定
|
|
|
</el-button>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
</el-dialog>
|
|
|
</template>
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
import { Star, Bell, MagicStick, Refresh, Compass } from '@element-plus/icons-vue';
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
import { onMounted, onUnmounted, ref, reactive } from 'vue';
|
|
|
import {
|
|
|
AxesHelper,
|
|
|
Color,
|
|
|
PerspectiveCamera,
|
|
|
Scene,
|
|
|
WebGLRenderer,
|
|
|
AmbientLight,
|
|
|
DirectionalLight,
|
|
|
PointLight,
|
|
|
Vector2,
|
|
|
Raycaster,
|
|
|
CanvasTexture,
|
|
|
SpriteMaterial,
|
|
|
Sprite,
|
|
|
Box3,
|
|
|
Vector3,
|
|
|
Mesh,
|
|
|
BoxHelper,
|
|
|
ConeGeometry,
|
|
|
SphereGeometry,
|
|
|
MeshBasicMaterial,
|
|
|
TextureLoader,
|
|
|
Group
|
|
|
} from 'three';
|
|
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
|
|
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
|
|
|
// // import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'; // 未使用 // 暂时未使用
|
|
|
import gsap from 'gsap';
|
|
|
import vueThreeEquipment from '@/components/three-components/vue-three-equipment.vue'; // 暂时注释掉未找到的组件
|
|
|
import { modelApi } from '@/utils/request';
|
|
|
import emitter from '@/utils/emitter';
|
|
|
|
|
|
// 导入全局类型定义
|
|
|
/// <reference path="../../../global.d.ts" />
|
|
|
|
|
|
// 定义 Rec 相关的类型
|
|
|
interface RecRuntimeData {
|
|
|
double: number;
|
|
|
node: {
|
|
|
name: string;
|
|
|
};
|
|
|
}
|
|
|
|
|
|
interface RecServiceType {
|
|
|
service: {
|
|
|
node: {
|
|
|
runtimes: Record<string, RecRuntimeData>;
|
|
|
};
|
|
|
};
|
|
|
}
|
|
|
|
|
|
interface ExtendedParentWindow extends Window {
|
|
|
Rec?: RecServiceType;
|
|
|
}
|
|
|
|
|
|
const containerRef = ref<HTMLDivElement>();
|
|
|
const dialogTableVisible = ref(false);
|
|
|
let dialogFacilityInfoVisible = ref(false);
|
|
|
|
|
|
// 场景
|
|
|
const scene = new Scene();
|
|
|
|
|
|
// 创建摄像机
|
|
|
const camera = new PerspectiveCamera(
|
|
|
45,
|
|
|
containerRef.value?.clientWidth! / containerRef.value?.clientHeight!,
|
|
|
0.1,
|
|
|
100
|
|
|
);
|
|
|
camera.position.set(0, 14, 13.5);
|
|
|
camera.lookAt(0, 0, 0);
|
|
|
|
|
|
// const axesHelper = new AxesHelper(100);
|
|
|
// scene.add(axesHelper);
|
|
|
|
|
|
// 渲染器
|
|
|
const renderer = new WebGLRenderer({
|
|
|
antialias: true, // 启用抗锯齿
|
|
|
alpha: true, // 启用透明背景
|
|
|
preserveDrawingBuffer: true // 保持绘图缓冲区
|
|
|
});
|
|
|
renderer.setClearColor(new Color('#131519'));
|
|
|
renderer.setSize(containerRef.value?.clientWidth!, containerRef.value?.clientHeight!);
|
|
|
renderer.shadowMap.enabled = true;
|
|
|
renderer.setPixelRatio(window.devicePixelRatio || 1); // 设置像素比率
|
|
|
|
|
|
// 轨道摄像机
|
|
|
const orbitControls = new OrbitControls(camera, renderer.domElement);
|
|
|
orbitControls.autoRotate = false;
|
|
|
orbitControls.autoRotateSpeed = 5;
|
|
|
orbitControls.dampingFactor = 0.1;
|
|
|
orbitControls.enableDamping = true;
|
|
|
orbitControls.enableRotate = true;
|
|
|
orbitControls.minDistance = 10;
|
|
|
orbitControls.maxPolarAngle = Math.PI * 0.5;
|
|
|
|
|
|
// 物体
|
|
|
// scene.add(new AxesHelper(20));
|
|
|
|
|
|
// 环境光:增强整体亮度
|
|
|
const ambientLight = new AmbientLight('#ffffff', 1.8); // 强度从 0.6 提高到 1.0
|
|
|
scene.add(ambientLight);
|
|
|
|
|
|
// 平行光
|
|
|
const directionalLight = new DirectionalLight('#ffffff', 1.8); // 强度从 0.2 提高到 0.8
|
|
|
scene.add(directionalLight);
|
|
|
directionalLight.position.set(20, 20, 10);
|
|
|
|
|
|
// // 点光源
|
|
|
const pointLight = new PointLight('#ffffff', 1.8, 1000); // 强度从 0.5 提高到 1.0
|
|
|
scene.add(pointLight);
|
|
|
pointLight.position.set(0, 40, 0);
|
|
|
|
|
|
function addSprite(text: string, color: string, position: Vector3) {
|
|
|
const canvas = document.createElement('canvas');
|
|
|
canvas.width = 150;
|
|
|
canvas.height = 150;
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
if (ctx) {
|
|
|
ctx.fillStyle = color; // rgba(0,0,0,.7)
|
|
|
ctx.fillRect(0, 0, 150, 60);
|
|
|
ctx.fillStyle = 'white';
|
|
|
ctx.font = '24px 黑体';
|
|
|
ctx.textAlign = 'center';
|
|
|
ctx.textBaseline = 'middle';
|
|
|
ctx.fillText(text, 75, 30);
|
|
|
}
|
|
|
|
|
|
const texture = new CanvasTexture(canvas);
|
|
|
const material = new SpriteMaterial({ map: texture });
|
|
|
const sprite = new Sprite(material);
|
|
|
|
|
|
sprite.position.set(position.x, position.y + 0.7, position.z);
|
|
|
scene.add(sprite);
|
|
|
return sprite;
|
|
|
}
|
|
|
|
|
|
// 创建一个悬浮的框
|
|
|
// const ctx = canvas.getContext('2d');
|
|
|
// if (ctx) {
|
|
|
// ctx.fillStyle = 'rgba(0,0,0,.7)';
|
|
|
// ctx.fillRect(0, 0, 200, 100);
|
|
|
// ctx.fillStyle = 'white';
|
|
|
// ctx.font = '21px 黑体';
|
|
|
// ctx.textAlign = 'center';
|
|
|
// ctx.textBaseline = 'middle';
|
|
|
// }
|
|
|
// const scaleFactor = 5;
|
|
|
// const texture = new CanvasTexture(canvas);
|
|
|
// const material = new SpriteMaterial({ map: texture });
|
|
|
|
|
|
// const sprite1 = new Sprite(material);
|
|
|
// sprite1.scale.set(scaleFactor, scaleFactor, scaleFactor);
|
|
|
// sprite1.position.set(-9.644502679789724, 5.815449539931501 + 0.5, 21.66345220492572);
|
|
|
|
|
|
//区域集合 - 修改为包含多种类型
|
|
|
let areaOneFloorArr: (Mesh | Sprite | BoxHelper)[] = [];
|
|
|
//光感报警器
|
|
|
let acoustoOpticArr: Mesh[] = [];
|
|
|
//手动报警器
|
|
|
let manualArr: Mesh[] = [];
|
|
|
// 烟雾探测器
|
|
|
let smokeDetectorArr: Mesh[] = [];
|
|
|
// 移动目标的集合
|
|
|
let moveArr: Mesh[] = []; // 只包含Mesh类型用于射线检测
|
|
|
|
|
|
interface meshController {
|
|
|
meshName: string;
|
|
|
targetMesh: any;
|
|
|
geometry: Mesh;
|
|
|
sprite: any;
|
|
|
controller: any;
|
|
|
}
|
|
|
|
|
|
// node楼层节点
|
|
|
interface FloorNode {
|
|
|
roomId: string;
|
|
|
floorName: string;
|
|
|
eqpList: EquipmentNode[];
|
|
|
}
|
|
|
|
|
|
//node
|
|
|
interface EquipmentNode {
|
|
|
id: string; //设备ID
|
|
|
name: string; //设备名称
|
|
|
eState: number; //设备状态 0正常 1故障
|
|
|
isBinding: boolean; //是否绑定
|
|
|
currentState: number; //当前状态 0运行 1停止 2报警
|
|
|
operation: boolean; //操作 true屏蔽 false取消屏蔽
|
|
|
}
|
|
|
|
|
|
let arr = ref<any[]>([]);
|
|
|
//数据初始化
|
|
|
async function dataInit() {
|
|
|
const response = await modelApi.oneRoomFor_Equipment_get();
|
|
|
if (response.code == 200 && response.data && response.data.length > 0) {
|
|
|
response.data.forEach((floor: any) => {
|
|
|
floor.eqpList.forEach((eqp: any) => {
|
|
|
let currentState = 1;
|
|
|
//故障
|
|
|
if (
|
|
|
(window.parent as ExtendedParentWindow).Rec?.service.node.runtimes[eqp.nodes[1]].double !=
|
|
|
1
|
|
|
)
|
|
|
currentState = 1;
|
|
|
else if (
|
|
|
(window.parent as ExtendedParentWindow).Rec?.service.node.runtimes[eqp.nodes[2]].double ==
|
|
|
1
|
|
|
)
|
|
|
currentState = 2;
|
|
|
else if (
|
|
|
(window.parent as ExtendedParentWindow).Rec?.service.node.runtimes[eqp.nodes[1]].double ==
|
|
|
1
|
|
|
)
|
|
|
currentState = 0;
|
|
|
|
|
|
let eqpNode: EquipmentNode = {
|
|
|
id: eqp.id,
|
|
|
name:
|
|
|
(window.parent as ExtendedParentWindow).Rec?.service.node.runtimes[eqp.nodes[0]].node
|
|
|
.name || '',
|
|
|
eState:
|
|
|
(window.parent as ExtendedParentWindow).Rec?.service.node.runtimes[eqp.nodes[0]]
|
|
|
.double || 0,
|
|
|
currentState: currentState,
|
|
|
isBinding: eqp.isBinding,
|
|
|
operation:
|
|
|
(window.parent as ExtendedParentWindow).Rec?.service.node.runtimes[eqp.nodes[3]]
|
|
|
.double == 1
|
|
|
};
|
|
|
arr.value.push({
|
|
|
...eqpNode,
|
|
|
floorName: floor.roomName,
|
|
|
roomId: floor.roomId
|
|
|
});
|
|
|
});
|
|
|
});
|
|
|
ElMessage.success('获取接口数据成功');
|
|
|
} else {
|
|
|
ElMessage.error('获取接口数据失败');
|
|
|
}
|
|
|
}
|
|
|
dataInit();
|
|
|
|
|
|
function operationController(controller: string) {
|
|
|
console.log(bindingObj, controller);
|
|
|
console.log(meshControllerArr);
|
|
|
let obj = meshControllerArr.find((item) => item.meshName === bindingObj.name);
|
|
|
if (obj) {
|
|
|
if (controller == 'restore') {
|
|
|
obj.controller.restore();
|
|
|
} else if (controller == 'alarm') {
|
|
|
obj.controller.startAlarm();
|
|
|
} else if (controller == 'fault') {
|
|
|
obj.controller.startMalfunction();
|
|
|
} else if (controller == 'stop') {
|
|
|
obj.controller.stop();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
let bindingArr: any[] = [];
|
|
|
async function getListBinding() {
|
|
|
const response = await modelApi.getListBinding_get();
|
|
|
if (response.code == 200 && response.data && response.data.length > 0) {
|
|
|
bindingArr = response.data;
|
|
|
} else {
|
|
|
ElMessage.error('获取接口数据失败');
|
|
|
}
|
|
|
}
|
|
|
getListBinding();
|
|
|
|
|
|
let meshControllerArr: meshController[] = [];
|
|
|
const loader = new GLTFLoader();
|
|
|
const textureLoader = new TextureLoader();
|
|
|
//alpha贴图
|
|
|
// F:\vue\workspace\maotu-webtopo\public\models\texture\alpha.png
|
|
|
const material = new MeshBasicMaterial({ color: 0x409eff, transparent: true, opacity: 0.3 });
|
|
|
const alphaTexture = textureLoader.load('/models/texture/alpha01.png');
|
|
|
const alphaMaterial = new MeshBasicMaterial({
|
|
|
color: 0x409eff,
|
|
|
alphaMap: alphaTexture, //alpha贴图
|
|
|
transparent: true // 允许透明
|
|
|
});
|
|
|
// const dracoLoader = new DRACOLoader();
|
|
|
// loader.setDRACOLoader(dracoLoader);
|
|
|
//public\models\groundFloor\ground_floor.gltf
|
|
|
let bindingColor = 'rgba(33,61,91,0.70)';
|
|
|
loader.load('/models/groundFloor/ground_floor08.gltf', (gltf: any) => {
|
|
|
gltf.scene.children.forEach((child: Mesh) => {
|
|
|
// 区域
|
|
|
if (child.name.includes('area_oneFloor')) {
|
|
|
const duckGeometry = child.geometry;
|
|
|
duckGeometry.computeBoundingBox();
|
|
|
duckGeometry.center();
|
|
|
const duckBox = duckGeometry.boundingBox?.clone() || new Box3();
|
|
|
child.updateWorldMatrix(true, true);
|
|
|
duckBox.applyMatrix4(child.matrixWorld);
|
|
|
const duckBoxHelper = new BoxHelper(child, 0xf0f0f0);
|
|
|
// 设置包围盒材质的透明度
|
|
|
(duckBoxHelper.material as any).transparent = true;
|
|
|
(duckBoxHelper.material as any).opacity = 0.38; // 50%透明度
|
|
|
scene.add(duckBoxHelper);
|
|
|
|
|
|
// 获取child对象的世界坐标
|
|
|
// 添加精灵
|
|
|
const worldPosition = new Vector3();
|
|
|
child.getWorldPosition(worldPosition);
|
|
|
|
|
|
// 创建文字精灵
|
|
|
const canvas = document.createElement('canvas');
|
|
|
// 提高Canvas分辨率以获得更清晰的文字
|
|
|
const scale = window.devicePixelRatio || 1;
|
|
|
canvas.width = 512 * scale;
|
|
|
canvas.height = 256 * scale;
|
|
|
canvas.style.width = '512px';
|
|
|
canvas.style.height = '256px';
|
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
if (ctx) {
|
|
|
// 缩放上下文以匹配设备像素比率
|
|
|
ctx.scale(scale, scale);
|
|
|
|
|
|
// 设置文字样式
|
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0)';
|
|
|
ctx.fillRect(0, 0, 512, 256);
|
|
|
ctx.fillStyle = 'white';
|
|
|
ctx.textAlign = 'center';
|
|
|
ctx.textBaseline = 'middle';
|
|
|
|
|
|
// 使用更大的字体和更清晰的字体族
|
|
|
ctx.font = '24px 微软雅黑, Arial, sans-serif';
|
|
|
ctx.fillText(getAreaOneFloorGetName(child.name), 256, 128);
|
|
|
}
|
|
|
|
|
|
// 创建精灵材质和对象
|
|
|
const texture = new CanvasTexture(canvas);
|
|
|
const spriteMaterial = new SpriteMaterial({ map: texture });
|
|
|
const sprite = new Sprite(spriteMaterial);
|
|
|
|
|
|
// 设置精灵大小和位置
|
|
|
sprite.scale.set(4, 2, 1); // 调整大小以适配新的Canvas尺寸
|
|
|
sprite.position.copy(worldPosition);
|
|
|
sprite.position.y += 1.5; // 稍微抬高一点
|
|
|
scene.add(sprite);
|
|
|
|
|
|
sprite.visible = false;
|
|
|
duckBoxHelper.visible = false;
|
|
|
child.visible = false;
|
|
|
|
|
|
areaOneFloorArr.push(sprite); // 将精灵也加入数组
|
|
|
areaOneFloorArr.push(duckBoxHelper);
|
|
|
areaOneFloorArr.push(child);
|
|
|
moveArr.push(child as any);
|
|
|
} else if (child.name.includes('acoustoOptic')) {
|
|
|
let acoustoPosition = child.position;
|
|
|
const geometry = new SphereGeometry(0.6, 20, 16);
|
|
|
|
|
|
const sphere = new Mesh(geometry, material);
|
|
|
sphere.position.set(acoustoPosition.x, acoustoPosition.y, acoustoPosition.z);
|
|
|
scene.add(sphere);
|
|
|
|
|
|
let text = '未绑定';
|
|
|
let color = 'rgba(0,0,0,.5)';
|
|
|
//如果bindingArr集合中的objId和child.name匹配,则设置文字
|
|
|
if (bindingArr.some((item: any) => item.objId === child.name)) {
|
|
|
text = '已绑定';
|
|
|
// rgba(45,72,31,0.70) rgba(33,61,91,0.70) rgba(33,61,91,0.70)
|
|
|
color = bindingColor;
|
|
|
}
|
|
|
let sprite = addSprite(
|
|
|
text,
|
|
|
color,
|
|
|
new Vector3(acoustoPosition.x, acoustoPosition.y - 0.2, acoustoPosition.z + 0.3)
|
|
|
);
|
|
|
|
|
|
child.visible = true;
|
|
|
sphere.visible = true;
|
|
|
acoustoOpticArr.push(sphere);
|
|
|
acoustoOpticArr.push(child);
|
|
|
|
|
|
meshControllerArr.push({
|
|
|
meshName: child.name,
|
|
|
targetMesh: child,
|
|
|
geometry: sphere,
|
|
|
sprite: sprite,
|
|
|
controller: setupAlarmEffect(sphere)
|
|
|
});
|
|
|
moveArr.push(child as any);
|
|
|
} else if (child.name.includes('manual')) {
|
|
|
let manualPosition = child.position;
|
|
|
const geometry = new SphereGeometry(0.6, 20, 16);
|
|
|
const sphere = new Mesh(geometry, material);
|
|
|
let text = '未绑定';
|
|
|
let color = 'rgba(0,0,0,.5)';
|
|
|
//如果bindingArr集合中的objId和child.name匹配,则设置文字
|
|
|
if (bindingArr.some((item: any) => item.objId === child.name)) {
|
|
|
text = '已绑定';
|
|
|
// rgba(45,72,31,0.70) rgba(33,61,91,0.70) rgba(33,61,91,0.70)
|
|
|
color = bindingColor;
|
|
|
}
|
|
|
let sprite = addSprite(
|
|
|
text,
|
|
|
color,
|
|
|
new Vector3(manualPosition.x, manualPosition.y - 0.2, manualPosition.z - 0.3)
|
|
|
);
|
|
|
|
|
|
sphere.position.set(manualPosition.x, manualPosition.y, manualPosition.z);
|
|
|
scene.add(sphere);
|
|
|
child.visible = true;
|
|
|
sphere.visible = true;
|
|
|
|
|
|
manualArr.push(sphere);
|
|
|
manualArr.push(child);
|
|
|
|
|
|
meshControllerArr.push({
|
|
|
meshName: child.name,
|
|
|
targetMesh: child,
|
|
|
geometry: sphere,
|
|
|
sprite: sprite,
|
|
|
controller: setupAlarmEffect(sphere)
|
|
|
});
|
|
|
|
|
|
moveArr.push(child as any);
|
|
|
} else if (child.name.includes('smokeDetecto')) {
|
|
|
// 烟感报警器
|
|
|
let smokePosition = child.position;
|
|
|
|
|
|
const geometry = new ConeGeometry(1.8, 1.2, 16, 1, true);
|
|
|
const cone = new Mesh(geometry, alphaMaterial);
|
|
|
cone.position.set(smokePosition.x, smokePosition.y - 0.5, smokePosition.z + 0.028);
|
|
|
|
|
|
let text = '未绑定';
|
|
|
let color = 'rgba(0,0,0,.5)';
|
|
|
//如果bindingArr集合中的objId和child.name匹配,则设置文字
|
|
|
if (bindingArr.some((item: any) => item.objId === child.name)) {
|
|
|
text = '已绑定';
|
|
|
// rgba(45,72,31,0.70) rgba(33,61,91,0.70) rgba(33,61,91,0.70)
|
|
|
color = bindingColor;
|
|
|
}
|
|
|
// let text:string[] = ['x:'+child.position.x.toString() , 'y:'+child.position.y.toString() , 'z:'+child.position.z.toString()];
|
|
|
// console.log(child.name+' x:'+child.position.x.toString() , 'y:'+child.position.y.toString() , 'z:'+child.position.z.toString());
|
|
|
// 烟感报警器
|
|
|
let sprite = addSprite(text, color, cone.position);
|
|
|
|
|
|
scene.add(cone);
|
|
|
cone.visible = true;
|
|
|
child.visible = true;
|
|
|
|
|
|
smokeDetectorArr.push(cone);
|
|
|
smokeDetectorArr.push(child);
|
|
|
|
|
|
meshControllerArr.push({
|
|
|
meshName: child.name,
|
|
|
targetMesh: child,
|
|
|
geometry: cone,
|
|
|
sprite: sprite,
|
|
|
controller: setupAlarmEffect(cone)
|
|
|
});
|
|
|
|
|
|
moveArr.push(child as any);
|
|
|
}
|
|
|
});
|
|
|
scene.add(gltf.scene);
|
|
|
});
|
|
|
|
|
|
// 安全的报警效果设置函数
|
|
|
function setupAlarmEffect(object: Mesh) {
|
|
|
// 安全检查 material 是否存在
|
|
|
if (!object.material) {
|
|
|
console.warn('对象没有材质属性:', object);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 处理材质可能是数组的情况
|
|
|
const materialArray = Array.isArray(object.material) ? object.material : [object.material];
|
|
|
const firstMaterial = materialArray[0];
|
|
|
|
|
|
// 保存原始材质
|
|
|
// 确保材质有color属性
|
|
|
if (!('color' in firstMaterial)) {
|
|
|
console.warn('材质没有color属性:', firstMaterial);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const originalMaterial = firstMaterial.clone();
|
|
|
const alarmMaterial = firstMaterial.clone();
|
|
|
(alarmMaterial as any).color.set(0xe6a23c);
|
|
|
const faultMaterial = firstMaterial.clone();
|
|
|
(faultMaterial as any).color.set(0xf56c6c);
|
|
|
|
|
|
let isAlarming = false;
|
|
|
let isMalfunctioning = false;
|
|
|
let isRestoring = false;
|
|
|
let alarmInterval: number | null = null;
|
|
|
|
|
|
// 报警控制函数 - 渐变闪烁版本
|
|
|
function startAlarm() {
|
|
|
// if (isAlarming) return;
|
|
|
// isAlarming = true;
|
|
|
|
|
|
object.material = alarmMaterial;
|
|
|
object.visible = true;
|
|
|
// 保存原始颜色
|
|
|
// const originalColor = object.material.color.clone();
|
|
|
|
|
|
const tl = gsap.timeline({ repeat: -1, yoyo: true });
|
|
|
|
|
|
// 只在黄色和透明之间切换,不显示原始颜色
|
|
|
tl.to(object.material, {
|
|
|
opacity: 0.7, // 完全不透明的黄色
|
|
|
duration: 0.5,
|
|
|
ease: 'sine.inOut'
|
|
|
}).to(object.material, {
|
|
|
opacity: 0.2, // 半透明的黄色
|
|
|
duration: 0.5,
|
|
|
ease: 'sine.inOut'
|
|
|
});
|
|
|
// object._alarmTimeline = tl;
|
|
|
// object._originalColor = originalColor; // 保存原始颜色用于恢复
|
|
|
}
|
|
|
|
|
|
//故障
|
|
|
function startMalfunction() {
|
|
|
// if (isMalfunctioning) return;
|
|
|
// isMalfunctioning = true;
|
|
|
|
|
|
object.material = faultMaterial;
|
|
|
object.visible = true;
|
|
|
// 保存原始颜色
|
|
|
// const originalColor = object.material.color.clone();
|
|
|
|
|
|
const tl = gsap.timeline({ repeat: -1, yoyo: true });
|
|
|
|
|
|
// 只在黄色和透明之间切换,不显示原始颜色
|
|
|
tl.to(object.material, {
|
|
|
opacity: 0.7, // 完全不透明的黄色
|
|
|
duration: 0.5,
|
|
|
ease: 'sine.inOut'
|
|
|
}).to(object.material, {
|
|
|
opacity: 0.2, // 半透明的黄色
|
|
|
duration: 0.5,
|
|
|
ease: 'sine.inOut'
|
|
|
});
|
|
|
// object._alarmTimeline = tl;
|
|
|
// object._originalColor = originalColor; // 保存原始颜色用于恢复
|
|
|
}
|
|
|
|
|
|
//还原
|
|
|
function restore() {
|
|
|
// if (!isRestoring) return;
|
|
|
// isRestoring = true;
|
|
|
|
|
|
if (alarmInterval !== null) {
|
|
|
clearInterval(alarmInterval);
|
|
|
alarmInterval = null;
|
|
|
}
|
|
|
|
|
|
// 恢复原始状态
|
|
|
object.visible = true;
|
|
|
object.material = originalMaterial;
|
|
|
}
|
|
|
|
|
|
//停止 - 隐藏对象以节约性能
|
|
|
function stop() {
|
|
|
// 停止所有动画
|
|
|
if (alarmInterval !== null) {
|
|
|
clearInterval(alarmInterval);
|
|
|
alarmInterval = null;
|
|
|
}
|
|
|
|
|
|
// 隐藏对象
|
|
|
object.visible = false;
|
|
|
|
|
|
// 也可以设置材质完全透明
|
|
|
// object.material.opacity = 0;
|
|
|
// object.material.transparent = true;
|
|
|
}
|
|
|
|
|
|
// 扩展对象类型以支持动态方法
|
|
|
(object as any).startAlarm = startAlarm;
|
|
|
(object as any).restore = restore;
|
|
|
(object as any).startMalfunction = startMalfunction;
|
|
|
(object as any).stop = stop;
|
|
|
|
|
|
// 返回对象本身,支持链式调用
|
|
|
return object;
|
|
|
}
|
|
|
|
|
|
function getAreaOneFloorGetName(areaOneFloorName: string): string {
|
|
|
switch (areaOneFloorName) {
|
|
|
case 'area_oneFloor_35kV':
|
|
|
return '35kV高压室';
|
|
|
case 'area_oneFloor_220kV':
|
|
|
return '220kV保护室';
|
|
|
case 'area_oneFloor_14':
|
|
|
return '蓄电池室';
|
|
|
case 'area_oneFloor_3':
|
|
|
return '站用电柜室';
|
|
|
case 'area_oneFloor_13':
|
|
|
return '380kV配电室';
|
|
|
case 'area_oneFloor_4':
|
|
|
return '1站用变室';
|
|
|
case 'area_oneFloor_6':
|
|
|
return '消弧线圈1室';
|
|
|
case 'area_oneFloor_7':
|
|
|
return '2号接地变室';
|
|
|
case 'area_oneFloor_8':
|
|
|
return '消弧线圈3室';
|
|
|
case 'area_oneFloor_9':
|
|
|
return '2站用变室';
|
|
|
case 'area_oneFloor_10':
|
|
|
return '110kV电缆间';
|
|
|
case 'area_oneFloor_12':
|
|
|
return '安全工器具间';
|
|
|
case 'area_oneFloor_11':
|
|
|
return '消弧线圈4室';
|
|
|
default:
|
|
|
return '未知';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function animate() {
|
|
|
requestAnimationFrame(animate);
|
|
|
orbitControls.update();
|
|
|
renderer.render(scene, camera);
|
|
|
}
|
|
|
|
|
|
// ResizeObserver 监听容器尺寸变化
|
|
|
let resizeObserver: ResizeObserver | null = null;
|
|
|
|
|
|
//补间动画
|
|
|
const timeline1 = gsap.timeline();
|
|
|
const timeline2 = gsap.timeline();
|
|
|
|
|
|
//相机移动
|
|
|
function tranlate(position: Vector3, target: Vector3) {
|
|
|
timeline1.to(camera.position, {
|
|
|
x: position.x,
|
|
|
y: position.y,
|
|
|
z: position.z,
|
|
|
duration: 1,
|
|
|
ease: 'power2.inOut'
|
|
|
});
|
|
|
|
|
|
timeline2.to(orbitControls.target, {
|
|
|
x: target.x,
|
|
|
y: target.y,
|
|
|
z: target.z,
|
|
|
duration: 1,
|
|
|
ease: 'power2.inOut'
|
|
|
});
|
|
|
}
|
|
|
|
|
|
function displayModel(modelName: string) {
|
|
|
tranlate(new Vector3(0, 14, 13.5), new Vector3(0, 0, 0));
|
|
|
if (modelName === 'area_oneFloor') {
|
|
|
areaOneFloorArr.forEach((obj) => {
|
|
|
obj.visible = true;
|
|
|
});
|
|
|
acoustoOpticArr.forEach((obj) => {
|
|
|
obj.visible = false;
|
|
|
});
|
|
|
manualArr.forEach((obj) => {
|
|
|
obj.visible = false;
|
|
|
});
|
|
|
smokeDetectorArr.forEach((obj) => {
|
|
|
obj.visible = false;
|
|
|
});
|
|
|
} else if (modelName === 'acoustoOptic') {
|
|
|
areaOneFloorArr.forEach((obj) => {
|
|
|
obj.visible = false;
|
|
|
});
|
|
|
acoustoOpticArr.forEach((obj) => {
|
|
|
obj.visible = true;
|
|
|
});
|
|
|
manualArr.forEach((obj) => {
|
|
|
obj.visible = false;
|
|
|
});
|
|
|
smokeDetectorArr.forEach((obj) => {
|
|
|
obj.visible = false;
|
|
|
});
|
|
|
} else if (modelName === 'manual') {
|
|
|
areaOneFloorArr.forEach((obj) => {
|
|
|
obj.visible = false;
|
|
|
});
|
|
|
acoustoOpticArr.forEach((obj) => {
|
|
|
obj.visible = false;
|
|
|
});
|
|
|
manualArr.forEach((obj) => {
|
|
|
obj.visible = true;
|
|
|
});
|
|
|
smokeDetectorArr.forEach((obj) => {
|
|
|
obj.visible = false;
|
|
|
});
|
|
|
} else if (modelName === 'smokeDetector') {
|
|
|
areaOneFloorArr.forEach((obj) => {
|
|
|
obj.visible = false;
|
|
|
});
|
|
|
acoustoOpticArr.forEach((obj) => {
|
|
|
obj.visible = false;
|
|
|
});
|
|
|
manualArr.forEach((obj) => {
|
|
|
obj.visible = false;
|
|
|
});
|
|
|
smokeDetectorArr.forEach((obj) => {
|
|
|
obj.visible = true;
|
|
|
});
|
|
|
} else if (modelName === 'refresh') {
|
|
|
areaOneFloorArr.forEach((obj) => {
|
|
|
obj.visible = false;
|
|
|
});
|
|
|
acoustoOpticArr.forEach((obj) => {
|
|
|
obj.visible = false;
|
|
|
});
|
|
|
manualArr.forEach((obj) => {
|
|
|
obj.visible = false;
|
|
|
});
|
|
|
smokeDetectorArr.forEach((obj) => {
|
|
|
obj.visible = false;
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 绑定
|
|
|
async function bindDevice(row: any) {
|
|
|
if (!bindingObj) ElMessage.warning('请选择要绑定的模型');
|
|
|
let id = null;
|
|
|
if (bindingObj.bindingMap && bindingObj.bindingMap.id) id = bindingObj.bindingMap.id;
|
|
|
|
|
|
const response = await modelApi.binding_post({ id, objId: bindingObj.name, facilityId: row.id });
|
|
|
if (response.code == 200 && response.data == true) {
|
|
|
emitter.emit('binding-data-update');
|
|
|
// bindingObj.name
|
|
|
// 根据 meshName 匹配并修改 sprite 颜色
|
|
|
const matchedController = meshControllerArr.find((item) => item.meshName === bindingObj.name);
|
|
|
if (matchedController && matchedController.sprite) {
|
|
|
// 获取原来 sprite 的位置
|
|
|
debugger;
|
|
|
const oldPosition = matchedController.sprite.position.clone();
|
|
|
// 移除原来的 sprite
|
|
|
scene.remove(matchedController.sprite);
|
|
|
// 创建新的淡绿色精灵,位置保持一致
|
|
|
const newSprite = addSprite(
|
|
|
'已绑定',
|
|
|
bindingColor,
|
|
|
new Vector3(oldPosition.x, oldPosition.y - 0.7, oldPosition.z)
|
|
|
);
|
|
|
// 更新控制器中的sprite引用
|
|
|
matchedController.sprite = newSprite;
|
|
|
}
|
|
|
|
|
|
ElMessage.success('绑定成功');
|
|
|
} else ElMessage.error('绑定失败');
|
|
|
dialogTableVisible.value = false;
|
|
|
}
|
|
|
|
|
|
async function getBinding(objId: string | null, facilityId: string | null) {
|
|
|
const response = await modelApi.getBinding_post({ objId: objId, facilityId: facilityId });
|
|
|
if (response.code == 200 && response.data) {
|
|
|
return response.data;
|
|
|
}
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
let bindingInfo: any = reactive({});
|
|
|
//获取绑定信息
|
|
|
function getBindingInfo(facilityId: string) {
|
|
|
console.log('获取绑定信息:', facilityId, arr);
|
|
|
for (const item of arr.value) {
|
|
|
if (item.id == facilityId) {
|
|
|
// 使用 Object.assign 保持响应式
|
|
|
Object.assign(bindingInfo, {
|
|
|
currentState: item.currentState,
|
|
|
eState: item.eState,
|
|
|
floorName: item.floorName,
|
|
|
id: item.id,
|
|
|
name: item.name,
|
|
|
operation: item.operation,
|
|
|
roomId: item.roomId
|
|
|
});
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function clearBoxHelper(boxHelper: BoxHelper | null) {
|
|
|
// 正确的资源清理顺序
|
|
|
if (boxHelper) {
|
|
|
scene.remove(boxHelper); // 从场景中移除
|
|
|
boxHelper.geometry.dispose(); // 释放几何体内存
|
|
|
if (boxHelper.material) {
|
|
|
if (Array.isArray(boxHelper.material)) {
|
|
|
boxHelper.material.forEach((material) => material.dispose());
|
|
|
} else {
|
|
|
boxHelper.material.dispose();
|
|
|
}
|
|
|
}
|
|
|
boxHelper = null; // 清空引用
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//绑定的元素
|
|
|
let bindingObj: any = null;
|
|
|
let boxHelper = <BoxHelper | null>null;
|
|
|
|
|
|
let cardisOpen = ref(false);
|
|
|
let moduleType = ref('');
|
|
|
onMounted(() => {
|
|
|
const container = containerRef.value!;
|
|
|
container.appendChild(renderer.domElement);
|
|
|
|
|
|
// 初始化摄像机和渲染器尺寸
|
|
|
camera.aspect = container.clientWidth / container.clientHeight;
|
|
|
camera.updateProjectionMatrix();
|
|
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
|
|
|
|
// 监听容器尺寸变化
|
|
|
resizeObserver = new ResizeObserver(() => {
|
|
|
camera.aspect = container.clientWidth / container.clientHeight;
|
|
|
camera.updateProjectionMatrix();
|
|
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
|
});
|
|
|
resizeObserver.observe(container);
|
|
|
|
|
|
// 创建射线检测器
|
|
|
const raycaster = new Raycaster();
|
|
|
const mouse = new Vector2();
|
|
|
|
|
|
// 添加点击事件监听
|
|
|
renderer.domElement.addEventListener('click', async (event: MouseEvent) => {
|
|
|
const rect = container.getBoundingClientRect();
|
|
|
const x = event.clientX - rect.left;
|
|
|
const y = event.clientY - rect.top;
|
|
|
|
|
|
mouse.x = (x / container.clientWidth) * 2 - 1;
|
|
|
mouse.y = -(y / container.clientHeight) * 2 + 1;
|
|
|
|
|
|
raycaster.setFromCamera(mouse, camera);
|
|
|
// 合并所有需要检测的数组并过滤出Mesh类型
|
|
|
const targetObjects = [...moveArr].filter((obj) => obj.visible);
|
|
|
const intersects = raycaster.intersectObjects(targetObjects, true);
|
|
|
|
|
|
// const targetScenes = scene.children.filter((obj) => obj.name == 'Scene');
|
|
|
// console.log('targetScenes[0].children:', targetScenes[0].children);
|
|
|
// const intersects = raycaster.intersectObjects(targetScenes[0].children, true);
|
|
|
|
|
|
if (intersects.length > 0) {
|
|
|
let position: Vector3 | undefined;
|
|
|
let x = 0;
|
|
|
let y = 0;
|
|
|
let z = 0;
|
|
|
let targetObj: any;
|
|
|
if (
|
|
|
intersects[0].object.name.includes('acoustoSon') ||
|
|
|
intersects[0].object.name.includes('manualSon') ||
|
|
|
intersects[0].object.name.includes('smokeSon')
|
|
|
) {
|
|
|
targetObj = intersects[0].object.parent;
|
|
|
position = intersects[0].object.parent?.position || new Vector3(0, 0, 0);
|
|
|
x = -0.3;
|
|
|
y = 0.5;
|
|
|
z = 0.5;
|
|
|
} else {
|
|
|
targetObj = intersects[0].object;
|
|
|
position = intersects[0].object.position;
|
|
|
x = -1;
|
|
|
y = 3;
|
|
|
z = 3;
|
|
|
}
|
|
|
targetObj.bindingMap = await getBinding(targetObj.name, null);
|
|
|
cardisOpen.value = true;
|
|
|
moduleType.value = targetObj.name;
|
|
|
bindingObj = targetObj;
|
|
|
if (targetObj.bindingMap) getBindingInfo(targetObj.bindingMap.facilityId);
|
|
|
else
|
|
|
Object.keys(bindingInfo).forEach((key) => {
|
|
|
delete bindingInfo[key];
|
|
|
});
|
|
|
if (position) {
|
|
|
tranlate(
|
|
|
new Vector3(position.x + x, position.y + y, position.z + z),
|
|
|
new Vector3(position.x, position.y, position.z)
|
|
|
);
|
|
|
}
|
|
|
clearBoxHelper(boxHelper);
|
|
|
// 创建包围盒辅助器
|
|
|
boxHelper = new BoxHelper(targetObj, 0xffff00);
|
|
|
scene.add(boxHelper);
|
|
|
} else {
|
|
|
cardisOpen.value = false;
|
|
|
moduleType.value = '';
|
|
|
bindingObj = null;
|
|
|
clearBoxHelper(boxHelper);
|
|
|
// 清空对象而不是重新创建
|
|
|
Object.keys(bindingInfo).forEach((key) => {
|
|
|
delete bindingInfo[key];
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
|
|
|
animate();
|
|
|
});
|
|
|
|
|
|
function getModelDroupName(moduleType: string): string {
|
|
|
if (moduleType.includes('acoustoOptic')) return '声光报警器';
|
|
|
else if (moduleType.includes('manual')) return '手动报警器';
|
|
|
else if (moduleType.includes('smokeDetector')) return '烟感报警器';
|
|
|
else return '未知';
|
|
|
}
|
|
|
|
|
|
emitter.on('binding-obj-facility-move', async (value: any) => {
|
|
|
const response = await getBinding(null, value);
|
|
|
if (!response) ElMessage.warning('该数据未绑定模型');
|
|
|
else {
|
|
|
console.log('getBinding:', response);
|
|
|
let targetObj: any;
|
|
|
let targetControllerObj: any;
|
|
|
targetObj = moveArr.find((obj) => obj.name === response.objId);
|
|
|
cardisOpen.value = true;
|
|
|
targetObj.visible = true;
|
|
|
|
|
|
//meshControllerArr
|
|
|
moduleType.value = targetObj.name;
|
|
|
|
|
|
targetControllerObj = meshControllerArr.find((obj) => obj.meshName === response.objId);
|
|
|
targetControllerObj.geometry.visible = true;
|
|
|
|
|
|
getBindingInfo(response.facilityId);
|
|
|
clearBoxHelper(boxHelper);
|
|
|
// 创建包围盒辅助器
|
|
|
boxHelper = new BoxHelper(targetObj, 0xffff00);
|
|
|
scene.add(boxHelper);
|
|
|
|
|
|
// position
|
|
|
let position = targetObj?.position;
|
|
|
if (position) {
|
|
|
tranlate(
|
|
|
new Vector3(position.x - 0.3, position.y + 0.5, position.z + 0.5),
|
|
|
new Vector3(position.x, position.y, position.z)
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
console.log('删除');
|
|
|
if (resizeObserver) {
|
|
|
resizeObserver.disconnect();
|
|
|
resizeObserver = null;
|
|
|
}
|
|
|
emitter.off('binding-obj-facility-move');
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
.scene-wrapper {
|
|
|
position: relative;
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
}
|
|
|
|
|
|
.container {
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
position: relative;
|
|
|
}
|
|
|
|
|
|
.control-panel {
|
|
|
position: absolute;
|
|
|
top: 50%;
|
|
|
right: 2%;
|
|
|
transform: translateY(-50%); /* 垂直居中 */
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 20px;
|
|
|
z-index: 101;
|
|
|
align-items: flex-end; /* 改为右对齐 */
|
|
|
/* background: rgba(0, 0, 0, 0.7); */
|
|
|
width: 35%;
|
|
|
height: 70%;
|
|
|
}
|
|
|
|
|
|
.control-buttons {
|
|
|
position: absolute;
|
|
|
top: 50%;
|
|
|
left: 2%;
|
|
|
transform: translateY(-50%); /* 垂直居中 */
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 20px;
|
|
|
z-index: 100;
|
|
|
align-items: flex-start; /* 确保所有按钮左对齐 */
|
|
|
}
|
|
|
|
|
|
/* Element Plus圆形按钮样式调整 */
|
|
|
.control-buttons :deep(.el-button) {
|
|
|
width: 40px !important;
|
|
|
height: 40px !important;
|
|
|
min-width: 40px !important;
|
|
|
padding: 0 !important;
|
|
|
display: flex !important;
|
|
|
align-items: center !important;
|
|
|
margin: 0 !important;
|
|
|
box-sizing: border-box !important;
|
|
|
}
|
|
|
|
|
|
.control-btn {
|
|
|
padding: 8px 16px;
|
|
|
background: rgba(0, 0, 0, 0.7);
|
|
|
color: white;
|
|
|
border: 1px solid #4fc3f7;
|
|
|
border-radius: 4px;
|
|
|
cursor: pointer;
|
|
|
font-size: 14px;
|
|
|
transition: all 0.3s ease;
|
|
|
min-width: 80px;
|
|
|
}
|
|
|
|
|
|
.card {
|
|
|
/* height: 100%; */
|
|
|
width: 100%;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
margin: 0;
|
|
|
padding: 0;
|
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.5) !important;
|
|
|
backdrop-filter: blur(5px);
|
|
|
border: none;
|
|
|
box-shadow: 0 2px 12px rgba(255, 0, 0, 0.1);
|
|
|
}
|
|
|
|
|
|
.card :deep(.el-card__body) {
|
|
|
flex: 1;
|
|
|
overflow-y: auto;
|
|
|
padding: 10px;
|
|
|
}
|
|
|
|
|
|
.cardHead {
|
|
|
margin: 0 0 15px 0;
|
|
|
padding: 0;
|
|
|
font-size: 16px;
|
|
|
font-weight: bold;
|
|
|
text-align: center;
|
|
|
color: #ffffff;
|
|
|
}
|
|
|
|
|
|
.cardBody {
|
|
|
font-size: 12px;
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.info-row {
|
|
|
margin-bottom: 5px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
width: 100%;
|
|
|
font-size: 12px;
|
|
|
|
|
|
/* border: 1px solid red; */
|
|
|
margin-bottom: 5px !important;
|
|
|
padding: 0 !important;
|
|
|
}
|
|
|
|
|
|
/* 让组件继承父容器宽度 */
|
|
|
.full-width-component {
|
|
|
width: 100% !important;
|
|
|
max-width: 100% !important;
|
|
|
}
|
|
|
|
|
|
.equipment-container {
|
|
|
width: 100%;
|
|
|
height: auto;
|
|
|
}
|
|
|
|
|
|
/* 确保设备组件行与信息行宽度一致 */
|
|
|
.equipment-row {
|
|
|
width: 100%;
|
|
|
margin-top: 10px; /* 与其他行保持间距 */
|
|
|
}
|
|
|
|
|
|
/* 确保设备组件继承父容器宽度 */
|
|
|
.equipment-wrapper {
|
|
|
width: 100% !important;
|
|
|
height: 150px; /* 设置固定高度或根据需要调整 */
|
|
|
position: relative;
|
|
|
}
|
|
|
|
|
|
.equipment-wrapper :deep(.div3D) {
|
|
|
width: 100% !important;
|
|
|
height: 100% !important;
|
|
|
}
|
|
|
|
|
|
.equipment-component {
|
|
|
width: 100% !important;
|
|
|
display: block !important;
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
/* 确保所有行都有相同的宽度基准 */
|
|
|
.info-row,
|
|
|
.equipment-row {
|
|
|
max-width: 100%;
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
/* 控制标签文字大小 */
|
|
|
.info-row :deep(.el-tag) {
|
|
|
font-size: 10.5px;
|
|
|
}
|
|
|
|
|
|
/* 统一内边距 */
|
|
|
.cardBody {
|
|
|
/* padding: 10px; */
|
|
|
}
|
|
|
|
|
|
.cardBody .el-row {
|
|
|
width: 100%;
|
|
|
margin-bottom: 2px;
|
|
|
}
|
|
|
|
|
|
/* 控制表头文字大小 */
|
|
|
.full-height-table :deep(.el-table__header th) {
|
|
|
font-size: 13px;
|
|
|
font-weight: bold;
|
|
|
}
|
|
|
|
|
|
/* 控制表格内容文字大小 */
|
|
|
.full-height-table :deep(.el-table__body td) {
|
|
|
font-size: 11.5px;
|
|
|
color: 12px;
|
|
|
}
|
|
|
|
|
|
/* 控制标签文字大小 */
|
|
|
.full-height-table :deep(.el-tag) {
|
|
|
font-size: 11px;
|
|
|
}
|
|
|
|
|
|
/* 控制按钮文字大小 */
|
|
|
.full-height-table :deep(.el-button) {
|
|
|
font-size: 11px;
|
|
|
}
|
|
|
|
|
|
.operation-btn {
|
|
|
font-size: 10px;
|
|
|
width: 50px;
|
|
|
min-width: 55px;
|
|
|
height: 20px;
|
|
|
padding: 8px 12px;
|
|
|
}
|
|
|
|
|
|
.info-row :deep(.el-col) {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
}
|
|
|
|
|
|
.info-row :deep(.el-tag) {
|
|
|
justify-content: left;
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
|
max-width: 100%;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
/* height: 100%; */
|
|
|
}
|
|
|
</style>
|