第一次提交
This commit is contained in:
560
jeecgboot-vue3/src/views/workshop/WorkshopVisualization.vue
Normal file
560
jeecgboot-vue3/src/views/workshop/WorkshopVisualization.vue
Normal file
@@ -0,0 +1,560 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user