268 lines
6.8 KiB
Vue
268 lines
6.8 KiB
Vue
<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>
|