第一次提交

This commit is contained in:
2026-04-03 09:56:14 +08:00
commit 60e2c8debd
3598 changed files with 746659 additions and 0 deletions

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,10 @@
<template>
<div style="padding: 20px;">
<h1>3D 车间测试页面</h1>
<p>如果你能看到这个页面说明路由配置正确</p>
</div>
</template>
<script setup>
console.log('Workshop3DTest 组件已加载')
</script>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>