|
|
<template>
|
|
|
<div
|
|
|
id="plane"
|
|
|
:style="{
|
|
|
left: infoRef.style.left + 'px',
|
|
|
top: infoRef.style.top + 'px',
|
|
|
display: infoRef.style.display
|
|
|
}"
|
|
|
>
|
|
|
<p>机柜名称:{{ infoRef.curCabinet.name }}</p>
|
|
|
<p>机柜温度:{{ infoRef.curCabinet.temperature }}°</p>
|
|
|
<p>使用情况:{{ infoRef.curCabinet.count }}/{{ infoRef.curCabinet.capacity }}</p>
|
|
|
</div>
|
|
|
<div ref="containerRef" class="container"></div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
import {
|
|
|
WebGLRenderer,
|
|
|
PerspectiveCamera,
|
|
|
Scene,
|
|
|
Color,
|
|
|
Texture,
|
|
|
Mesh,
|
|
|
TextureLoader,
|
|
|
MeshBasicMaterial,
|
|
|
Raycaster,
|
|
|
Vector2,
|
|
|
MeshStandardMaterial,
|
|
|
Object3D,
|
|
|
RepeatWrapping
|
|
|
} from 'three';
|
|
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
|
|
|
import { ref, reactive, onMounted, onUnmounted, watchEffect } from 'vue';
|
|
|
import TWEEN from '@tweenjs/tween.js';
|
|
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
|
|
|
|
|
|
const containerRef = ref<HTMLDivElement>();
|
|
|
const rendererRef = ref<WebGLRenderer>();
|
|
|
const cameraRef = ref<PerspectiveCamera>();
|
|
|
const controlRef = ref<OrbitControls>();
|
|
|
const maps: Map<string, Texture> = new Map();
|
|
|
const cabinets: Mesh[] = [];
|
|
|
const scene = new Scene();
|
|
|
|
|
|
const raycaster = new Raycaster();
|
|
|
const pointer = new Vector2();
|
|
|
const curCabinetRef = ref<Mesh | null>();
|
|
|
const infoRef = reactive({
|
|
|
style: { left: 0, top: 0, display: 'none' },
|
|
|
curCabinet: {
|
|
|
name: 'Loading……',
|
|
|
temperature: 0,
|
|
|
capacity: 0,
|
|
|
count: 0
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// 初始化相机
|
|
|
function initCamera() {
|
|
|
const container = containerRef.value!;
|
|
|
cameraRef.value = new PerspectiveCamera(
|
|
|
45,
|
|
|
container.clientWidth / container.clientHeight, // 使用容器宽高比
|
|
|
0.1,
|
|
|
1000
|
|
|
);
|
|
|
const start = { x: 0, y: 0, z: 0 };
|
|
|
new TWEEN.Tween(start)
|
|
|
.to({ x: 0, y: 10, z: 15 }, 800)
|
|
|
.easing(TWEEN.Easing.Quadratic.InOut)
|
|
|
.onUpdate(() => {
|
|
|
const { x, y, z } = start;
|
|
|
cameraRef.value?.position.set(x, y, z);
|
|
|
})
|
|
|
.start();
|
|
|
controlRef.value = new OrbitControls(cameraRef.value, rendererRef.value?.domElement);
|
|
|
controlRef.value.autoRotate = false;
|
|
|
controlRef.value.autoRotateSpeed = 1;
|
|
|
controlRef.value.dampingFactor = 0.2;
|
|
|
controlRef.value.rotateSpeed = 0.3; // 降低旋转速度(默认为 1.0)
|
|
|
|
|
|
controlRef.value.enableDamping = true;
|
|
|
controlRef.value.enableRotate = true;
|
|
|
|
|
|
controlRef.value.minDistance = 10;
|
|
|
controlRef.value.maxPolarAngle = Math.PI * 0.5;
|
|
|
}
|
|
|
|
|
|
// 初始化渲染器
|
|
|
function initRenderer() {
|
|
|
const container = containerRef.value!;
|
|
|
rendererRef.value = new WebGLRenderer({
|
|
|
antialias: true, // 启用抗锯齿
|
|
|
alpha: true // 启用透明背景
|
|
|
});
|
|
|
rendererRef.value.setClearColor(new Color('#212d36'));
|
|
|
rendererRef.value.setSize(container.clientWidth, container.clientHeight); // 使用容器尺寸
|
|
|
rendererRef.value.shadowMap.enabled = true;
|
|
|
}
|
|
|
|
|
|
// 渲染循环
|
|
|
function render() {
|
|
|
requestAnimationFrame(render);
|
|
|
if (cameraRef.value) {
|
|
|
rendererRef.value?.render(scene, cameraRef.value);
|
|
|
}
|
|
|
controlRef.value?.update();
|
|
|
TWEEN.update();
|
|
|
}
|
|
|
|
|
|
// 纹理加载
|
|
|
function crtTexture(imgName: string) {
|
|
|
let curTexture = maps.get(imgName);
|
|
|
if (!curTexture) {
|
|
|
curTexture = new TextureLoader().load('/models/' + imgName);
|
|
|
curTexture.flipY = false;
|
|
|
curTexture.wrapS = RepeatWrapping;
|
|
|
curTexture.wrapT = RepeatWrapping;
|
|
|
maps.set(imgName, curTexture);
|
|
|
}
|
|
|
return curTexture;
|
|
|
}
|
|
|
|
|
|
// 材质切换
|
|
|
function changeMat(obj: Mesh, map: Texture | null, color: Color) {
|
|
|
if (map) {
|
|
|
obj.material = new MeshBasicMaterial({
|
|
|
map: crtTexture(map.name)
|
|
|
});
|
|
|
} else {
|
|
|
obj.material = new MeshBasicMaterial({ color });
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 鼠标交互逻辑
|
|
|
function onMouseOverCabinet({ name }: { name: string }) {
|
|
|
infoRef.style.display = 'block';
|
|
|
infoRef.curCabinet.name = name;
|
|
|
infoRef.curCabinet.temperature = Math.round(Math.random() * 100);
|
|
|
const capacity = Math.ceil(200 * Math.random());
|
|
|
infoRef.curCabinet.capacity = capacity;
|
|
|
infoRef.curCabinet.count = capacity - 5;
|
|
|
}
|
|
|
|
|
|
function onMouseOutCabinet() {
|
|
|
infoRef.style.display = 'none';
|
|
|
}
|
|
|
|
|
|
//原逻辑
|
|
|
function onMouseMoveCabinet(left: number, top: number) {
|
|
|
infoRef.style.left = left;
|
|
|
infoRef.style.top = top;
|
|
|
}
|
|
|
|
|
|
//原逻辑
|
|
|
// function selectCabinet(x: number, y: number) {
|
|
|
// const container = rendererRef.value!.domElement;
|
|
|
// const { width, height } = container;
|
|
|
// pointer.set((x / width) * 2 - 1, -(y / height) * 2 + 1);
|
|
|
// if (cameraRef.value) {
|
|
|
// raycaster.setFromCamera(pointer, cameraRef.value);
|
|
|
// }
|
|
|
// const intersect = raycaster.intersectObjects(cabinets)[0];
|
|
|
// let intersectObj = intersect ? (intersect.object as Mesh) : null;
|
|
|
|
|
|
// if (curCabinetRef.value && curCabinetRef.value !== intersectObj) {
|
|
|
// const material = curCabinetRef.value.material as MeshBasicMaterial;
|
|
|
// material.setValues({
|
|
|
// map: maps.get('cabinet.jpg')
|
|
|
// });
|
|
|
// }
|
|
|
|
|
|
// if (intersectObj) {
|
|
|
// onMouseMoveCabinet(x, y);
|
|
|
// if (intersectObj !== curCabinetRef.value) {
|
|
|
// curCabinetRef.value = intersectObj;
|
|
|
// const material = intersectObj.material as MeshBasicMaterial;
|
|
|
// material.setValues({
|
|
|
// map: maps.get('cabinet-hover.jpg')
|
|
|
// });
|
|
|
// onMouseOverCabinet(intersectObj);
|
|
|
// }
|
|
|
// } else if (curCabinetRef.value) {
|
|
|
// curCabinetRef.value = null;
|
|
|
// onMouseOutCabinet();
|
|
|
// }
|
|
|
// }
|
|
|
|
|
|
function selectCabinet(x: number, y: number) {
|
|
|
const container = rendererRef.value!.domElement;
|
|
|
const { width, height } = container;
|
|
|
|
|
|
// 鼠标坐标转裁剪坐标(注意:Y 轴翻转)
|
|
|
pointer.set((x / width) * 2 - 1, -(y / height) * 2 + 1);
|
|
|
|
|
|
if (cameraRef.value) {
|
|
|
raycaster.setFromCamera(pointer, cameraRef.value);
|
|
|
}
|
|
|
|
|
|
const intersect = raycaster.intersectObjects(cabinets)[0];
|
|
|
let intersectObj = intersect ? (intersect.object as Mesh) : null;
|
|
|
|
|
|
if (curCabinetRef.value && curCabinetRef.value !== intersectObj) {
|
|
|
const material = curCabinetRef.value.material as MeshBasicMaterial;
|
|
|
material.setValues({
|
|
|
map: maps.get('cabinet.jpg')
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if (intersectObj) {
|
|
|
onMouseMoveCabinet(x, y);
|
|
|
if (intersectObj !== curCabinetRef.value) {
|
|
|
curCabinetRef.value = intersectObj;
|
|
|
const material = intersectObj.material as MeshBasicMaterial;
|
|
|
material.setValues({
|
|
|
map: maps.get('cabinet-hover.jpg')
|
|
|
});
|
|
|
onMouseOverCabinet(intersectObj);
|
|
|
}
|
|
|
} else if (curCabinetRef.value) {
|
|
|
curCabinetRef.value = null;
|
|
|
onMouseOutCabinet();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//原逻辑
|
|
|
// function mouseMove({ clientX, clientY }: { clientX: number; clientY: number }) {
|
|
|
// console.log('mouseMove', clientX, clientY);
|
|
|
// selectCabinet(clientX, clientY);
|
|
|
// }
|
|
|
|
|
|
function mouseMove(event: MouseEvent) {
|
|
|
const container = containerRef.value!;
|
|
|
const rect = container.getBoundingClientRect(); // 获取容器相对于视口的位置
|
|
|
const x = event.clientX - rect.left; // 相对于容器的 X 坐标
|
|
|
const y = event.clientY - rect.top; // 相对于容器的 Y 坐标
|
|
|
|
|
|
selectCabinet(x, y);
|
|
|
}
|
|
|
|
|
|
// 尺寸变化监听
|
|
|
let resizeObserver: ResizeObserver | null = null;
|
|
|
|
|
|
onMounted(() => {
|
|
|
crtTexture('cabinet-hover.jpg');
|
|
|
containerRef.value!.onmousemove = mouseMove;
|
|
|
|
|
|
const loader = new GLTFLoader();
|
|
|
loader.load('/models/machineRoom.gltf', (gltf: any) => {
|
|
|
gltf.scene.children.forEach((obj: Object3D) => {
|
|
|
console.log('obj', obj);
|
|
|
if (obj instanceof Mesh) {
|
|
|
const { map, color } = obj.material as MeshStandardMaterial;
|
|
|
changeMat(obj, map, color);
|
|
|
if (obj.name.includes('cabinet')) {
|
|
|
cabinets.push(obj);
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
scene.add(...gltf.scene.children);
|
|
|
});
|
|
|
|
|
|
initRenderer();
|
|
|
initCamera();
|
|
|
render();
|
|
|
|
|
|
// 监听容器尺寸变化
|
|
|
resizeObserver = new ResizeObserver(() => {
|
|
|
const container = containerRef.value!;
|
|
|
cameraRef.value!.aspect = container.clientWidth / container.clientHeight;
|
|
|
cameraRef.value!.updateProjectionMatrix();
|
|
|
rendererRef.value!.setSize(container.clientWidth, container.clientHeight);
|
|
|
});
|
|
|
resizeObserver.observe(containerRef.value!);
|
|
|
});
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
// 清理 ResizeObserver
|
|
|
if (resizeObserver) {
|
|
|
resizeObserver.disconnect();
|
|
|
resizeObserver = null;
|
|
|
}
|
|
|
});
|
|
|
|
|
|
watchEffect(() => {
|
|
|
if (rendererRef.value) {
|
|
|
containerRef.value?.append(rendererRef.value.domElement);
|
|
|
}
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
.container {
|
|
|
position: relative; /* 确保容器定位正确 */
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
overflow: hidden; /* 防止滚动条干扰 */
|
|
|
}
|
|
|
|
|
|
#plane {
|
|
|
position: absolute;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
|
color: #b1acac;
|
|
|
padding: 0 18px;
|
|
|
transform: translate(12px, -100%);
|
|
|
display: block;
|
|
|
z-index: 9999; /* 提高层级,确保在最上层 */
|
|
|
}
|
|
|
</style>
|