You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
maotu-webtopo/src/components/vue-xq-test/vue-three-groundFloor.vue

1522 lines
44 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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>