| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |