<template>
|
<div class="production-container">
|
<div class="header">
|
<h1 class="title">生产监控中心</h1>
|
<div class="datetime">{{ currentTime }}</div>
|
</div>
|
|
<div class="nav-menu">
|
<router-link to="/dashboard" class="nav-item">综合看板</router-link>
|
<router-link to="/warehouse" class="nav-item">仓库监控</router-link>
|
<router-link to="/production" class="nav-item active">生产监控</router-link>
|
<router-link to="/inventory" class="nav-item">库存预警</router-link>
|
</div>
|
|
<div class="main-content">
|
<!-- 生产指标 -->
|
<div class="metrics-row">
|
<div class="metric-card" v-for="(metric, index) in metrics" :key="index">
|
<div class="metric-icon" :style="{ background: metric.color }">
|
<el-icon :size="28">
|
<Calendar v-if="index === 0" />
|
<SuccessFilled v-else-if="index === 1" />
|
<CircleCheckFilled v-else-if="index === 2" />
|
<Setting v-else />
|
</el-icon>
|
</div>
|
<div class="metric-info">
|
<div class="metric-value">{{ metric.value }}</div>
|
<div class="metric-label">{{ metric.label }}</div>
|
<div class="metric-target">目标: {{ metric.target }}</div>
|
</div>
|
<div class="metric-progress" :style="{ width: metric.progress + '%', background: metric.color }"></div>
|
</div>
|
</div>
|
|
<!-- 生产线和设备状态 -->
|
<div class="production-lines">
|
<div class="line-card" v-for="(line, index) in productionLines" :key="index">
|
<div class="line-header">
|
<div class="line-title">
|
<span class="line-name">{{ line.name }}</span>
|
<el-tag :type="line.status === '运行中' ? 'success' : 'warning'">{{ line.status }}</el-tag>
|
</div>
|
<div class="line-output">今日产量: {{ line.output }}</div>
|
</div>
|
<div class="line-devices">
|
<div class="device-item" v-for="(device, dIndex) in line.devices" :key="dIndex">
|
<div class="device-icon" :class="device.status === '运行' ? 'running' : 'stopped'">
|
<el-icon>
|
<Cpu v-if="device.name.includes('CNC')" />
|
<Operation v-else-if="device.name.includes('输送带')" />
|
<Connection v-else />
|
</el-icon>
|
</div>
|
<div class="device-info">
|
<div class="device-name">{{ device.name }}</div>
|
<div class="device-status">{{ device.status }}</div>
|
</div>
|
<div class="device-data">
|
<div class="data-item">
|
<span class="data-label">温度</span>
|
<span class="data-value">{{ device.temperature }}°C</span>
|
</div>
|
<div class="data-item">
|
<span class="data-label">转速</span>
|
<span class="data-value">{{ device.speed }}</span>
|
</div>
|
<div class="data-item">
|
<span class="data-label">负载</span>
|
<span class="data-value">{{ device.load }}</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 生产统计图表 -->
|
<div class="charts-row">
|
<div class="chart-card">
|
<div class="card-title">
|
<span>产量趋势</span>
|
<el-radio-group v-model="trendPeriod" size="small">
|
<el-radio-button label="日">日</el-radio-button>
|
<el-radio-button label="周">周</el-radio-button>
|
<el-radio-button label="月">月</el-radio-button>
|
</el-radio-group>
|
</div>
|
<div ref="productionChartRef" class="chart-container"></div>
|
</div>
|
|
<div class="chart-card">
|
<div class="card-title">良品率统计</div>
|
<div ref="qualityChartRef" class="chart-container"></div>
|
</div>
|
|
<div class="chart-card">
|
<div class="card-title">设备OEE</div>
|
<div ref="oeeChartRef" class="chart-container"></div>
|
</div>
|
</div>
|
|
<!-- 实时生产任务 -->
|
<div class="tasks-panel">
|
<div class="panel-title">实时生产任务</div>
|
<el-table :data="tasks" style="width: 100%" height="200">
|
<el-table-column prop="orderNo" label="生产订单" width="140" />
|
<el-table-column prop="product" label="产品名称" />
|
<el-table-column prop="planQty" label="计划数量" width="100" />
|
<el-table-column prop="completedQty" label="完成数量" width="100" />
|
<el-table-column prop="progress" label="进度" width="150">
|
<template #default="{ row }">
|
<el-progress :percentage="row.progress" :color="getProgressColor(row.progress)" />
|
</template>
|
</el-table-column>
|
<el-table-column prop="line" label="产线" width="100" />
|
<el-table-column prop="status" label="状态" width="100">
|
<template #default="{ row }">
|
<el-tag :type="getTaskStatusType(row.status)">{{ row.status }}</el-tag>
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script>
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
import * as echarts from 'echarts'
|
import { http } from '@/utils/http'
|
import { formatDateTime } from '@/utils'
|
|
export default {
|
name: 'Production',
|
setup() {
|
const currentTime = ref('')
|
const trendPeriod = ref('日')
|
const productionChartRef = ref(null)
|
const qualityChartRef = ref(null)
|
const oeeChartRef = ref(null)
|
|
const metrics = ref([
|
{ label: '计划产量', value: '5,000', target: '5,000', progress: 100, icon: 'Calendar', color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
|
{ label: '实际产量', value: '4,280', target: '5,000', progress: 85.6, icon: 'Check', color: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
|
{ label: '良品数量', value: '4,156', target: '4,280', progress: 97.1, icon: 'CircleCheck', color: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' },
|
{ label: '设备运行', value: '12/15', target: '15', progress: 80, icon: 'Setting', color: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' }
|
])
|
|
const productionLines = ref([
|
{
|
name: '生产线 A',
|
status: '运行中',
|
output: '1,850',
|
devices: [
|
{ name: 'CNC机床-01', status: '运行', temperature: 45, speed: '2400rpm', load: '78%', icon: 'Cpu' },
|
{ name: 'CNC机床-02', status: '运行', temperature: 48, speed: '2200rpm', load: '82%', icon: 'Cpu' },
|
{ name: '机器人-01', status: '运行', temperature: 38, speed: '1200rpm', load: '65%', icon: 'Robot' },
|
{ name: '输送带-01', status: '运行', temperature: 35, speed: '800rpm', load: '55%', icon: 'Operation' }
|
]
|
},
|
{
|
name: '生产线 B',
|
status: '运行中',
|
output: '1,620',
|
devices: [
|
{ name: 'CNC机床-03', status: '运行', temperature: 50, speed: '2500rpm', load: '85%', icon: 'Cpu' },
|
{ name: 'CNC机床-04', status: '停机', temperature: 25, speed: '0rpm', load: '0%', icon: 'Cpu' },
|
{ name: '机器人-02', status: '运行', temperature: 40, speed: '1100rpm', load: '70%', icon: 'Robot' },
|
{ name: '输送带-02', status: '运行', temperature: 36, speed: '750rpm', load: '60%', icon: 'Operation' }
|
]
|
},
|
{
|
name: '生产线 C',
|
status: '维护中',
|
output: '810',
|
devices: [
|
{ name: 'CNC机床-05', status: '停机', temperature: 28, speed: '0rpm', load: '0%', icon: 'Cpu' },
|
{ name: 'CNC机床-06', status: '运行', temperature: 42, speed: '2300rpm', load: '75%', icon: 'Cpu' },
|
{ name: '机器人-03', status: '运行', temperature: 39, speed: '1150rpm', load: '68%', icon: 'Robot' },
|
{ name: '输送带-03', status: '运行', temperature: 34, speed: '850rpm', load: '58%', icon: 'Operation' }
|
]
|
}
|
])
|
|
const tasks = ref([
|
{ orderNo: 'PO20241224001', product: '精密齿轮-A型', planQty: 2000, completedQty: 1850, progress: 92.5, line: 'A线', status: '生产中' },
|
{ orderNo: 'PO20241224002', product: '传动轴-B型', planQty: 1500, completedQty: 1240, progress: 82.7, line: 'B线', status: '生产中' },
|
{ orderNo: 'PO20241224003', product: '轴承座-C型', planQty: 1000, completedQty: 810, progress: 81, line: 'C线', status: '生产中' },
|
{ orderNo: 'PO20241224004', product: '连接器-D型', planQty: 500, completedQty: 380, progress: 76, line: 'A线', status: '生产中' }
|
])
|
|
let timer
|
const updateTime = () => {
|
currentTime.value = formatDateTime(new Date())
|
}
|
|
const getProgressColor = (progress) => {
|
if (progress >= 90) return '#67c23a'
|
if (progress >= 70) return '#409eff'
|
if (progress >= 50) return '#e6a23c'
|
return '#f56c6c'
|
}
|
|
const getTaskStatusType = (status) => {
|
const map = {
|
'生产中': 'primary',
|
'已完成': 'success',
|
'待排产': 'info',
|
'暂停': 'warning'
|
}
|
return map[status] || 'info'
|
}
|
|
// 初始化产量趋势图
|
const initProductionChart = () => {
|
const chart = echarts.init(productionChartRef.value)
|
const option = {
|
tooltip: { trigger: 'axis' },
|
grid: {
|
left: '3%', right: '4%', bottom: '3%', top: '10%',
|
containLabel: true
|
},
|
xAxis: {
|
type: 'category',
|
data: ['08:00', '10:00', '12:00', '14:00', '16:00', '18:00'],
|
axisLine: { lineStyle: { color: '#fff' } }
|
},
|
yAxis: {
|
type: 'value',
|
axisLine: { lineStyle: { color: '#fff' } },
|
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
|
},
|
series: [
|
{
|
name: '产量',
|
type: 'bar',
|
data: [420, 680, 850, 920, 780, 630],
|
itemStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: '#83bff6' },
|
{ offset: 1, color: '#188df0' }
|
]),
|
borderRadius: [5, 5, 0, 0]
|
}
|
}
|
]
|
}
|
chart.setOption(option)
|
return chart
|
}
|
|
// 初始化良品率图
|
const initQualityChart = () => {
|
const chart = echarts.init(qualityChartRef.value)
|
const option = {
|
tooltip: { trigger: 'item' },
|
series: [
|
{
|
name: '良品率',
|
type: 'gauge',
|
progress: { show: true, width: 18 },
|
axisLine: { lineStyle: { width: 18, color: [[1, '#4facfe']] } },
|
axisTick: { show: false },
|
splitLine: { length: 15, lineStyle: { width: 2, color: '#fff' } },
|
axisLabel: { distance: 25, color: '#fff', fontSize: 20 },
|
anchor: { show: true, showAbove: true, size: 25, itemStyle: { borderWidth: 10 } },
|
detail: {
|
valueAnimation: true,
|
fontSize: 40,
|
offsetCenter: [0, '70%'],
|
color: '#fff',
|
formatter: '{value}%'
|
},
|
data: [{ value: 97.1 }]
|
}
|
]
|
}
|
chart.setOption(option)
|
return chart
|
}
|
|
// 初始化OEE图
|
const initOeeChart = () => {
|
const chart = echarts.init(oeeChartRef.value)
|
const option = {
|
tooltip: { trigger: 'axis' },
|
legend: {
|
data: ['可用率', '表现指数', '质量指数'],
|
textStyle: { color: '#fff' },
|
top: 5
|
},
|
grid: {
|
left: '3%', right: '4%', bottom: '3%', top: '20%',
|
containLabel: true
|
},
|
xAxis: {
|
type: 'category',
|
data: ['A线', 'B线', 'C线'],
|
axisLine: { lineStyle: { color: '#fff' } }
|
},
|
yAxis: {
|
type: 'value',
|
max: 100,
|
axisLine: { lineStyle: { color: '#fff' } },
|
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
|
},
|
series: [
|
{ name: '可用率', type: 'bar', data: [92, 88, 75], itemStyle: { color: '#5470c6' } },
|
{ name: '表现指数', type: 'bar', data: [85, 82, 78], itemStyle: { color: '#91cc75' } },
|
{ name: '质量指数', type: 'bar', data: [97, 96, 95], itemStyle: { color: '#fac858' } }
|
]
|
}
|
chart.setOption(option)
|
return chart
|
}
|
|
const handleResize = () => {
|
try {
|
const refs = [productionChartRef.value, qualityChartRef.value, oeeChartRef.value]
|
refs.forEach(dom => {
|
if (dom) {
|
const chart = echarts.getInstanceByDom(dom)
|
if (chart) {
|
chart.resize()
|
}
|
}
|
})
|
} catch (error) {
|
console.warn('图表重绘时出错:', error)
|
}
|
}
|
|
onMounted(() => {
|
updateTime()
|
timer = setInterval(updateTime, 1000)
|
initProductionChart()
|
initQualityChart()
|
initOeeChart()
|
window.addEventListener('resize', handleResize)
|
})
|
|
onUnmounted(() => {
|
clearInterval(timer)
|
window.removeEventListener('resize', handleResize)
|
|
// 销毁图表实例
|
try {
|
const refs = [productionChartRef.value, qualityChartRef.value, oeeChartRef.value]
|
refs.forEach(dom => {
|
if (dom) {
|
const chart = echarts.getInstanceByDom(dom)
|
if (chart) {
|
chart.dispose()
|
}
|
}
|
})
|
} catch (error) {
|
console.warn('图表销毁时出错:', error)
|
}
|
})
|
|
return {
|
currentTime,
|
trendPeriod,
|
metrics,
|
productionLines,
|
tasks,
|
productionChartRef,
|
qualityChartRef,
|
oeeChartRef,
|
getProgressColor,
|
getTaskStatusType
|
}
|
}
|
}
|
</script>
|
|
<style scoped>
|
.production-container {
|
width: 100%;
|
height: 100%;
|
padding: 20px;
|
display: flex;
|
flex-direction: column;
|
gap: 15px;
|
overflow-y: auto;
|
}
|
|
.header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding: 0 20px;
|
height: 60px;
|
background: linear-gradient(90deg, rgba(30, 58, 138, 0.5) 0%, rgba(30, 58, 138, 0.1) 100%);
|
border-radius: 10px;
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
}
|
|
.title {
|
font-size: 24px;
|
font-weight: bold;
|
background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
|
-webkit-background-clip: text;
|
-webkit-text-fill-color: transparent;
|
}
|
|
.datetime {
|
font-size: 16px;
|
color: #4facfe;
|
}
|
|
.nav-menu {
|
display: flex;
|
gap: 15px;
|
padding: 0 20px;
|
}
|
|
.nav-item {
|
padding: 10px 25px;
|
background: rgba(255, 255, 255, 0.05);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
border-radius: 8px;
|
color: #fff;
|
text-decoration: none;
|
transition: all 0.3s;
|
}
|
|
.nav-item:hover {
|
background: rgba(79, 172, 254, 0.2);
|
border-color: #4facfe;
|
}
|
|
.nav-item.active {
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
border-color: transparent;
|
}
|
|
.main-content {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
gap: 15px;
|
}
|
|
.metrics-row {
|
display: grid;
|
grid-template-columns: repeat(4, 1fr);
|
gap: 15px;
|
}
|
|
.metric-card {
|
position: relative;
|
display: flex;
|
align-items: center;
|
gap: 15px;
|
padding: 20px;
|
background: rgba(255, 255, 255, 0.05);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
border-radius: 10px;
|
overflow: hidden;
|
}
|
|
.metric-icon {
|
width: 50px;
|
height: 50px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
border-radius: 10px;
|
color: #fff;
|
flex-shrink: 0;
|
}
|
|
.metric-info {
|
flex: 1;
|
}
|
|
.metric-value {
|
font-size: 24px;
|
font-weight: bold;
|
margin-bottom: 3px;
|
}
|
|
.metric-label {
|
font-size: 13px;
|
color: rgba(255, 255, 255, 0.6);
|
margin-bottom: 3px;
|
}
|
|
.metric-target {
|
font-size: 12px;
|
color: rgba(255, 255, 255, 0.5);
|
}
|
|
.metric-progress {
|
position: absolute;
|
bottom: 0;
|
left: 0;
|
height: 3px;
|
transition: width 0.5s;
|
}
|
|
.production-lines {
|
display: grid;
|
grid-template-columns: repeat(3, 1fr);
|
gap: 15px;
|
}
|
|
.line-card {
|
padding: 20px;
|
background: rgba(255, 255, 255, 0.05);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
border-radius: 10px;
|
}
|
|
.line-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 15px;
|
padding-bottom: 15px;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
}
|
|
.line-title {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
}
|
|
.line-name {
|
font-size: 16px;
|
font-weight: bold;
|
color: #4facfe;
|
}
|
|
.line-output {
|
font-size: 14px;
|
color: rgba(255, 255, 255, 0.7);
|
}
|
|
.line-devices {
|
display: flex;
|
flex-direction: column;
|
gap: 12px;
|
}
|
|
.device-item {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
padding: 12px;
|
background: rgba(0, 0, 0, 0.2);
|
border-radius: 8px;
|
}
|
|
.device-icon {
|
width: 40px;
|
height: 40px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
border-radius: 8px;
|
background: rgba(255, 255, 255, 0.05);
|
color: rgba(255, 255, 255, 0.5);
|
}
|
|
.device-icon.running {
|
background: rgba(103, 194, 58, 0.2);
|
color: #67c23a;
|
}
|
|
.device-icon.stopped {
|
background: rgba(245, 108, 108, 0.2);
|
color: #f56c6c;
|
}
|
|
.device-info {
|
flex: 1;
|
}
|
|
.device-name {
|
font-size: 14px;
|
font-weight: bold;
|
color: #fff;
|
margin-bottom: 3px;
|
}
|
|
.device-status {
|
font-size: 12px;
|
color: rgba(255, 255, 255, 0.5);
|
}
|
|
.device-data {
|
display: flex;
|
gap: 15px;
|
}
|
|
.data-item {
|
text-align: center;
|
}
|
|
.data-label {
|
font-size: 10px;
|
color: rgba(255, 255, 255, 0.5);
|
display: block;
|
margin-bottom: 2px;
|
}
|
|
.data-value {
|
font-size: 12px;
|
color: #4facfe;
|
font-weight: bold;
|
}
|
|
.charts-row {
|
display: grid;
|
grid-template-columns: repeat(3, 1fr);
|
gap: 15px;
|
}
|
|
.chart-card {
|
padding: 20px;
|
background: rgba(255, 255, 255, 0.05);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
border-radius: 10px;
|
}
|
|
.card-title {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 15px;
|
font-size: 16px;
|
font-weight: bold;
|
color: #4facfe;
|
}
|
|
.chart-container {
|
width: 100%;
|
height: 220px;
|
}
|
|
.tasks-panel {
|
padding: 20px;
|
background: rgba(255, 255, 255, 0.05);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
border-radius: 10px;
|
}
|
|
.panel-title {
|
font-size: 16px;
|
font-weight: bold;
|
color: #4facfe;
|
margin-bottom: 15px;
|
}
|
|
:deep(.el-table) {
|
background: transparent !important;
|
}
|
|
:deep(.el-table th) {
|
background: rgba(255, 255, 255, 0.1) !important;
|
color: #fff !important;
|
border-color: rgba(255, 255, 255, 0.1) !important;
|
}
|
|
:deep(.el-table td) {
|
border-color: rgba(255, 255, 255, 0.1) !important;
|
}
|
|
:deep(.el-table tr) {
|
background: transparent !important;
|
}
|
|
:deep(.el-table__row:hover td) {
|
background: rgba(255, 255, 255, 0.05) !important;
|
}
|
|
:deep(.el-progress__text) {
|
color: #fff !important;
|
}
|
|
:deep(.el-progress-bar__outer) {
|
background: rgba(255, 255, 255, 0.1) !important;
|
}
|
|
/* 响应式适配 */
|
@media screen and (max-width: 1600px) {
|
.metrics-row {
|
grid-template-columns: repeat(2, 1fr);
|
}
|
|
.production-lines {
|
grid-template-columns: repeat(2, 1fr);
|
}
|
}
|
|
@media screen and (max-width: 1366px) {
|
.production-container {
|
padding: 15px;
|
}
|
|
.header {
|
height: 50px;
|
padding: 0 15px;
|
}
|
|
.title {
|
font-size: 18px;
|
}
|
|
.metrics-row {
|
grid-template-columns: repeat(2, 1fr);
|
gap: 10px;
|
}
|
|
.metric-card {
|
padding: 15px;
|
}
|
|
.metric-value {
|
font-size: 20px;
|
}
|
|
.production-lines {
|
grid-template-columns: repeat(2, 1fr);
|
gap: 10px;
|
}
|
|
.line-card {
|
padding: 15px;
|
}
|
|
.charts-row {
|
grid-template-columns: 1fr;
|
gap: 10px;
|
}
|
|
.chart-container {
|
height: 180px;
|
}
|
}
|
|
@media screen and (max-width: 1024px) {
|
.production-lines {
|
grid-template-columns: 1fr;
|
}
|
|
.device-item {
|
flex-direction: column;
|
align-items: flex-start;
|
gap: 8px;
|
}
|
|
.device-data {
|
width: 100%;
|
justify-content: space-between;
|
}
|
}
|
|
@media screen and (max-width: 768px) {
|
.production-container {
|
padding: 10px;
|
}
|
|
.header {
|
flex-direction: column;
|
height: auto;
|
padding: 10px;
|
}
|
|
.title {
|
font-size: 16px;
|
}
|
|
.nav-menu {
|
gap: 8px;
|
padding: 0;
|
flex-wrap: wrap;
|
}
|
|
.nav-item {
|
padding: 6px 12px;
|
font-size: 12px;
|
}
|
|
.metrics-row {
|
grid-template-columns: 1fr;
|
}
|
|
.metric-card {
|
padding: 12px;
|
}
|
|
.metric-icon {
|
width: 45px;
|
height: 45px;
|
}
|
|
.metric-value {
|
font-size: 18px;
|
}
|
|
.production-lines {
|
grid-template-columns: 1fr;
|
}
|
|
.line-card {
|
padding: 12px;
|
}
|
|
.device-item {
|
padding: 10px;
|
}
|
|
.device-icon {
|
width: 35px;
|
height: 35px;
|
}
|
|
.device-data {
|
flex-wrap: wrap;
|
gap: 10px;
|
}
|
|
.chart-container {
|
height: 150px;
|
}
|
|
.tasks-panel {
|
padding: 12px;
|
}
|
}
|
</style>
|