Files
qhmes/jeecgboot-vue3/src/views/workshop/Workshop3D.vue

268 lines
6.8 KiB
Vue
Raw Normal View History

2026-04-03 09:56:14 +08:00
<template>
<div class="workshop-container">
<div ref="container" class="three-container"></div>
<!-- 信息面板 -->
<div v-if="selectedCube" class="info-panel">
<div class="info-header">
<h3>设备信息</h3>
<button @click="selectedCube = null" class="close-btn">×</button>
</div>
<div class="info-content">
<p><strong>设备名称:</strong> {{ selectedCube.name }}</p>
<p><strong>设备ID:</strong> {{ selectedCube.id }}</p>
<p><strong>状态:</strong> <span :style="{ color: selectedCube.statusColor }">{{ selectedCube.status }}</span></p>
<p><strong>位置:</strong> X: {{ selectedCube.position.x.toFixed(2) }}, Y: {{ selectedCube.position.y.toFixed(2) }}, Z: {{ selectedCube.position.z.toFixed(2) }}</p>
<p><strong>温度:</strong> {{ selectedCube.temperature }}°C</p>
<p><strong>运行时长:</strong> {{ selectedCube.runtime }}小时</p>
</div>
</div>
</div>
</template>
<script setup>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { onMounted, onBeforeUnmount, ref } from 'vue'
const container = ref(null)
const selectedCube = ref(null)
let scene, camera, renderer, controls, animationId
let cubes = []
let raycaster, mouse
// 设备数据
const cubeData = [
{ id: 'DEV-001', name: '加工中心A', status: '运行中', statusColor: '#52c41a', temperature: 45, runtime: 1234, color: 0x1890ff },
{ id: 'DEV-002', name: '加工中心B', status: '待机', statusColor: '#faad14', temperature: 28, runtime: 2456, color: 0x52c41a },
{ id: 'DEV-003', name: '装配机器人', status: '运行中', statusColor: '#52c41a', temperature: 38, runtime: 3678, color: 0xf5222d },
{ id: 'DEV-004', name: '质检设备', status: '故障', statusColor: '#f5222d', temperature: 65, runtime: 890, color: 0xfa8c16 },
{ id: 'DEV-005', name: '包装机', status: '运行中', statusColor: '#52c41a', temperature: 42, runtime: 5432, color: 0x722ed1 }
]
onMounted(() => {
initScene()
createCubes()
setupInteraction()
animate()
window.addEventListener('resize', onWindowResize)
})
onBeforeUnmount(() => {
cancelAnimationFrame(animationId)
window.removeEventListener('resize', onWindowResize)
window.removeEventListener('click', onMouseClick)
controls.dispose()
renderer.dispose()
scene.clear()
})
function initScene() {
// 场景
scene = new THREE.Scene()
scene.background = new THREE.Color(0xf0f2f5)
// 相机
camera = new THREE.PerspectiveCamera(
75,
container.value.clientWidth / container.value.clientHeight,
0.1,
1000
)
camera.position.set(8, 8, 8)
camera.lookAt(0, 0, 0)
// 渲染器
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(container.value.clientWidth, container.value.clientHeight)
renderer.shadowMap.enabled = true
container.value.appendChild(renderer.domElement)
// 轨道控制器
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
// 光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
directionalLight.position.set(10, 10, 5)
directionalLight.castShadow = true
scene.add(directionalLight)
// 地面
const groundGeometry = new THREE.PlaneGeometry(20, 20)
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc })
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
ground.rotation.x = -Math.PI / 2
ground.receiveShadow = true
scene.add(ground)
// 网格辅助线
const gridHelper = new THREE.GridHelper(20, 20, 0x888888, 0xdddddd)
scene.add(gridHelper)
}
function createCubes() {
const positions = [
{ x: -4, y: 1, z: -4 },
{ x: 4, y: 1, z: -4 },
{ x: 0, y: 1, z: 0 },
{ x: -4, y: 1, z: 4 },
{ x: 4, y: 1, z: 4 }
]
cubeData.forEach((data, index) => {
const geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5)
const material = new THREE.MeshStandardMaterial({
color: data.color,
metalness: 0.3,
roughness: 0.4
})
const cube = new THREE.Mesh(geometry, material)
cube.position.set(positions[index].x, positions[index].y, positions[index].z)
cube.castShadow = true
cube.receiveShadow = true
// 存储设备数据
cube.userData = {
...data,
position: positions[index],
originalColor: data.color
}
scene.add(cube)
cubes.push(cube)
})
}
function setupInteraction() {
raycaster = new THREE.Raycaster()
mouse = new THREE.Vector2()
window.addEventListener('click', onMouseClick)
}
function onMouseClick(event) {
const rect = container.value.getBoundingClientRect()
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
raycaster.setFromCamera(mouse, camera)
const intersects = raycaster.intersectObjects(cubes)
// 重置所有立方体颜色
cubes.forEach(cube => {
cube.material.color.setHex(cube.userData.originalColor)
cube.material.emissive.setHex(0x000000)
})
if (intersects.length > 0) {
const clickedCube = intersects[0].object
// 高亮选中的立方体
clickedCube.material.emissive.setHex(0x555555)
// 显示信息面板
selectedCube.value = clickedCube.userData
} else {
selectedCube.value = null
}
}
function onWindowResize() {
camera.aspect = container.value.clientWidth / container.value.clientHeight
camera.updateProjectionMatrix()
renderer.setSize(container.value.clientWidth, container.value.clientHeight)
}
function animate() {
animationId = requestAnimationFrame(animate)
// 轻微旋转动画
cubes.forEach((cube, index) => {
cube.rotation.y += 0.005 * (index % 2 === 0 ? 1 : -1)
})
controls.update()
renderer.render(scene, camera)
}
</script>
<style scoped>
.workshop-container {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
}
.three-container {
width: 100%;
height: 100%;
}
.info-panel {
position: absolute;
top: 20px;
right: 20px;
width: 320px;
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
overflow: hidden;
}
.info-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: #1890ff;
color: white;
}
.info-header h3 {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.close-btn {
background: none;
border: none;
color: white;
font-size: 28px;
cursor: pointer;
padding: 0;
width: 28px;
height: 28px;
line-height: 1;
transition: opacity 0.2s;
}
.close-btn:hover {
opacity: 0.8;
}
.info-content {
padding: 20px;
}
.info-content p {
margin: 12px 0;
font-size: 14px;
line-height: 1.6;
}
.info-content strong {
color: #333;
font-weight: 500;
}
</style>