Files
qhmes/jeecgboot-vue3/src/views/workshop/WorkshopVisualization.vue
2026-04-03 09:56:14 +08:00

561 lines
15 KiB
Vue

<template>
<div class="workshop-visualization">
<div class="workshop-container">
<!-- 3D 场景容器 -->
<div ref="sceneContainer" class="scene-container"></div>
<!-- 顶部状态栏 -->
<div class="top-bar">
<a-space direction="vertical" :size="4">
<div class="title">智能车间可视化系统</div>
<div class="subtitle">实时监控 · 数据驱动 · 智能分析</div>
</a-space>
<div class="time-display">{{ currentTime }}</div>
</div>
<!-- 左侧面板 - 设备状态 -->
<div class="left-panel">
<DeviceStatusPanel :devices="deviceData" @select-device="handleSelectDevice" />
</div>
<!-- 右侧面板 - 生产数据 -->
<div class="right-panel">
<ProductionDataPanel :productionData="productionData" />
</div>
<!-- 底部工具栏 -->
<div class="bottom-toolbar">
<a-space>
<a-button type="primary" @click="resetCamera">重置视角</a-button>
<a-button @click="toggleAutoRotate">自动旋转: {{ autoRotate ? '' : '' }}</a-button>
<a-button @click="toggleWireframe">线框模式</a-button>
<a-button @click="refreshData">刷新数据</a-button>
</a-space>
</div>
<!-- 设备详情弹窗 -->
<a-modal v-model:open="showDeviceDetail" title="设备详情" :footer="null" width="600px">
<DeviceDetail :device="selectedDevice" @close="showDeviceDetail = false" />
</a-modal>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import DeviceStatusPanel from './components/DeviceStatusPanel.vue';
import ProductionDataPanel from './components/ProductionDataPanel.vue';
import DeviceDetail from './components/DeviceDetail.vue';
// 时间显示
const currentTime = ref('');
const updateTime = () => {
const now = new Date();
currentTime.value = now.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
};
updateTime();
const timer = setInterval(updateTime, 1000);
// Three.js 相关变量
const sceneContainer = ref<HTMLDivElement>();
let scene: THREE.Scene | null = null;
let camera: THREE.PerspectiveCamera | null = null;
let renderer: THREE.WebGLRenderer | null = null;
let controls: OrbitControls | null = null;
let animationId: number | null = null;
const autoRotate = ref(true);
const wireframeMode = ref(false);
// 设备数据
const deviceData = ref([
{
id: 'DEV001',
name: '数控机床 #1',
type: 'CNC机床',
status: 'running',
temperature: 65,
efficiency: 92,
location: { x: 0, y: 0, z: 0 },
production: 150,
maintenance: '正常',
operator: '张工',
},
{
id: 'DEV002',
name: '数控机床 #2',
type: 'CNC机床',
status: 'idle',
temperature: 45,
efficiency: 88,
location: { x: 8, y: 0, z: 0 },
production: 120,
maintenance: '正常',
operator: '李工',
},
{
id: 'DEV003',
name: '工业机器人 #1',
type: '机器人',
status: 'running',
temperature: 55,
efficiency: 95,
location: { x: 0, y: 0, z: 8 },
production: 200,
maintenance: '正常',
operator: '王工',
},
{
id: 'DEV004',
name: '工业机器人 #2',
type: '机器人',
status: 'warning',
temperature: 78,
efficiency: 70,
location: { x: 8, y: 0, z: 8 },
production: 180,
maintenance: '需要检查',
operator: '赵工',
},
{
id: 'DEV005',
name: '自动化流水线',
type: '流水线',
status: 'running',
temperature: 40,
efficiency: 90,
location: { x: 4, y: 0, z: 4 },
production: 500,
maintenance: '正常',
operator: '系统',
},
{
id: 'DEV006',
name: '检测设备',
type: '检测设备',
status: 'stopped',
temperature: 30,
efficiency: 0,
location: { x: -4, y: 0, z: 4 },
production: 0,
maintenance: '维护中',
operator: '孙工',
},
]);
// 生产数据
const productionData = computed(() => ({
totalProduction: deviceData.value.reduce((sum, dev) => sum + dev.production, 0),
efficiency: Math.round(deviceData.value.reduce((sum, dev) => sum + dev.efficiency, 0) / deviceData.value.length),
deviceCount: deviceData.value.length,
runningDevices: deviceData.value.filter((dev) => dev.status === 'running').length,
warningDevices: deviceData.value.filter((dev) => dev.status === 'warning').length,
stoppedDevices: deviceData.value.filter((dev) => dev.status === 'stopped').length,
}));
// 设备详情
const selectedDevice = ref<any>(null);
const showDeviceDetail = ref(false);
const handleSelectDevice = (device: any) => {
selectedDevice.value = device;
showDeviceDetail.value = true;
};
// 初始化Three.js场景
const initScene = () => {
if (!sceneContainer.value) return;
// 创建场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
// 创建相机
camera = new THREE.PerspectiveCamera(
60,
sceneContainer.value.clientWidth / sceneContainer.value.clientHeight,
0.1,
1000
);
camera.position.set(20, 15, 20);
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(sceneContainer.value.clientWidth, sceneContainer.value.clientHeight);
renderer.shadowMap.enabled = true;
sceneContainer.value.appendChild(renderer.domElement);
// 添加控制器
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.autoRotate = autoRotate.value;
controls.autoRotateSpeed = 2;
// 添加灯光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 20, 10);
directionalLight.castShadow = true;
scene.add(directionalLight);
const pointLight = new THREE.PointLight(0x00ffff, 0.5);
pointLight.position.set(0, 10, 0);
scene.add(pointLight);
// 创建车间地面
createWorkshopFloor();
// 创建设备模型
deviceData.value.forEach((device) => {
createDeviceModel(device);
});
// 添加网格辅助
const gridHelper = new THREE.GridHelper(30, 30, 0x444444, 0x333333);
scene.add(gridHelper);
// 添加坐标轴辅助
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 窗口大小调整处理
window.addEventListener('resize', onWindowResize);
// 开始动画循环
animate();
};
// 创建车间地面
const createWorkshopFloor = () => {
if (!scene) return;
const floorGeometry = new THREE.PlaneGeometry(30, 30);
const floorMaterial = new THREE.MeshStandardMaterial({
color: 0x2a2a4a,
roughness: 0.8,
metalness: 0.2,
});
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -Math.PI / 2;
floor.position.y = -0.1;
floor.receiveShadow = true;
scene.add(floor);
// 添加车间分隔线
const lineMaterial = new THREE.LineBasicMaterial({ color: 0x4a4a6a });
const points: THREE.Vector3[] = [];
for (let i = -15; i <= 15; i += 10) {
points.push(new THREE.Vector3(i, 0.1, -15));
points.push(new THREE.Vector3(i, 0.1, 15));
points.push(new THREE.Vector3(-15, 0.1, i));
points.push(new THREE.Vector3(15, 0.1, i));
}
const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
const lines = new THREE.LineSegments(lineGeometry, lineMaterial);
scene.add(lines);
};
// 创建设备模型
const createDeviceModel = (device: any) => {
if (!scene) return;
const group = new THREE.Group();
group.userData = { deviceId: device.id };
// 根据设备类型创建不同的模型
let mainGeometry: THREE.BoxGeometry;
let height = 2;
if (device.type === 'CNC机床') {
mainGeometry = new THREE.BoxGeometry(3, height, 2);
} else if (device.type === '机器人') {
mainGeometry = new THREE.BoxGeometry(1.5, height, 1.5);
} else if (device.type === '流水线') {
mainGeometry = new THREE.BoxGeometry(8, height * 0.5, 1.5);
} else {
mainGeometry = new THREE.BoxGeometry(2, height, 2);
}
// 根据设备状态设置颜色
let color = 0x4CAF50; // 运行中 - 绿色
if (device.status === 'idle') color = 0xFFC107; // 空闲 - 黄色
if (device.status === 'warning') color = 0xFF5722; // 警告 - 橙色
if (device.status === 'stopped') color = 0xF44336; // 停止 - 红色
const mainMaterial = new THREE.MeshStandardMaterial({
color: color,
roughness: 0.3,
metalness: 0.7,
wireframe: wireframeMode.value,
});
const mainMesh = new THREE.Mesh(mainGeometry, mainMaterial);
mainMesh.position.y = height / 2;
mainMesh.castShadow = true;
mainMesh.receiveShadow = true;
group.add(mainMesh);
// 添加设备标识
const labelGeometry = new THREE.BoxGeometry(0.5, 1, 0.1);
const labelMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
const label = new THREE.Mesh(labelGeometry, labelMaterial);
label.position.y = height + 0.5;
group.add(label);
// 添加灯光效果(运行中的设备)
if (device.status === 'running') {
const light = new THREE.PointLight(color, 1, 5);
light.position.set(0, height + 1, 0);
group.add(light);
}
// 设置位置
group.position.set(device.location.x - 4, 0, device.location.z - 4);
scene.add(group);
};
// 窗口大小调整
const onWindowResize = () => {
if (!camera || !renderer || !sceneContainer.value) return;
camera.aspect = sceneContainer.value.clientWidth / sceneContainer.value.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(sceneContainer.value.clientWidth, sceneContainer.value.clientHeight);
};
// 动画循环
const animate = () => {
animationId = requestAnimationFrame(animate);
if (controls) {
controls.update();
}
if (renderer && scene && camera) {
renderer.render(scene, camera);
}
};
// 重置相机
const resetCamera = () => {
if (!camera || !controls) return;
camera.position.set(20, 15, 20);
camera.lookAt(0, 0, 0);
controls.reset();
};
// 切换自动旋转
const toggleAutoRotate = () => {
autoRotate.value = !autoRotate.value;
if (controls) {
controls.autoRotate = autoRotate.value;
}
};
// 切换线框模式
const toggleWireframe = () => {
wireframeMode.value = !wireframeMode.value;
if (scene) {
scene.traverse((object) => {
if (object instanceof THREE.Mesh && object.material instanceof THREE.MeshStandardMaterial) {
object.material.wireframe = wireframeMode.value;
}
});
}
};
// 刷新数据
const refreshData = () => {
// 模拟数据更新
deviceData.value.forEach((device) => {
device.temperature += Math.floor(Math.random() * 5) - 2;
device.efficiency = Math.max(0, Math.min(100, device.efficiency + Math.floor(Math.random() * 10) - 5));
if (device.status === 'running') {
device.production += Math.floor(Math.random() * 5);
}
});
};
// 清理资源
const cleanup = () => {
if (animationId) {
cancelAnimationFrame(animationId);
}
window.removeEventListener('resize', onWindowResize);
if (renderer) {
renderer.dispose();
}
if (scene) {
scene.traverse((object) => {
if (object instanceof THREE.Mesh) {
if (object.geometry) {
object.geometry.dispose();
}
if (object.material instanceof THREE.Material) {
object.material.dispose();
}
}
});
}
};
onMounted(() => {
initScene();
});
onUnmounted(() => {
cleanup();
clearInterval(timer);
});
</script>
<style scoped>
.workshop-visualization {
width: 100%;
height: 100vh;
background: #0a0a1a;
position: relative;
overflow: hidden;
}
.workshop-container {
position: relative;
width: 100%;
height: 100%;
}
.scene-container {
width: 100%;
height: 100%;
}
.top-bar {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 20, 40, 0.8);
border: 1px solid #00d4ff;
border-radius: 8px;
padding: 12px 24px;
backdrop-filter: blur(10px);
z-index: 100;
display: flex;
align-items: center;
gap: 40px;
box-shadow: 0 0 20px rgba(0, 212, 255, 0.3);
}
.title {
font-size: 24px;
font-weight: bold;
color: #00d4ff;
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
}
.subtitle {
font-size: 14px;
color: #7ec8e3;
}
.time-display {
font-size: 18px;
color: #ffffff;
font-family: 'Courier New', monospace;
background: rgba(0, 212, 255, 0.2);
padding: 6px 16px;
border-radius: 4px;
border: 1px solid rgba(0, 212, 255, 0.3);
}
.left-panel {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
width: 300px;
max-height: 80vh;
overflow-y: auto;
z-index: 50;
}
.right-panel {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
width: 320px;
z-index: 50;
}
.bottom-toolbar {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 20, 40, 0.9);
border: 1px solid #00d4ff;
border-radius: 8px;
padding: 12px 24px;
backdrop-filter: blur(10px);
z-index: 100;
box-shadow: 0 0 20px rgba(0, 212, 255, 0.3);
}
/* 自定义滚动条 */
:deep(.ant-list-item) {
background: rgba(0, 20, 40, 0.8);
border: 1px solid rgba(0, 212, 255, 0.3);
margin-bottom: 8px;
border-radius: 4px;
transition: all 0.3s;
}
:deep(.ant-list-item:hover) {
border-color: #00d4ff;
box-shadow: 0 0 10px rgba(0, 212, 255, 0.3);
transform: translateX(5px);
}
:deep(.ant-modal-content) {
background: rgba(0, 20, 40, 0.95);
border: 1px solid #00d4ff;
border-radius: 8px;
}
:deep(.ant-modal-header) {
background: rgba(0, 40, 80, 0.8);
border-bottom: 1px solid #00d4ff;
}
:deep(.ant-modal-title) {
color: #00d4ff;
}
:deep(.ant-btn-primary) {
background: linear-gradient(135deg, #0066cc, #00d4ff);
border: none;
box-shadow: 0 0 10px rgba(0, 212, 255, 0.3);
}
:deep(.ant-btn) {
color: #ffffff;
background: rgba(0, 40, 80, 0.8);
border: 1px solid #00d4ff;
}
:deep(.ant-btn:hover) {
border-color: #00d4ff;
box-shadow: 0 0 10px rgba(0, 212, 255, 0.3);
}
</style>