| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="kpl-monitor-container"> |
| | | <!-- 页é¢å¤´é¨ --> |
| | | <div class="page-header"> |
| | | <div class="header-content"> |
| | | <h1>KPLçæ§åæ</h1> |
| | | <p>设å¤å
³é®æ§è½ææ çæ§ä¸ç»´ä¿çç¥ä¼å</p> |
| | | </div> |
| | | <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-overview"> |
| | | <div class="metric-card mtbf-card"> |
| | | <div class="metric-icon">â±ï¸</div> |
| | | <div class="metric-content"> |
| | | <div class="metric-title">MTBF</div> |
| | | <div class="metric-subtitle">å¹³åæ æ
éæ¶é´</div> |
| | | <div class="metric-value">{{ currentMTBF }}<span class="unit">å°æ¶</span></div> |
| | | <div class="metric-trend" :class="mtbfTrendClass"> |
| | | <span class="trend-icon">{{ mtbfTrendText }}</span> |
| | | <span class="trend-text">{{ Math.abs(mtbfChange) }}%</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="metric-card mttr-card"> |
| | | <div class="metric-icon">ð§</div> |
| | | <div class="metric-content"> |
| | | <div class="metric-title">MTTR</div> |
| | | <div class="metric-subtitle">å¹³åä¿®å¤æ¶é´</div> |
| | | <div class="metric-value">{{ currentMTTR }}<span class="unit">å°æ¶</span></div> |
| | | <div class="metric-trend" :class="mttrTrendClass"> |
| | | <span class="trend-icon">{{ mttrTrendText }}</span> |
| | | <span class="trend-text">{{ Math.abs(mttrChange) }}%</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="metric-card availability-card"> |
| | | <div class="metric-icon">ð</div> |
| | | <div class="metric-content"> |
| | | <div class="metric-title">设å¤å¯ç¨ç</div> |
| | | <div class="metric-subtitle">è¿è¡æçææ </div> |
| | | <div class="metric-value">{{ currentAvailability }}<span class="unit">%</span></div> |
| | | <div class="metric-trend" :class="availabilityTrendClass"> |
| | | <span class="trend-icon">{{ availabilityTrendText }}</span> |
| | | <span class="trend-text">{{ Math.abs(availabilityChange) }}%</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- è¶å¿åæå¾è¡¨ --> |
| | | <div class="charts-section"> |
| | | <div class="chart-container"> |
| | | <div class="chart-header"> |
| | | <h3>MTBF & MTTR è¶å¿åæ</h3> |
| | | <div class="chart-legend"> |
| | | <span class="legend-item mtbf-legend"> |
| | | <span class="legend-color"></span> |
| | | MTBF (å¹³åæ æ
éæ¶é´) |
| | | </span> |
| | | <span class="legend-item mttr-legend"> |
| | | <span class="legend-color"></span> |
| | | MTTR (å¹³åä¿®å¤æ¶é´) |
| | | </span> |
| | | </div> |
| | | </div> |
| | | <div class="chart-wrapper"> |
| | | <div ref="trendChart" class="chart"></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- ç»´ä¿çç¥å»ºè®® --> |
| | | <div class="recommendations-section"> |
| | | <div class="section-header"> |
| | | <h3>ç»´ä¿çç¥ä¼å建议</h3> |
| | | <p>åºäºå½åææ åæï¼ä¸ºæ¨æä¾é对æ§çä¼å建议</p> |
| | | </div> |
| | | <div class="recommendations-grid"> |
| | | <div |
| | | v-for="(recommendation, index) in recommendations" |
| | | :key="index" |
| | | class="recommendation-card" |
| | | > |
| | | <div class="recommendation-icon">{{ recommendation.icon }}</div> |
| | | <div class="recommendation-content"> |
| | | <h4>{{ recommendation.title }}</h4> |
| | | <p>{{ recommendation.description }}</p> |
| | | <div class="recommendation-priority" :class="recommendation.priority"> |
| | | {{ recommendation.priorityText }} |
| | | </div> |
| | | </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 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 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 mtbfTrendText = computed(() => mtbfChange.value >= 0 ? 'â' : 'â') |
| | | const mttrTrendText = computed(() => mttrChange.value <= 0 ? 'â' : 'â') |
| | | const availabilityTrendText = computed(() => availabilityChange.value >= 0 ? 'â' : 'â') |
| | | |
| | | // æºè½ç»´ä¿å»ºè®® |
| | | const recommendations = computed(() => { |
| | | const suggestions = [] |
| | | |
| | | // MTBFç¸å
³å»ºè®® |
| | | if (currentMTBF.value < 400) { |
| | | suggestions.push({ |
| | | icon: 'ð§', |
| | | title: 'æåMTBF', |
| | | description: 'å½åMTBFè¾ä½ï¼å»ºè®®å 强é¢é²æ§ç»´æ¤ï¼å®ææ£æ¥å
³é®é¨ä»¶ï¼å»¶é¿è®¾å¤æ æ
éè¿è¡æ¶é´', |
| | | priority: 'high', |
| | | priorityText: 'é«ä¼å
级' |
| | | }) |
| | | } |
| | | |
| | | // MTTRç¸å
³å»ºè®® |
| | | if (currentMTTR.value > 8) { |
| | | suggestions.push({ |
| | | icon: 'â¡', |
| | | title: 'ä¼åMTTR', |
| | | description: 'å½åMTTRè¾é«ï¼å»ºè®®ä¼åç»´ä¿®æµç¨ï¼æé«ç»´ä¿®äººåæè½ï¼ç¼©çæ
éä¿®å¤æ¶é´', |
| | | priority: 'high', |
| | | priorityText: 'é«ä¼å
级' |
| | | }) |
| | | } |
| | | |
| | | // å¯ç¨çç¸å
³å»ºè®® |
| | | if (currentAvailability.value < 95) { |
| | | suggestions.push({ |
| | | icon: 'ð', |
| | | title: 'æåå¯ç¨ç', |
| | | description: '设å¤å¯ç¨çæå¾
æåï¼å»ºè®®ä¼åç»´ä¿è®¡å宿ï¼åå°è®¡åå¤åæºæ¶é´', |
| | | priority: 'medium', |
| | | priorityText: 'ä¸ä¼å
级' |
| | | }) |
| | | } |
| | | |
| | | // 综å建议 |
| | | if (currentMTBF.value >= 400 && currentMTTR.value <= 8 && currentAvailability.value >= 95) { |
| | | suggestions.push({ |
| | | icon: 'â
', |
| | | title: 'è¿è¡ç¶åµè¯å¥½', |
| | | description: 'å½å设å¤è¿è¡ç¶åµè¯å¥½ï¼åé¡¹ææ åè¾¾å°é¢æï¼å»ºè®®ç»§ç»ä¿æç°æç»´ä¿çç¥', |
| | | priority: 'low', |
| | | priorityText: 'ä½ä¼å
级' |
| | | }) |
| | | } |
| | | |
| | | // é¢é²æ§å»ºè®® |
| | | suggestions.push({ |
| | | icon: 'ð', |
| | | title: 'é¢é²æ§ç»´æ¤', |
| | | description: '建议建ç«è®¾å¤å¥åº·æ¡£æ¡ï¼å®æåæè®¾å¤è¿è¡æ°æ®ï¼æåè¯å«æ½å¨æ
éé£é©', |
| | | priority: 'medium', |
| | | priorityText: 'ä¸ä¼å
级' |
| | | }) |
| | | |
| | | return suggestions |
| | | }) |
| | | |
| | | // å¾è¡¨å®ä¾ |
| | | let trendChart = null |
| | | |
| | | const initChart = () => { |
| | | nextTick(() => { |
| | | trendChart = echarts.init(document.querySelector('.chart')) |
| | | trendChart.setOption({ |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'cross' |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['MTBF', 'MTTR'], |
| | | top: 10 |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: mockData.months, |
| | | axisLine: { |
| | | lineStyle: { |
| | | color: '#e0e0e0' |
| | | } |
| | | } |
| | | }, |
| | | yAxis: [ |
| | | { |
| | | type: 'value', |
| | | name: 'MTBF (å°æ¶)', |
| | | position: 'left', |
| | | axisLine: { |
| | | lineStyle: { |
| | | color: '#1890ff' |
| | | } |
| | | }, |
| | | axisLabel: { |
| | | formatter: '{value}h' |
| | | } |
| | | }, |
| | | { |
| | | type: 'value', |
| | | name: 'MTTR (å°æ¶)', |
| | | position: 'right', |
| | | axisLine: { |
| | | lineStyle: { |
| | | color: '#52c41a' |
| | | } |
| | | }, |
| | | axisLabel: { |
| | | formatter: '{value}h' |
| | | } |
| | | } |
| | | ], |
| | | series: [ |
| | | { |
| | | name: 'MTBF', |
| | | type: 'line', |
| | | yAxisIndex: 0, |
| | | data: mockData.mtbfData, |
| | | smooth: true, |
| | | lineStyle: { |
| | | color: '#1890ff', |
| | | width: 3 |
| | | }, |
| | | itemStyle: { |
| | | 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)' } |
| | | ]) |
| | | } |
| | | }, |
| | | { |
| | | name: 'MTTR', |
| | | type: 'line', |
| | | yAxisIndex: 1, |
| | | data: mockData.mttrData, |
| | | smooth: true, |
| | | lineStyle: { |
| | | color: '#52c41a', |
| | | width: 3 |
| | | }, |
| | | itemStyle: { |
| | | 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 (trendChart) { |
| | | trendChart.setOption({ |
| | | xAxis: { |
| | | data: mockData.months |
| | | }, |
| | | series: [ |
| | | { |
| | | data: mockData.mtbfData |
| | | }, |
| | | { |
| | | data: mockData.mttrData |
| | | } |
| | | ] |
| | | }) |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | initChart() |
| | | |
| | | // çå¬çªå£å¤§å°ååï¼éæ°è°æ´å¾è¡¨å¤§å° |
| | | window.addEventListener('resize', () => { |
| | | if (trendChart) trendChart.resize() |
| | | }) |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .kpl-monitor-container { |
| | | min-height: 100vh; |
| | | background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); |
| | | padding: 24px; |
| | | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
| | | } |
| | | |
| | | /* 页é¢å¤´é¨ */ |
| | | .page-header { |
| | | background: white; |
| | | border-radius: 12px; |
| | | padding: 24px; |
| | | margin-bottom: 24px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .header-content h1 { |
| | | margin: 0 0 8px 0; |
| | | color: #1f2937; |
| | | font-size: 28px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .header-content p { |
| | | margin: 0; |
| | | color: #6b7280; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .time-range { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | |
| | | /* ææ æ¦è§ */ |
| | | .metrics-overview { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); |
| | | gap: 20px; |
| | | margin-bottom: 24px; |
| | | } |
| | | |
| | | .metric-card { |
| | | background: white; |
| | | border-radius: 12px; |
| | | padding: 24px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16px; |
| | | transition: transform 0.2s ease, box-shadow 0.2s ease; |
| | | } |
| | | |
| | | .metric-card:hover { |
| | | transform: translateY(-2px); |
| | | box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); |
| | | } |
| | | |
| | | .metric-icon { |
| | | font-size: 32px; |
| | | width: 60px; |
| | | height: 60px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 12px; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | } |
| | | |
| | | .mtbf-card .metric-icon { |
| | | background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); |
| | | } |
| | | |
| | | .mttr-card .metric-icon { |
| | | background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); |
| | | } |
| | | |
| | | .availability-card .metric-icon { |
| | | background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); |
| | | } |
| | | |
| | | .metric-content { |
| | | flex: 1; |
| | | } |
| | | |
| | | .metric-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1f2937; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .metric-subtitle { |
| | | font-size: 14px; |
| | | color: #6b7280; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .metric-value { |
| | | font-size: 32px; |
| | | font-weight: 700; |
| | | color: #1f2937; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .unit { |
| | | font-size: 16px; |
| | | font-weight: 500; |
| | | color: #6b7280; |
| | | margin-left: 4px; |
| | | } |
| | | |
| | | .metric-trend { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .trend-up { |
| | | color: #10b981; |
| | | } |
| | | |
| | | .trend-down { |
| | | color: #ef4444; |
| | | } |
| | | |
| | | .trend-icon { |
| | | font-size: 16px; |
| | | } |
| | | |
| | | /* å¾è¡¨åºå */ |
| | | .charts-section { |
| | | margin-bottom: 24px; |
| | | } |
| | | |
| | | .chart-container { |
| | | background: white; |
| | | border-radius: 12px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .chart-header { |
| | | padding: 20px 24px; |
| | | border-bottom: 1px solid #e5e7eb; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .chart-header h3 { |
| | | margin: 0; |
| | | color: #1f2937; |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .chart-legend { |
| | | display: flex; |
| | | gap: 20px; |
| | | } |
| | | |
| | | .legend-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | font-size: 14px; |
| | | color: #6b7280; |
| | | } |
| | | |
| | | .legend-color { |
| | | width: 12px; |
| | | height: 12px; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .mtbf-legend .legend-color { |
| | | background: #1890ff; |
| | | } |
| | | |
| | | .mttr-legend .legend-color { |
| | | background: #52c41a; |
| | | } |
| | | |
| | | .chart-wrapper { |
| | | padding: 20px; |
| | | height: 400px; |
| | | } |
| | | |
| | | .chart { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | /* 建议åºå */ |
| | | .recommendations-section { |
| | | background: white; |
| | | border-radius: 12px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .section-header { |
| | | padding: 24px; |
| | | border-bottom: 1px solid #e5e7eb; |
| | | } |
| | | |
| | | .section-header h3 { |
| | | margin: 0 0 8px 0; |
| | | color: #1f2937; |
| | | font-size: 20px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .section-header p { |
| | | margin: 0; |
| | | color: #6b7280; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .recommendations-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); |
| | | gap: 20px; |
| | | padding: 24px; |
| | | } |
| | | |
| | | .recommendation-card { |
| | | background: #f8fafc; |
| | | border-radius: 8px; |
| | | padding: 20px; |
| | | display: flex; |
| | | gap: 16px; |
| | | transition: transform 0.2s ease, box-shadow 0.2s ease; |
| | | } |
| | | |
| | | .recommendation-card:hover { |
| | | transform: translateY(-1px); |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .recommendation-icon { |
| | | font-size: 24px; |
| | | width: 40px; |
| | | height: 40px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 8px; |
| | | background: white; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .recommendation-content { |
| | | flex: 1; |
| | | } |
| | | |
| | | .recommendation-content h4 { |
| | | margin: 0 0 8px 0; |
| | | color: #1f2937; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .recommendation-content p { |
| | | margin: 0 0 12px 0; |
| | | color: #6b7280; |
| | | font-size: 14px; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .recommendation-priority { |
| | | display: inline-block; |
| | | padding: 4px 8px; |
| | | border-radius: 4px; |
| | | font-size: 12px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .priority.high { |
| | | background: #fef2f2; |
| | | color: #dc2626; |
| | | } |
| | | |
| | | .priority.medium { |
| | | background: #fffbeb; |
| | | color: #d97706; |
| | | } |
| | | |
| | | .priority.low { |
| | | background: #f0fdf4; |
| | | color: #16a34a; |
| | | } |
| | | |
| | | /* ååºå¼è®¾è®¡ */ |
| | | @media (max-width: 768px) { |
| | | .kpl-monitor-container { |
| | | padding: 16px; |
| | | } |
| | | |
| | | .page-header { |
| | | flex-direction: column; |
| | | gap: 16px; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | .metrics-overview { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | |
| | | .recommendations-grid { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | |
| | | .chart-wrapper { |
| | | height: 300px; |
| | | } |
| | | |
| | | .chart-legend { |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 480px) { |
| | | .metric-card { |
| | | flex-direction: column; |
| | | text-align: center; |
| | | } |
| | | |
| | | .recommendation-card { |
| | | flex-direction: column; |
| | | text-align: center; |
| | | } |
| | | } |
| | | </style> |