<template>
|
<div class="kpl-monitor-container">
|
<div class="page-header">
|
<h1>KPL监控分析</h1>
|
<div class="time-range">
|
<el-date-picker
|
v-model="timeRange"
|
type="daterange"
|
range-separator="至"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期"
|
value-format="YYYY-MM-DD"
|
@change="fetchKPLData"
|
/>
|
</div>
|
</div>
|
|
<!-- 关键指标卡片 -->
|
<div class="metrics-cards">
|
<div class="metric-card">
|
<div class="metric-header">
|
<span class="metric-title">MTBF</span>
|
<span class="metric-trend" :class="mtbfTrendClass">
|
{{ mtbfTrendText }}
|
</span>
|
</div>
|
<div class="metric-value">{{ currentMTBF }} 小时</div>
|
<div class="metric-change">
|
较上月: {{ mtbfChange }}%
|
</div>
|
</div>
|
|
<div class="metric-card">
|
<div class="metric-header">
|
<span class="metric-title">MTTR</span>
|
<span class="metric-trend" :class="mttrTrendClass">
|
{{ mttrTrendText }}
|
</span>
|
</div>
|
<div class="metric-value">{{ currentMTTR }} 小时</div>
|
<div class="metric-change">
|
较上月: {{ mttrChange }}%
|
</div>
|
</div>
|
|
<div class="metric-card">
|
<div class="metric-header">
|
<span class="metric-title">设备可用率</span>
|
<span class="metric-trend" :class="availabilityTrendClass">
|
{{ availabilityTrendText }}
|
</span>
|
</div>
|
<div class="metric-value">{{ currentAvailability }}%</div>
|
<div class="metric-change">
|
较上月: {{ availabilityChange }}%
|
</div>
|
</div>
|
|
<div class="metric-card">
|
<div class="metric-header">
|
<span class="metric-title">故障次数</span>
|
<span class="metric-trend" :class="failureTrendClass">
|
{{ failureTrendText }}
|
</span>
|
</div>
|
<div class="metric-value">{{ currentFailures }} 次</div>
|
<div class="metric-change">
|
较上月: {{ failureChange }}%
|
</div>
|
</div>
|
</div>
|
|
<!-- 趋势图表 -->
|
<div class="charts-section">
|
<div class="chart-card">
|
<div class="chart-header">MTBF趋势分析</div>
|
<div class="chart-container">
|
<div ref="mtbfChart" class="chart"></div>
|
</div>
|
</div>
|
|
<div class="chart-card">
|
<div class="chart-header">MTTR趋势分析</div>
|
<div class="chart-container">
|
<div ref="mttrChart" class="chart"></div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 优化建议 -->
|
<div class="recommendation-card">
|
<div class="card-header">维保策略优化建议</div>
|
<div class="recommendation-content">
|
<div
|
v-for="(recommendation, index) in recommendations"
|
:key="index"
|
class="recommendation-item"
|
>
|
<div class="recommendation-icon">💡</div>
|
<div class="recommendation-text">{{ recommendation }}</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
|
import * as echarts from 'echarts'
|
|
// 模拟数据
|
const generateMockData = () => {
|
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
|
const mtbfData = months.map(() => Math.floor(Math.random() * 200 + 300)) // 300-500小时
|
const mttrData = months.map(() => Math.floor(Math.random() * 8 + 4)) // 4-12小时
|
|
return {
|
months,
|
mtbfData,
|
mttrData
|
}
|
}
|
|
const timeRange = ref([
|
new Date(new Date().getFullYear(), 0, 1).toISOString().split('T')[0],
|
new Date().toISOString().split('T')[0]
|
])
|
|
const mockData = reactive(generateMockData())
|
|
// 计算当前值(最新月份的数据)
|
const currentMTBF = computed(() => mockData.mtbfData[mockData.mtbfData.length - 1])
|
const currentMTTR = computed(() => mockData.mttrData[mockData.mttrData.length - 1])
|
const currentAvailability = computed(() =>
|
Math.round((currentMTBF.value / (currentMTBF.value + currentMTTR.value)) * 100)
|
)
|
const currentFailures = computed(() => Math.floor(Math.random() * 10 + 5))
|
|
// 计算变化趋势
|
const mtbfChange = computed(() => {
|
const current = currentMTBF.value
|
const previous = mockData.mtbfData[mockData.mtbfData.length - 2] || current
|
return Math.round(((current - previous) / previous) * 100)
|
})
|
|
const mttrChange = computed(() => {
|
const current = currentMTTR.value
|
const previous = mockData.mttrData[mockData.mttrData.length - 2] || current
|
return Math.round(((current - previous) / previous) * 100)
|
})
|
|
const availabilityChange = computed(() => {
|
const current = currentAvailability.value
|
const previous = Math.round(
|
(mockData.mtbfData[mockData.mtbfData.length - 2] /
|
(mockData.mtbfData[mockData.mtbfData.length - 2] + mockData.mttrData[mockData.mttrData.length - 2])) * 100
|
) || current
|
return Math.round(((current - previous) / previous) * 100)
|
})
|
|
const failureChange = computed(() => {
|
const current = currentFailures.value
|
const previous = Math.floor(Math.random() * 10 + 5)
|
return Math.round(((current - previous) / previous) * 100)
|
})
|
|
// 趋势判断
|
const mtbfTrendClass = computed(() => mtbfChange.value >= 0 ? 'trend-up' : 'trend-down')
|
const mttrTrendClass = computed(() => mttrChange.value <= 0 ? 'trend-up' : 'trend-down')
|
const availabilityTrendClass = computed(() => availabilityChange.value >= 0 ? 'trend-up' : 'trend-down')
|
const failureTrendClass = computed(() => failureChange.value <= 0 ? 'trend-up' : 'trend-down')
|
|
const mtbfTrendText = computed(() => mtbfChange.value >= 0 ? '↑' : '↓')
|
const mttrTrendText = computed(() => mttrChange.value <= 0 ? '↑' : '↓')
|
const availabilityTrendText = computed(() => availabilityChange.value >= 0 ? '↑' : '↓')
|
const failureTrendText = computed(() => failureChange.value <= 0 ? '↑' : '↓')
|
|
// 优化建议
|
const recommendations = computed(() => {
|
const suggestions = []
|
|
if (currentMTBF.value < 400) {
|
suggestions.push('MTBF较低,建议加强预防性维护,定期检查关键部件')
|
}
|
|
if (currentMTTR.value > 8) {
|
suggestions.push('MTTR较高,建议优化维修流程,提高维修效率')
|
}
|
|
if (currentAvailability.value < 95) {
|
suggestions.push('设备可用率有待提升,建议优化维保计划安排')
|
}
|
|
if (currentFailures.value > 8) {
|
suggestions.push('故障次数较多,建议加强设备日常巡检')
|
}
|
|
if (suggestions.length === 0) {
|
suggestions.push('当前设备运行状况良好,继续保持现有维保策略')
|
}
|
|
return suggestions
|
})
|
|
// 图表实例
|
let mtbfChart = null
|
let mttrChart = null
|
|
const initCharts = () => {
|
nextTick(() => {
|
// MTBF图表
|
mtbfChart = echarts.init(document.querySelector('.chart-card:first-child .chart'))
|
mtbfChart.setOption({
|
tooltip: {
|
trigger: 'axis'
|
},
|
xAxis: {
|
type: 'category',
|
data: mockData.months
|
},
|
yAxis: {
|
type: 'value',
|
name: '小时'
|
},
|
series: [{
|
data: mockData.mtbfData,
|
type: 'line',
|
smooth: true,
|
lineStyle: {
|
color: '#1890ff'
|
},
|
areaStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: 'rgba(24, 144, 255, 0.3)' },
|
{ offset: 1, color: 'rgba(24, 144, 255, 0.1)' }
|
])
|
}
|
}]
|
})
|
|
// MTTR图表
|
mttrChart = echarts.init(document.querySelector('.chart-card:last-child .chart'))
|
mttrChart.setOption({
|
tooltip: {
|
trigger: 'axis'
|
},
|
xAxis: {
|
type: 'category',
|
data: mockData.months
|
},
|
yAxis: {
|
type: 'value',
|
name: '小时'
|
},
|
series: [{
|
data: mockData.mttrData,
|
type: 'line',
|
smooth: true,
|
lineStyle: {
|
color: '#52c41a'
|
},
|
areaStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: 'rgba(82, 196, 26, 0.3)' },
|
{ offset: 1, color: 'rgba(82, 196, 26, 0.1)' }
|
])
|
}
|
}]
|
})
|
})
|
}
|
|
const fetchKPLData = () => {
|
// 模拟数据刷新
|
Object.assign(mockData, generateMockData())
|
|
// 重新渲染图表
|
if (mtbfChart && mttrChart) {
|
mtbfChart.setOption({
|
xAxis: {
|
data: mockData.months
|
},
|
series: [{
|
data: mockData.mtbfData
|
}]
|
})
|
|
mttrChart.setOption({
|
xAxis: {
|
data: mockData.months
|
},
|
series: [{
|
data: mockData.mttrData
|
}]
|
})
|
}
|
}
|
|
onMounted(() => {
|
initCharts()
|
|
// 监听窗口大小变化,重新调整图表大小
|
window.addEventListener('resize', () => {
|
if (mtbfChart) mtbfChart.resize()
|
if (mttrChart) mttrChart.resize()
|
})
|
})
|
</script>
|
|
<style scoped>
|
.kpl-monitor-container {
|
min-height: 100vh;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
padding: 20px;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
}
|
|
.page-header {
|
background: rgba(255, 255, 255, 0.95);
|
border-radius: 16px;
|
padding: 20px;
|
margin-bottom: 20px;
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.page-header h1 {
|
margin: 0;
|
color: #2c3e50;
|
font-size: 24px;
|
}
|
|
.time-range {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
}
|
|
.metrics-cards {
|
display: grid;
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
gap: 20px;
|
margin-bottom: 20px;
|
}
|
|
.metric-card {
|
background: rgba(255, 255, 255, 0.95);
|
border-radius: 16px;
|
padding: 20px;
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
}
|
|
.metric-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 15px;
|
}
|
|
.metric-title {
|
font-size: 16px;
|
color: #666;
|
font-weight: 500;
|
}
|
|
.metric-trend {
|
font-size: 14px;
|
font-weight: 600;
|
}
|
|
.trend-up {
|
color: #52c41a;
|
}
|
|
.trend-down {
|
color: #ff4d4f;
|
}
|
|
.metric-value {
|
font-size: 32px;
|
font-weight: 700;
|
color: #2c3e50;
|
margin-bottom: 8px;
|
}
|
|
.metric-change {
|
font-size: 14px;
|
color: #666;
|
}
|
|
.charts-section {
|
display: grid;
|
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
|
gap: 20px;
|
margin-bottom: 20px;
|
}
|
|
.chart-card {
|
background: rgba(255, 255, 255, 0.95);
|
border-radius: 16px;
|
overflow: hidden;
|
}
|
|
.chart-header {
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
color: white;
|
padding: 16px 20px;
|
font-weight: 500;
|
font-size: 16px;
|
}
|
|
.chart-container {
|
padding: 20px;
|
height: 300px;
|
}
|
|
.chart {
|
width: 100%;
|
height: 100%;
|
}
|
|
.recommendation-card {
|
background: rgba(255, 255, 255, 0.95);
|
border-radius: 16px;
|
overflow: hidden;
|
}
|
|
.card-header {
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
color: white;
|
padding: 16px 20px;
|
font-weight: 500;
|
font-size: 16px;
|
}
|
|
.recommendation-content {
|
padding: 20px;
|
}
|
|
.recommendation-item {
|
display: flex;
|
align-items: flex-start;
|
margin-bottom: 15px;
|
padding: 12px;
|
background: #f8f9fa;
|
border-radius: 8px;
|
}
|
|
.recommendation-icon {
|
font-size: 18px;
|
margin-right: 12px;
|
margin-top: 2px;
|
}
|
|
.recommendation-text {
|
flex: 1;
|
color: #2c3e50;
|
line-height: 1.5;
|
}
|
|
@media (max-width: 768px) {
|
.kpl-monitor-container {
|
padding: 16px;
|
}
|
|
.page-header {
|
flex-direction: column;
|
gap: 15px;
|
align-items: stretch;
|
}
|
|
.metrics-cards {
|
grid-template-columns: 1fr;
|
}
|
|
.charts-section {
|
grid-template-columns: 1fr;
|
}
|
|
.chart-container {
|
height: 250px;
|
}
|
}
|
</style>
|