561 lines
15 KiB
Vue
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>
|