| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container data-analysis-container"> |
| | | <!-- ç鿡件 --> |
| | | <div class="search_form"> |
| | | <div class="search-left"> |
| | | <span class="search_title">æ¶é´èå´ï¼</span> |
| | | <el-date-picker |
| | | v-model="dateRange" |
| | | type="daterange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | @change="handleQuery" |
| | | style="width: 260px" |
| | | /> |
| | | <span class="search_title" style="margin-left: 20px">æ°æ®ç±»åï¼</span> |
| | | <el-select |
| | | v-model="searchForm.dataType" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | @change="handleQuery" |
| | | style="width: 140px" |
| | | > |
| | | <el-option label="温度" value="temperature" /> |
| | | <el-option label="湿度" value="humidity" /> |
| | | <el-option label="åå" value="pressure" /> |
| | | <el-option label="æµé" value="flow" /> |
| | | <el-option label="æµåº¦" value="concentration" /> |
| | | </el-select> |
| | | <span class="search_title" style="margin-left: 20px">设å¤ç¼å·ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.deviceCode" |
| | | placeholder="请è¾å
¥è®¾å¤ç¼å·" |
| | | clearable |
| | | @change="handleQuery" |
| | | style="width: 160px" |
| | | /> |
| | | <span class="search_title" style="margin-left: 20px">è¶å¿ç²åº¦ï¼</span> |
| | | <el-radio-group v-model="searchForm.granularity" @change="handleQuery"> |
| | | <el-radio-button value="day">æå¤©</el-radio-button> |
| | | <el-radio-button value="hour">æå°æ¶</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <div class="search-right"> |
| | | <el-button type="primary" @click="handleQuery">æ¥è¯¢</el-button> |
| | | <el-button @click="resetQuery">éç½®</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- æ¦è§ææ å¡ç --> |
| | | <div class="overview-cards"> |
| | | <div class="overview-card"> |
| | | <div class="card-icon total"> |
| | | <el-icon><DataLine /></el-icon> |
| | | </div> |
| | | <div class="card-content"> |
| | | <div class="card-label">ééæ»æ¡æ°</div> |
| | | <div class="card-value">{{ dashboardData.overview?.totalCollections || 0 }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="overview-card"> |
| | | <div class="card-icon today"> |
| | | <el-icon><Calendar /></el-icon> |
| | | </div> |
| | | <div class="card-content"> |
| | | <div class="card-label">仿¥éé</div> |
| | | <div class="card-value">{{ dashboardData.overview?.todayCollections || 0 }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="overview-card"> |
| | | <div class="card-icon abnormal"> |
| | | <el-icon><Warning /></el-icon> |
| | | </div> |
| | | <div class="card-content"> |
| | | <div class="card-label">å¼å¸¸æ°æ®</div> |
| | | <div class="card-value">{{ dashboardData.overview?.abnormalCollections || 0 }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="overview-card"> |
| | | <div class="card-icon rate"> |
| | | <el-icon><CircleCheck /></el-icon> |
| | | </div> |
| | | <div class="card-content"> |
| | | <div class="card-label">åæ ¼ç</div> |
| | | <div class="card-value">{{ dashboardData.overview?.qualifiedRate || 0 }}%</div> |
| | | </div> |
| | | </div> |
| | | <div class="overview-card"> |
| | | <div class="card-icon experiment"> |
| | | <el-icon><Collection /></el-icon> |
| | | </div> |
| | | <div class="card-content"> |
| | | <div class="card-label">è¿è¡ä¸å®éª</div> |
| | | <div class="card-value">{{ dashboardData.overview?.inProgressExperiments || 0 }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="overview-card"> |
| | | <div class="card-icon warning"> |
| | | <el-icon><Bell /></el-icon> |
| | | </div> |
| | | <div class="card-content"> |
| | | <div class="card-label">é¢è¦çæ§</div> |
| | | <div class="card-value">{{ dashboardData.overview?.warningMonitors || 0 }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="overview-card"> |
| | | <div class="card-icon sample"> |
| | | <el-icon><Box /></el-icon> |
| | | </div> |
| | | <div class="card-content"> |
| | | <div class="card-label">å¨åºæ ·å</div> |
| | | <div class="card-value">{{ dashboardData.overview?.inStockSamples || 0 }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- å¾è¡¨åºå --> |
| | | <div class="charts-container"> |
| | | <!-- è¶å¿åæ --> |
| | | <div class="chart-panel trend-panel"> |
| | | <div class="panel-header"> |
| | | <div class="panel-title"> |
| | | <el-icon><TrendCharts /></el-icon> |
| | | æ°æ®è¶å¿åæ |
| | | </div> |
| | | <div class="panel-extra"> |
| | | <span v-if="dashboardData.startTime && dashboardData.endTime"> |
| | | {{ formatDateTime(dashboardData.startTime) }} ~ {{ formatDateTime(dashboardData.endTime) }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | <div class="chart-content"> |
| | | <div ref="trendChartRef" style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- æ¯è¾åæ --> |
| | | <div class="chart-panel comparison-panel"> |
| | | <div class="panel-header"> |
| | | <div class="panel-title"> |
| | | <el-icon><Histogram /></el-icon> |
| | | æ°æ®æ¯è¾åæ |
| | | </div> |
| | | <el-radio-group v-model="searchForm.dimension" size="small" @change="handleQuery"> |
| | | <el-radio-button value="dataType">ææ°æ®ç±»å</el-radio-button> |
| | | <el-radio-button value="deviceName">æè®¾å¤åç§°</el-radio-button> |
| | | <el-radio-button value="deviceCode">æè®¾å¤ç¼å·</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <div class="chart-content"> |
| | | <div ref="comparisonChartRef" style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- è´¨éåå¸ --> |
| | | <div class="chart-panel quality-panel"> |
| | | <div class="panel-header"> |
| | | <div class="panel-title"> |
| | | <el-icon><PieChart /></el-icon> |
| | | è´¨éåå¸ç»è®¡ |
| | | </div> |
| | | </div> |
| | | <div class="chart-content"> |
| | | <div ref="qualityChartRef" style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | <!-- è´¨éåå¸å¾ä¾ --> |
| | | <div class="quality-legend" v-if="qualityChartData.length > 0"> |
| | | <div class="legend-item" v-for="item in qualityChartData" :key="item.category"> |
| | | <span class="legend-dot" :style="{ backgroundColor: item.color }"></span> |
| | | <span class="legend-label">{{ item.name }}</span> |
| | | <span class="legend-value">{{ item.value }}</span> |
| | | <span class="legend-ratio">({{ item.ratio }}%)</span> |
| | | </div> |
| | | </div> |
| | | <div v-else class="no-data-text">ææ æ°æ®</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed, onMounted, onBeforeUnmount, nextTick } from 'vue' |
| | | import { |
| | | DataLine, Calendar, Warning, CircleCheck, |
| | | Collection, Bell, Box, TrendCharts, Histogram, PieChart |
| | | } from '@element-plus/icons-vue' |
| | | import { getDashboard } from '@/api/lims/dataAnalysis' |
| | | import { ElMessage } from 'element-plus' |
| | | import * as echarts from 'echarts' |
| | | |
| | | // æç´¢è¡¨å |
| | | const searchForm = reactive({ |
| | | dataType: '', |
| | | deviceCode: '', |
| | | granularity: 'day', |
| | | dimension: 'dataType' |
| | | }) |
| | | |
| | | // æ¥æèå´ |
| | | const dateRange = ref([]) |
| | | |
| | | // çæ¿æ°æ® |
| | | const dashboardData = ref({ |
| | | overview: {}, |
| | | trend: [], |
| | | comparison: [], |
| | | qualityDistribution: [] |
| | | }) |
| | | |
| | | // å¾è¡¨å®ä¾ |
| | | const trendChartRef = ref(null) |
| | | const comparisonChartRef = ref(null) |
| | | const qualityChartRef = ref(null) |
| | | let trendChart = null |
| | | let comparisonChart = null |
| | | let qualityChart = null |
| | | |
| | | // å è½½ç¶æ |
| | | const loading = ref(false) |
| | | |
| | | // è·åé»è®¤æ¶é´èå´ï¼æè¿7å¤©ï¼ |
| | | const getDefaultDateRange = () => { |
| | | const end = new Date() |
| | | const start = new Date() |
| | | start.setDate(start.getDate() - 7) |
| | | return [ |
| | | start.toISOString().slice(0, 10), |
| | | end.toISOString().slice(0, 10) |
| | | ] |
| | | } |
| | | |
| | | // æ ¼å¼åæ¥ææ¶é´ |
| | | const formatDateTime = (dateTime) => { |
| | | if (!dateTime) return '' |
| | | return dateTime.replace('T', ' ') |
| | | } |
| | | |
| | | // åå§åè¶å¿å¾è¡¨ |
| | | const initTrendChart = () => { |
| | | if (!trendChartRef.value) return |
| | | trendChart = echarts.init(trendChartRef.value) |
| | | updateTrendChart() |
| | | } |
| | | |
| | | // æ´æ°è¶å¿å¾è¡¨ |
| | | const updateTrendChart = () => { |
| | | if (!trendChart) return |
| | | |
| | | const trend = dashboardData.value.trend || [] |
| | | const xAxis = trend.map(item => item.time) |
| | | const pointCount = trend.map(item => item.pointCount) |
| | | const avgValue = trend.map(item => item.avgValue) |
| | | const maxValue = trend.map(item => item.maxValue) |
| | | const minValue = trend.map(item => item.minValue) |
| | | |
| | | const option = { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'cross' |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['ééç¹æ°', 'å¹³åå¼', 'æå¤§å¼', 'æå°å¼'], |
| | | bottom: 0 |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '10%', |
| | | top: '10%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | boundaryGap: false, |
| | | data: xAxis |
| | | }, |
| | | yAxis: [ |
| | | { |
| | | type: 'value', |
| | | name: 'ééç¹æ°', |
| | | position: 'left' |
| | | }, |
| | | { |
| | | type: 'value', |
| | | name: 'æ°å¼', |
| | | position: 'right' |
| | | } |
| | | ], |
| | | series: [ |
| | | { |
| | | name: 'ééç¹æ°', |
| | | type: 'line', |
| | | data: pointCount, |
| | | smooth: true, |
| | | areaStyle: { |
| | | opacity: 0.1 |
| | | } |
| | | }, |
| | | { |
| | | name: 'å¹³åå¼', |
| | | type: 'line', |
| | | yAxisIndex: 1, |
| | | data: avgValue, |
| | | smooth: true |
| | | }, |
| | | { |
| | | name: 'æå¤§å¼', |
| | | type: 'line', |
| | | yAxisIndex: 1, |
| | | data: maxValue, |
| | | smooth: true |
| | | }, |
| | | { |
| | | name: 'æå°å¼', |
| | | type: 'line', |
| | | yAxisIndex: 1, |
| | | data: minValue, |
| | | smooth: true |
| | | } |
| | | ] |
| | | } |
| | | |
| | | trendChart.setOption(option, true) |
| | | } |
| | | |
| | | // åå§åæ¯è¾å¾è¡¨ |
| | | const initComparisonChart = () => { |
| | | if (!comparisonChartRef.value) return |
| | | comparisonChart = echarts.init(comparisonChartRef.value) |
| | | updateComparisonChart() |
| | | } |
| | | |
| | | // æ´æ°æ¯è¾å¾è¡¨ |
| | | const updateComparisonChart = () => { |
| | | if (!comparisonChart) return |
| | | |
| | | const comparison = dashboardData.value.comparison || [] |
| | | |
| | | if (comparison.length === 0) { |
| | | comparisonChart.clear() |
| | | return |
| | | } |
| | | |
| | | const xAxis = comparison.map(item => item.dimensionValue) |
| | | const pointCount = comparison.map(item => item.pointCount) |
| | | const avgValue = comparison.map(item => item.avgValue) |
| | | |
| | | const option = { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'shadow' |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['ééç¹æ°', 'å¹³åå¼'], |
| | | bottom: 0 |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '10%', |
| | | top: '10%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: xAxis |
| | | }, |
| | | yAxis: [ |
| | | { |
| | | type: 'value', |
| | | name: 'ééç¹æ°' |
| | | }, |
| | | { |
| | | type: 'value', |
| | | name: 'å¹³åå¼' |
| | | } |
| | | ], |
| | | series: [ |
| | | { |
| | | name: 'ééç¹æ°', |
| | | type: 'bar', |
| | | data: pointCount, |
| | | itemStyle: { |
| | | borderRadius: [4, 4, 0, 0] |
| | | } |
| | | }, |
| | | { |
| | | name: 'å¹³åå¼', |
| | | type: 'bar', |
| | | yAxisIndex: 1, |
| | | data: avgValue, |
| | | itemStyle: { |
| | | borderRadius: [4, 4, 0, 0] |
| | | } |
| | | } |
| | | ] |
| | | } |
| | | |
| | | comparisonChart.setOption(option, true) |
| | | } |
| | | |
| | | // è´¨éåå¸é¢è²æ å° |
| | | const qualityColorMap = { |
| | | qualified: '#67C23A', |
| | | abnormal: '#F56C6C', |
| | | pending: '#E6A23C' |
| | | } |
| | | |
| | | const qualityNameMap = { |
| | | qualified: 'åæ ¼', |
| | | abnormal: 'å¼å¸¸', |
| | | pending: 'å¾
å¤ç' |
| | | } |
| | | |
| | | // è´¨éåå¸å¾è¡¨æ°æ® |
| | | const qualityChartData = computed(() => { |
| | | const distribution = dashboardData.value.qualityDistribution || [] |
| | | return distribution.map(item => ({ |
| | | ...item, |
| | | name: qualityNameMap[item.category] || item.category, |
| | | value: item.pointCount, |
| | | color: qualityColorMap[item.category] || '#909399' |
| | | })) |
| | | }) |
| | | |
| | | // åå§åè´¨éåå¸å¾è¡¨ |
| | | const initQualityChart = () => { |
| | | if (!qualityChartRef.value) return |
| | | qualityChart = echarts.init(qualityChartRef.value) |
| | | updateQualityChart() |
| | | } |
| | | |
| | | // æ´æ°è´¨éåå¸å¾è¡¨ |
| | | const updateQualityChart = () => { |
| | | if (!qualityChart) return |
| | | |
| | | const distribution = dashboardData.value.qualityDistribution || [] |
| | | |
| | | if (distribution.length === 0) { |
| | | qualityChart.clear() |
| | | return |
| | | } |
| | | |
| | | const data = distribution.map(item => ({ |
| | | name: qualityNameMap[item.category] || item.category, |
| | | value: item.pointCount, |
| | | itemStyle: { |
| | | color: qualityColorMap[item.category] || '#909399' |
| | | } |
| | | })) |
| | | |
| | | const option = { |
| | | tooltip: { |
| | | trigger: 'item', |
| | | formatter: '{b}: {c} ({d}%)' |
| | | }, |
| | | series: [ |
| | | { |
| | | type: 'pie', |
| | | radius: ['40%', '70%'], |
| | | avoidLabelOverlap: false, |
| | | itemStyle: { |
| | | borderRadius: 10, |
| | | borderColor: '#fff', |
| | | borderWidth: 2 |
| | | }, |
| | | label: { |
| | | show: false, |
| | | position: 'center' |
| | | }, |
| | | emphasis: { |
| | | label: { |
| | | show: true, |
| | | fontSize: 20, |
| | | fontWeight: 'bold' |
| | | } |
| | | }, |
| | | labelLine: { |
| | | show: false |
| | | }, |
| | | data: data |
| | | } |
| | | ] |
| | | } |
| | | |
| | | qualityChart.setOption(option, true) |
| | | } |
| | | |
| | | // æ¥è¯¢æ°æ® |
| | | const handleQuery = async () => { |
| | | loading.value = true |
| | | try { |
| | | const params = { |
| | | ...searchForm |
| | | } |
| | | |
| | | // å¤çæ¶é´èå´ - å°æ¥æè½¬æ¢ä¸ºæ¥ææ¶é´æ ¼å¼ |
| | | if (dateRange.value && dateRange.value.length === 2) { |
| | | params.startTime = dateRange.value[0] + ' 00:00:00' |
| | | params.endTime = dateRange.value[1] + ' 23:59:59' |
| | | } |
| | | |
| | | const res = await getDashboard(params) |
| | | if (res.code === 200) { |
| | | dashboardData.value = res.data || {} |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | nextTick(() => { |
| | | updateTrendChart() |
| | | updateComparisonChart() |
| | | updateQualityChart() |
| | | }) |
| | | } else { |
| | | ElMessage.error(res.msg || 'è·åæ°æ®å¤±è´¥') |
| | | } |
| | | } catch (error) { |
| | | console.error('è·åçæ¿æ°æ®å¤±è´¥:', error) |
| | | ElMessage.error('è·åæ°æ®å¤±è´¥') |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | // éç½®æ¥è¯¢ |
| | | const resetQuery = () => { |
| | | searchForm.dataType = '' |
| | | searchForm.deviceCode = '' |
| | | searchForm.granularity = 'day' |
| | | searchForm.dimension = 'dataType' |
| | | dateRange.value = getDefaultDateRange() |
| | | handleQuery() |
| | | } |
| | | |
| | | // çªå£å¤§å°æ¹åæ¶éæ°è°æ´å¾è¡¨ |
| | | const handleResize = () => { |
| | | trendChart?.resize() |
| | | comparisonChart?.resize() |
| | | qualityChart?.resize() |
| | | } |
| | | |
| | | // åå§å |
| | | onMounted(() => { |
| | | dateRange.value = getDefaultDateRange() |
| | | nextTick(() => { |
| | | initTrendChart() |
| | | initComparisonChart() |
| | | initQualityChart() |
| | | handleQuery() |
| | | }) |
| | | window.addEventListener('resize', handleResize) |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | window.removeEventListener('resize', handleResize) |
| | | trendChart?.dispose() |
| | | comparisonChart?.dispose() |
| | | qualityChart?.dispose() |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .data-analysis-container { |
| | | .search_form { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | gap: 10px; |
| | | margin-bottom: 20px; |
| | | |
| | | .search-left { |
| | | display: flex; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .search-right { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | } |
| | | |
| | | // æ¦è§å¡ç |
| | | .overview-cards { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
| | | gap: 16px; |
| | | margin-bottom: 20px; |
| | | |
| | | .overview-card { |
| | | background: #fff; |
| | | border-radius: 8px; |
| | | padding: 20px; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16px; |
| | | box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); |
| | | transition: transform 0.3s; |
| | | |
| | | &:hover { |
| | | transform: translateY(-2px); |
| | | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .card-icon { |
| | | width: 56px; |
| | | height: 56px; |
| | | border-radius: 12px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 28px; |
| | | |
| | | &.total { |
| | | background: linear-gradient(135deg, #409EFF 0%, #1677FF 100%); |
| | | color: #fff; |
| | | } |
| | | |
| | | &.today { |
| | | background: linear-gradient(135deg, #67C23A 0%, #52C41A 100%); |
| | | color: #fff; |
| | | } |
| | | |
| | | &.abnormal { |
| | | background: linear-gradient(135deg, #F56C6C 0%, #FF4D4F 100%); |
| | | color: #fff; |
| | | } |
| | | |
| | | &.rate { |
| | | background: linear-gradient(135deg, #E6A23C 0%, #FAAD14 100%); |
| | | color: #fff; |
| | | } |
| | | |
| | | &.experiment { |
| | | background: linear-gradient(135deg, #909399 0%, #666666 100%); |
| | | color: #fff; |
| | | } |
| | | |
| | | &.warning { |
| | | background: linear-gradient(135deg, #FF6B6B 0%, #EE5A6F 100%); |
| | | color: #fff; |
| | | } |
| | | |
| | | &.sample { |
| | | background: linear-gradient(135deg, #36CFC9 0%, #13C2C2 100%); |
| | | color: #fff; |
| | | } |
| | | } |
| | | |
| | | .card-content { |
| | | flex: 1; |
| | | |
| | | .card-label { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .card-value { |
| | | font-size: 28px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | line-height: 1; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // å¾è¡¨å®¹å¨ |
| | | .charts-container { |
| | | display: grid; |
| | | grid-template-columns: 2fr 1fr; |
| | | grid-template-rows: 1fr 1fr; |
| | | gap: 20px; |
| | | |
| | | .chart-panel { |
| | | background: #fff; |
| | | border-radius: 8px; |
| | | padding: 20px; |
| | | box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); |
| | | |
| | | .panel-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20px; |
| | | |
| | | .panel-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | |
| | | .el-icon { |
| | | font-size: 20px; |
| | | color: #409EFF; |
| | | } |
| | | } |
| | | |
| | | .panel-extra { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | } |
| | | |
| | | .chart-content { |
| | | height: 300px; |
| | | } |
| | | } |
| | | |
| | | .trend-panel { |
| | | grid-row: span 2; |
| | | |
| | | .chart-content { |
| | | height: 620px; |
| | | } |
| | | } |
| | | |
| | | .quality-panel { |
| | | .chart-content { |
| | | height: 220px; |
| | | } |
| | | |
| | | .quality-legend { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | margin-top: 16px; |
| | | padding-top: 16px; |
| | | border-top: 1px solid #EBEEF5; |
| | | |
| | | .legend-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | |
| | | .legend-dot { |
| | | width: 12px; |
| | | height: 12px; |
| | | border-radius: 50%; |
| | | } |
| | | |
| | | .legend-label { |
| | | flex: 1; |
| | | font-size: 14px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .legend-value { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .legend-ratio { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .no-data-text { |
| | | text-align: center; |
| | | color: #909399; |
| | | font-size: 14px; |
| | | padding: 40px 0; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 1200px) { |
| | | .data-analysis-container { |
| | | .charts-container { |
| | | grid-template-columns: 1fr; |
| | | grid-template-rows: auto; |
| | | |
| | | .trend-panel { |
| | | grid-row: span 1; |
| | | |
| | | .chart-content { |
| | | height: 300px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |