餐饮油烟智能监测与监管一体化平台
riku
2026-03-13 e365192a36d6d9432fbd00ea9d577a38f8679707
src/views/monitor/DataDashboard.vue
@@ -1,254 +1,865 @@
<!-- 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="top-cards">
      <div class="cards-container">
        <!-- 时间周期选项卡片 -->
        <div class="time-period-card">
          <div class="card-title">时间周期</div>
          <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>
      <!-- 设备实时数据区域 -->
      <RealTimeData :current-device="currentDevice" :hourly-data="hourlyData" />
        <!-- 超标数 -->
        <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 }}</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>
      <!-- 分区数据排名区域 -->
      <DistrictRanking
        :selected-month="selectedMonth"
        :ranking-type="rankingType"
        :ranking-data="rankingData"
        :sorted-ranking-data="sortedRankingData"
        @month-change="handleMonthChange"
        @type-change="handleTypeChange"
      />
        <!-- 在线率 -->
        <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 }}%</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>
      <!-- 在线设备和店铺清单区域 -->
      <ShopList
        :shops="shops"
        :shop-types="shopTypes"
        :districts="districts"
        :filter="filter"
        @filter-change="handleFilterChange"
      />
    </el-main>
  </el-container>
        <!-- 净化器运行效率 -->
        <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 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2Z"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
                <path
                  d="M12 6V12L16 14"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
              </svg>
            </div>
          </div>
          <div class="card-value">{{ metrics.purifierEfficiency }}%</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 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.taskCompletionRate }}%</div>
          <div class="card-trend">
            <span
              class="trend-arrow"
              :class="{
                up: metrics.taskCompletionRateTrend > 0,
                down: metrics.taskCompletionRateTrend < 0,
              }"
            >
              {{ metrics.taskCompletionRateTrend > 0 ? '↑' : '↓' }}
            </span>
            <span class="trend-text">{{ Math.abs(metrics.taskCompletionRateTrend) }}%</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></BaseMap>
        </div>
        <!-- 地图点位弹窗 -->
        <el-dialog v-model="dialogVisible" title="企业实时数据" width="400px">
          <div class="dialog-content">
            <el-descriptions :column="1" border>
              <el-descriptions-item label="企业名称">{{
                selectedPoint.enterpriseName
              }}</el-descriptions-item>
              <el-descriptions-item label="设备编号">{{
                selectedPoint.deviceId
              }}</el-descriptions-item>
              <el-descriptions-item label="油烟浓度"
                >{{ selectedPoint.oilSmokeConcentration }} mg/m³</el-descriptions-item
              >
              <el-descriptions-item label="颗粒物"
                >{{ selectedPoint.particulateMatter }} mg/m³</el-descriptions-item
              >
              <el-descriptions-item label="非甲烷总烃"
                >{{ selectedPoint.nonMethaneHydrocarbon }} mg/m³</el-descriptions-item
              >
              <el-descriptions-item label="监测时间">{{
                selectedPoint.monitoringTime
              }}</el-descriptions-item>
              <el-descriptions-item label="超标情况">
                <el-tag :type="selectedPoint.isOverStandard ? 'danger' : 'success'">
                  {{ selectedPoint.isOverStandard ? '超标' : '正常' }}
                </el-tag>
              </el-descriptions-item>
            </el-descriptions>
          </div>
          <template #footer>
            <span class="dialog-footer">
              <el-button @click="dialogVisible = false">关闭</el-button>
              <el-button type="primary" @click="viewDetails">查看详情</el-button>
            </span>
          </template>
        </el-dialog>
      </div>
      <!-- 右侧实时监测总览区 -->
      <div class="overview-section">
        <div class="section-header">
          <h3>实时监测总览</h3>
          <span class="view-more">查看更多</span>
        </div>
        <div class="overview-items-container">
          <div class="overview-item">
            <div class="overview-label">餐饮店铺总数</div>
            <div class="overview-value">{{ overview.totalShops }}</div>
          </div>
          <div class="overview-item">
            <div class="overview-label">在线设备数</div>
            <div class="overview-value">{{ overview.onlineDevices }}</div>
          </div>
          <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>
    </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, map, AMap } from '@/utils/map/index'
import districtSearch from '@/utils/map/districtsearch.js'
export default {
  name: 'DataDashboard',
  components: {
    DeviceStatus,
    RealTimeData,
    DistrictRanking,
    ShopList,
  },
  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',
      timeTabs: [
        { label: '日', value: 'day' },
        { label: '周', value: 'week' },
        { label: '月', value: 'month' },
      ],
      dialogVisible: false,
      selectedPoint: {
        enterpriseName: '',
        deviceId: '',
        oilSmokeConcentration: 0,
        particulateMatter: 0,
        nonMethaneHydrocarbon: 0,
        monitoringTime: '',
        isOverStandard: false,
      },
      metrics: {
        overStandardCount: 12,
        overStandardTrend: 5,
        onlineRate: 92,
        onlineRateTrend: 2,
        purifierEfficiency: 85,
        purifierEfficiencyTrend: -3,
        taskCompletionRate: 78,
        taskCompletionRateTrend: 10,
      },
      overview: {
        totalShops: 245,
        onlineDevices: 220,
        offlineDevices: 25,
      },
      map: null,
      refreshTimer: null,
    }
  },
  mounted() {
    this.initData()
    this.startRealTimeUpdate()
    this.initMap()
    this.initDeviceStatusChart()
    this.startAutoRefresh()
  },
  beforeUnmount() {
    if (this.refreshTimer) {
      clearInterval(this.refreshTimer)
    }
  },
  methods: {
    initData() {
      // 初始化设备在线情况数据
      this.updateDeviceStatus()
      // 初始化实时数据
      this.updateRealTimeData()
      // 初始化分区数据排名
      this.updateRankingData()
    handleTimeChange(tab) {
      this.activeTime = tab.value
      // 模拟切换时间周期后的数据更新
      this.updateMetrics()
    },
    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
    getPeriodLabel() {
      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(() => {
        this.metrics = {
          overStandardCount: Math.floor(Math.random() * 30),
          overStandardTrend: Math.floor(Math.random() * 20) - 10,
          onlineRate: Math.floor(Math.random() * 20) + 80,
          onlineRateTrend: Math.floor(Math.random() * 10) - 5,
          purifierEfficiency: Math.floor(Math.random() * 30) + 70,
          purifierEfficiencyTrend: Math.floor(Math.random() * 10) - 5,
          taskCompletionRate: Math.floor(Math.random() * 40) + 60,
          taskCompletionRateTrend: Math.floor(Math.random() * 15) - 7,
        }
      }, 300)
    },
    initMap() {
      // setTimeout(() => {
      districtSearch.removeDistrict()
      districtSearch.drawDistrict('上海市')
      // districtSearch.districtLayer('310106')
      // }, 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)
    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),
        // 响应式调整
        window.addEventListener('resize', () => {
          chart.resize()
        })
      }
    },
    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),
      )
    },
    startRealTimeUpdate() {
      // 每30秒更新一次数据
      this.realTimeInterval = setInterval(() => {
        this.updateDeviceStatus()
        this.updateRealTimeData()
    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 - 60px);
  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: 24px;
  left: 24px;
  z-index: 10;
  margin-bottom: 24px;
}
.cards-container {
  display: grid;
  grid-template-columns: 280px;
  grid-template-rows: auto repeat(4, auto);
  gap: 16px;
  background-color: rgba(255, 255, 255, 0.9);
  padding: 16px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 时间周期卡片 */
.time-period-card {
  background-color: #ffffff;
  border-radius: 8px;
  padding: 20px;
  background-color: #f5f7fa;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.el-header h1 {
  font-size: 24px;
.time-period-card .card-title {
  font-size: 14px;
  color: #86909c;
  font-weight: 500;
  margin-bottom: 16px;
  text-align: center;
}
.time-tab-container {
  display: flex;
  flex-direction: row;
  gap: 8px;
  width: 100%;
  justify-content: center;
}
.time-tab {
  padding: 2px 4px;
  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;
}
.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;
}
/* 指标卡片 */
.metric-card {
  background-color: #ffffff;
  border-radius: 8px;
  padding: 20px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
  transition: all 0.3s ease;
  position: relative;
  overflow: hidden;
}
.metric-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.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: #722ed1;
}
.task-icon {
  color: #1890ff;
}
.card-value {
  font-size: 32px;
  font-weight: bold;
  margin: 12px 0;
  color: #262626;
  line-height: 1.2;
}
.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;
}
/* 右侧实时监测总览区 */
.overview-section {
  position: absolute;
  top: 200px;
  right: 24px;
  width: 320px;
  background-color: rgba(255, 255, 255, 0.9);
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  padding: 20px;
  display: flex;
  flex-direction: column;
  z-index: 10;
  max-height: calc(100vh - 220px);
}
.overview-items-container {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 24px;
  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;
}
.device-status-chart {
  flex: 1;
  margin-top: 16px;
  min-height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* 弹窗样式 */
.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;
}
/* 响应式设计 */
@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;
  }
  .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>