餐饮油烟智能监测与监管一体化平台
feiyu02
2026-03-20 20cdb83586daabfb15fc056c4c97eb8e7ccaf928
src/views/inspection/MonitorControl.vue
@@ -1 +1,550 @@
<template>s</template>
<template>
  <!-- 总览现场巡查卡片 -->
  <el-card class="mb-4">
    <!-- <template #header>
      <div class="card-header">
        <span>现场巡查总览</span>
        <el-form :model="params" label-position="left" label-width="70px">
          <FYOptionTime
            :initValue="false"
            type="daterange"
            v-model:value="params.timeRange"
            style="width: 300px"
            :shortcuts="shortcuts"
          ></FYOptionTime>
          <FYOptionLocation
            :allOption="false"
            :level="3"
            :checkStrictly="false"
            :initValue="false"
            v-model:value="params.locations"
            style="width: 300px; margin-bottom: 0px"
          ></FYOptionLocation>
        </el-form>
      </div>
    </template> -->
    <el-scrollbar>
      <!-- 统计数据区域 -->
      <div class="stats-sections">
        <!-- 左侧:已巡查店铺率、巡查点次、复查点次 -->
        <div class="stats-section left-section">
          <h3>巡查概况</h3>
          <el-row justify="space-between">
            <div class="chart-item">
              <div class="progress-container">
                <el-progress
                  type="dashboard"
                  :percentage="parseFloat(inspectionStats.inspectedRate)"
                  :color="['#409EFF', '#67C23A']"
                  :width="120"
                />
                <div class="progress-label">已巡查店铺率</div>
                <div class="progress-value">
                  {{ `${inspectionStats.inspectedShops}/${inspectionStats.totalShops}` }}
                </div>
              </div>
            </div>
            <div class="stats-grid m-l-16">
              <el-statistic
                class="stat-item"
                :value="inspectionStats.inspectionPoints"
                title="巡查点次"
              />
              <el-statistic
                class="stat-item"
                :value="inspectionStats.reviewPoints"
                title="复查点次"
              />
            </div>
          </el-row>
        </div>
        <!-- 右侧:问题数、问题整改数、问题整改率统计图 -->
        <div class="stats-section right-section">
          <h3>问题整改概况</h3>
          <div class="stats-grid">
            <el-row justify="space-around">
              <el-text>问题数:{{ inspectionStats.problemCount }}</el-text>
              <el-text>整改数:{{ inspectionStats.rectifiedProblems }}</el-text>
            </el-row>
            <!-- <el-statistic class="stat-item" :value="inspectionStats.problemCount" title="问题数" />
            <el-statistic
              class="stat-item"
              :value="inspectionStats.rectifiedProblems"
              title="问题整改数"
            /> -->
          </div>
          <!-- <div class="chart-item"> -->
          <div ref="rectificationRateChart" class="chart"></div>
          <!-- </div> -->
        </div>
      </div>
      <!-- 其他图表展示 -->
      <div class="chart-container">
        <div class="chart-item">
          <h3>巡查完成情况趋势</h3>
          <div ref="inspectionTrendChart" class="chart"></div>
        </div>
        <div class="chart-item">
          <h3>问题类型分布</h3>
          <div ref="problemTypeChart" class="chart"></div>
        </div>
      </div>
    </el-scrollbar>
  </el-card>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
import * as echarts from 'echarts'
import { Icon } from '@iconify/vue'
import dayjs from 'dayjs'
import { ElMessage } from 'element-plus'
// 总览现场巡查数据
const dayStart = dayjs('2023-08-01').startOf('date')
const dayEnd = dayStart.endOf('date')
const shortcuts = [
  {
    text: '今天',
    value: [dayStart.toDate(), dayEnd.toDate()],
  },
  {
    text: '本周',
    value: [dayStart.startOf('week').toDate(), dayEnd.endOf('week').toDate()],
  },
  {
    text: '上周',
    value: [dayStart.day(-7).toDate(), dayEnd.day(-1).toDate()],
  },
  {
    text: '本月',
    value: [dayStart.startOf('month').toDate(), dayEnd.endOf('month').toDate()],
  },
  {
    text: '上月',
    value: [
      dayStart.subtract(1, 'month').startOf('month').toDate(),
      dayEnd.subtract(1, 'month').endOf('month').toDate(),
    ],
  },
  {
    text: '本季度',
    value: [dayStart.startOf('quarter').toDate(), dayEnd.endOf('quarter').toDate()],
  },
  {
    text: '上季度',
    value: [
      dayStart.subtract(1, 'quarter').startOf('quarter').toDate(),
      dayEnd.subtract(1, 'quarter').endOf('quarter').toDate(),
    ],
  },
  {
    text: '去年',
    value: [
      dayStart.subtract(1, 'year').startOf('year').toDate(),
      dayEnd.subtract(1, 'year').endOf('year').toDate(),
    ],
  },
  {
    text: '今年',
    value: [dayStart.startOf('year').toDate(), dayEnd.endOf('year').toDate()],
  },
]
const params = ref({
  prodBaseTypes: [],
  prodCheck: '',
  scenetype: '',
  topTask: '',
  locations: {
    aCode: null,
    aName: null,
    cCode: '3100',
    cName: '上海市',
    dCode: '310104',
    dName: '徐汇区',
    mCode: null,
    mName: null,
    pCode: '31',
    pName: '上海市',
    tCode: null,
    tName: null,
  },
  timeRange: [dayStart.startOf('month').toDate(), dayEnd.endOf('month').toDate()],
})
const inspectionStats = ref({
  // totalShops: 1250,
  // inspectedShops: 980,
  // inspectedRate: '78.4%',
  // inspectionPoints: 2350,
  // reviewPoints: 450,
  // problemCount: 320,
  // rectifiedProblems: 280,
  // sameDayRectificationRate: '65.2%',
  // effectiveRectificationRate: '78.5%',
  // comprehensiveRectificationRate: '82.3%',
  // auditPassRate: '87.5%',
})
// 图表引用
const inspectionTrendChart = ref(null)
const problemTypeChart = ref(null)
const rectificationRateChart = ref(null)
const refreshInspectionData = () => {
  // 模拟刷新数据
  console.log('刷新巡查数据', params.value)
  // 生成新的模拟数据
  const totalShops = 90 + Math.floor(Math.random() * 10)
  const inspectedShops = 70 + Math.floor(Math.random() * 10)
  const inspectedRate = ((inspectedShops / totalShops) * 100).toFixed(1) + '%'
  // 计算时间范围
  const startTime = params.value.timeRange[0]
  const endTime = params.value.timeRange[1]
  const startDate = dayjs(startTime)
  const endDate = dayjs(endTime)
  const daysDiff = endDate.diff(startDate, 'day')
  const months = daysDiff / 30
  // 根据时间范围调整数据量级
  const inspectionPoints = Math.floor(100 * months) + Math.floor(Math.random() * 20)
  const problemCount = Math.floor(200 * months) + Math.floor(Math.random() * 30)
  const reviewPoints = Math.floor(inspectionPoints * 0.2) + Math.floor(Math.random() * 10)
  const rectifiedProblems = Math.floor(problemCount * 0.8) + Math.floor(Math.random() * 20)
  inspectionStats.value = {
    totalShops,
    inspectedShops,
    inspectedRate,
    inspectionPoints,
    reviewPoints,
    problemCount,
    rectifiedProblems,
    sameDayRectificationRate: (60 + Math.random() * 20).toFixed(1) + '%',
    effectiveRectificationRate: (70 + Math.random() * 20).toFixed(1) + '%',
    comprehensiveRectificationRate: (75 + Math.random() * 15).toFixed(1) + '%',
    auditPassRate: (80 + Math.random() * 15).toFixed(1) + '%',
  }
  // 重新初始化图表以更新数据
  initCharts()
}
const handleExceed = (files, fileList) => {
  ElMessage.warning('只能上传一个文件')
}
// 初始化图表
const initCharts = () => {
  // 巡查完成情况趋势图
  if (inspectionTrendChart.value) {
    const chart = echarts.init(inspectionTrendChart.value)
    // 计算时间范围并生成x轴数据
    const startTime = params.value.timeRange[0]
    const endTime = params.value.timeRange[1]
    const startDate = dayjs(startTime)
    const endDate = dayjs(endTime)
    const daysDiff = endDate.diff(startDate, 'day')
    let xAxisData = []
    let seriesData = []
    if (daysDiff <= 30) {
      // 同一个月内,按日显示
      for (let i = 0; i <= daysDiff; i++) {
        const date = startDate.add(i, 'day')
        xAxisData.push(date.format('MM/DD'))
        seriesData.push(60 + Math.floor(Math.random() * 30)) // 生成随机数据
      }
    } else {
      // 超过一个月,按月显示
      const startMonth = startDate.startOf('month')
      const endMonth = endDate.endOf('month')
      const monthsDiff = endMonth.diff(startMonth, 'month')
      for (let i = 0; i <= monthsDiff; i++) {
        const date = startMonth.add(i, 'month')
        xAxisData.push(date.format('YYYY/MM'))
        seriesData.push(60 + Math.floor(Math.random() * 30)) // 生成随机数据
      }
    }
    chart.setOption({
      xAxis: {
        type: 'category',
        data: xAxisData,
      },
      yAxis: {
        type: 'value',
        axisLabel: {
          formatter: '{value}%',
        },
      },
      series: [
        {
          data: seriesData,
          type: 'bar',
        },
      ],
    })
  }
  // 问题类型分布图
  if (problemTypeChart.value) {
    const chart = echarts.init(problemTypeChart.value)
    chart.setOption({
      series: [
        {
          type: 'pie',
          radius: ['20%', '45%'],
          center: ['50%', '45%'],
          data: [
            { value: 30, name: '油烟在线监测设备' },
            { value: 25, name: '油烟净化设施设备' },
            { value: 20, name: '油烟在线监测设备维护' },
            { value: 15, name: '油烟净化设施设备维护' },
            { value: 10, name: '油烟在线监测数据量级' },
            { value: 20, name: '空调和风机噪声' },
            { value: 18, name: '台账管理' },
            { value: 22, name: '信用承诺自评' },
          ],
          label: {
            show: true,
            formatter: '{b}: {d}%',
          },
        },
      ],
    })
  }
  // 问题整改率统计图
  if (rectificationRateChart.value) {
    const chart = echarts.init(rectificationRateChart.value)
    chart.setOption({
      xAxis: {
        type: 'category',
        data: [
          '当日整改率',
          // '48小时内整改率',
          '综合整改率',
          '审核通过率',
        ],
      },
      yAxis: {
        type: 'value',
        axisLabel: {
          formatter: '{value}%',
        },
      },
      series: [
        {
          data: [
            parseFloat(inspectionStats.value.sameDayRectificationRate),
            // parseFloat(inspectionStats.value.effectiveRectificationRate),
            parseFloat(inspectionStats.value.comprehensiveRectificationRate),
            parseFloat(inspectionStats.value.auditPassRate),
          ],
          type: 'bar',
          itemStyle: {
            color: function (params) {
              const colors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C']
              return colors[params.dataIndex]
            },
          },
        },
      ],
    })
  }
}
// 监听窗口大小变化
const handleResize = () => {
  // 重新调整图表大小
  if (inspectionTrendChart.value) {
    echarts.init(inspectionTrendChart.value).resize()
  }
  if (problemTypeChart.value) {
    echarts.init(problemTypeChart.value).resize()
  }
  if (rectificationRateChart.value) {
    echarts.init(rectificationRateChart.value).resize()
  }
}
// 监听params变化
watch(
  () => params.value,
  () => {
    refreshInspectionData()
  },
  { deep: true },
)
// 生命周期
onMounted(() => {
  refreshInspectionData()
  initCharts()
  window.addEventListener('resize', handleResize)
})
// 组件卸载时清理事件监听
onUnmounted(() => {
  cleanup()
})
// 清理
const cleanup = () => {
  window.removeEventListener('resize', handleResize)
}
</script>
<style scoped>
:deep() .el-card__body {
  padding: 8px;
}
.mb-4 {
  /* width: 600px; */
}
.card-header {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
}
.filter-group {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  width: 100%;
  gap: 10px;
}
.mr-2 {
  margin-right: 10px;
}
.stats-sections {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.stats-section {
  flex: 1;
  background-color: #fff;
  border-radius: 8px;
  /* border: 1px solid #e4e7ed; */
}
.stats-section h3 {
  margin-bottom: 15px;
  font-size: 16px;
  font-weight: 600;
  color: #303133;
}
.stats-grid {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.stat-item {
  flex: 1;
  background-color: #fff;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.progress-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
}
.progress-label {
  margin-top: 10px;
  font-size: 14px;
  font-weight: 500;
  color: #606266;
}
.progress-value {
  margin-top: 5px;
  font-size: 18px;
  font-weight: 600;
  color: #409eff;
}
.chart-container {
  display: flex;
  flex-direction: column;
  gap: 20px;
  margin-bottom: 30px;
}
.chart-item {
  flex: 1;
  background-color: #fff;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.chart-item h3 {
  margin-bottom: 15px;
  font-size: 16px;
  font-weight: 600;
}
.chart-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
}
.chart-summary {
  font-size: 14px;
  color: #606266;
  font-weight: 500;
}
.chart {
  width: 100%;
  height: 250px;
}
.pagination-container {
  margin-top: 20px;
  display: flex;
  justify-content: flex-end;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
}
.import-container {
  padding: 20px 0;
}
.import-tip {
  margin-bottom: 20px;
  font-size: 14px;
  color: #606266;
}
.upload-demo {
  margin-bottom: 20px;
}
</style>