| | |
| | | <!-- e:\VSprojects\fume-supervision-vue\src\views\monitor\DataDashboard.vue --> |
| | | <template> |
| | | <el-container class="data-dashboard"> |
| | | <el-main> |
| | | <!-- 设备在线情况区域 --> |
| | | <DeviceStatus |
| | | :online-count="onlineCount" |
| | | :offline-count="offlineCount" |
| | | :normal-count="normalCount" |
| | | :fault-count="faultCount" |
| | | /> |
| | | <div class="data-dashboard"> |
| | | <!-- 地图模式切换 --> |
| | | <div class="map-mode-card"> |
| | | <div class="mode-tab-container"> |
| | | <div |
| | | v-for="mode in mapModes" |
| | | :key="mode.value" |
| | | class="mode-tab" |
| | | :class="{ active: activeMode === mode.value }" |
| | | @click="handleModeChange(mode)" |
| | | > |
| | | {{ mode.label }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 设备实时数据区域 --> |
| | | <RealTimeData :current-device="currentDevice" :hourly-data="hourlyData" /> |
| | | <!-- 顶部指标卡片区 --> |
| | | <div class="top-cards"> |
| | | <!-- 时间周期选项卡片 --> |
| | | <div class="time-period-card"> |
| | | <!-- <div class="card-title">时间选择</div> --> |
| | | <div class="time-controls"> |
| | | <div class="time-tab-container"> |
| | | <div |
| | | v-for="tab in timeTabs" |
| | | :key="tab.value" |
| | | class="time-tab" |
| | | :class="{ active: activeTime === tab.value }" |
| | | @click="handleTimeChange(tab)" |
| | | > |
| | | {{ tab.label }} |
| | | </div> |
| | | </div> |
| | | <div class="time-navigator"> |
| | | <button class="nav-btn" @click="navigateTime(-1)" title="上一个周期"> |
| | | <svg |
| | | width="16" |
| | | height="16" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | xmlns="http://www.w3.org/2000/svg" |
| | | > |
| | | <path |
| | | d="M15 18L9 12L15 6" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | </svg> |
| | | </button> |
| | | <div class="current-time">{{ currentTimeDisplay }}</div> |
| | | <button class="nav-btn" @click="navigateTime(1)" title="下一个周期"> |
| | | <svg |
| | | width="16" |
| | | height="16" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | xmlns="http://www.w3.org/2000/svg" |
| | | > |
| | | <path |
| | | d="M9 18L15 12L9 6" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | </svg> |
| | | </button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="cards-container"> |
| | | <!-- 设备在线率 --> |
| | | <el-popover placement="right-start" title="设备监控" width="400" trigger="click"> |
| | | <div class="popover-content"> |
| | | <div class="overview-items-container"> |
| | | <div class="overview-item"> |
| | | <div class="overview-label">餐饮店铺总数</div> |
| | | <div class="overview-value">{{ overview.totalShops }}</div> |
| | | </div> |
| | | |
| | | <!-- 分区数据排名区域 --> |
| | | <DistrictRanking |
| | | :selected-month="selectedMonth" |
| | | :ranking-type="rankingType" |
| | | :ranking-data="rankingData" |
| | | :sorted-ranking-data="sortedRankingData" |
| | | @month-change="handleMonthChange" |
| | | @type-change="handleTypeChange" |
| | | /> |
| | | <div class="overview-item"> |
| | | <div class="overview-label">在线设备数</div> |
| | | <div class="overview-value">{{ overview.onlineDevices }}</div> |
| | | </div> |
| | | |
| | | <!-- 在线设备和店铺清单区域 --> |
| | | <ShopList |
| | | :shops="shops" |
| | | :shop-types="shopTypes" |
| | | :districts="districts" |
| | | :filter="filter" |
| | | @filter-change="handleFilterChange" |
| | | <div class="overview-item"> |
| | | <div class="overview-label">离线设备数</div> |
| | | <div class="overview-value">{{ overview.offlineDevices }}</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 设备状态饼图 --> |
| | | <div class="device-status-chart"> |
| | | <canvas id="deviceStatusChart"></canvas> |
| | | </div> |
| | | </div> |
| | | <template #reference> |
| | | <div class="metric-card"> |
| | | <div class="card-header"> |
| | | <div class="card-title">设备在线率</div> |
| | | <div class="card-icon online-icon"> |
| | | <svg |
| | | width="20" |
| | | height="20" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | xmlns="http://www.w3.org/2000/svg" |
| | | > |
| | | <path |
| | | d="M9 12L11 14L15 10" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | <path |
| | | d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | </svg> |
| | | </div> |
| | | </div> |
| | | <div class="card-value">{{ metrics.onlineRate }}<el-text>%</el-text></div> |
| | | <div class="card-trend"> |
| | | <span |
| | | class="trend-arrow" |
| | | :class="{ up: metrics.onlineRateTrend > 0, down: metrics.onlineRateTrend < 0 }" |
| | | > |
| | | {{ metrics.onlineRateTrend > 0 ? '↑' : '↓' }} |
| | | </span> |
| | | <span class="trend-text">{{ Math.abs(metrics.onlineRateTrend) }}%</span> |
| | | <span class="trend-label">{{ getCompareLabel() }}</span> |
| | | </div> |
| | | <div class="view-details"> |
| | | <span>详情</span> |
| | | <svg |
| | | width="12" |
| | | height="12" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | xmlns="http://www.w3.org/2000/svg" |
| | | > |
| | | <path |
| | | d="M9 18L15 12L9 6" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | </svg> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </el-popover> |
| | | |
| | | <!-- 浓度预警 --> |
| | | <div class="metric-card"> |
| | | <div class="card-header"> |
| | | <div class="card-title">{{ getPeriodLabel() }}浓度预警</div> |
| | | <div class="card-icon warning-icon"> |
| | | <svg |
| | | width="20" |
| | | height="20" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | xmlns="http://www.w3.org/2000/svg" |
| | | > |
| | | <path d="M12 9V13" stroke="currentColor" stroke-width="2" stroke-linecap="round" /> |
| | | <path |
| | | d="M12 17.5V17" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | /> |
| | | <path |
| | | d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | </svg> |
| | | </div> |
| | | </div> |
| | | <div class="card-value">{{ metrics.overStandardCount }}<el-text>次</el-text></div> |
| | | <div class="card-trend"> |
| | | <span |
| | | class="trend-arrow" |
| | | :class="{ up: metrics.overStandardTrend > 0, down: metrics.overStandardTrend < 0 }" |
| | | > |
| | | {{ metrics.overStandardTrend > 0 ? '↑' : '↓' }} |
| | | </span> |
| | | <span class="trend-text">{{ Math.abs(metrics.overStandardTrend) }}%</span> |
| | | <span class="trend-label">{{ getCompareLabel() }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 现场巡查 --> |
| | | <el-popover placement="right-start" title="现场巡查统计" width="350" trigger="click"> |
| | | <div class="inspection-popover-content"> |
| | | <!-- 巡查量统计 --> |
| | | <div class="inspection-metrics"> |
| | | <div class="inspection-metric-item"> |
| | | <div class="inspection-metric-label">店铺总计</div> |
| | | <div class="inspection-metric-value">{{ inspectionStats.totalShops }}</div> |
| | | </div> |
| | | <div class="inspection-metric-item"> |
| | | <div class="inspection-metric-label">巡查店铺</div> |
| | | <div class="inspection-metric-value">{{ inspectionStats.inspectedShops }}</div> |
| | | </div> |
| | | <div class="inspection-metric-item"> |
| | | <div class="inspection-metric-label">巡查点次</div> |
| | | <div class="inspection-metric-value">{{ inspectionStats.inspectionPoints }}</div> |
| | | </div> |
| | | <div class="inspection-metric-item"> |
| | | <div class="inspection-metric-label">复查点次</div> |
| | | <div class="inspection-metric-value">{{ inspectionStats.reviewPoints }}</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 问题整改情况 --> |
| | | <div class="inspection-chart-container"> |
| | | <div class="section-header"><h3>问题整改</h3></div> |
| | | <canvas id="rectificationChart"></canvas> |
| | | </div> |
| | | |
| | | <!-- 问题审核情况 --> |
| | | <div class="inspection-table-container" style="display: none"> |
| | | <div class="section-header"><h3>审核汇总</h3></div> |
| | | <div class="inspection-metric-label">问题审核</div> |
| | | <div class="inspection-table"> |
| | | <div class="inspection-metric-item"> |
| | | <div class="inspection-metric-label">无问题</div> |
| | | <div class="inspection-metric-value">{{ inspectionStats.noProblemShops }}</div> |
| | | </div> |
| | | <div class="inspection-metric-item"> |
| | | <div class="inspection-metric-label">未审核</div> |
| | | <div class="inspection-metric-value"> |
| | | {{ inspectionStats.unreviewedProblemShops }} |
| | | </div> |
| | | </div> |
| | | <div class="inspection-metric-item"> |
| | | <div class="inspection-metric-label">部分审核</div> |
| | | <div class="inspection-metric-value"> |
| | | {{ inspectionStats.partiallyReviewedProblemShops }} |
| | | </div> |
| | | </div> |
| | | <div class="inspection-metric-item"> |
| | | <div class="inspection-metric-label">全部审核</div> |
| | | <div class="inspection-metric-value"> |
| | | {{ inspectionStats.fullyReviewedProblemShops }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="inspection-metric-label">整改审核</div> |
| | | <div class="inspection-table"> |
| | | <div class="inspection-metric-item"> |
| | | <div class="inspection-metric-label">未整改</div> |
| | | <div class="inspection-metric-value">{{ inspectionStats.unrectifiedShops }}</div> |
| | | </div> |
| | | <div class="inspection-metric-item"> |
| | | <div class="inspection-metric-label">未审核</div> |
| | | <div class="inspection-metric-value"> |
| | | {{ inspectionStats.unreviewedRectifiedShops }} |
| | | </div> |
| | | </div> |
| | | <div class="inspection-metric-item"> |
| | | <div class="inspection-metric-label">部分审核</div> |
| | | <div class="inspection-metric-value"> |
| | | {{ inspectionStats.partiallyReviewedRectifiedShops }} |
| | | </div> |
| | | </div> |
| | | <div class="inspection-metric-item"> |
| | | <div class="inspection-metric-label">全部审核</div> |
| | | <div class="inspection-metric-value"> |
| | | {{ inspectionStats.fullyReviewedRectifiedShops }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <template #reference> |
| | | <div class="metric-card"> |
| | | <div class="card-header"> |
| | | <div class="card-title">现场巡查</div> |
| | | <div class="card-icon task-icon"> |
| | | <svg |
| | | width="20" |
| | | height="20" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | xmlns="http://www.w3.org/2000/svg" |
| | | > |
| | | <path |
| | | d="M22 11.08V12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C15.7376 2 19.0503 4.16113 20.7748 7.33007" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | <path |
| | | d="M22 4L12 14.01L9 11.01" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | </svg> |
| | | </div> |
| | | </div> |
| | | <div class="card-value">{{ metrics.inspectionPoints }}<el-text>点次</el-text></div> |
| | | <div class="card-trend"> |
| | | <span |
| | | class="trend-arrow" |
| | | :class="{ |
| | | up: metrics.inspectionPointsTrend > 0, |
| | | down: metrics.inspectionPointsTrend < 0, |
| | | }" |
| | | > |
| | | {{ metrics.inspectionPointsTrend > 0 ? '↑' : '↓' }} |
| | | </span> |
| | | <span class="trend-text">{{ Math.abs(metrics.inspectionPointsTrend) }}</span> |
| | | <span class="trend-label">{{ getCompareLabel() }}</span> |
| | | </div> |
| | | <div class="view-details"> |
| | | <span>详情</span> |
| | | <svg |
| | | width="12" |
| | | height="12" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | xmlns="http://www.w3.org/2000/svg" |
| | | > |
| | | <path |
| | | d="M9 18L15 12L9 6" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | </svg> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </el-popover> |
| | | |
| | | <!-- 信访投诉 --> |
| | | <div class="metric-card"> |
| | | <div class="card-header"> |
| | | <div class="card-title">信访投诉</div> |
| | | <div class="card-icon warning-icon"> |
| | | <svg |
| | | width="20" |
| | | height="20" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | xmlns="http://www.w3.org/2000/svg" |
| | | > |
| | | <path |
| | | d="M21 6H3C2.46957 6 1.96086 6.21071 1.58579 6.58579C1.21071 6.96086 1 7.46957 1 8V18C1 19.1046 1.89543 20 3 20H21C22.1046 20 23 19.1046 23 18V8C23 7.46957 22.7893 6.96086 22.4142 6.58579C22.0391 6.21071 21.5304 6 21 6Z" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | <path |
| | | d="M8 12H16" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | <path |
| | | d="M8 16H16" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | <path |
| | | d="M8 8H16" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | </svg> |
| | | </div> |
| | | </div> |
| | | <div class="card-value">{{ metrics.overStandardCount }}<el-text>件</el-text></div> |
| | | <div class="card-trend"> |
| | | <span |
| | | class="trend-arrow" |
| | | :class="{ up: metrics.overStandardTrend > 0, down: metrics.overStandardTrend < 0 }" |
| | | > |
| | | {{ metrics.overStandardTrend > 0 ? '↑' : '↓' }} |
| | | </span> |
| | | <span class="trend-text">{{ Math.abs(metrics.overStandardTrend) }}%</span> |
| | | <span class="trend-label">{{ getCompareLabel() }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 环信码 --> |
| | | <div class="metric-card"> |
| | | <div class="card-header"> |
| | | <div class="card-title">环信码</div> |
| | | <div class="card-icon efficiency-icon"> |
| | | <svg |
| | | width="20" |
| | | height="20" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | xmlns="http://www.w3.org/2000/svg" |
| | | > |
| | | <path |
| | | d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | <path |
| | | d="M12 8V16" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | <path |
| | | d="M8 12H16" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | </svg> |
| | | </div> |
| | | </div> |
| | | <div class="card-value" style="color: #52c41a"> |
| | | <div>{{ metrics.purifierEfficiency }}<el-text>%</el-text></div> |
| | | <div class="card-subvalues"> |
| | | <span style="color: #faad14; font-size: 14px">黄码:3%</span> |
| | | <span style="color: #f5222d; font-size: 14px; margin-left: 12px">红码:2%</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="card-trend"> |
| | | <span |
| | | class="trend-arrow" |
| | | :class="{ |
| | | up: metrics.purifierEfficiencyTrend > 0, |
| | | down: metrics.purifierEfficiencyTrend < 0, |
| | | }" |
| | | > |
| | | {{ metrics.purifierEfficiencyTrend > 0 ? '↑' : '↓' }} |
| | | </span> |
| | | <span class="trend-text">{{ Math.abs(metrics.purifierEfficiencyTrend) }}%</span> |
| | | <span class="trend-label">{{ getCompareLabel() }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 行政处罚 --> |
| | | <div class="metric-card"> |
| | | <div class="card-header"> |
| | | <div class="card-title">行政处罚</div> |
| | | <div class="card-icon warning-icon"> |
| | | <svg |
| | | width="20" |
| | | height="20" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | xmlns="http://www.w3.org/2000/svg" |
| | | > |
| | | <path |
| | | d="M12 15C15.866 15 19 11.866 19 8C19 4.13401 15.866 1 12 1C8.13401 1 5 4.13401 5 8C5 11.866 8.13401 15 12 15Z" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | <path |
| | | d="M12 15C12 15 15 21 15 21H9C9 21 12 15 12 15Z" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | <path |
| | | d="M11 8H13" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | <path |
| | | d="M11 11H13" |
| | | stroke="currentColor" |
| | | stroke-width="2" |
| | | stroke-linecap="round" |
| | | stroke-linejoin="round" |
| | | /> |
| | | </svg> |
| | | </div> |
| | | </div> |
| | | <div class="card-value">{{ metrics.overStandardCount }}<el-text>次</el-text></div> |
| | | <div class="card-trend"> |
| | | <span |
| | | class="trend-arrow" |
| | | :class="{ up: metrics.overStandardTrend > 0, down: metrics.overStandardTrend < 0 }" |
| | | > |
| | | {{ metrics.overStandardTrend > 0 ? '↑' : '↓' }} |
| | | </span> |
| | | <span class="trend-text">{{ Math.abs(metrics.overStandardTrend) }}%</span> |
| | | <span class="trend-label">{{ getCompareLabel() }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 主要内容区 --> |
| | | <div class="main-content"> |
| | | <!-- 中部GIS地图区 --> |
| | | <div class="map-section"> |
| | | <div id="map" class="map-container"> |
| | | <BaseMap :showSatellite="false"></BaseMap> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- --> |
| | | <div class="monitor-control-container"> |
| | | <el-button size="large" @click="toggleMonitorControl" class="push-btn"> |
| | | <div style="display: flex; flex-direction: column"> |
| | | <el-icon> |
| | | <ArrowRight v-if="isMonitorControlExpanded" /> |
| | | <ArrowLeft v-else /> |
| | | </el-icon> |
| | | <!-- <div>现</div> |
| | | <div>场</div> |
| | | <div>巡</div> |
| | | <div>查</div> --> |
| | | </div> |
| | | </el-button> |
| | | <MonitorControl |
| | | v-if="isMonitorControlExpanded" |
| | | :class="{ 'monitor-control': true, collapsed: !isMonitorControlExpanded }" |
| | | style="height: calc(90vh - 40px)" |
| | | /> |
| | | </el-main> |
| | | </el-container> |
| | | </div> |
| | | |
| | | <!-- 地图图例 --> |
| | | <div class="map-legend"> |
| | | <div class="legend-header"> |
| | | <h4>图例</h4> |
| | | </div> |
| | | <div class="legend-items"> |
| | | <!-- 污染态势模式下显示的图例 --> |
| | | <div v-if="activeMode === 'pollution'" class="legend-item"> |
| | | <img src="@/assets/exceed.png" alt="油烟浓度超标" class="legend-icon" /> |
| | | <span class="legend-text">油烟浓度超标</span> |
| | | </div> |
| | | <div v-if="activeMode === 'pollution'" class="legend-item"> |
| | | <img src="@/assets/exception.png" alt="供电异常" class="legend-icon" /> |
| | | <span class="legend-text">供电异常</span> |
| | | </div> |
| | | <div v-if="activeMode === 'pollution'" class="legend-item"> |
| | | <img src="@/assets/offline.png" alt="设备或网络异常" class="legend-icon" /> |
| | | <span class="legend-text">设备或网络异常</span> |
| | | </div> |
| | | <!-- 设备状态模式下也显示在线状态 --> |
| | | <div v-if="activeMode === 'device'" class="legend-item"> |
| | | <img |
| | | src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSI1IiB5PSI4IiB3aWR0aD0iMjIiIGhlaWdodD0iMTYiIHJ4PSIzIiBmaWxsPSIjNTJjNDFhIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiLz48cGF0aCBkPSJNNSA4IFEgMTYgMyAyNyA4IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9IiMzODllMGQiLz48cGF0aCBkPSJNNSAyNCBRIDE2IDI5IDI3IDI0IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9IiM2NjY2NjYiLz48cmVjdCB4PSI4IiB5PSIxMSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjEwIiByeD0iMiIgZmlsbD0id2hpdGUiLz48cGF0aCBkPSJNMTIgMTQgTCAyMSAxNCIgc3Ryb2tlPSIjNTJjNDFhIiBzdHJva2Utd2lkdGg9IjEuNSIvPjxwYXRoIGQ9Ik0xMiAxNyBMIDE4IDE3IiBzdHJva2U9IiM1MmM0MWEiIHN0cm9rZS13aWR0aD0iMS41Ii8+PHBhdGggZD0iTTEyIDIwIEwgMTUgMjAiIHN0cm9rZT0iIzUyYzQxYSIgc3Ryb2tlLXdpZHRoPSIxLjUiLz48bGluZSB4MT0iMTYiIHkxPSI4IiB4Mj0iMTYiIHkyPSIzIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEuNSIvPjxjaXJjbGUgY3g9IjE2IiBjeT0iMyIgcj0iMS41IiBmaWxsPSJ3aGl0ZSIvPjxjaXJjbGUgY3g9IjI3IiBjeT0iMTYiIHI9IjMiIGZpbGw9IiNmZmZmZmYiLz48Y2lyY2xlIGN4PSIyNyIgY3k9IjE2IiByPSIxLjUiIGZpbGw9IiM1MmM0MWEiLz48bGluZSB4MT0iNSIgeTE9IjEzIiB4Mj0iNiIgeTI9IjEzIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEuNSIvPjxsaW5lIHgxPSI1IiB5MT0iMTkiIHgyPSI2IiB5Mj0iMTkiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS41Ii8+PC9zdmc+" |
| | | alt="在线状态" |
| | | class="legend-icon" |
| | | /> |
| | | <span class="legend-text">在线状态</span> |
| | | </div> |
| | | <div v-if="activeMode === 'device'" class="legend-item"> |
| | | <img |
| | | src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSI1IiB5PSI4IiB3aWR0aD0iMjIiIGhlaWdodD0iMTYiIHJ4PSIzIiBmaWxsPSIjOGM4YzhjIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiLz48cGF0aCBkPSJNNSA4IFEgMTYgMyAyNyA4IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9IiM2NjY2NjYiLz48cGF0aCBkPSJNNSAyNCBRIDE2IDI5IDI3IDI0IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9IiM2NjY2NjYiLz48cmVjdCB4PSI4IiB5PSIxMSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjEwIiByeD0iMiIgZmlsbD0id2hpdGUiLz48bGluZSB4MT0iMTEiIHkxPSIxMiIgeDI9IjIxIiB5Mj0iMjIiIHN0cm9rZT0iIzhjOGM4YyIgc3Ryb2tlLXdpZHRoPSIyIi8+PGxpbmUgeDE9IjExIiB5MT0iMjIiIHgyPSIyMSIgeTI9IjEyIiBzdHJva2U9IiM4YzhjOGMiIHN0cm9rZS13aWR0aD0iMiIvPjxsaW5lIHgxPSIxNiIgeTE9IjgiIHgyPSIxNiIgeTI9IjMiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS41Ii8+PGNpcmNsZSBjeD0iMTYiIGN5PSIzIiByPSIxLjUiIGZpbGw9IndoaXRlIi8+PGNpcmNsZSBjeD0iMjciIGN5PSIxNiIgcj0iMyIgZmlsbD0iI2ZmZmZmZiIvPjxjaXJjbGUgY3g9IjI3IiBjeT0iMTYiIHI9IjEuNSIgZmlsbD0iIzhjOGM4YyIvPjxsaW5lIHgxPSI1IiB5MT0iMTMiIHgyPSI2IiB5Mj0iMTMiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS41Ii8+PGxpbmUgeDE9IjUiIHkxPSIxOSIgeDI9IjYiIHkyPSIxOSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiLz48L3N2Zz4=" |
| | | alt="离线状态" |
| | | class="legend-icon" |
| | | /> |
| | | <span class="legend-text">离线状态</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import DeviceStatus from '@/components/monitor/DeviceStatus.vue' |
| | | import RealTimeData from '@/components/monitor/RealTimeData.vue' |
| | | import DistrictRanking from '@/components/monitor/DistrictRanking.vue' |
| | | import ShopList from '@/components/monitor/ShopList.vue' |
| | | import * as echarts from 'echarts' |
| | | import { onMapMounted, satellite } from '@/utils/map/index' |
| | | import districtSearch from '@/utils/map/districtsearch.js' |
| | | import marks from '@/utils/map/marks.js' |
| | | import { generateTestShops } from '@/debug/debugdata' |
| | | import MonitorControl from '@/views/inspection/MonitorControl.vue' |
| | | |
| | | export default { |
| | | name: 'DataDashboard', |
| | | components: { |
| | | DeviceStatus, |
| | | RealTimeData, |
| | | DistrictRanking, |
| | | ShopList, |
| | | MonitorControl, |
| | | }, |
| | | data() { |
| | | return { |
| | | // 设备在线情况数据 |
| | | onlineCount: 0, |
| | | offlineCount: 0, |
| | | normalCount: 0, |
| | | faultCount: 0, |
| | | |
| | | // 设备实时数据 |
| | | currentDevice: null, |
| | | hourlyData: [], |
| | | |
| | | // 分区数据排名 |
| | | selectedMonth: '2023-12', |
| | | rankingType: 'hourly', |
| | | rankingData: [], |
| | | sortedRankingData: [], |
| | | |
| | | // 筛选条件 |
| | | filter: { |
| | | district: '', |
| | | shopType: '', |
| | | status: '', |
| | | }, |
| | | shopTypes: ['中餐', '西餐', '快餐', '火锅', '烧烤'], |
| | | districts: ['东城区', '西城区', '朝阳区', '海淀区', '丰台区'], |
| | | |
| | | // 店铺数据 |
| | | shops: [ |
| | | { |
| | | id: 1, |
| | | name: '张三餐厅', |
| | | deviceCount: 2, |
| | | status: '在线', |
| | | district: '东城区', |
| | | type: '中餐', |
| | | }, |
| | | { |
| | | id: 2, |
| | | name: '李四饭店', |
| | | deviceCount: 1, |
| | | status: '离线', |
| | | district: '西城区', |
| | | type: '西餐', |
| | | }, |
| | | { |
| | | id: 3, |
| | | name: '王五小吃', |
| | | deviceCount: 3, |
| | | status: '在线', |
| | | district: '朝阳区', |
| | | type: '快餐', |
| | | }, |
| | | { |
| | | id: 4, |
| | | name: '赵六火锅', |
| | | deviceCount: 2, |
| | | status: '在线', |
| | | district: '海淀区', |
| | | type: '火锅', |
| | | }, |
| | | { |
| | | id: 5, |
| | | name: '钱七烧烤', |
| | | deviceCount: 1, |
| | | status: '离线', |
| | | district: '丰台区', |
| | | type: '烧烤', |
| | | }, |
| | | activeTime: 'day', |
| | | activeMode: 'pollution', // 默认污染态势模式 |
| | | currentDate: new Date('2025-08-01'), |
| | | timeTabs: [ |
| | | { label: '日', value: 'day' }, |
| | | { label: '周', value: 'week' }, |
| | | { label: '月', value: 'month' }, |
| | | ], |
| | | mapModes: [ |
| | | { label: '污染态势', value: 'pollution' }, |
| | | { label: '设备状态', value: 'device' }, |
| | | ], |
| | | selectedPoint: { |
| | | enterpriseName: '', |
| | | deviceId: '', |
| | | oilSmokeConcentration: 0, |
| | | particulateMatter: 0, |
| | | nonMethaneHydrocarbon: 0, |
| | | monitoringTime: '', |
| | | isOverStandard: false, |
| | | }, |
| | | metrics: { |
| | | overStandardCount: 12, |
| | | overStandardTrend: 5, |
| | | onlineRate: 92, |
| | | onlineRateTrend: 2, |
| | | purifierEfficiency: 95, |
| | | purifierEfficiencyTrend: 2, |
| | | inspectionPoints: 350, |
| | | inspectionPointsTrend: 50, |
| | | }, |
| | | overview: { |
| | | totalShops: 245, |
| | | onlineDevices: 220, |
| | | offlineDevices: 25, |
| | | }, |
| | | inspectionStats: { |
| | | // 巡查量 |
| | | totalShops: 245, |
| | | inspectedShops: 180, |
| | | inspectionPoints: 350, |
| | | reviewPoints: 80, |
| | | // 问题整改情况 |
| | | problemCount: 45, |
| | | rectifiedCount: 38, |
| | | rectificationRate: 84.4, |
| | | // 问题审核情况 |
| | | noProblemShops: 135, |
| | | unreviewedProblemShops: 8, |
| | | partiallyReviewedProblemShops: 5, |
| | | fullyReviewedProblemShops: 32, |
| | | unreviewedRectifiedShops: 3, |
| | | partiallyReviewedRectifiedShops: 2, |
| | | fullyReviewedRectifiedShops: 33, |
| | | unrectifiedShops: 7, |
| | | }, |
| | | map: null, |
| | | refreshTimer: null, |
| | | isMonitorControlExpanded: true, |
| | | } |
| | | }, |
| | | mounted() { |
| | | this.initData() |
| | | this.startRealTimeUpdate() |
| | | }, |
| | | methods: { |
| | | initData() { |
| | | // 初始化设备在线情况数据 |
| | | this.updateDeviceStatus() |
| | | |
| | | // 初始化实时数据 |
| | | this.updateRealTimeData() |
| | | |
| | | // 初始化分区数据排名 |
| | | this.updateRankingData() |
| | | }, |
| | | |
| | | updateDeviceStatus() { |
| | | // 模拟数据 - 实际应从API获取 |
| | | this.onlineCount = Math.floor(Math.random() * 50) + 50 |
| | | this.offlineCount = Math.floor(Math.random() * 20) |
| | | this.normalCount = this.onlineCount |
| | | this.faultCount = this.offlineCount |
| | | }, |
| | | |
| | | updateRealTimeData() { |
| | | // 模拟数据 - 实际应从API获取 |
| | | const devices = [ |
| | | { |
| | | deviceId: 'DEV-001', |
| | | supplier: '供应商A', |
| | | 油烟浓度: (Math.random() * 2).toFixed(2), |
| | | 风机电流: (Math.random() * 5 + 1).toFixed(2), |
| | | 净化器电流: (Math.random() * 3 + 0.5).toFixed(2), |
| | | }, |
| | | ] |
| | | |
| | | this.currentDevice = devices[0] |
| | | |
| | | // 生成模拟的近一小时数据 |
| | | this.hourlyData = [] |
| | | for (let i = 59; i >= 0; i--) { |
| | | const time = new Date() |
| | | time.setMinutes(time.getMinutes() - i) |
| | | this.hourlyData.push({ |
| | | time: time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }), |
| | | 油烟浓度: (Math.random() * 2).toFixed(2), |
| | | 风机电流: (Math.random() * 5 + 1).toFixed(2), |
| | | 净化器电流: (Math.random() * 3 + 0.5).toFixed(2), |
| | | }) |
| | | computed: { |
| | | currentTimeDisplay() { |
| | | const date = this.currentDate |
| | | let weekStart = new Date(date) |
| | | let weekEnd = new Date(date) |
| | | switch (this.activeTime) { |
| | | case 'day': |
| | | return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` |
| | | case 'week': |
| | | // 简单计算周显示,实际项目中可能需要更复杂的周计算逻辑 |
| | | weekStart.setDate(date.getDate() - date.getDay() + 1) |
| | | weekEnd.setDate(date.getDate() + (7 - date.getDay())) |
| | | return `${weekStart.getFullYear()}-${String(weekStart.getMonth() + 1).padStart(2, '0')}-${String(weekStart.getDate()).padStart(2, '0')} ~ ${weekEnd.getFullYear()}-${String(weekEnd.getMonth() + 1).padStart(2, '0')}-${String(weekEnd.getDate()).padStart(2, '0')}` |
| | | case 'month': |
| | | return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}` |
| | | default: |
| | | return '' |
| | | } |
| | | }, |
| | | |
| | | updateRankingData() { |
| | | // 模拟数据 - 实际应从API获取 |
| | | this.rankingData = [ |
| | | { name: '东城区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 0 }, |
| | | { name: '西城区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: -1 }, |
| | | { name: '朝阳区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 1 }, |
| | | { name: '海淀区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 0 }, |
| | | { name: '丰台区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: -2 }, |
| | | { name: '石景山区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 2 }, |
| | | { name: '通州区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 0 }, |
| | | { name: '顺义区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 1 }, |
| | | { name: '昌平区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: -1 }, |
| | | { name: '大兴区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 0 }, |
| | | ] |
| | | |
| | | // 排序 |
| | | this.sortedRankingData = [...this.rankingData].sort( |
| | | (a, b) => parseFloat(b.value) - parseFloat(a.value), |
| | | ) |
| | | }, |
| | | mounted() { |
| | | this.initMap() |
| | | this.initDeviceStatusChart() |
| | | this.initRectificationChart() |
| | | // this.startAutoRefresh() |
| | | }, |
| | | beforeUnmount() { |
| | | if (this.refreshTimer) { |
| | | clearInterval(this.refreshTimer) |
| | | } |
| | | }, |
| | | methods: { |
| | | toggleMonitorControl() { |
| | | this.isMonitorControlExpanded = !this.isMonitorControlExpanded |
| | | }, |
| | | handleTimeChange(tab) { |
| | | this.activeTime = tab.value |
| | | // 模拟切换时间周期后的数据更新 |
| | | this.updateMetrics() |
| | | }, |
| | | handleModeChange(mode) { |
| | | this.activeMode = mode.value |
| | | }, |
| | | navigateTime(direction) { |
| | | const newDate = new Date(this.currentDate) |
| | | switch (this.activeTime) { |
| | | case 'day': |
| | | newDate.setDate(newDate.getDate() + direction) |
| | | break |
| | | case 'week': |
| | | newDate.setDate(newDate.getDate() + direction * 7) |
| | | break |
| | | case 'month': |
| | | newDate.setMonth(newDate.getMonth() + direction) |
| | | break |
| | | } |
| | | this.currentDate = newDate |
| | | // 模拟切换时间后的数据更新 |
| | | this.updateMetrics() |
| | | }, |
| | | getPeriodLabel() { |
| | | const today = new Date() |
| | | const isToday = |
| | | this.activeTime === 'day' && |
| | | this.currentDate.getDate() === today.getDate() && |
| | | this.currentDate.getMonth() === today.getMonth() && |
| | | this.currentDate.getFullYear() === today.getFullYear() |
| | | |
| | | startRealTimeUpdate() { |
| | | // 每30秒更新一次数据 |
| | | this.realTimeInterval = setInterval(() => { |
| | | this.updateDeviceStatus() |
| | | this.updateRealTimeData() |
| | | if (isToday) { |
| | | return '今日' |
| | | } |
| | | |
| | | switch (this.activeTime) { |
| | | case 'day': |
| | | return '当日' |
| | | case 'week': |
| | | return '当周' |
| | | case 'month': |
| | | return '当月' |
| | | default: |
| | | return '当日' |
| | | } |
| | | }, |
| | | getCompareLabel() { |
| | | switch (this.activeTime) { |
| | | case 'day': |
| | | return '较昨日' |
| | | case 'week': |
| | | return '较上周' |
| | | case 'month': |
| | | return '较上月' |
| | | default: |
| | | return '较昨日' |
| | | } |
| | | }, |
| | | updateMetrics() { |
| | | // 这里应该根据选择的时间周期从接口获取数据 |
| | | // 模拟数据更新 |
| | | setTimeout(() => { |
| | | const m = Math.floor(Math.random() * 50) + 150 |
| | | this.overview = { |
| | | totalShops: 245, |
| | | onlineDevices: m, |
| | | offlineDevices: 245 - m, |
| | | } |
| | | this.metrics = { |
| | | overStandardCount: Math.floor(Math.random() * 30), |
| | | overStandardTrend: Math.floor(Math.random() * 20) - 10, |
| | | onlineRate: ((this.overview.onlineDevices / this.overview.totalShops) * 100).toFixed(0), |
| | | onlineRateTrend: Math.floor(Math.random() * 10) - 5, |
| | | purifierEfficiency: Math.floor(Math.random() * 20) + 80, |
| | | purifierEfficiencyTrend: Math.floor(Math.random() * 10) - 5, |
| | | inspectionPoints: Math.floor(Math.random() * 100) + 300, |
| | | inspectionPointsTrend: Math.floor(Math.random() * 100) - 50, |
| | | } |
| | | |
| | | // 更新巡查统计数据 |
| | | this.inspectionStats = { |
| | | totalShops: 245, |
| | | inspectedShops: Math.floor(Math.random() * 50) + 150, |
| | | inspectionPoints: Math.floor(Math.random() * 100) + 300, |
| | | reviewPoints: Math.floor(Math.random() * 50) + 50, |
| | | problemCount: Math.floor(Math.random() * 30) + 20, |
| | | rectifiedCount: Math.floor(Math.random() * 25) + 15, |
| | | rectificationRate: Math.round((Math.random() * 30 + 70) * 10) / 10, |
| | | noProblemShops: Math.floor(Math.random() * 50) + 100, |
| | | unreviewedProblemShops: Math.floor(Math.random() * 10), |
| | | partiallyReviewedProblemShops: Math.floor(Math.random() * 8), |
| | | fullyReviewedProblemShops: Math.floor(Math.random() * 20) + 15, |
| | | unreviewedRectifiedShops: Math.floor(Math.random() * 5), |
| | | partiallyReviewedRectifiedShops: Math.floor(Math.random() * 5), |
| | | fullyReviewedRectifiedShops: Math.floor(Math.random() * 20) + 15, |
| | | unrectifiedShops: Math.floor(Math.random() * 10), |
| | | } |
| | | |
| | | // 更新图表 |
| | | this.initDeviceStatusChart() |
| | | this.initRectificationChart() |
| | | }, 300) |
| | | }, |
| | | initMap() { |
| | | // setTimeout(() => { |
| | | districtSearch.removeDistrict() |
| | | districtSearch.drawDistrictMask('上海市') |
| | | // districtSearch.districtLayer('310106') |
| | | // }, 2000) |
| | | |
| | | onMapMounted(() => { |
| | | setTimeout(() => { |
| | | marks.clearMassMarks() |
| | | const shops = generateTestShops() |
| | | console.log(shops) |
| | | |
| | | marks.drawMassMarks(shops) |
| | | }, 2000) |
| | | }) |
| | | }, |
| | | initDeviceStatusChart() { |
| | | const chartDom = document.getElementById('deviceStatusChart') |
| | | if (chartDom) { |
| | | const chart = echarts.init(chartDom) |
| | | const option = { |
| | | tooltip: { |
| | | trigger: 'item', |
| | | formatter: '{b}: {c} ({d}%)', |
| | | backgroundColor: 'rgba(255, 255, 255, 0.95)', |
| | | borderColor: '#e8e8e8', |
| | | borderWidth: 1, |
| | | textStyle: { |
| | | color: '#333', |
| | | }, |
| | | }, |
| | | legend: { |
| | | bottom: '0%', |
| | | left: 'center', |
| | | textStyle: { |
| | | color: '#86909c', |
| | | fontSize: 12, |
| | | }, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: '设备状态', |
| | | type: 'pie', |
| | | radius: ['50%', '75%'], |
| | | center: ['50%', '45%'], |
| | | avoidLabelOverlap: false, |
| | | itemStyle: { |
| | | borderRadius: 8, |
| | | borderColor: '#ffffff', |
| | | borderWidth: 2, |
| | | shadowBlur: 5, |
| | | shadowOffsetX: 0, |
| | | shadowColor: 'rgba(0, 0, 0, 0.1)', |
| | | }, |
| | | label: { |
| | | show: true, |
| | | position: 'center', |
| | | formatter: '{d}%', |
| | | fontSize: 18, |
| | | fontWeight: 'bold', |
| | | color: '#262626', |
| | | }, |
| | | labelLine: { |
| | | show: false, |
| | | }, |
| | | data: [ |
| | | { |
| | | value: this.overview.onlineDevices, |
| | | name: '在线', |
| | | itemStyle: { |
| | | color: '#1890ff', |
| | | }, |
| | | }, |
| | | { |
| | | value: this.overview.offlineDevices, |
| | | name: '离线', |
| | | itemStyle: { |
| | | color: '#f5222d', |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | ], |
| | | } |
| | | chart.setOption(option) |
| | | |
| | | // 响应式调整 |
| | | // window.addEventListener('resize', () => { |
| | | // chart.resize() |
| | | // }) |
| | | } |
| | | }, |
| | | initRectificationChart() { |
| | | const chartDom = document.getElementById('rectificationChart') |
| | | if (chartDom) { |
| | | const chart = echarts.init(chartDom) |
| | | const option = { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'shadow', |
| | | }, |
| | | backgroundColor: 'rgba(255, 255, 255, 0.95)', |
| | | borderColor: '#e8e8e8', |
| | | borderWidth: 1, |
| | | textStyle: { |
| | | color: '#333', |
| | | }, |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | top: '5%', |
| | | containLabel: true, |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: ['问题数', '整改数'], |
| | | axisLabel: { |
| | | color: '#86909c', |
| | | fontSize: 12, |
| | | }, |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | axisLabel: { |
| | | color: '#86909c', |
| | | fontSize: 12, |
| | | }, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: '数量', |
| | | type: 'bar', |
| | | data: [ |
| | | { |
| | | value: this.inspectionStats.problemCount, |
| | | itemStyle: { |
| | | color: '#fa8c16', |
| | | }, |
| | | }, |
| | | { |
| | | value: this.inspectionStats.rectifiedCount, |
| | | itemStyle: { |
| | | color: '#52c41a', |
| | | }, |
| | | }, |
| | | ], |
| | | barWidth: '60%', |
| | | label: { |
| | | show: true, |
| | | position: 'top', |
| | | color: '#262626', |
| | | fontSize: 12, |
| | | }, |
| | | }, |
| | | ], |
| | | } |
| | | chart.setOption(option) |
| | | |
| | | // 响应式调整 |
| | | // window.addEventListener('resize', () => { |
| | | // chart.resize() |
| | | // }) |
| | | } |
| | | }, |
| | | startAutoRefresh() { |
| | | // 每30秒自动刷新数据 |
| | | this.refreshTimer = setInterval(() => { |
| | | this.updateMetrics() |
| | | // 这里应该同时更新地图点位数据 |
| | | }, 30000) |
| | | }, |
| | | |
| | | handleMonthChange(val) { |
| | | this.selectedMonth = val |
| | | this.updateRankingData() |
| | | }, |
| | | |
| | | handleTypeChange(val) { |
| | | this.rankingType = val |
| | | this.updateRankingData() |
| | | }, |
| | | |
| | | handleFilterChange(val) { |
| | | this.filter = { ...val } |
| | | viewDetails() { |
| | | // 跳转到企业监控详情页 |
| | | this.$router.push('/monitor/enterprise-detail') |
| | | }, |
| | | }, |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | /* 全局样式 */ |
| | | .data-dashboard { |
| | | min-height: 100vh; |
| | | width: 100%; |
| | | height: calc(100vh - 70px); |
| | | background-color: #f5f7fa; |
| | | color: #333; |
| | | box-sizing: border-box; |
| | | font-family: |
| | | -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; |
| | | position: relative; |
| | | } |
| | | |
| | | .el-header { |
| | | /* 顶部指标卡片区 */ |
| | | .top-cards { |
| | | position: absolute; |
| | | top: 4px; |
| | | left: 4px; |
| | | z-index: 10; |
| | | margin-bottom: 24px; |
| | | } |
| | | |
| | | .cards-container { |
| | | display: grid; |
| | | grid-template-columns: repeat(2, 180px); |
| | | grid-template-rows: auto repeat(2, auto); |
| | | gap: 8px; |
| | | /* background-color: rgba(255, 255, 255, 0.9); */ |
| | | /* padding: 16px; */ |
| | | border-radius: 8px; |
| | | /* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); */ |
| | | } |
| | | |
| | | /* 监控控制卡片 */ |
| | | .monitor-control { |
| | | /* position: absolute; */ |
| | | width: 400px; |
| | | transition: all 0.3s ease; |
| | | /* top: 0px; */ |
| | | /* right: 0px; */ |
| | | /* z-index: 10; */ |
| | | } |
| | | |
| | | .push-btn { |
| | | z-index: 1; |
| | | width: 2.5rem; |
| | | height: 40px; |
| | | margin: initial; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | /* background-color: white; */ |
| | | /* border-color: white; */ |
| | | /* border-top: 1px solid; |
| | | border-left: 1px solid; |
| | | border-bottom: 1px solid; */ |
| | | border-top-right-radius: 0px; |
| | | border-bottom-right-radius: 0px; |
| | | /* box-shadow: var(--el-box-shadow-light); */ |
| | | } |
| | | |
| | | /* 时间周期卡片 */ |
| | | .time-period-card { |
| | | background-color: #ffffff; |
| | | border-radius: 8px; |
| | | padding: 4px 16px; |
| | | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | | margin-bottom: 8px; |
| | | min-width: 300px; |
| | | } |
| | | |
| | | /* 地图模式卡片 */ |
| | | .map-mode-card { |
| | | position: absolute; |
| | | top: 4px; |
| | | left: 50%; |
| | | transform: translateX(-50%); |
| | | z-index: 10; |
| | | background-color: #ffffff; |
| | | border-radius: 8px; |
| | | padding: 4px 16px; |
| | | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | | margin-bottom: 8px; |
| | | min-width: 300px; |
| | | } |
| | | |
| | | .mode-tab-container { |
| | | display: flex; |
| | | flex-direction: row; |
| | | gap: 8px; |
| | | width: 100%; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .mode-tab { |
| | | padding: 4px 8px; |
| | | border-radius: 4px; |
| | | cursor: pointer; |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | transition: all 0.3s ease; |
| | | color: #4e5969; |
| | | text-align: center; |
| | | border: 1px solid #e8e8e8; |
| | | background-color: #fafafa; |
| | | flex: 1; |
| | | } |
| | | |
| | | .mode-tab.active { |
| | | background-color: #1890ff; |
| | | color: #ffffff; |
| | | border-color: #1890ff; |
| | | box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2); |
| | | } |
| | | |
| | | .mode-tab:hover:not(.active) { |
| | | color: #1890ff; |
| | | border-color: #e6f7ff; |
| | | background-color: #e6f7ff; |
| | | } |
| | | |
| | | .time-period-card .card-title { |
| | | font-size: 14px; |
| | | color: #86909c; |
| | | font-weight: 500; |
| | | margin-bottom: 16px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .time-controls { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .time-tab-container { |
| | | display: flex; |
| | | flex-direction: row; |
| | | gap: 8px; |
| | | width: 100%; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .time-tab { |
| | | padding: 4px 8px; |
| | | border-radius: 4px; |
| | | cursor: pointer; |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | transition: all 0.3s ease; |
| | | color: #4e5969; |
| | | text-align: center; |
| | | border: 1px solid #e8e8e8; |
| | | background-color: #fafafa; |
| | | flex: 1; |
| | | } |
| | | |
| | | .time-tab.active { |
| | | background-color: #1890ff; |
| | | color: #ffffff; |
| | | border-color: #1890ff; |
| | | box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2); |
| | | } |
| | | |
| | | .time-tab:hover:not(.active) { |
| | | color: #1890ff; |
| | | border-color: #e6f7ff; |
| | | background-color: #e6f7ff; |
| | | } |
| | | |
| | | .time-navigator { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 12px; |
| | | padding: 8px 0; |
| | | } |
| | | |
| | | .nav-btn { |
| | | width: 32px; |
| | | height: 32px; |
| | | border: 1px solid #e8e8e8; |
| | | background-color: #fafafa; |
| | | border-radius: 4px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | cursor: pointer; |
| | | transition: all 0.3s ease; |
| | | color: #4e5969; |
| | | } |
| | | |
| | | .nav-btn:hover { |
| | | border-color: #1890ff; |
| | | color: #1890ff; |
| | | background-color: #e6f7ff; |
| | | } |
| | | |
| | | .current-time { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #262626; |
| | | min-width: 180px; |
| | | text-align: center; |
| | | padding: 0 12px; |
| | | } |
| | | |
| | | /* 指标卡片 */ |
| | | .metric-card { |
| | | background-color: #ffffff; |
| | | border-radius: 8px; |
| | | padding: 20px; |
| | | background-color: #f5f7fa; |
| | | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); |
| | | transition: all 0.3s ease; |
| | | position: relative; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .el-header h1 { |
| | | font-size: 24px; |
| | | .metric-card:hover { |
| | | transform: translateY(-2px); |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .card-title { |
| | | font-size: 14px; |
| | | color: #86909c; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .card-icon { |
| | | color: #1890ff; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .warning-icon { |
| | | color: #fa8c16; |
| | | } |
| | | |
| | | .online-icon { |
| | | color: #52c41a; |
| | | } |
| | | |
| | | .efficiency-icon { |
| | | color: #52c41a; |
| | | } |
| | | |
| | | .task-icon { |
| | | color: #1890ff; |
| | | } |
| | | |
| | | .card-value { |
| | | font-size: 32px; |
| | | font-weight: bold; |
| | | margin: 12px 0; |
| | | color: #262626; |
| | | line-height: 1.2; |
| | | /* display: flex; |
| | | justify-content: space-between; |
| | | align-items: flex-end; */ |
| | | } |
| | | |
| | | .card-subvalues { |
| | | display: flex; |
| | | align-items: center; |
| | | margin: 8px 0; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .card-trend { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | font-size: 12px; |
| | | padding-top: 12px; |
| | | border-top: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .trend-arrow { |
| | | font-size: 14px; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | .trend-arrow.up { |
| | | color: #52c41a; |
| | | } |
| | | |
| | | .trend-arrow.down { |
| | | color: #f5222d; |
| | | } |
| | | |
| | | .trend-text { |
| | | color: #262626; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .trend-label { |
| | | color: #86909c; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | /* 主要内容区 */ |
| | | .main-content { |
| | | width: 100%; |
| | | height: 100%; |
| | | position: relative; |
| | | } |
| | | |
| | | /* 中部GIS地图区 */ |
| | | .map-section { |
| | | width: 100%; |
| | | height: 100%; |
| | | position: relative; |
| | | overflow: hidden; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .section-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 16px; |
| | | color: #262626; |
| | | } |
| | | |
| | | .section-header h3 { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #262626; |
| | | margin: 0; |
| | | } |
| | | |
| | | .view-more { |
| | | font-size: 12px; |
| | | color: #1890ff; |
| | | cursor: pointer; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .view-more:hover { |
| | | text-decoration: underline; |
| | | } |
| | | |
| | | .map-container { |
| | | flex: 1; |
| | | position: relative; |
| | | overflow: hidden; |
| | | background-color: #fafafa; |
| | | } |
| | | |
| | | .map-placeholder { |
| | | width: 100%; |
| | | height: 100%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: #86909c; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | /* 右侧实时监测总览区 */ |
| | | .popover-content { |
| | | padding: 10px; |
| | | } |
| | | |
| | | .overview-items-container { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding-bottom: 16px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .overview-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | flex: 1; |
| | | text-align: center; |
| | | } |
| | | |
| | | .overview-label { |
| | | font-size: 12px; |
| | | color: #86909c; |
| | | font-weight: 500; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .overview-value { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #262626; |
| | | } |
| | | |
| | | .device-status-chart { |
| | | flex: 1; |
| | | min-height: 200px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .view-details { |
| | | position: absolute; |
| | | bottom: 12px; |
| | | right: 16px; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | font-size: 12px; |
| | | color: #1890ff; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .view-details:hover { |
| | | text-decoration: underline; |
| | | } |
| | | |
| | | .overview-items-container { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding-bottom: 16px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .overview-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | flex: 1; |
| | | text-align: center; |
| | | } |
| | | |
| | | .overview-label { |
| | | font-size: 12px; |
| | | color: #86909c; |
| | | font-weight: 500; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .overview-value { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #262626; |
| | | } |
| | | |
| | | /* 巡查情况统计 */ |
| | | .inspection-popover-content { |
| | | padding: 10px; |
| | | max-height: 400px; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .monitor-control-container { |
| | | position: absolute; |
| | | top: 4px; |
| | | right: 4px; |
| | | z-index: 10; |
| | | transition: all 0.3s ease; |
| | | /* background-color: rgba(255, 255, 255, 0.9); */ |
| | | display: flex; |
| | | border-radius: 8px; |
| | | /* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); */ |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .monitor-control-container.collapsed { |
| | | width: 60px; |
| | | } |
| | | |
| | | .monitor-control-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 10px 15px; |
| | | border-bottom: 1px solid #e8e8e8; |
| | | height: 40px; |
| | | position: relative; |
| | | } |
| | | |
| | | .monitor-control-header span { |
| | | font-weight: 600; |
| | | color: #333; |
| | | writing-mode: vertical-rl; |
| | | text-orientation: mixed; |
| | | letter-spacing: 2px; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .collapse-btn { |
| | | /* transform: translateY(-50%); */ |
| | | } |
| | | |
| | | .inspection-metrics { |
| | | display: grid; |
| | | grid-template-columns: repeat(2, 1fr); |
| | | gap: 12px; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .inspection-metric-item { |
| | | background-color: #fafafa; |
| | | border-radius: 6px; |
| | | padding: 12px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .inspection-metric-label { |
| | | font-size: 12px; |
| | | color: #86909c; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .inspection-metric-value { |
| | | font-size: 18px; |
| | | font-weight: bold; |
| | | color: #262626; |
| | | } |
| | | |
| | | .inspection-chart-container { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .chart-title { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #262626; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .inspection-table-container { |
| | | /* max-height: 200px; */ |
| | | /* overflow-y: auto; */ |
| | | } |
| | | |
| | | .inspection-table { |
| | | /* width: 100%; |
| | | border-collapse: collapse; */ |
| | | display: grid; |
| | | grid-template-columns: repeat(4, 1fr); |
| | | gap: 2px; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .table-row { |
| | | display: flex; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | padding: 8px 0; |
| | | } |
| | | |
| | | .table-row:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .table-cell { |
| | | flex: 1; |
| | | font-size: 12px; |
| | | color: #4e5969; |
| | | } |
| | | |
| | | .table-cell.value { |
| | | font-weight: 500; |
| | | color: #262626; |
| | | text-align: right; |
| | | } |
| | | |
| | | /* 弹窗样式 */ |
| | | .dialog-content { |
| | | color: #333; |
| | | } |
| | | |
| | | .el-main { |
| | | padding: 20px; |
| | | .dialog-footer { |
| | | text-align: right; |
| | | } |
| | | |
| | | /* 闪烁效果 */ |
| | | @keyframes blink { |
| | | 0%, |
| | | 100% { |
| | | opacity: 1; |
| | | } |
| | | 50% { |
| | | opacity: 0.5; |
| | | } |
| | | } |
| | | |
| | | .blink { |
| | | animation: blink 1s infinite; |
| | | } |
| | | |
| | | /* 地图图例样式 */ |
| | | .map-legend { |
| | | position: absolute; |
| | | display: flex; |
| | | align-items: center; |
| | | bottom: 4px; |
| | | left: 50%; |
| | | transform: translateX(-50%); |
| | | /* width: 200px; */ |
| | | background-color: rgba(255, 255, 255, 0.9); |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| | | padding: 4px; |
| | | z-index: 10; |
| | | } |
| | | |
| | | .legend-header { |
| | | margin-right: 12px; |
| | | } |
| | | |
| | | .legend-header h4 { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #262626; |
| | | margin: 0; |
| | | text-align: center; |
| | | } |
| | | |
| | | .legend-items { |
| | | display: flex; |
| | | flex-direction: row; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .legend-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | font-size: 12px; |
| | | color: #4e5969; |
| | | } |
| | | |
| | | .legend-icon { |
| | | width: 24px; |
| | | height: 24px; |
| | | object-fit: contain; |
| | | } |
| | | |
| | | /* 响应式设计 */ |
| | | @media (max-width: 768px) { |
| | | .el-row { |
| | | flex-direction: column; |
| | | /* @media (max-width: 1200px) { |
| | | .top-cards { |
| | | position: relative; |
| | | margin-bottom: 24px; |
| | | } |
| | | |
| | | .el-col { |
| | | width: 100% !important; |
| | | margin-bottom: 10px; |
| | | .cards-container { |
| | | grid-template-columns: repeat(2, 1fr); |
| | | grid-template-rows: auto auto; |
| | | background-color: #ffffff; |
| | | } |
| | | } |
| | | </style> |
| | | |
| | | .main-content { |
| | | height: calc(100vh - 300px); |
| | | } |
| | | |
| | | .overview-section { |
| | | position: relative; |
| | | top: 0; |
| | | right: 0; |
| | | width: 100%; |
| | | max-height: none; |
| | | height: 300px; |
| | | margin-top: 16px; |
| | | background-color: #ffffff; |
| | | } |
| | | } */ |
| | | |
| | | /* @media (max-width: 768px) { |
| | | .data-dashboard { |
| | | padding: 16px; |
| | | } |
| | | |
| | | .top-cards { |
| | | left: 16px; |
| | | } |
| | | |
| | | .cards-container { |
| | | grid-template-columns: 1fr; |
| | | grid-template-rows: auto repeat(4, auto); |
| | | } |
| | | |
| | | .time-period-card { |
| | | order: -1; |
| | | } |
| | | |
| | | .time-tab-container { |
| | | flex-direction: row; |
| | | } |
| | | |
| | | .time-tab { |
| | | flex: 1; |
| | | padding: 8px 0; |
| | | } |
| | | |
| | | .overview-section { |
| | | right: 16px; |
| | | } |
| | | |
| | | .overview-items-container { |
| | | flex-direction: column; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .overview-item { |
| | | flex-direction: row; |
| | | justify-content: space-between; |
| | | width: 100%; |
| | | text-align: left; |
| | | } |
| | | } */ |
| | | </style> |