第一次提交
This commit is contained in:
50
jeecgboot-vue3/src/views/workshop/SimpleTest.vue
Normal file
50
jeecgboot-vue3/src/views/workshop/SimpleTest.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div style="padding: 40px; text-align: center; background: #1a1a2e; min-height: 100vh;">
|
||||
<h1 style="color: #00d4ff; font-size: 32px;">车间可视化测试页面</h1>
|
||||
<p style="color: #ffffff; margin: 20px 0; font-size: 18px;">
|
||||
如果你能看到这个页面,说明路由配置是正确的!
|
||||
</p>
|
||||
|
||||
<div style="background: rgba(0, 20, 40, 0.9); padding: 30px; border-radius: 8px; margin: 30px auto; max-width: 600px; border: 1px solid #00d4ff;">
|
||||
<h2 style="color: #7ec8e3; margin-bottom: 20px;">测试检查清单</h2>
|
||||
<ul style="color: #ffffff; text-align: left; line-height: 2; list-style: none; padding: 0;">
|
||||
<li style="margin: 10px 0;">✅ Vue 组件正常加载</li>
|
||||
<li style="margin: 10px 0;">✅ 样式正常显示</li>
|
||||
<li style="margin: 10px 0;">✅ 路由正确配置</li>
|
||||
<li style="margin: 10px 0;">✅ 页面无权限限制</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 40px;">
|
||||
<a-button type="primary" size="large" @click="showMessage" style="margin-right: 10px;">
|
||||
点击测试
|
||||
</a-button>
|
||||
<a-button size="large" @click="goToMain">
|
||||
返回首页
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div v-if="message" style="margin-top: 30px; padding: 15px; border-radius: 4px; background: #52c41a; color: white; display: inline-block;">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const message = ref('');
|
||||
const router = useRouter();
|
||||
|
||||
const showMessage = () => {
|
||||
message.value = '✅ 页面交互正常!现在可以测试完整的车间可视化页面了。';
|
||||
setTimeout(() => {
|
||||
message.value = '';
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const goToMain = () => {
|
||||
router.push('/');
|
||||
};
|
||||
</script>
|
||||
56
jeecgboot-vue3/src/views/workshop/TestPage.vue
Normal file
56
jeecgboot-vue3/src/views/workshop/TestPage.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div style="padding: 20px; text-align: center;">
|
||||
<h1 style="color: #00d4ff;">车间可视化测试页面</h1>
|
||||
<p style="color: #ffffff; margin: 20px 0;">如果你能看到这个页面,说明路由配置是正确的!</p>
|
||||
|
||||
<div style="background: rgba(0, 20, 40, 0.9); padding: 20px; border-radius: 8px; margin: 20px auto; max-width: 400px;">
|
||||
<h2 style="color: #7ec8e3;">测试检查清单</h2>
|
||||
<ul style="color: #ffffff; text-align: left; line-height: 2;">
|
||||
<li>✅ Vue 组件正常加载</li>
|
||||
<li>✅ 样式正常显示</li>
|
||||
<li>✅ 路由正确配置</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px;">
|
||||
<button @click="testThreeJS" style="padding: 10px 20px; background: #00d4ff; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
测试 Three.js
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="threeTestResult" :style="{ marginTop: '20px', padding: '10px', borderRadius: '4px', backgroundColor: threeTestResult.success ? '#52c41a' : '#f5222d' }">
|
||||
<p style="color: white; margin: 0;">{{ threeTestResult.message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const threeTestResult = ref<{ success: boolean; message: string } | null>(null);
|
||||
|
||||
const testThreeJS = () => {
|
||||
try {
|
||||
// 动态导入 Three.js 来测试
|
||||
import('three').then((three) => {
|
||||
threeTestResult.value = {
|
||||
success: true,
|
||||
message: '✅ Three.js 加载成功!版本: ' + three.VERSION
|
||||
};
|
||||
console.log('Three.js loaded successfully, version:', three.VERSION);
|
||||
}).catch((error) => {
|
||||
threeTestResult.value = {
|
||||
success: false,
|
||||
message: '❌ Three.js 加载失败: ' + error.message
|
||||
};
|
||||
console.error('Three.js loading failed:', error);
|
||||
});
|
||||
} catch (error) {
|
||||
threeTestResult.value = {
|
||||
success: false,
|
||||
message: '❌ 测试失败: ' + (error as Error).message
|
||||
};
|
||||
console.error('Test failed:', error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
267
jeecgboot-vue3/src/views/workshop/Workshop3D.vue
Normal file
267
jeecgboot-vue3/src/views/workshop/Workshop3D.vue
Normal file
@@ -0,0 +1,267 @@
|
||||
<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>
|
||||
10
jeecgboot-vue3/src/views/workshop/Workshop3DTest.vue
Normal file
10
jeecgboot-vue3/src/views/workshop/Workshop3DTest.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div style="padding: 20px;">
|
||||
<h1>3D 车间测试页面</h1>
|
||||
<p>如果你能看到这个页面,说明路由配置正确。</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
console.log('Workshop3DTest 组件已加载')
|
||||
</script>
|
||||
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>
|
||||
411
jeecgboot-vue3/src/views/workshop/components/DeviceDetail.vue
Normal file
411
jeecgboot-vue3/src/views/workshop/components/DeviceDetail.vue
Normal file
@@ -0,0 +1,411 @@
|
||||
<template>
|
||||
<div class="device-detail">
|
||||
<div v-if="device" class="detail-content">
|
||||
<!-- 设备基本信息 -->
|
||||
<div class="detail-section">
|
||||
<h4>基本信息</h4>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="label">设备编号:</span>
|
||||
<span class="value">{{ device.id }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">设备名称:</span>
|
||||
<span class="value">{{ device.name }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">设备类型:</span>
|
||||
<span class="value">{{ device.type }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">当前状态:</span>
|
||||
<a-tag :color="getStatusColor(device.status)" class="status-tag">
|
||||
{{ getStatusText(device.status) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 运行状态 -->
|
||||
<div class="detail-section">
|
||||
<h4>运行状态</h4>
|
||||
<div class="status-indicators">
|
||||
<div class="indicator temperature">
|
||||
<div class="indicator-icon">🌡️</div>
|
||||
<div class="indicator-info">
|
||||
<div class="indicator-label">温度</div>
|
||||
<div class="indicator-value" :class="{ 'warning': device.temperature > 70 }">
|
||||
{{ device.temperature }}°C
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="indicator efficiency">
|
||||
<div class="indicator-icon">⚡</div>
|
||||
<div class="indicator-info">
|
||||
<div class="indicator-label">运行效率</div>
|
||||
<div class="indicator-value">{{ device.efficiency }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 生产统计 -->
|
||||
<div class="detail-section">
|
||||
<h4>生产统计</h4>
|
||||
<div class="production-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">📦</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">累计产量</div>
|
||||
<div class="stat-value">{{ device.production }}</div>
|
||||
<div class="stat-unit">件</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon">👷</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">操作员</div>
|
||||
<div class="stat-value">{{ device.operator }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 维护信息 -->
|
||||
<div class="detail-section">
|
||||
<h4>维护信息</h4>
|
||||
<div class="maintenance-info">
|
||||
<div class="maintenance-status">
|
||||
<span class="label">维护状态:</span>
|
||||
<a-badge :status="getMaintenanceStatus(device.maintenance)" :text="device.maintenance" />
|
||||
</div>
|
||||
<div class="maintenance-schedule">
|
||||
<span class="label">下次维护:</span>
|
||||
<span class="value">2024-04-15</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 位置信息 -->
|
||||
<div class="detail-section">
|
||||
<h4>位置信息</h4>
|
||||
<div class="location-info">
|
||||
<div class="location-item">
|
||||
<span class="label">X坐标:</span>
|
||||
<span class="value">{{ device.location.x }}m</span>
|
||||
</div>
|
||||
<div class="location-item">
|
||||
<span class="label">Y坐标:</span>
|
||||
<span class="value">{{ device.location.y }}m</span>
|
||||
</div>
|
||||
<div class="location-item">
|
||||
<span class="label">Z坐标:</span>
|
||||
<span class="value">{{ device.location.z }}m</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="detail-actions">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleStart" :disabled="device.status === 'running'">启动设备</a-button>
|
||||
<a-button @click="handleStop" :disabled="device.status === 'stopped'">停止设备</a-button>
|
||||
<a-button @click="handleMaintenance" :disabled="device.status === 'stopped'">预约维护</a-button>
|
||||
<a-button @click="handleExport">导出数据</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-empty v-else description="暂无设备信息" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { message } from 'ant-design-vue';
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
interface Device {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
status: string;
|
||||
temperature: number;
|
||||
efficiency: number;
|
||||
location: any;
|
||||
production: number;
|
||||
maintenance: string;
|
||||
operator: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
device: {
|
||||
type: Object as PropType<Device | null>,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
running: 'success',
|
||||
idle: 'warning',
|
||||
warning: 'orange',
|
||||
stopped: 'error',
|
||||
};
|
||||
return colors[status] || 'default';
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const texts: Record<string, string> = {
|
||||
running: '运行中',
|
||||
idle: '空闲',
|
||||
warning: '警告',
|
||||
stopped: '停止',
|
||||
};
|
||||
return texts[status] || status;
|
||||
};
|
||||
|
||||
const getMaintenanceStatus = (maintenance: string) => {
|
||||
if (maintenance === '正常') return 'success';
|
||||
if (maintenance === '需要检查') return 'warning';
|
||||
if (maintenance === '维护中') return 'error';
|
||||
return 'default';
|
||||
};
|
||||
|
||||
const handleStart = () => {
|
||||
message.success('设备启动指令已发送');
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
message.success('设备停止指令已发送');
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const handleMaintenance = () => {
|
||||
message.success('维护预约已创建');
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
message.success('设备数据导出中...');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.device-detail {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detail-section h4 {
|
||||
color: #00d4ff;
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid rgba(0, 212, 255, 0.3);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
color: #7ec8e3;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.info-item .value {
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status-indicators {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: rgba(0, 40, 80, 0.5);
|
||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.indicator-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.indicator-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.indicator-label {
|
||||
font-size: 10px;
|
||||
color: #7ec8e3;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.indicator-value {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #00d4ff;
|
||||
}
|
||||
|
||||
.indicator-value.warning {
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.production-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: rgba(0, 40, 80, 0.5);
|
||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 10px;
|
||||
color: #7ec8e3;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #00d4ff;
|
||||
}
|
||||
|
||||
.stat-unit {
|
||||
font-size: 10px;
|
||||
color: #7ec8e3;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.maintenance-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.maintenance-status,
|
||||
.maintenance-schedule {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.maintenance-status .label,
|
||||
.maintenance-schedule .label {
|
||||
color: #7ec8e3;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.maintenance-status .value,
|
||||
.maintenance-schedule .value {
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.location-info {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.location-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: rgba(0, 40, 80, 0.5);
|
||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.location-item .label {
|
||||
color: #7ec8e3;
|
||||
font-size: 10px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.location-item .value {
|
||||
color: #00d4ff;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
border-top: 1px solid rgba(0, 212, 255, 0.3);
|
||||
padding-top: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
: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:not(:disabled)) {
|
||||
border-color: #00d4ff;
|
||||
box-shadow: 0 0 10px rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
|
||||
:deep(.ant-btn:disabled) {
|
||||
border-color: rgba(0, 212, 255, 0.3);
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
:deep(.ant-badge-status-text) {
|
||||
color: #ffffff;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<div class="device-status-panel">
|
||||
<div class="panel-header">
|
||||
<h3>设备状态监控</h3>
|
||||
<div class="status-summary">
|
||||
<span class="status-tag running">运行中: {{ runningCount }}</span>
|
||||
<span class="status-tag warning">警告: {{ warningCount }}</span>
|
||||
<span class="status-tag stopped">停止: {{ stoppedCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-list :data-source="devices" size="small" class="device-list">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item class="device-item" @click="selectDevice(item)">
|
||||
<template #actions>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
</template>
|
||||
|
||||
<a-list-item-meta>
|
||||
<template #title>
|
||||
<div class="device-title">
|
||||
<span class="device-name">{{ item.name }}</span>
|
||||
<a-tag :color="getStatusColor(item.status)" class="status-badge">
|
||||
{{ getStatusText(item.status) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="device-info">
|
||||
<div class="info-row">
|
||||
<span class="label">类型:</span>
|
||||
<span>{{ item.type }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">温度:</span>
|
||||
<span :class="{ 'temp-warning': item.temperature > 70, 'temp-normal': item.temperature <= 70 }">
|
||||
{{ item.temperature }}°C
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">效率:</span>
|
||||
<a-progress
|
||||
:percent="item.efficiency"
|
||||
:stroke-color="getEfficiencyColor(item.efficiency)"
|
||||
size="small"
|
||||
style="width: 80px"
|
||||
/>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">产量:</span>
|
||||
<span>{{ item.production }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">操作员:</span>
|
||||
<span>{{ item.operator }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
|
||||
<div class="panel-footer">
|
||||
<a-statistic-group direction="row">
|
||||
<a-statistic title="总设备" :value="devices.length" />
|
||||
<a-statistic title="平均效率" :value="averageEfficiency" suffix="%" :precision="1" />
|
||||
</a-statistic-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
interface Device {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
status: string;
|
||||
temperature: number;
|
||||
efficiency: number;
|
||||
location: any;
|
||||
production: number;
|
||||
maintenance: string;
|
||||
operator: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
devices: {
|
||||
type: Array as PropType<Device[]>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['select-device']);
|
||||
|
||||
const runningCount = computed(() => props.devices.filter((d) => d.status === 'running').length);
|
||||
const warningCount = computed(() => props.devices.filter((d) => d.status === 'warning').length);
|
||||
const stoppedCount = computed(() => props.devices.filter((d) => d.status === 'stopped').length);
|
||||
|
||||
const averageEfficiency = computed(() => {
|
||||
const total = props.devices.reduce((sum, d) => sum + d.efficiency, 0);
|
||||
return (total / props.devices.length).toFixed(1);
|
||||
});
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
running: 'success',
|
||||
idle: 'warning',
|
||||
warning: 'orange',
|
||||
stopped: 'error',
|
||||
};
|
||||
return colors[status] || 'default';
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const texts: Record<string, string> = {
|
||||
running: '运行中',
|
||||
idle: '空闲',
|
||||
warning: '警告',
|
||||
stopped: '停止',
|
||||
};
|
||||
return texts[status] || status;
|
||||
};
|
||||
|
||||
const getEfficiencyColor = (efficiency: number) => {
|
||||
if (efficiency >= 90) return '#52c41a';
|
||||
if (efficiency >= 70) return '#faad14';
|
||||
return '#f5222d';
|
||||
};
|
||||
|
||||
const selectDevice = (device: Device) => {
|
||||
emit('select-device', device);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.device-status-panel {
|
||||
background: rgba(0, 20, 40, 0.9);
|
||||
border: 1px solid #00d4ff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 0 20px rgba(0, 212, 255, 0.3);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid rgba(0, 212, 255, 0.3);
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
color: #00d4ff;
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 18px;
|
||||
text-shadow: 0 0 5px rgba(0, 212, 255, 0.5);
|
||||
}
|
||||
|
||||
.status-summary {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-tag.running {
|
||||
background: rgba(82, 196, 26, 0.2);
|
||||
color: #52c41a;
|
||||
border: 1px solid #52c41a;
|
||||
}
|
||||
|
||||
.status-tag.warning {
|
||||
background: rgba(250, 173, 20, 0.2);
|
||||
color: #faad14;
|
||||
border: 1px solid #faad14;
|
||||
}
|
||||
|
||||
.status-tag.stopped {
|
||||
background: rgba(245, 34, 45, 0.2);
|
||||
color: #f5222d;
|
||||
border: 1px solid #f5222d;
|
||||
}
|
||||
|
||||
.device-list {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.device-item {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.device-item:hover {
|
||||
transform: translateX(5px);
|
||||
box-shadow: 0 0 15px rgba(0, 212, 255, 0.4);
|
||||
}
|
||||
|
||||
.device-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.device-info {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.info-row .label {
|
||||
color: #7ec8e3;
|
||||
}
|
||||
|
||||
.info-row span:not(.label) {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.temp-normal {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.temp-warning {
|
||||
color: #f5222d;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
border-top: 1px solid rgba(0, 212, 255, 0.3);
|
||||
padding-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.ant-statistic-title) {
|
||||
color: #7ec8e3;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.ant-statistic-content) {
|
||||
color: #00d4ff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-list-item-action) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.ant-list-item:hover .ant-list-item-action) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-link) {
|
||||
color: #00d4ff;
|
||||
}
|
||||
|
||||
:deep(.ant-progress-bg) {
|
||||
background: linear-gradient(90deg, #0066cc, #00d4ff);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,458 @@
|
||||
<template>
|
||||
<div class="production-data-panel">
|
||||
<div class="panel-header">
|
||||
<h3>生产数据统计</h3>
|
||||
</div>
|
||||
|
||||
<!-- 主要指标卡片 -->
|
||||
<div class="main-metrics">
|
||||
<div class="metric-card total-production">
|
||||
<div class="metric-icon">📊</div>
|
||||
<div class="metric-info">
|
||||
<div class="metric-label">总产量</div>
|
||||
<div class="metric-value">{{ productionData.totalProduction }}</div>
|
||||
<div class="metric-unit">件</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card efficiency">
|
||||
<div class="metric-icon">⚡</div>
|
||||
<div class="metric-info">
|
||||
<div class="metric-label">综合效率</div>
|
||||
<div class="metric-value">{{ productionData.efficiency }}</div>
|
||||
<div class="metric-unit">%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card device-count">
|
||||
<div class="metric-icon">🏭</div>
|
||||
<div class="metric-info">
|
||||
<div class="metric-label">设备总数</div>
|
||||
<div class="metric-value">{{ productionData.deviceCount }}</div>
|
||||
<div class="metric-unit">台</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设备状态分布 -->
|
||||
<div class="status-distribution">
|
||||
<h4>设备状态分布</h4>
|
||||
<div class="status-bars">
|
||||
<div class="status-bar running">
|
||||
<div class="bar-label">运行中</div>
|
||||
<div class="bar-container">
|
||||
<div class="bar-fill" :style="{ width: runningPercentage + '%' }"></div>
|
||||
</div>
|
||||
<div class="bar-value">{{ productionData.runningDevices }}</div>
|
||||
</div>
|
||||
|
||||
<div class="status-bar warning">
|
||||
<div class="bar-label">警告</div>
|
||||
<div class="bar-container">
|
||||
<div class="bar-fill" :style="{ width: warningPercentage + '%' }"></div>
|
||||
</div>
|
||||
<div class="bar-value">{{ productionData.warningDevices }}</div>
|
||||
</div>
|
||||
|
||||
<div class="status-bar stopped">
|
||||
<div class="bar-label">停止</div>
|
||||
<div class="bar-container">
|
||||
<div class="bar-fill" :style="{ width: stoppedPercentage + '%' }"></div>
|
||||
</div>
|
||||
<div class="bar-value">{{ productionData.stoppedDevices }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 产量趋势 -->
|
||||
<div class="production-trend">
|
||||
<h4>生产趋势</h4>
|
||||
<div class="trend-chart">
|
||||
<div class="chart-container">
|
||||
<svg class="trend-svg" viewBox="0 0 280 100">
|
||||
<defs>
|
||||
<linearGradient id="trendGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#00d4ff;stop-opacity:0.3" />
|
||||
<stop offset="100%" style="stop-color:#00d4ff;stop-opacity:0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
:d="trendPath"
|
||||
fill="url(#trendGradient)"
|
||||
stroke="#00d4ff"
|
||||
stroke-width="2"
|
||||
fill-opacity="0.3"
|
||||
/>
|
||||
<path :d="trendLine" fill="none" stroke="#00d4ff" stroke-width="2" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="trend-labels">
|
||||
<span v-for="(label, index) in trendLabels" :key="index">{{ label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 实时产量 -->
|
||||
<div class="realtime-production">
|
||||
<h4>实时产量</h4>
|
||||
<div class="production-counter">
|
||||
<a-statistic
|
||||
:value="currentProduction"
|
||||
:precision="0"
|
||||
:value-style="{ color: '#00d4ff', fontSize: '28px' }"
|
||||
>
|
||||
<template #suffix>
|
||||
<span style="color: #7ec8e3; font-size: 14px">件/小时</span>
|
||||
</template>
|
||||
</a-statistic>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 质量指标 -->
|
||||
<div class="quality-metrics">
|
||||
<h4>质量指标</h4>
|
||||
<div class="quality-grid">
|
||||
<div class="quality-item">
|
||||
<div class="quality-label">合格率</div>
|
||||
<div class="quality-value">98.5%</div>
|
||||
</div>
|
||||
<div class="quality-item">
|
||||
<div class="quality-label">不良率</div>
|
||||
<div class="quality-value bad">1.5%</div>
|
||||
</div>
|
||||
<div class="quality-item">
|
||||
<div class="quality-label">返工率</div>
|
||||
<div class="quality-value warning">2.3%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
interface ProductionData {
|
||||
totalProduction: number;
|
||||
efficiency: number;
|
||||
deviceCount: number;
|
||||
runningDevices: number;
|
||||
warningDevices: number;
|
||||
stoppedDevices: number;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
productionData: {
|
||||
type: Object as PropType<ProductionData>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const runningPercentage = computed(
|
||||
() => ((props.productionData.runningDevices / props.productionData.deviceCount) * 100).toFixed(1)
|
||||
);
|
||||
const warningPercentage = computed(
|
||||
() => ((props.productionData.warningDevices / props.productionData.deviceCount) * 100).toFixed(1)
|
||||
);
|
||||
const stoppedPercentage = computed(
|
||||
() => ((props.productionData.stoppedDevices / props.productionData.deviceCount) * 100).toFixed(1)
|
||||
);
|
||||
|
||||
// 模拟产量趋势数据
|
||||
const trendData = [45, 52, 48, 65, 58, 72, 68, 80, 75, 85];
|
||||
const trendLabels = ['08:00', '10:00', '12:00', '14:00', '16:00', '18:00', '20:00', '22:00', '00:00', '02:00'];
|
||||
|
||||
const currentProduction = computed(() => {
|
||||
return trendData[trendData.length - 1];
|
||||
});
|
||||
|
||||
const trendPath = computed(() => {
|
||||
const points = trendData.map((value, index) => {
|
||||
const x = (index / (trendData.length - 1)) * 280;
|
||||
const y = 100 - (value / 100) * 80;
|
||||
return `${x},${y}`;
|
||||
});
|
||||
return `M ${points.join(' L ')} L 280,100 L 0,100 Z`;
|
||||
});
|
||||
|
||||
const trendLine = computed(() => {
|
||||
const points = trendData.map((value, index) => {
|
||||
const x = (index / (trendData.length - 1)) * 280;
|
||||
const y = 100 - (value / 100) * 80;
|
||||
return `${x},${y}`;
|
||||
});
|
||||
return `M ${points.join(' L ')}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.production-data-panel {
|
||||
background: rgba(0, 20, 40, 0.9);
|
||||
border: 1px solid #00d4ff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 0 20px rgba(0, 212, 255, 0.3);
|
||||
color: #ffffff;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid rgba(0, 212, 255, 0.3);
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
color: #00d4ff;
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
text-shadow: 0 0 5px rgba(0, 212, 255, 0.5);
|
||||
}
|
||||
|
||||
.main-metrics {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(0, 40, 80, 0.5);
|
||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.metric-card:hover {
|
||||
border-color: #00d4ff;
|
||||
box-shadow: 0 0 15px rgba(0, 212, 255, 0.4);
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
|
||||
.metric-icon {
|
||||
font-size: 32px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.metric-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 12px;
|
||||
color: #7ec8e3;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #00d4ff;
|
||||
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
|
||||
}
|
||||
|
||||
.metric-unit {
|
||||
font-size: 12px;
|
||||
color: #7ec8e3;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.status-distribution {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.status-distribution h4 {
|
||||
color: #7ec8e3;
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
width: 50px;
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.bar-container {
|
||||
flex: 1;
|
||||
height: 20px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
transition: width 0.5s ease-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.status-bar.running .bar-fill {
|
||||
background: linear-gradient(90deg, #52c41a, #73d13d);
|
||||
}
|
||||
|
||||
.status-bar.warning .bar-fill {
|
||||
background: linear-gradient(90deg, #faad14, #ffc53d);
|
||||
}
|
||||
|
||||
.status-bar.stopped .bar-fill {
|
||||
background: linear-gradient(90deg, #f5222d, #ff4d4f);
|
||||
}
|
||||
|
||||
.bar-value {
|
||||
width: 30px;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.production-trend {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.production-trend h4 {
|
||||
color: #7ec8e3;
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.trend-chart {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 100px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.trend-svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.trend-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 10px;
|
||||
color: #7ec8e3;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.realtime-production {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.realtime-production h4 {
|
||||
color: #7ec8e3;
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.quality-metrics {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.quality-metrics h4 {
|
||||
color: #7ec8e3;
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.quality-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.quality-item {
|
||||
background: rgba(0, 40, 80, 0.5);
|
||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quality-label {
|
||||
font-size: 10px;
|
||||
color: #7ec8e3;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.quality-value {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.quality-value.bad {
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.quality-value.warning {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
.production-data-panel::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.production-data-panel::-webkit-scrollbar-track {
|
||||
background: rgba(0, 20, 40, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.production-data-panel::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 212, 255, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.production-data-panel::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 212, 255, 0.7);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user