餐饮油烟智能监测与监管一体化平台
riku
2026-03-04 49e2b7ea866695957633855f71f9e2f943b11ec7
2026.3.4
已修改7个文件
已添加13个文件
3211 ■■■■■ 文件已修改
components.d.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/head/FYPageHeader.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/monitor/DeviceStatus.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/monitor/DistrictRanking.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/monitor/RealTimeData.vue 543 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/table/FYTable.vue 95 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/constants/envCreditCode.js 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/evalution/EvalutationEdit.vue 279 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/evalution/EvalutationRecord.vue 451 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/evalution/EvalutationTask.vue 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/evalution/components/CompDataResultEdit.vue 556 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/evalution/components/CompEvaTask.vue 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/evalution/components/CompReport.vue 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/evalution/components/precheck/CompPreCheck.vue 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/evalution/components/precheck/components/CompCheckArea.vue 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/evalution/components/precheck/components/CompCheckConfirm.vue 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/evalution/components/precheck/components/CompCheckExemption.vue 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/evalution/components/precheck/components/CompCheckSource.vue 334 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/DataDashboard.vue 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components.d.ts
@@ -99,6 +99,7 @@
    FYOptionTime: typeof import('./src/components/search-option/FYOptionTime.vue')['default']
    FYOptionTopTask: typeof import('./src/components/search-option/FYOptionTopTask.vue')['default']
    FYOptionUserType: typeof import('./src/components/search-option/FYOptionUserType.vue')['default']
    FYPageHeader: typeof import('./src/components/head/FYPageHeader.vue')['default']
    FYReconfrimButton: typeof import('./src/components/button/FYReconfrimButton.vue')['default']
    FYSearchBar: typeof import('./src/components/search-option/FYSearchBar.vue')['default']
    FYTable: typeof import('./src/components/table/FYTable.vue')['default']
@@ -213,6 +214,7 @@
  const FYOptionTime: typeof import('./src/components/search-option/FYOptionTime.vue')['default']
  const FYOptionTopTask: typeof import('./src/components/search-option/FYOptionTopTask.vue')['default']
  const FYOptionUserType: typeof import('./src/components/search-option/FYOptionUserType.vue')['default']
  const FYPageHeader: typeof import('./src/components/head/FYPageHeader.vue')['default']
  const FYReconfrimButton: typeof import('./src/components/button/FYReconfrimButton.vue')['default']
  const FYSearchBar: typeof import('./src/components/search-option/FYSearchBar.vue')['default']
  const FYTable: typeof import('./src/components/table/FYTable.vue')['default']
src/components/head/FYPageHeader.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
<template>
  <el-page-header @back="onBack" class="page-header">
    <template #content>
      <span> {{ title }} </span>
    </template>
  </el-page-header>
  <el-divider />
</template>
<script>
export default {
  props: {
    title: {
      type: String,
      default: '当前为默认页面标题,请传入标题名字'
    }
  },
  methods: {
    // å›žé€€é¡µé¢
    onBack() {
      this.$router.back();
    }
  }
};
</script>
src/components/monitor/DeviceStatus.vue
@@ -6,9 +6,9 @@
        <span>设备在线情况</span>
      </div>
    </template>
    <el-row :gutter="20">
    <el-row :gutter="0">
      <el-col :span="12">
        <el-row :gutter="20">
        <el-row :gutter="10">
          <el-col :span="12">
            <el-card class="status-card online-card" shadow="hover">
              <div class="status-content">
@@ -126,6 +126,7 @@
          formatter: '{a} <br/>{b}: {c} ({d}%)',
        },
        legend: {
          show: false,
          bottom: '0%',
          data: ['在线设备', '离线设备', '正常设备', '故障设备'],
        },
@@ -133,7 +134,7 @@
          {
            name: '在线状态',
            type: 'pie',
            radius: ['40%', '70%'],
            radius: ['20%', '40%'],
            center: ['48%', '50%'],
            startAngle: 270,
            endAngle: 90,
@@ -152,7 +153,7 @@
          {
            name: '运行状态',
            type: 'pie',
            radius: ['40%', '70%'],
            radius: ['30%', '50%'],
            center: ['52%', '50%'],
            startAngle: 90,
            endAngle: 270,
src/components/monitor/DistrictRanking.vue
@@ -24,25 +24,31 @@
        </el-radio-group>
      </el-col>
    </el-row>
    <div ref="rankingChart" class="chart-container"></div>
    <el-table :data="sortedRankingData" style="width: 100%" stripe class="ranking-table">
      <el-table-column label="排名" type="index" width="80" />
      <el-table-column prop="name" label="区县名称" />
      <el-table-column prop="value" label="浓度均值 (mg/m³)" />
      <el-table-column label="排名变化" width="120">
        <template #default="scope">
          <div v-if="scope.row.rankChange > 0" class="rank-up">
            <el-icon><ArrowUp /></el-icon> {{ scope.row.rankChange }}
          </div>
          <div v-else-if="scope.row.rankChange < 0" class="rank-down">
            <el-icon><ArrowDown /></el-icon> {{ Math.abs(scope.row.rankChange) }}
          </div>
          <div v-else class="rank-no-change">
            <el-icon><Minus /></el-icon> 0
          </div>
        </template>
      </el-table-column>
    </el-table>
    <el-row :gutter="20">
      <el-col :span="10">
        <div ref="rankingChart" class="chart-container"></div>
      </el-col>
      <el-col :span="14">
        <el-table :data="sortedRankingData" size="small" stripe class="ranking-table">
          <el-table-column label="排名" type="index" width="80" />
          <el-table-column prop="name" label="区县名称" />
          <el-table-column prop="value" label="浓度均值 (mg/m³)" />
          <el-table-column label="排名变化" width="120">
            <template #default="scope">
              <div v-if="scope.row.rankChange > 0" class="rank-up">
                <el-icon><ArrowUp /></el-icon> {{ scope.row.rankChange }}
              </div>
              <div v-else-if="scope.row.rankChange < 0" class="rank-down">
                <el-icon><ArrowDown /></el-icon> {{ Math.abs(scope.row.rankChange) }}
              </div>
              <div v-else class="rank-no-change">
                <el-icon><Minus /></el-icon> 0
              </div>
            </template>
          </el-table-column>
        </el-table>
      </el-col>
    </el-row>
  </el-card>
</template>
@@ -112,26 +118,29 @@
          },
        },
        grid: {
          left: '3%',
          left: '0%',
          right: '4%',
          bottom: '3%',
          bottom: '6%',
          top: '4%',
          containLabel: true,
        },
        xAxis: {
          type: 'category',
          data: this.sortedRankingData.map((item) => item.name),
          type: 'value',
          nameLocation: 'middle',
          nameGap: 30,
          name: this.rankingType === 'hourly' ? '小时均值 (mg/m³)' : '月均值 (mg/m³)',
        },
        yAxis: {
          type: 'value',
          name: this.rankingType === 'hourly' ? '小时均值 (mg/m³)' : '月均值 (mg/m³)',
          type: 'category',
          data: this.sortedRankingData.map((item) => item.name).reverse(),
        },
        series: [
          {
            name: '浓度均值',
            type: 'bar',
            data: this.sortedRankingData.map((item) => parseFloat(item.value)),
            data: this.sortedRankingData.map((item) => parseFloat(item.value)).reverse(),
            itemStyle: {
              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
              color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
                { offset: 0, color: '#83bff6' },
                { offset: 0.5, color: '#188df0' },
                { offset: 1, color: '#188df0' },
@@ -153,7 +162,7 @@
}
</script>
<style scoped>
<style scoped lang="scss">
.section {
  margin-bottom: 20px;
}
@@ -165,7 +174,7 @@
}
.chart-container {
  height: 300px;
  height: 400px;
  width: 100%;
}
src/components/monitor/RealTimeData.vue
@@ -6,153 +6,313 @@
        <span>设备实时数据</span>
      </div>
    </template>
    <el-row :gutter="20">
      <el-col :span="8">
        <el-card class="realtime-data" shadow="hover">
          <template #header>
            <span>实时分钟数据</span>
          </template>
          <el-descriptions :column="1" v-if="currentDevice">
            <el-descriptions-item label="设备编号">{{
              currentDevice.deviceId
            }}</el-descriptions-item>
            <el-descriptions-item label="设备供应商">{{
              currentDevice.supplier
            }}</el-descriptions-item>
            <el-descriptions-item label="油烟浓度"
              >{{ currentDevice.油烟浓度 }} mg/m³</el-descriptions-item
    <el-space v-if="devices && devices.length > 0" justify="center" wrap>
      <el-card
        v-for="(device, index) in devices"
        :key="device.deviceId"
        class="device-card"
        shadow="hover"
        :class="{ 'abnormal-device': isDeviceAbnormal(device) }"
      >
        <template #header>
          <div class="device-header">
            <span class="device-id">{{ device.deviceId }}</span>
            <div class="device-status">
              <el-icon v-if="device.status === '正常'" class="status-icon normal-icon"
                ><iconify-icon icon="mdi:check-circle"
              /></el-icon>
              <el-icon v-else class="status-icon abnormal-icon"
                ><iconify-icon icon="mdi:alert-circle"
              /></el-icon>
              <span>{{ device.status }}</span>
            </div>
          </div>
        </template>
        <!-- å®žæ—¶åˆ†é’Ÿæ•°æ® -->
        <div class="realtime-data">
          <div class="basic-info">
            <span class="supplier">{{ device.supplier }}</span>
            <span class="monitor-time">{{
              device.monitorTime || new Date().toLocaleString()
            }}</span>
          </div>
          <div class="monitor-values">
            <div
              class="value-item"
              :class="{ 'abnormal-value': isAbnormal('smokeDensity', device.smokeDensity) }"
            >
            <el-descriptions-item label="风机电流"
              >{{ currentDevice.风机电流 }} A</el-descriptions-item
              <span class="value-label">油烟浓度</span>
              <span class="value">{{ device.smokeDensity }} <span class="unit">mg/m³</span></span>
            </div>
            <div
              class="value-item"
              :class="{ 'abnormal-value': isAbnormal('fanCurrent', device.fanCurrent) }"
            >
            <el-descriptions-item label="净化器电流"
              >{{ currentDevice.净化器电流 }} A</el-descriptions-item
              <span class="value-label">风机电流</span>
              <span class="value">{{ device.fanCurrent }} <span class="unit">A</span></span>
            </div>
            <div
              class="value-item"
              :class="{ 'abnormal-value': isAbnormal('purifierCurrent', device.purifierCurrent) }"
            >
          </el-descriptions>
        </el-card>
      </el-col>
      <el-col :span="16">
        <el-card class="realtime-chart" shadow="hover">
          <template #header>
            <span>近一小时数据</span>
          </template>
          <div ref="hourlyChart" class="chart-container"></div>
        </el-card>
      </el-col>
    </el-row>
              <span class="value-label">净化器电流</span>
              <span class="value">{{ device.purifierCurrent }} <span class="unit">A</span></span>
            </div>
          </div>
        </div>
        <!-- è¿‘一小时数据 -->
        <div class="hourly-charts">
          <el-popover
            placement="left"
            :width="600"
            trigger="click"
            @show="() => initDeviceCharts(device)"
          >
            <template #reference>
              <div class="chart-header">
                <span class="date">{{ new Date().toLocaleDateString() }}</span>
                <el-button size="small" type="primary" link> æŸ¥çœ‹è¿‘一小时数据 </el-button>
              </div>
            </template>
            <div class="popover-content">
              <div class="popover-header">
                <h3>{{ device.deviceId }} è¿‘一小时数据</h3>
              </div>
              <div ref="charts" :key="device.deviceId" class="charts-container">
                <div class="chart-item">
                  <div class="chart-title">油烟浓度(mg/m³)</div>
                  <div ref="smokeChart" :data-device-id="device.deviceId" class="small-chart"></div>
                </div>
                <div class="chart-item">
                  <div class="chart-title">风机电流(A)</div>
                  <div ref="fanChart" :data-device-id="device.deviceId" class="small-chart"></div>
                </div>
                <div class="chart-item">
                  <div class="chart-title">净化器电流(A)</div>
                  <div
                    ref="purifierChart"
                    :data-device-id="device.deviceId"
                    class="small-chart"
                  ></div>
                </div>
              </div>
            </div>
          </el-popover>
        </div>
      </el-card>
    </el-space>
    <div v-else class="no-data">
      <el-empty description="暂无设备数据" />
    </div>
  </el-card>
</template>
<script>
import * as echarts from 'echarts'
import { Icon } from '@iconify/vue'
export default {
  name: 'RealTimeData',
  components: {
    IconifyIcon: Icon,
  },
  props: {
    currentDevice: {
      type: Object,
      default: null,
    },
    hourlyData: {
    devices: {
      type: Array,
      default: () => [],
    },
  },
  data() {
    return {
      hourlyChart: null,
      charts: {},
    }
  },
  mounted() {
    this.initChart()
    // ä¸éœ€è¦è‡ªåŠ¨åˆå§‹åŒ–å›¾è¡¨ï¼Œåªåœ¨å¼¹å‡ºæ¡†æ˜¾ç¤ºæ—¶åˆå§‹åŒ–
  },
  beforeUnmount() {
    if (this.hourlyChart) {
      this.hourlyChart.dispose()
    }
    Object.values(this.charts).forEach((chart) => {
      if (chart) {
        chart.dispose()
      }
    })
  },
  watch: {
    hourlyData() {
      this.updateChart()
    devices: {
      handler(newDevices) {
        // æ¸…除旧图表
        Object.values(this.charts).forEach((chart) => {
          if (chart) {
            chart.dispose()
          }
        })
        this.charts = {}
      },
      deep: true,
    },
  },
  methods: {
    initChart() {
      this.hourlyChart = echarts.init(this.$refs.hourlyChart)
      this.updateChart()
    initDeviceCharts(device) {
      const deviceId = device.deviceId
      window.addEventListener('resize', () => {
        this.hourlyChart.resize()
      // æ¸…除该设备的旧图表
      Object.keys(this.charts).forEach((key) => {
        if (key.startsWith(deviceId)) {
          this.charts[key].dispose()
          delete this.charts[key]
        }
      })
      this.$nextTick(() => {
        // æ²¹çƒŸæµ“度图表
        const smokeChartEl = document.querySelector(`[data-device-id="${deviceId}"]`)
        if (smokeChartEl) {
          this.charts[`${deviceId}_smoke`] = echarts.init(smokeChartEl)
        }
        // é£Žæœºç”µæµå›¾è¡¨
        const fanChartEl = document.querySelectorAll(`[data-device-id="${deviceId}"]`)[1]
        if (fanChartEl) {
          this.charts[`${deviceId}_fan`] = echarts.init(fanChartEl)
        }
        // å‡€åŒ–器电流图表
        const purifierChartEl = document.querySelectorAll(`[data-device-id="${deviceId}"]`)[2]
        if (purifierChartEl) {
          this.charts[`${deviceId}_purifier`] = echarts.init(purifierChartEl)
        }
        this.updateDeviceCharts(device)
      })
    },
    updateChart() {
      if (!this.hourlyChart) return
    updateCharts() {
      this.devices.forEach((device) => {
        this.updateDeviceCharts(device)
      })
    },
    updateDeviceCharts(device) {
      const deviceId = device.deviceId
      const deviceHourlyData = device.hourlyData || []
      const option = {
      const _baseOption = {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'cross',
            label: {
              backgroundColor: '#6a7985',
            },
          },
        },
        legend: {
          data: ['油烟浓度', '风机电流', '净化器电流'],
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          left: '0%',
          right: '0%',
          top: '10%',
          bottom: '0%',
          containLabel: true,
        },
        xAxis: {
          type: 'category',
          boundaryGap: false,
          data: this.hourlyData.map((item) => item.time),
          data: deviceHourlyData.map((item) => item.time),
          axisLabel: {
            fontSize: 10,
          },
        },
        yAxis: [
          {
            type: 'value',
            name: '油烟浓度 (mg/m³)',
            position: 'left',
          },
          {
            type: 'value',
            name: '电流 (A)',
            position: 'right',
          },
        ],
        series: [
          {
            name: '油烟浓度',
            type: 'line',
            data: this.hourlyData.map((item) => parseFloat(item.油烟浓度)),
            yAxisIndex: 0,
          },
          {
            name: '风机电流',
            type: 'line',
            data: this.hourlyData.map((item) => parseFloat(item.风机电流)),
            yAxisIndex: 1,
          },
          {
            name: '净化器电流',
            type: 'line',
            data: this.hourlyData.map((item) => parseFloat(item.净化器电流)),
            yAxisIndex: 1,
          },
        ],
      }
      this.hourlyChart.setOption(option)
      // æ›´æ–°æ²¹çƒŸæµ“度图表
      if (this.charts[`${deviceId}_smoke`]) {
        const smokeOption = {
          ..._baseOption,
          yAxis: {
            type: 'value',
            name: 'mg/m³',
            // nameLocation: 'middle',
            // nameGap: 30,
            axisLabel: {
              fontSize: 10,
            },
          },
          series: [
            {
              name: '油烟浓度',
              type: 'line',
              data: deviceHourlyData.map((item) => parseFloat(item.smokeDensity)),
              smooth: true,
            },
          ],
        }
        this.charts[`${deviceId}_smoke`].setOption(smokeOption)
      }
      // æ›´æ–°é£Žæœºç”µæµå›¾è¡¨
      if (this.charts[`${deviceId}_fan`]) {
        const fanOption = {
          ..._baseOption,
          yAxis: {
            type: 'value',
            name: 'A',
            // nameLocation: 'middle',
            // nameGap: 30,
            axisLabel: {
              fontSize: 10,
            },
          },
          series: [
            {
              name: '风机电流',
              type: 'line',
              data: deviceHourlyData.map((item) => parseFloat(item.fanCurrent)),
              smooth: true,
            },
          ],
        }
        this.charts[`${deviceId}_fan`].setOption(fanOption)
      }
      // æ›´æ–°å‡€åŒ–器电流图表
      if (this.charts[`${deviceId}_purifier`]) {
        const purifierOption = {
          ..._baseOption,
          yAxis: {
            type: 'value',
            name: 'A',
            // nameLocation: 'middle',
            // nameGap: 30,
            axisLabel: {
              fontSize: 10,
            },
          },
          series: [
            {
              name: '净化器电流',
              type: 'line',
              data: deviceHourlyData.map((item) => parseFloat(item.purifierCurrent)),
              smooth: true,
            },
          ],
        }
        this.charts[`${deviceId}_purifier`].setOption(purifierOption)
      }
    },
    isDeviceAbnormal(device) {
      return (
        this.isAbnormal('smokeDensity', device.smokeDensity) ||
        this.isAbnormal('fanCurrent', device.fanCurrent) ||
        this.isAbnormal('purifierCurrent', device.purifierCurrent)
      )
    },
    isAbnormal(type, value) {
      // è¿™é‡Œå¯ä»¥æ ¹æ®å®žé™…情况定义异常值判断逻辑
      const thresholds = {
        smokeDensity: 10,
        fanCurrent: 5,
        purifierCurrent: 3,
      }
      return parseFloat(value) > (thresholds[type] || 0)
    },
  },
}
</script>
<style scoped>
<style scoped lang="scss">
.section {
  margin-bottom: 20px;
}
@@ -163,8 +323,191 @@
  align-items: center;
}
.chart-container {
  height: 300px;
.device-card {
  // width: 100%;
  margin-bottom: 20px;
  transition: all 0.3s ease;
}
.device-card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.abnormal-device {
  border-bottom: 4px solid #f56c6c;
}
.device-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.device-id {
  font-size: 16px;
  font-weight: bold;
  color: #303133;
}
.device-status {
  display: flex;
  align-items: center;
  gap: 5px;
}
.status-icon {
  font-size: 16px;
}
.normal-icon {
  color: #67c23a;
}
.abnormal-icon {
  color: #f56c6c;
}
.realtime-data {
  margin-bottom: 20px;
}
.basic-info {
  display: flex;
  justify-content: space-between;
  margin-bottom: 15px;
  font-size: 12px;
  color: #909399;
}
.supplier {
  font-style: italic;
}
.monitor-time {
  font-family: monospace;
}
.monitor-values {
  display: flex;
  justify-content: space-around;
  gap: 10px;
}
.value-item {
  flex: 1;
  width: 60px;
  text-align: center;
  padding: 10px;
  border-radius: 4px;
  background-color: #f9f9f9;
  border: 1px solid transparent;
}
.value-label {
  display: block;
  font-size: 12px;
  color: #606266;
  margin-bottom: 5px;
}
.value {
  display: block;
  font-size: 20px;
  font-weight: bold;
  color: #303133;
}
.unit {
  font-size: 12px;
  font-weight: normal;
  color: #909399;
}
.abnormal-value {
  background-color: #fef0f0;
  border: 1px solid #fbc4c4;
}
.abnormal-value .value {
  color: #f56c6c;
}
.hourly-charts {
  margin-top: 20px;
}
.chart-header {
  margin-top: 10px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 5px 0;
}
.popover-content {
  padding: 10px;
}
.popover-header {
  margin-bottom: 15px;
  border-bottom: 1px solid #f0f0f0;
  padding-bottom: 10px;
}
.popover-header h3 {
  margin: 0;
  font-size: 14px;
  font-weight: bold;
  color: #303133;
}
.date {
  font-size: 12px;
  color: #909399;
  font-family: monospace;
}
.charts-container {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.chart-item {
  background-color: #f9f9f9;
  border-radius: 4px;
  padding: 10px;
}
.chart-title {
  font-size: 12px;
  color: #606266;
  margin-bottom: 5px;
  text-align: center;
}
.small-chart {
  height: 120px;
  width: 100%;
}
.no-data {
  padding: 40px 0;
  text-align: center;
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .monitor-values {
    flex-direction: column;
  }
  .value-item {
    width: 100%;
  }
  .small-chart {
    height: 100px;
  }
}
</style>
src/components/table/FYTable.vue
@@ -1,4 +1,5 @@
<template>
  <div>
  <el-row ref="searchRef">
    <FYSearchBar @search="onSearch">
      <template #options v-if="$slots.options">
@@ -75,30 +76,30 @@
    cellClassName: Function || String,
    pagination: {
      type: Boolean,
      default: true
      default: true,
    },
    // '' | 'small' | 'default' | 'large'
    size: {
      type: String,
      default: 'default'
      default: 'default',
    },
    data: {
      type: Array,
      default: () => []
      default: () => [],
    },
    totalCount: {
      type: Number,
      default: 0
      default: 0,
    },
    defaultPageSize: {
      type: Number,
      default: 20
      default: 20,
    },
    // é¢å¤–的高度,用于计算表格高度
    extraHeight: {
      type: Number,
      default: 0
    }
      default: 0,
    },
  },
  data() {
    return {
@@ -108,46 +109,46 @@
      currentPage: 1,
      pageSize: this.defaultPageSize,
      loading: false,
      fontSize: 'default'
    };
      fontSize: 'default',
    }
  },
  emits: ['search', 'cellClick', 'tablePaste', 'sortChange'],
  watch: {
    currentPage(nValue, oValue) {
      if (nValue != oValue) {
        this.onSearch();
        this.onSearch()
      }
    },
    pageSize(nValue, oValue) {
      if (nValue != oValue) {
        this.onSearch();
        this.onSearch()
      }
    },
    size: {
      handler(nValue, oValue) {
        if (nValue != oValue) {
          this.fontSize = nValue;
          this.fontSize = nValue
        }
      },
      immediate: true
      immediate: true,
    },
    data(nValue, oValue) {
      if (nValue != oValue) {
        this.tableData = nValue;
        this.tableData = nValue
      }
    },
    totalCount(nValue, oValue) {
      if (nValue != oValue) {
        this.total = nValue;
        this.total = nValue
      }
    },
    extraHeight: {
      handler(nValue, oValue) {
        if (nValue != oValue) {
          this.tableHeight = this.calcTableHeight();
          this.tableHeight = this.calcTableHeight()
        }
      }
    }
      },
    },
  },
  computed: {},
  methods: {
@@ -157,73 +158,71 @@
     * å›žè°ƒå‡½æ•°æŽ¥æ”¶ä¸€ä¸ªå¯¹è±¡ï¼ŒåŒ…括表格数据数组data和数据总数total
     */
    onSearch() {
      this.loading = true;
      this.loading = true
      this.$emit(
        'search',
        {
          currentPage: this.currentPage,
          pageSize: this.pageSize
          pageSize: this.pageSize,
        },
        (res) => {
          if (res) {
            if (res.data) {
              this.tableData = res.data;
              this.tableData = res.data
            }
            if (res.total) {
              this.total = res.total;
              this.total = res.total
            }
          }
          this.loading = false;
          this.doLayout();
        }
      );
          this.loading = false
          this.doLayout()
        },
      )
    },
    calcTableHeight() {
      const h1 = this.$refs.searchRef.$el.offsetHeight;
      const h2 = this.$refs.paginationRef
        ? this.$refs.paginationRef.$el.offsetHeight
        : 0;
      const h3 = this.$refs.expandRef.$el.offsetHeight;
      const h4 = this.$refs.expand2Ref.offsetHeight;
      const h1 = this.$refs.searchRef.$el.offsetHeight
      const h2 = this.$refs.paginationRef ? this.$refs.paginationRef.$el.offsetHeight : 0
      const h3 = this.$refs.expandRef.$el.offsetHeight
      const h4 = this.$refs.expand2Ref.offsetHeight
      const h = h1 + h2 + h3 + h4 + this.extraHeight;
      return this.contentMaxHeight.value - h + 'px';
      const h = h1 + h2 + h3 + h4 + this.extraHeight
      return this.contentMaxHeight - h + 'px'
      // return `calc(100vh - ${h}px - 60px - var(--el-main-padding) * 2)`;
    },
    tableRowClassName({ row }) {
      if (this.rowClassName) {
        if (typeof this.rowClassName == 'string') {
          return this.rowClassName;
          return this.rowClassName
        } else if (typeof this.rowClassName == 'function') {
          return this.rowClassName({ row });
          return this.rowClassName({ row })
        }
      } else {
        return row.extension1 != '0' ? 'online-row' : 'offline-row';
        return row.extension1 != '0' ? 'online-row' : 'offline-row'
      }
    },
    cellClick(row, column, cell, event) {
      this.$emit('cellClick', row, column, cell, event);
      this.$emit('cellClick', row, column, cell, event)
    },
    handlePaste(event) {
      this.$emit('tablePaste', event);
      this.$emit('tablePaste', event)
    },
    doLayout() {
      this.$refs.tableRef.doLayout();
      this.$refs.tableRef.doLayout()
    },
    handleSortChange({ column, prop, order }) {
      this.$emit('sortChange', { column, prop, order });
      this.$emit('sortChange', { column, prop, order })
    },
    clearSort() {
      this.$refs.tableRef.clearSort();
    }
      this.$refs.tableRef.clearSort()
    },
  },
  mounted() {
    setTimeout(() => {
      this.tableHeight = this.calcTableHeight();
    }, 100);
    this.onSearch();
  }
};
      this.tableHeight = this.calcTableHeight()
    }, 100)
    this.onSearch()
  },
}
</script>
<style>
src/constants/envCreditCode.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
// çŽ¯ä¿¡ç è½¬æ¢
function envCreditCode(score) {
  const s = parseInt(score)
  if (s <= 59) {
    return {
      value: 2,
      name: '红码',
      color: '#db2828'
    };
  } else if (s <= 89) {
    return {
      value: 1,
      name: '黄码',
      color: '#f7a62c'
    };
  } else {
    return {
      value: 0,
      name: '绿码',
      color: '#21ba45'
    };
  }
}
export { envCreditCode };
src/router/index.js
@@ -98,7 +98,7 @@
            {
              name: 'auto-evalution',
              path: 'auto-evalution',
              component: () => import('@/views/analysis/evalution/AutoEvalution.vue'),
              component: () => import('@/views/analysis/evalution/EvalutationRecord.vue'),
            },
            {
              name: 'huanxincode-manage',
src/views/analysis/evalution/EvalutationEdit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,279 @@
<template>
  <FYPageHeader title="评估结果详情"></FYPageHeader>
  <el-row v-for="item in evaluation" :key="item.id"> </el-row>
  <div class="btns">
    <el-button type="primary" @click="submit" :disabled="!isUpdated">提交</el-button>
  </div>
  <el-table
    class="table-style"
    :data="tableData"
    ref="tableRef"
    :span-method="objectSpanMethod"
    table-layout="fixed"
    :cell-style="cellClassName"
    border
    stripe
  >
    <el-table-column v-slot="scope" prop="one_title" label="一级指标" width="200">
      <!-- <el-checkbox v-model="scope.row.one_select" @change="checked => oneSelectChange(checked, scope.row)">{{ scope.row.one_title }}</el-checkbox> -->
    </el-table-column>
    <el-table-column prop="one_score" label="分值" width="55" />
    <el-table-column prop="one_maxScore" label="最大分值" width="90" />
    <el-table-column v-slot="scope" prop="two_title" label="二级指标" width="200">
      <!-- <el-checkbox v-model="scope.row.two_select" @change="checked => twoSelectChange(checked, scope.row)">{{ scope.row.two_title }}</el-checkbox> -->
    </el-table-column>
    <el-table-column prop="two_score" label="分值" width="55" />
    <el-table-column prop="two_maxScore" label="最大分值" width="90" />
    <el-table-column v-slot="scope" prop="three_title" label="具体问题">
      <el-checkbox
        v-model="scope.row.three_select"
        @change="(checked) => threeSelectChange(checked, scope.row)"
        >{{ scope.row.three_title }}</el-checkbox
      >
    </el-table-column>
    <el-table-column prop="three_score" label="单项扣分" width="90" />
  </el-table>
</template>
<script>
import evaluateApi from '@/api/fysp/evaluateApi';
import { useFetchData } from '@/composables/fetchData';
import { ElMessage } from 'element-plus';
export default {
  setup() {
    const { loading, fetchData } = useFetchData();
    return { loading, fetchData };
  },
  data() {
    return {
      tableData: [],
      evaluation: [],
      subTaskId: '',
      isUpdated: false
    };
  },
  created() {
    // // watch è·¯ç”±çš„参数,以便再次获取数据
    // this.$watch(
    //   () => this.$route.params,
    //   () => {
    //     this.getScore();
    //   },
    //   // ç»„件创建完后获取数据,
    //   // æ­¤æ—¶ data å·²ç»è¢« observed äº†
    //   { immediate: true }
    // );
  },
  computed: {
    // å·²è¢«å‹¾é€‰çš„item
    checkedUpdatedList() {
      var list = [];
      for (let index = 0; index < this.tableData.length; index++) {
        const element = this.tableData[index];
        if (element.three_select) {
          list.push(element.three_id);
        }
      }
      return list;
    }
  },
  mounted() {
    this.getList();
  },
  methods: {
    // æ¯ä¸€ä¸ªå•元格的class
    cellClassName({ row, column, rowIndex, columnIndex }) {
      if (column.property === 'one_score') {
        if (row.one_score < 0) {
          return { color: 'red' };
        }
      } else if (column.property === 'two_score') {
        if (row.two_score < 0) {
          return { color: 'red' };
        }
      } else if (column.property === 'three_score') {
        if (row.three_score < 0) {
          return { color: 'red' };
        }
      }
      return { color: 'black' };
    },
    /** æä»· */
    submit() {
      evaluateApi
        .updateScore({
          subTaskId: this.subTaskId,
          itemList: this.checkedUpdatedList
        })
        .then((res) => {
          if (res.success) {
            ElMessage({
              message: res.message,
              type: 'success'
            });
          }else {
            ElMessage({
              message: res.message,
              type: 'error'
            });
          }
        });
      setTimeout(() => {
        this.getList();
      }, 1000);
    },
    /** é€šè¿‡ç¬¬ä¸‰çº§çš„id获取上级以及顶级 */
    getSuperObjByThreeId(threeId, list, path = []) {
      for (let index = 0; index < list.length; index++) {
        const item = list[index];
        // å°†å½“前项添加到路径中
        const currentPath = path.concat(item);
        if (item.id === threeId) {
          // å¦‚果找到匹配的 id,返回路径数组
          return currentPath;
        }
        const subList = item.subList;
        if (subList) {
          // é€’归查找子列表
          const result = this.getSuperObjByThreeId(threeId, subList, currentPath);
          if (result) {
            return result; // å¦‚果找到匹配的 id,返回结果
          }
        }
      }
      return null; // å¦‚果没有找到匹配的 id,返回 null
    },
    /** é—®é¢˜é€‰æ‹©æ¡† */
    threeSelectChange(isSelect, row) {
      this.isUpdated = true;
    },
    /** åˆ—合并 */
    objectSpanMethod({ row, column, rowIndex, columnIndex }) {
      if (columnIndex === 0 || columnIndex === 1 || columnIndex === 2) {
        // å¯¹ ä¸€çº§æŒ‡æ ‡ åˆ—进行合并
        let rowSpan = 1;
        for (let i = rowIndex + 1; i < this.tableData.length; i++) {
          if (this.tableData[i].one_id === row.one_id) {
            rowSpan++;
          } else {
            break;
          }
        }
        if (rowIndex > 0) {
          if (this.tableData[rowIndex - 1].one_id === row.one_id) {
            return { rowspan: 0, colspan: 0 };
          }
        }
        return { rowspan: rowSpan, colspan: 1 };
      } else if (columnIndex === 3 || columnIndex === 4 || columnIndex === 5) {
        // å¯¹ äºŒçº§æŒ‡æ ‡ åˆ—进行合并,确保 ä¸€çº§æŒ‡æ ‡ ä¸€æ ·
        let rowSpan = 1;
        for (let i = rowIndex + 1; i < this.tableData.length; i++) {
          if (this.tableData[i].one_id === row.one_id && this.tableData[i].two_id === row.two_id) {
            rowSpan++;
          } else {
            break;
          }
        }
        if (rowIndex > 0) {
          if (
            this.tableData[rowIndex - 1].one_id === row.one_id &&
            this.tableData[rowIndex - 1].two_id === row.two_id
          ) {
            return { rowspan: 0, colspan: 0 };
          }
        }
        return { rowspan: rowSpan, colspan: 1 };
      }
    },
    /** å¯¹è±¡å±žæ€§æ‹·è´ */
    deepCopyWithPrefix(obj, target, prefix) {
      // ç¡®ä¿ target æ˜¯ä¸€ä¸ªå¯¹è±¡
      if (typeof target !== 'object' || target === null) {
        target = {};
      }
      // éåŽ†å¯¹è±¡çš„æ‰€æœ‰å±žæ€§
      for (const key in obj) {
        if (obj.hasOwnProperty(key)) {
          // ä¸ºå±žæ€§ååŠ ä¸Šå‰ç¼€
          const newKey = prefix + key;
          // å¦‚果属性值是对象,则递归复制
          if (typeof obj[key] === 'object' && obj[key] !== null) {
            this.deepCopyWithPrefix(obj[key], (target[newKey] = {}), prefix);
          } else {
            // å¦åˆ™ç›´æŽ¥å¤åˆ¶å±žæ€§
            target[newKey] = obj[key];
          }
        }
      }
      return target;
    },
    /** @param data åˆ—表数据 */
    genTableData(data) {
      var result = [];
      if (data) {
        for (let i = 0; i < data.length; i++) {
          const firstLevelItem = data[i];
          var secondLevel = firstLevelItem.subList;
          if (secondLevel) {
            for (let j = 0; j < secondLevel.length; j++) {
              const secondLevelItem = secondLevel[j];
              var thirdLevel = secondLevelItem.subList;
              if (thirdLevel) {
                for (let q = 0; q < thirdLevel.length; q++) {
                  const thirdLevelItem = thirdLevel[q];
                  var item = {};
                  this.deepCopyWithPrefix(firstLevelItem, item, 'one_');
                  this.deepCopyWithPrefix(secondLevelItem, item, 'two_');
                  this.deepCopyWithPrefix(thirdLevelItem, item, 'three_');
                  result.push(item);
                }
              }
            }
          }
        }
      }
      return result;
    },
    getList() {
      this.subTaskId = this.$route.params.subTaskId;
      evaluateApi.fetchItemEvaluation(this.subTaskId).then((res) => {
        this.isUpdated = false;
        this.tableData = this.genTableData(res.data.details);
      });
    },
    onSearch(page, func) {
      evaluateApi.fetchItemEvaluation(this.$route.params.subTaskId).then((res) => {
        if (typeof func === 'function') {
          // å¤„理数据
          var data = this.genTableData(res.data);
          func({ data: data });
        }
        this.tableData = this.genTableData(res.data);
      });
    }
  }
};
</script>
<style scoped>
.table-style {
  width: 100%;
  padding-bottom: 30px;
}
.btns {
  padding-bottom: 10px;
  padding-right: 30px;
  display: flex;
  flex-direction: row-reverse;
}
/* æ”¹å˜è¡¨æ ¼å†…单元格边框颜色 */
.el-table {
  --el-table-border-color: #000000;
}
.red-cell {
  background-color: red;
}
</style>
src/views/analysis/evalution/EvalutationRecord.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,451 @@
<template>
  <FYTable
    @search="onSearch"
    :pagination="false"
    ref="tableRef"
    @cell-click="cellClick"
    :cell-class-name="cellClassName"
    @table-paste="handlePaste"
    @sort-change="handleSortChange"
  >
    <template #options>
      <!-- åŒºåŽ¿ -->
      <FYOptionLocation
        :allOption="false"
        :level="3"
        :checkStrictly="false"
        v-model:value="formSearch.locations"
      ></FYOptionLocation>
      <!-- åœºæ™¯ç±»åž‹ -->
      <FYOptionScene
        :allOption="false"
        :type="2"
        v-model:value="formSearch.scenetype"
      ></FYOptionScene>
      <!-- æ—¶é—´ -->
      <FYOptionTime
        :initValue="false"
        type="month"
        v-model:value="formSearch.time"
      ></FYOptionTime>
    </template>
    <template #buttons>
      <!-- <el-button icon="Download" size="default" type="success" @click="download"
        >规范性评估与分析报告</el-button
      > -->
      <CompReport
        :locations="formSearch.locations"
        :scenetype="formSearch.scenetype"
        :time="formSearch.time"
      ></CompReport>
    </template>
    <template #options-expand>
      <el-form :inline="true">
        <CompQuickSet @quick-set="setOptions"></CompQuickSet>
      </el-form>
    </template>
    <template #table-column="{ size }">
      <el-table-column
        fixed="left"
        sortable="custom"
        prop="sceneIndex"
        label="编号"
        width="80"
      >
      </el-table-column>
      <el-table-column
        prop="sceneName"
        :show-overflow-tooltip="true"
        label="名称"
        width="300"
      >
      </el-table-column>
      <el-table-column
        prop="subTaskTime"
        label="巡查日期"
        width="110"
        sortable="custom"
        :formatter="timeFormat"
      />
      <el-table-column
        prop="evaluation.resultscorebef"
        label="得分"
        width="90"
        sortable="custom"
      />
      <el-table-column
        prop="evaluation.resultscorebef"
        label="环信码"
        width="100"
      >
        <template #default="{ row }">
          <span :style="`color: ${toCode(row).color};`">{{
            toCode(row).name
          }}</span>
        </template>
      </el-table-column>
      <el-table-column prop="dname" label="区县" width="90" />
      <el-table-column
        prop="tname"
        label="街道"
        width="110"
        :filters="townFilters"
        :filter-method="filterHandler"
      />
      <el-table-column prop="evaluation.scenseaddress" label="地址" />
      <!-- <el-table-column prop="biArea" label="集中区" width="110" />
      <el-table-column prop="biManagementCompany" label="物业" min-width="110"/> -->
      <el-table-column
        v-if="scoreShow"
        fixed="right"
        align="right"
        label="监测数据超标"
        width="160"
      >
        <template #default="{ row }">
          <el-input :size="size" v-model="row.score1" />
        </template>
      </el-table-column>
      <el-table-column
        v-if="scoreShow"
        fixed="right"
        align="right"
        label="监测数据超区月均值"
        width="160"
      >
        <template #default="{ row }">
          <el-input :size="size" v-model="row.score2" />
        </template>
      </el-table-column>
      <el-table-column fixed="right" align="right" label="操作" width="160">
        <template #header>
          <el-button
            v-show="!scoreShow"
            icon="ArrowLeft"
            size="small"
            type="success"
            @click="scoreShow = true"
            >监测数据得分</el-button
          >
          <el-button
            v-show="scoreShow"
            size="small"
            type="primary"
            @click="updateMultipleScore"
            :loading="updateLoading"
            >上传</el-button
          >
          <el-button
            v-show="scoreShow"
            size="small"
            type="error"
            @click="scoreShow = false"
            >取消</el-button
          >
        </template>
        <template #default="{ row }">
          <el-button type="primary" size="small" @click="editRow(row)"
            >查看</el-button
          >
        </template>
      </el-table-column>
    </template>
  </FYTable>
</template>
<script>
import dayjs from 'dayjs';
import evaluateApi from '@/api/fysp/evaluateApi';
import { envCreditCode } from '@/constants/index';
import CompReport from './components/CompReport.vue';
import { useTablePaste } from '@/composables/tablePaste';
import { useCloned } from '@vueuse/core';
import { useMessageBoxTip } from '@/composables/messageBox';
export default {
  setup() {
    const {
      cellClick,
      cellClassName,
      handlePaste,
      setTableData,
      addRefreshEvent,
      tableData
    } = useTablePaste({
      score1: 8,
      score2: 9
    });
    return {
      cellClick,
      cellClassName,
      handlePaste,
      setTableData,
      addRefreshEvent,
      tableData
    };
  },
  components: { CompReport },
  data() {
    return {
      formSearch: {
        locations: {},
        scenetype: {},
        time: dayjs().add(-1, 'M').date(1).toDate()
      },
      townFilters: [],
      // åŽŸå§‹æ•°æ®ï¼Œç”¨äºŽæŽ’åºå–æ¶ˆåŽ
      orginData: [],
      evaluationRule: undefined,
      evaluationSubRule: undefined,
      //监测数据规则名称
      ruleName: {
        score1: {
          id: undefined,
          name: '监测数据出现单日及以上有效超标'
        },
        score2: {
          id: undefined,
          name: '监测数据月均值超区域月均值20%以上或数据明显异常'
        }
      },
      scoreShow: false,
      updateLoading: false
    };
  },
  computed: {
    area() {
      const { locations, scenetype, time } = this.formSearch;
      return {
        provincecode: locations.pCode,
        provincename: locations.pName,
        citycode: locations.cCode,
        cityname: locations.cName,
        districtcode: locations.dCode,
        districtname: locations.dName,
        starttime: dayjs(time).format('YYYY-MM-DD HH:mm:ss'),
        scensetypeid: scenetype.value
      };
    }
  },
  methods: {
    // _getParam() {
    //   const { locations, scenetype, time } = this.formSearch;
    //   return {
    //     provincecode: locations.pCode,
    //     provincename: locations.pName,
    //     citycode: locations.cCode,
    //     cityname: locations.cName,
    //     districtcode: locations.dCode,
    //     districtname: locations.dName,
    //     starttime: dayjs(time).format('YYYY-MM-DD HH:mm:ss'),
    //     scensetypeid: scenetype.value
    //   };
    // },
    editRow(row) {
      this.$router.push(`evalutationEdit/${row.subTaskId}`);
    },
    setOptions(param) {
      this.formSearch.locations = param.locations;
      this.formSearch.scenetype = param.scenetype;
      this.formSearch.sourceType = param.sourceType;
      this.$refs.tableRef.onSearch();
    },
    onSearch(page, func) {
      this.$refs.tableRef.clearSort();
      this.fetchEvaluationRule(this.area).then(() => {
        evaluateApi.fetchAutoEvaluation(this.area).then((res) => {
          if (res.data) {
            this.tableData = res.data;
            this.tableData.forEach((v) => {
              // å°†åŽŸå§‹çš„å¾—åˆ†å±•ç¤ºåˆ°è¡¨æ ¼ä¸­
              if (v.itemEvaluations) {
                for (const key in this.ruleName) {
                  const value = this.ruleName[key];
                  const itemEva = v.itemEvaluations.find((ie) => {
                    return ie.esrguid == value.id && ie.extension1 == 'true';
                  });
                  if (itemEva) {
                    v[key] = itemEva.value;
                  }
                }
              }
            });
            this.orginData = useCloned(this.tableData).cloned;
            this.getFilters(res.data);
            if (typeof func === 'function') {
              func({ data: this.tableData });
            }
          } else {
            this.tableData = [];
            this.orginData = [];
            if (typeof func === 'function') {
              func({ data: this.tableData });
            }
          }
        });
      });
    },
    fetchEvaluationRule() {
      const param = {
        // è‡ªåŠ¨è¯„ä¼°ç±»åž‹
        taskTypeId: 99,
        ...this.area
      };
      // èŽ·å–è¯„ä¼°æ€»è§„åˆ™
      return evaluateApi.fetchEvaluationRule(param).then((res) => {
        if (res.data.length > 0) {
          this.evaluationRule = res.data[0];
          // èŽ·å–å…·ä½“å­è§„åˆ™
          return evaluateApi
            .getSubRules(this.evaluationRule.guid)
            .then((res) => {
              this.evaluationSubRule = res.data;
              // æŸ¥æ‰¾å¯å¯¼å…¥å¾—分的规则id
              for (const key in this.ruleName) {
                const value = this.ruleName[key];
                const subrule = this.evaluationSubRule.find((v) => {
                  return v.itemname == value.name;
                });
                if (subrule) {
                  value.id = subrule.guid;
                }
              }
            });
        }
      });
    },
    // æ‰¹é‡æ›´æ–°ç›‘测数据得分
    updateMultipleScore() {
      this.updateLoading = true;
      useMessageBoxTip({
        confirmMsg: '是否上传监测数据得分',
        confirmTitle: '上传监测数据得分',
        onConfirm: async () => {
          if (this.evaluationRule) {
            const subTaskEvaList = this.tableData.map((v) => {
              const subRule = [];
              for (const key in this.ruleName) {
                const value = this.ruleName[key];
                subRule.push({
                  id: value.id,
                  selected: v[key] && (v[key] + '').trim() != ''
                });
              }
              return {
                subTaskId: v.subTaskId,
                ruleId: this.evaluationRule.guid,
                subRule
              };
            });
            const param = {
              ...this.area,
              subTaskEvaList
            };
            return await evaluateApi
              .updateMultipleScore(param)
              .finally(() => (this.updateLoading = false));
          }
        }
      });
    },
    // è§„范性评估与分析报告后台生成任务
    download() {
      evaluateApi.downloadAutoEvaluation(this.area).then((res) => {
        if (res == false) {
          // æœªä¸‹è½½æ–‡æ¡£ï¼Œè€Œæ˜¯å¼€å¯äº†æ–‡æ¡£ç”ŸæˆåŽå°ä»»åŠ¡
          this.$parent;
        }
      });
    },
    // å¯¼å‡ºè¡¨æ ¼ä¸ºexcel格式
    exportExcel() {},
    getFilters(data) {
      const townList = [];
      data.forEach((e) => {
        if (townList.indexOf(e.tname) == -1) {
          townList.push(e.tname);
        }
      });
      this.townFilters = townList.map((v) => {
        return { text: v, value: v };
      });
    },
    toCode(row, column) {
      if (row.evaluation) {
        return envCreditCode(row.evaluation.resultscorebef);
      } else {
        return '';
      }
    },
    timeFormat(row, column) {
      const time = row.subTaskTime;
      if (time) {
        return dayjs(time).format('MM-DD');
      } else {
        return '';
      }
    },
    filterHandler(value, row, column) {
      const property = column['property'];
      return row[property] === value;
    },
    sortScore(a, b) {
      const s1 = a.evaluation ? parseInt(a.evaluation.resultscorebef) : 0;
      const s2 = b.evaluation ? parseInt(b.evaluation.resultscorebef) : 0;
      return s1 - s2;
    },
    handleSortChange({ column, prop, order }) {
      console.log(column, prop, order);
      if (order == null) {
        this.orginData.forEach((e, i) => {
          this.tableData[i] = e;
        });
      } else if (prop == 'evaluation.resultscorebef') {
        this.tableData.sort((a, b) => {
          const s1 = a.evaluation ? parseInt(a.evaluation.resultscorebef) : 0;
          const s2 = b.evaluation ? parseInt(b.evaluation.resultscorebef) : 0;
          if (order == 'ascending') {
            return s1 - s2;
          } else if (order == 'descending') {
            return s2 - s1;
          }
        });
      } else if (prop == 'sceneIndex') {
        this.tableData.sort((a, b) => {
          if (order == 'ascending') {
            if (a.sceneIndex === b.sceneIndex) {
              return a.subTaskTime > b.subTaskTime ? 1 : -1;
            } else {
              return a.sceneIndex - b.sceneIndex;
            }
          } else if (order == 'descending') {
            if (a.sceneIndex === b.sceneIndex) {
              return b.subTaskTime > a.subTaskTime ? 1 : -1;
            } else {
              return b.sceneIndex - a.sceneIndex;
            }
          }
        });
      } else if (prop == 'subTaskTime') {
        this.tableData.sort((a, b) => {
          if (order == 'ascending') {
            return a[prop] > b[prop] ? 1 : -1;
            // return dayjs(a).isAfter(dayjs(b)) ? 1 : -1;
          } else if (order == 'descending') {
            return b[prop] > a[prop] ? 1 : -1;
            // return dayjs(b).isAfter(dayjs(a)) ? 1 : -1;
          }
        });
      }
    }
  },
  mounted() {
    this.addRefreshEvent(this.$refs.tableRef.doLayout);
  }
};
</script>
<style scoped></style>
src/views/analysis/evalution/EvalutationTask.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
<template>
  <el-row :gutter="16">
    <el-col :span="16">
      <CompPreCheck @start-task="refreshTask"></CompPreCheck>
    </el-col>
    <el-col :span="8">
      <CompEvaTask ref="refEvaTask"></CompEvaTask>
    </el-col>
  </el-row>
</template>
<script>
import CompEvaTask from './components/CompEvaTask.vue';
import CompPreCheck from './components/precheck/CompPreCheck.vue';
export default {
  name: 'EvalutationTask',
  components: { CompPreCheck, CompEvaTask },
  data() {
    return {};
  },
  methods:{
    refreshTask(){
      this.$refs.refEvaTask.fetchTask()
    }
  }
};
</script>
<style scoped>
.radius {
  height: 80vh;
  /* border: 1px solid var(--el-border-color); */
  /* border-radius: var(--el-border-radius-base); */
}
</style>
src/views/analysis/evalution/components/CompDataResultEdit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,556 @@
<template>
  <el-row align="top" justify="space-between">
    <el-row align="top">
      <el-upload
        ref="upload"
        class="upload-file"
        :limit="1"
        accept=".xls,.xlsx"
        :on-change="handleChange"
        :on-exceed="handleExceed"
        :auto-upload="false"
      >
        <template #trigger>
          <el-button type="success" :loading="tableLoading">导入文件</el-button>
        </template>
        <template #tip>
          <div>
            <el-text type="danger">{{ tips }}</el-text>
          </div>
        </template>
      </el-upload>
      <div v-if="tableLoading">
        <el-icon class="is-loading"><Loading /></el-icon>
        <el-text>{{ loadTxt }}</el-text>
      </div>
    </el-row>
    <el-button type="default" icon="download" @click="downloadTemplate"
      >下载导入模板</el-button
    >
  </el-row>
  <el-table
    ref="tableRef"
    :data="data"
    v-loading="tableLoading"
    table-layout="fixed"
    row-key="id"
    :expand-row-keys="expandRowKeys"
    :row-class-name="tableRowClassName"
    size="small"
    height="60vh"
    border
  >
    <!-- <el-table-column type="expand">
      <template #default="{ row }">
        {{ row.drSceneName }}
      </template>
    </el-table-column> -->
    <el-table-column
      v-if="isUploadNewFile"
      prop="isFound"
      label="合规"
      width="30"
    >
      <template #default="{ row }">
        <el-icon class="is-loading" v-if="row.loading">
          <Loading color="#409eff" />
        </el-icon>
        <el-icon v-else>
          <Check v-if="row.isFound" />
          <Close v-else />
        </el-icon>
      </template>
    </el-table-column>
    <el-table-column
      v-if="isUploadNewFile"
      :show-overflow-tooltip="true"
      prop="sceneIndex"
      label="唯一编号"
      width="70"
    >
      <template #default="{ row }">
        <el-input
          v-if="isUploadNewFile && !row.isFound"
          size="small"
          v-model="row.sceneIndex"
          @change="(e) => handleSceneNameChange(e, row)"
        />
        <span v-else>{{ row.sceneIndex }}</span>
      </template>
    </el-table-column>
    <el-table-column
      :show-overflow-tooltip="true"
      prop="drSceneName"
      label="场景名称"
    >
      <template #default="{ row }">
        <el-input
          v-if="isUploadNewFile && !row.isFound"
          size="small"
          v-model="row.drSceneName"
        />
        <span v-else>{{ row.drSceneName }}</span>
      </template>
    </el-table-column>
    <el-table-column
      v-if="isUploadNewFile"
      :show-overflow-tooltip="true"
      prop="drDeviceCode"
      label="设备名称"
    >
      <template #default="{ row }">
        <el-input
          v-if="isUploadNewFile && !row.isFound"
          size="small"
          v-model="row.deviceName"
        />
        <span v-else>{{ row.deviceName }}</span>
      </template>
    </el-table-column>
    <el-table-column prop="drDeviceCode" label="设备号" width="130">
      <template #default="{ row }">
        <el-input
          v-if="isUploadNewFile && !row.isFound"
          size="small"
          v-model="row.drDeviceCode"
        />
        <span v-else>{{ row.drDeviceCode }}</span>
      </template>
    </el-table-column>
    <el-table-column prop="drTime" label="时间" width="70">
      <template #default="{ row }">
        <span>{{ $fm.formatYM(row.drTime) }}</span>
      </template>
    </el-table-column>
    <el-table-column prop="drExceedTimes" label="超标次数" width="50">
      <template #default="{ row }">
        <el-input
          v-if="isUploadNewFile && !row.isFound"
          size="small"
          v-model="row.drExceedTimes"
        />
        <span v-else>{{ row.drExceedTimes }}</span>
      </template>
    </el-table-column>
    <el-table-column prop="drAvg" label="平均值" width="65">
      <template #default="{ row }">
        <el-input
          v-if="isUploadNewFile && !row.isFound"
          size="small"
          v-model="row.drAvg"
        />
        <span v-else>{{ row.drAvg }}</span>
      </template>
    </el-table-column>
    <el-table-column prop="drMax" label="最大值" width="65">
      <template #default="{ row }">
        <el-input
          v-if="isUploadNewFile && !row.isFound"
          size="small"
          v-model="row.drMax"
        />
        <span v-else>{{ row.drMax }}</span>
      </template>
    </el-table-column>
    <el-table-column prop="drMin" label="最小值" width="65">
      <template #default="{ row }">
        <el-input
          v-if="isUploadNewFile && !row.isFound"
          size="small"
          v-model="row.drMin"
        />
        <span v-else>{{ row.drMin }}</span>
      </template>
    </el-table-column>
    <el-table-column prop="drOverAvgPer" label="超区均值百分比" width="70">
      <template #default="{ row }">
        <el-input
          v-if="isUploadNewFile && !row.isFound"
          size="small"
          v-model="row.drOverAvgPer"
        />
        <span v-else>{{ row.drOverAvgPer }}</span>
      </template>
    </el-table-column>
    <el-table-column prop="drDataNum" label="数据量" width="65">
      <template #default="{ row }">
        <el-input
          v-if="isUploadNewFile && !row.isFound"
          size="small"
          v-model="row.drDataNum"
        />
        <span v-else>{{ row.drDataNum }}</span>
      </template>
    </el-table-column>
    <el-table-column prop="drEffectiveRate" label="有效率" width="65">
      <template #default="{ row }">
        <el-input
          v-if="isUploadNewFile && !row.isFound"
          size="small"
          v-model="row.drEffectiveRate"
          placeholder="场景名称"
        />
        <span v-else>{{ row.drEffectiveRate }}</span>
      </template>
    </el-table-column>
    <el-table-column v-if="isUploadNewFile" type="expand">
      <template #default="{ row }">
        <div class="p-v-4">
          <div v-if="!row.isFound" class="p-h-16">
            <div v-if="row.notSure">
              <el-text type="warning" size="small"
                >根据唯一编号及行政区划找到了相关场景,但与已有场景名称不匹配,请确定是哪个场景</el-text
              >
              <div class="m-t-8">
                <el-button
                  v-for="(v, i) in row.sourceScene"
                  :key="v.guid"
                  type="primary"
                  text
                  bg
                  size="small"
                  class="m-b-2"
                  @click="handleRadioChange(v, row)"
                >
                  {{ v.name }}
                </el-button>
                <!-- <el-radio-group v-model="row.radioValue">
                <el-radio
                  v-for="(v, i) in row.sourceScene"
                  :key="v.guid"
                  :value="i"
                  size="small"
                  border
                  @change="handleRadioChange(v, row)"
                  >{{ v.name }}</el-radio
                >
              </el-radio-group> -->
              </div>
            </div>
            <div v-else>
              <el-text type="danger" size="small"
                >根据唯一编号及行政区划未找到相关场景,请修改唯一编号</el-text
              >
            </div>
          </div>
          <div v-else class="p-h-16">
            <el-text type="success" size="small"> å·²æ­£ç¡®åŒ¹é…åˆ°è¯¥åœºæ™¯ </el-text>
            <el-text v-if="row.remark" type="success" size="small">
              {{ ',' + row.remark }}
            </el-text>
          </div>
        </div>
      </template>
    </el-table-column>
  </el-table>
  <el-button
    class="m-t-8"
    type="primary"
    :loading="uploadLoading"
    :disabled="!isUploadNewFile"
    icon="upload"
    @click="uploadFile"
    >上传统计结果</el-button
  >
</template>
<script setup>
import { ref, reactive, watch, onMounted, getCurrentInstance } from 'vue';
import { useMessageBoxTip, useMessageBox } from '@/composables/messageBox';
import { genFileId } from 'element-plus';
import monitordataApi from '@/api/fysp/monitordataApi';
import sceneApi from '@/api/fysp/sceneApi';
import * as XLSX from 'xlsx';
import { exportDocx } from '@/utils/doc';
const cns = getCurrentInstance();
const $fm = cns.appContext.config.globalProperties.$fm;
const props = defineProps({
  areaInfo: { type: Object }
});
let workbook;
const isUploadNewFile = ref(false);
const data = ref([]);
const expandRowKeys = ref([]);
const upload = ref();
const tableLoading = ref(false);
const loadTxt = ref('');
const tips = ref('');
const uploadLoading = ref(false);
const tableRowClassName = ({ row, rowIndex }) => {
  if (row.loading) {
    return 'loading-row';
  } else if (row.isFound == undefined) {
    return '';
  } else {
    return row.isFound
      ? 'success-row'
      : row.notSure
        ? 'warning-row'
        : 'danger-row';
  }
};
// èŽ·å–åŽ†å²ç»Ÿè®¡ç»“æžœ
function fetchDustDataResult() {
  monitordataApi.fetchDustDataResult(props.areaInfo).then((res) => {
    data.value = res.data;
  });
}
function handleExceed(files, uploadFiles) {
  upload.value.clearFiles();
  const file = files[0];
  file.uid = genFileId();
  upload.value.handleStart(file);
}
/**
 * å¤„理上传文件解析
 * @param uploadFile
 * @param uploadFiles
 */
function handleChange(uploadFile, uploadFiles) {
  expandRowKeys.value = [];
  tableLoading.value = true;
  loadTxt.value = '文件解析中...';
  // console.log(uploadFile, uploadFiles);
  const fileReader = new FileReader();
  fileReader.onload = (file) => {
    const fileData = file.target.result;
    workbook = XLSX.read(fileData, { type: 'array' });
    console.log(workbook.SheetNames);
    if (workbook.SheetNames.length == 0) {
      tips.value = 'excel文件错误,没有sheet表单';
      return;
    }
    const worksheet = workbook.Sheets[workbook.SheetNames[0]];
    const tableData = XLSX.utils.sheet_to_json(worksheet);
    const _data = tableData.map((v, i) => {
      return {
        id: i,
        sceneIndex: v['唯一编号'],
        drSceneName: v['场景名称'],
        deviceName: v['设备名称'],
        drDeviceCode: v['设备号'],
        drTime: $fm.formatDateFromExcel(v['时间'], '-'),
        drExceedTimes: v['超标次数'],
        drAvg: v['平均值'],
        drMax: v['最大值'],
        drMin: v['最小值'],
        drOverAvgPer: v['超区均值百分比'],
        drDataNum: v['数据量'],
        drEffectiveRate: v['有效率']
      };
    });
    data.value = combineSameScene(_data);
    // console.log(tableData);
    setTimeout(() => {
      tableLoading.value = false;
      isUploadNewFile.value = true;
      data.value.forEach((d) => {
        searchScene(d);
      });
    }, 1000);
  };
  fileReader.readAsArrayBuffer(uploadFile.raw);
}
/**
 * åˆå¹¶ç›¸åŒåœºæ™¯çš„多台监测设备,默认取区均值最高的一台设备
 */
function combineSameScene(dataList) {
  // æ ¹æ®åœºæ™¯å”¯ä¸€ç¼–号进行相同设备归类
  const tempMap = new Map();
  dataList.forEach((d) => {
    if (!tempMap.has(d.sceneIndex)) {
      tempMap.set(d.sceneIndex, []);
    }
    tempMap.get(d.sceneIndex).push(d);
  });
  const res = [];
  // ç›¸åŒåœºæ™¯ä¸‹ï¼Œå–区均值最高的一台设备作为结果
  for (const [k, v] of tempMap) {
    v.sort((a, b) => b.drAvg - a.drAvg);
    if (v.length > 1) {
      v[0].remark = `本场景共有${v.length}台设备,已自动选择区均值最高的一台为统计结果`;
    }
    res.push(reactive(v[0]));
  }
  return res;
}
// æŸ¥è¯¢ä»Žæ–‡ä»¶ä¸Šä¼ çš„æ¯ä¸ªåœºæ™¯æ˜¯å¦èƒ½åœ¨ç³»ç»Ÿä¸­æ‰¾åˆ°å¯¹åº”的场景信息
function searchScene(d) {
  d.loading = true;
  // æ ¹æ®åœºæ™¯çš„唯一编号、行政区划和场景类型进行查找
  if (!d.sceneIndex) {
    d.isFound = false;
    d.loading = false;
    return Promise;
  } else {
    sceneApi
      .findScene({
        // name: d.drSceneName,
        typeid: props.areaInfo.scensetypeid,
        provincecode: props.areaInfo.provincecode,
        citycode: props.areaInfo.citycode,
        districtcode: props.areaInfo.districtcode,
        towncode: props.areaInfo.towncode,
        index: d.sceneIndex,
        // ç­›é€‰æŸ¥è¯¢åœ¨çº¿çš„场景
        extension1: '1'
      })
      .then((res) => {
        setTimeout(() => {
          if (res.length > 0) {
            // 1. æ ¹æ®åœºæ™¯å”¯ä¸€ç¼–号进行查询时,一般情况下应该只有一个唯一结果
            // 2-1. å½“上传文件中场景名称为空白时,自动匹配查询所得场景
            // 2-2. å½“上传文件中场景名称不为空白时,比对两者的场景名称,若不同则进行警告提示
            if (res.length == 1) {
              const findRes = res[0];
              if (!d.drSceneName || d.drSceneName == '') {
                d.drSceneId = findRes.guid;
                d.drSceneName = findRes.name;
                d.isFound = true;
                d.notSure = false;
                if (d.remark) expandRowKeys.value.push(d.id);
              } else {
                if (d.drSceneName == findRes.name) {
                  d.drSceneId = findRes.guid;
                  d.isFound = true;
                  d.notSure = false;
                } else {
                  d.isFound = false;
                  d.notSure = true;
                  expandRowKeys.value.push(d.id);
                }
              }
            } else {
              d.isFound = false;
              d.notSure = true;
              expandRowKeys.value.push(d.id);
            }
            d.sourceScene = res;
            // const findRes = res.find((v) => v.name == d.drSceneName);
            // if (findRes) {
            //   d.drSceneId = findRes.guid;
            //   d.isFound = true;
            // } else {
            //   d.isFound = false;
            //   d.notSure = true;
            //   expandRowKeys.value.push(d.id);
            // }
            // d.sourceScene = res;
          } else {
            d.isFound = false;
            d.notSure = false;
            expandRowKeys.value.push(d.id);
          }
          d.loading = false;
        }, 1000);
      })
      .finally(() => {
        setTimeout(() => {
          d.loading = false;
        }, 1000);
      });
  }
}
function handleSceneNameChange(newName, row) {
  searchScene(row);
}
function handleRadioChange(value, row) {
  const scene = value;
  row.sceneIndex = scene.index;
  row.drSceneId = scene.guid;
  row.drSceneName = scene.name;
  searchScene(row);
}
// ä¸Šä¼ ç»Ÿè®¡ç»“果文档
function uploadFile() {
  useMessageBoxTip({
    confirmMsg: `是否确认上传?`,
    confirmTitle: '上传',
    onConfirm: () => {
      uploadLoading.value = true;
      return monitordataApi
        .uploadDustDataResult(data.value)
        .finally(() => (uploadLoading.value = false));
    }
  });
}
/**
 * ä¸‹è½½æ¨¡æ¿æ–‡ä»¶
 */
function downloadTemplate() {
  const fName = '扬尘监测数据月度统计模板.xlsx';
  const path = `/${fName}`;
  const link = document.createElement('a');
  link.href = path;
  link.download = fName;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}
onMounted(() => {
  fetchDustDataResult();
});
</script>
<style scoped>
.upload-file {
  /* background-color: aliceblue; */
  width: 300px;
  min-height: 60px;
}
:deep(.el-text) {
  align-self: auto;
}
:deep(.el-table__expanded-cell) {
  padding: 0;
  /* background-color: var(--el-bg-color-page); */
}
/* :deep(.el-table__body tr>td.hover-cell) {
  background-color: red !important;
} */
/* .el-table--enable-row-hover
  .el-table__body
  tr:hover
  > td
  :deep(.el-table__cell) {
  background-color: red !important;
} */
</style>
<style>
.el-table .warning-row {
  --el-table-tr-bg-color: var(--el-color-warning-light-5);
}
.el-table .success-row {
  --el-table-tr-bg-color: var(--el-color-success-light-7);
}
.el-table .danger-row {
  --el-table-tr-bg-color: var(--el-color-danger-light-5);
}
.el-table .loading-row {
  color: var(--el-text-color-disabled);
  /* --el-table-tr-bg-color: var(--el-text-color-placeholder); */
  --el-table-tr-bg-color: var(--el-bg-color);
}
/* .el-table__body tr>td.hover-cell {
  background-color: red !important;
} */
/* .el-table--enable-row-hover .el-table__body tr:hover>td.el-table__cell {
  background-color: unset;
} */
</style>
src/views/analysis/evalution/components/CompEvaTask.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,144 @@
<template>
  <el-card shadow="never" :body-style="{ padding: 0 }">
    <template #header>
      <el-row justify="space-between">
        <div>
          <div><el-text tag="b" size="large">自动评估任务</el-text></div>
          <el-text size="small" type="info">显示当前正在进行的自动评估任务状态</el-text>
        </div>
        <el-button
          icon="Refresh"
          type="primary"
          size="default"
          :loading="loading"
          @click="fetchTask"
          >刷新任务</el-button
        >
      </el-row>
      <!-- <el-row>
        <el-button type="default" size="default" @click="newTestTask">新增测试任务</el-button>
        <el-button type="default" size="default" @click="startNewTestTask"
          >新建并运行一个测试任务</el-button
        >
        <el-button type="default" size="default" @click="shutDownTask"
          >强制关闭所有测试任务</el-button
        >
      </el-row> -->
    </template>
    <el-scrollbar height="70vh" class="scrollbar">
      <template v-for="(v, i) in taskList" :key="i">
        <FYBgTaskItem
          :model="v"
          :index="i"
          @start="startTask"
          @shutDown="shutDownTask"
          @remove="removeTask"
          @gotoResult="gotoResult"
        ></FYBgTaskItem>
      </template>
    </el-scrollbar>
  </el-card>
</template>
<script>
/**
 * è‡ªåŠ¨è¯„ä¼°ä»»åŠ¡ç®¡ç†
 */
import { useFetchData } from '@/composables/fetchData';
import bgtaskApi from '@/api/fysp/bgtaskApi';
import { enumBgTask, BG_TASK_TYPE } from '@/enum/bgTask';
export default {
  setup() {
    const { loading, fetchData } = useFetchData();
    return { loading, fetchData };
  },
  data() {
    return {
      taskList: [],
      taskIndex: 0
    };
  },
  methods: {
    addTask(){
    },
    newTestTask() {
      this.fetchData((page, pageSize) => {
        return bgtaskApi.newTestTask(`Test-Task-${++this.taskIndex}`).then((res) => {
          this.taskList.push(res.data);
        });
      });
    },
    startNewTestTask() {
      this.fetchData((page, pageSize) => {
        return bgtaskApi.startNewTestTask(`Test-Task-${++this.taskIndex}`).then((res) => {
          this.taskList.push(res.data);
        });
      });
    },
    _getParam(taskStatus) {
      return {
        type: taskStatus.type,
        id: taskStatus.id
      };
    },
    fetchTask() {
      this.fetchData((page, pageSize) => {
        return bgtaskApi
          .fetchTaskStatus({
            type: BG_TASK_TYPE.AUTO_SCORE.name
          })
          .then((res) => {
            this.taskList = res.data;
          });
      });
    },
    startTask(index, callback) {
      this.fetchData((page, pageSize) => {
        const param = this._getParam(this.taskList[index]);
        return bgtaskApi.startTask(param).then((res) => {
          this.taskList[index] = res.data;
          callback(true);
        });
      });
    },
    shutDownTask(index, callback) {
      this.fetchData((page, pageSize) => {
        const param = this._getParam(this.taskList[index]);
        return bgtaskApi.shutDownTask(param).then((res) => {
          if (index && res.data && res.data.length == 1) {
            this.taskList[index] = res.data[0];
          } else {
            res.data.forEach((e) => {
              let v = this.taskList.find((value) => {
                return value.id == e.id;
              });
              const i = this.taskList.indexOf(v);
              this.taskList[i] = e;
            });
          }
          callback(true);
        });
      });
    },
    removeTask(index, callback) {
      this.fetchData((page, pageSize) => {
        const param = this._getParam(this.taskList[index]);
        return bgtaskApi.removeTask(param).then((res) => {
          if (res.data) {
            this.taskList.splice(index, 1);
            callback(true);
          }
        });
      });
    },
    gotoResult(index) {}
  }
};
</script>
<style scoped>
.scrollbar {
  padding: 8px;
}
</style>
src/views/analysis/evalution/components/CompReport.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,101 @@
<template>
  <el-button icon="Download" type="success" @click="dialogVisible = true"
    >规范性评估与分析报告</el-button
  >
  <el-dialog
    v-model="dialogVisible"
    title="规范性评估与分析报告生成"
    width="500"
  >
    <el-text tag="b" size="large">数据范围确认</el-text>
    <el-text tag="div">区域:{{ locationText }}</el-text>
    <el-text tag="div">类型:{{ scenetype.label }}</el-text>
    <el-text tag="div">时间:{{ timeText }}</el-text>
    <template #footer>
      <div class="dialog-footer">
        <el-row align="middle">
          <el-checkbox v-model="forceUpdate" label="强制生成新报告" />
          <el-tooltip placement="bottom-start" effect="light">
            <template #content>
              <el-text tag="b" size="small">不勾选:</el-text><br />
              <el-text size="small"
                >不勾选时,如果已生成过相同区域的报告,则直接获取该份报告记录</el-text
              ><br />
              <el-text tag="b" size="small">勾选:</el-text><br />
              <el-text size="small"
                >勾选时,无论是否有历史记录,都会启动报告生成任务覆盖旧记录,可在后台任务界面查看任务进度</el-text
              ><br />
            </template>
            <el-icon class="m-l-8 cursor-p" :size="16" color="var(--el-color-warning)"
              ><QuestionFilled
            /></el-icon>
          </el-tooltip>
        </el-row>
        <div>
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="download">确定</el-button>
        </div>
      </div>
    </template>
  </el-dialog>
</template>
<script>
import dayjs from 'dayjs';
import evaluateApi from '@/api/fysp/evaluateApi';
export default {
  props: ['locations', 'time', 'scenetype'],
  data() {
    return {
      dialogVisible: false,
      forceUpdate: false
    };
  },
  computed: {
    locationText() {
      const loc = this.locations;
      let text = '';
      text = loc.pName == loc.cName ? loc.pName : loc.pName + loc.cName;
      text += loc.dName;
      return text;
    },
    timeText() {
      return dayjs(this.time).format('YYYYå¹´MM月');
    }
  },
  methods: {
    // è§„范性评估与分析报告后台生成任务
    download() {
      const locations = this.locations;
      const time = this.time;
      const scenetype = this.scenetype;
      const area = {
        provincecode: locations.pCode,
        provincename: locations.pName,
        citycode: locations.cCode,
        cityname: locations.cName,
        districtcode: locations.dCode,
        districtname: locations.dName,
        starttime: dayjs(this.time).format('YYYY-MM-DD HH:mm:ss'),
        scensetypeid: scenetype.value
      };
      evaluateApi.downloadAutoEvaluation(area, this.forceUpdate).then((res) => {
        if (res == false) {
          // æœªä¸‹è½½æ–‡æ¡£ï¼Œè€Œæ˜¯å¼€å¯äº†æ–‡æ¡£ç”ŸæˆåŽå°ä»»åŠ¡
          this.$parent;
        }
        this.dialogVisible = false;
      });
    }
  }
};
</script>
<style scoped>
.dialog-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 2px;
}
</style>
src/views/analysis/evalution/components/precheck/CompPreCheck.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,97 @@
<template>
  <el-steps :active="stepIndex" finish-status="success" style="" align-center>
    <el-step title="评估范围" />
    <el-step title="数据源检查" />
    <el-step title="条目豁免" />
    <el-step title="自动评估" />
  </el-steps>
  <CompCheckArea v-show="stepIndex == 0" v-model="stepIndex" @change="onAreaChange"></CompCheckArea>
  <CompCheckSource
    v-show="stepIndex == 1"
    v-model="stepIndex"
    ref="refSource"
    @change="onDataSourceChange"
  ></CompCheckSource>
  <CompCheckExemption
    v-show="stepIndex == 2"
    v-model="stepIndex"
    @change="onExemptionChange"
  ></CompCheckExemption>
  <CompCheckConfirm
    v-show="stepIndex == 3"
    v-model="stepIndex"
    :area-info="area"
    :data-source="dataSource"
    :exemption-items="exemptionItems"
    @start="onNewTask"
  ></CompCheckConfirm>
</template>
<script>
import dayjs from 'dayjs';
import CompCheckArea from './components/CompCheckArea.vue';
import CompCheckSource from './components/CompCheckSource.vue';
import CompCheckExemption from './components/CompCheckExemption.vue';
import CompCheckConfirm from './components/CompCheckConfirm.vue';
/**
 * è‡ªåŠ¨è¯„ä¼°æ¡ä»¶åˆè§„æ€§æ£€æŸ¥
 */
export default {
  name: 'CompPreCheck',
  components: { CompCheckArea, CompCheckSource, CompCheckExemption, CompCheckConfirm },
  props: {},
  emits: ['startTask'],
  data() {
    return {
      // æ“ä½œæ­¥éª¤ä¸‹æ ‡
      stepIndex: 0,
      area: {
        _locations: {},
        _scenetype: {}
      },
      dataSource: {},
      // è±å…æ¡ç›®
      exemptionItems: {}
    };
  },
  methods: {
    /**
     * ç›‘听评估范围变更
     */
    onAreaChange(val) {
      const v = val.value;
      this.area = v;
      const a = {
        provincecode: v._locations.pCode,
        provincename: v._locations.pName,
        citycode: v._locations.cCode,
        cityname: v._locations.cName,
        districtcode: v._locations.dCode,
        districtname: v._locations.dName,
        towncode: v._locations.tCode,
        townname: v._locations.tName,
        starttime: this.$fm.formatYMDHMS(v.time),
        endtime: this.$fm.formatYMDHMS(v.time),
        scensetypeid: v._scenetype.value,
        online: true,
        sourceType: v.sourceType
      };
      this.$refs.refSource.startCheck(a);
    },
    onDataSourceChange(val) {
      this.dataSource = val;
    },
    onExemptionChange(val) {
      this.exemptionItems = val;
    },
    /**
     * è‡ªåŠ¨è¯„ä¼°å‰ç½®åˆè§„æ€§æ£€æŸ¥
     * æ£€æŸ¥æ‰€é€‰èŒƒå›´å†…各项评估数据源是否完整
     */
    onNewTask() {
      this.$emit('startTask');
    }
  }
};
</script>
src/views/analysis/evalution/components/precheck/components/CompCheckArea.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,138 @@
<template>
  <el-card shadow="never">
    <template #header>
      <div><el-text tag="b" size="large">选择评估范围</el-text></div>
      <el-text size="small" type="info">包括区县、场景类型以及月份</el-text>
    </template>
    <FormCol>
      <FYForm ref="formRef" :rules="evaConditionRules" :showButtons="false" @submit="nextStep">
        <template #form-item="{ formObj }">
          <CompQuickSet @quick-set="setOptions"></CompQuickSet>
          <el-form-item label="主数据源" prop="sourceType">
            <el-radio-group
              v-model="formObj.sourceType"
              size="small"
              @change="sceneOptionSourceInit = true"
            >
              <el-radio-button :value="1">守法服务记录</el-radio-button>
              <el-radio-button :value="2">现场巡查记录</el-radio-button>
            </el-radio-group>
            <el-tooltip placement="bottom-start" effect="light">
              <template #content>
                <el-text tag="i" size="default" type="warning"
                  >该选项是用于决定评估主体对象的获取方式</el-text
                ><br />
                <el-text tag="b" size="small">守法服务记录:</el-text><br />
                <el-text size="small"
                  >表示在评估时,评估对象是从守法服务小程序系统中获取的当前可用的用户;<br />
                  ä¸€èˆ¬æƒ…况下,当评估对象没有进行现场巡查,只有守法服务相关记录时,采用此选项;</el-text
                ><br />
                <el-text tag="b" size="small">现场巡查记录:</el-text><br />
                <el-text size="small"
                  >表示在评估时,评估对象是从现场巡查监管系统中获取的总任务下的所有监管场景;<br />
                  ä¸€èˆ¬æƒ…况下,当评估对象有进行现场巡查,采用此选项; </el-text
                ><br />
              </template>
              <el-icon class="m-l-8 cursor-p" :size="16" color="var(--el-color-warning)"
                ><QuestionFilled
              /></el-icon>
            </el-tooltip>
          </el-form-item>
          <!-- åŒºåŽ¿ -->
          <FYOptionLocation
            :allOption="false"
            :level="3"
            :initValue="false"
            :checkStrictly="false"
            v-model:value="formObj._locations"
          ></FYOptionLocation>
          <!-- åœºæ™¯ç±»åž‹ -->
          <FYOptionScene
            :allOption="false"
            :initValue="false"
            :sourceInit="sceneOptionSourceInit"
            :type="formObj.sourceType"
            v-model:value="formObj._scenetype"
          ></FYOptionScene>
          <!-- æ—¶é—´ -->
          <FYOptionTime
            prop="time"
            :initValue="true"
            type="month"
            v-model:value="formObj.time"
          ></FYOptionTime>
        </template>
      </FYForm>
    </FormCol>
    <template #footer>
      <el-row justify="space-around">
        <el-button type="primary" size="default" :loading="loading" @click="submit"
          >下一步</el-button
        >
      </el-row>
    </template>
  </el-card>
</template>
<script>
/**
 * è¯„估范围合规性检查
 */
export default {
  props: {
    // æ­¥éª¤ä¸‹æ ‡
    modelValue: Number
  },
  emits: ['update:modelValue', 'change'],
  data() {
    return {
      loading: false,
      evaConditionRules: {
        time: [
          {
            required: true,
            message: '时间不能为空',
            trigger: 'change'
          }
        ],
        sourceType: [
          {
            required: true,
            message: '主数据源必须选择',
            trigger: 'change'
          }
        ]
      },
      // å½“场景选项切换数据源时,是否清空当前选项值
      sceneOptionSourceInit: true
    };
  },
  methods: {
    setOptions(param) {
      this.sceneOptionSourceInit = false;
      this.$refs.formRef.formObj._locations = param.locations;
      this.$refs.formRef.formObj._scenetype = param.scenetype;
      this.$refs.formRef.formObj.sourceType = param.sourceType;
    },
    submit() {
      this.$refs.formRef.onSubmit(false);
    },
    // è·³è½¬ä¸‹ä¸€æ­¥
    nextStep(formObj, success, fail) {
      // todo: æ£€æŸ¥æ˜¯å¦å·²æœ‰è¯„估记录,提示用户可直接跳转查看或继续下一步
      this.loading = true;
      return new Promise((reslove, reject) => {
        setTimeout(() => {
          this.$emit('change', formObj);
          this.$emit('update:modelValue', this.modelValue + 1);
          this.loading = false;
          success();
          reslove();
        }, 1000);
      });
    }
  }
};
</script>
src/views/analysis/evalution/components/precheck/components/CompCheckConfirm.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,108 @@
<template>
  <el-card shadow="never">
    <template #header>
      <div><el-text tag="b" size="large">自动评估确认</el-text></div>
      <el-text size="small" type="info">最终确认自动评估各配置项,并开始自动评估</el-text>
    </template>
    <el-form label-width="160px" label-position="left">
      <div class="m-b-16"><el-text tag="b" size="large">评估范围</el-text></div>
      <el-form-item label="区域">
        <el-text size="default">{{ areaInfo._locations.pName }}</el-text>
        <el-text size="default">{{ areaInfo._locations.cName }}</el-text>
        <el-text size="default">{{ areaInfo._locations.dName }}</el-text>
        <el-text size="default">{{ areaInfo.starttime }}</el-text>
      </el-form-item>
      <el-form-item label="时间">
        <el-text size="default">{{ $fm.formatYM(areaInfo.time) }}</el-text>
      </el-form-item>
      <el-form-item label="场景">
        <el-text size="default">{{ areaInfo._scenetype.label }}</el-text>
      </el-form-item>
      <el-divider />
      <div class="m-b-16"><el-text tag="b" size="large">评估数据源完整度</el-text></div>
      <el-form-item :label="v.name" v-for="v in dataSource" :key="v.name">
        <template v-if="v.pass == true">
          <el-icon color="var(--el-color-success)"><Check /></el-icon>
          <el-text size="default" type="success">通过</el-text>
        </template>
        <template v-else-if="v.pass == false">
          <el-icon color="var(--el-color-danger)"><Close /></el-icon>
          <el-text size="default" type="danger">缺失</el-text>
        </template>
        <template v-else>
          <el-icon color="var(--el-color-warning)"><Warning /></el-icon>
          <el-text size="default" type="warning">暂略过</el-text>
        </template>
      </el-form-item>
      <el-divider />
      <div class="m-b-16"><el-text tag="b" size="large">豁免条目</el-text></div>
      <div class="m-b-16"><el-text size="default">无豁免条目</el-text></div>
    </el-form>
    <template #footer>
      <el-row justify="space-around">
        <el-button type="primary" size="default" @click="lastStep">上一步</el-button>
        <el-button type="primary" size="default" @click="startEvaluate">开始评估</el-button>
      </el-row>
    </template>
  </el-card>
</template>
<script>
import evaluateApi from '@/api/fysp/evaluateApi';
/**
 * è¯„估任务最终开启确认
 */
export default {
  props: {
    // æ­¥éª¤ä¸‹æ ‡
    modelValue: Number,
    // åŒºåŸŸä¿¡æ¯
    areaInfo: {
      type: Object,
      default: () => {
        return {
          _locations: {},
          _scenetype: {}
        };
      }
    },
    // æ•°æ®æºå®Œæ•´åº¦æƒ…况
    dataSource: Array,
    // æ¡ç›®è±å…æƒ…况
    exemptionItems: Array
  },
  emits: ['update:modelValue', 'start'],
  data() {
    return {};
  },
  methods: {
    // å¼€å¯è‡ªåŠ¨è¯„ä¼°ä»»åŠ¡
    startEvaluate() {
      const v = this.areaInfo
      const a = {
        provincecode: v._locations.pCode,
        provincename: v._locations.pName,
        citycode: v._locations.cCode,
        cityname: v._locations.cName,
        districtcode: v._locations.dCode,
        districtname: v._locations.dName,
        towncode: v._locations.tCode,
        townname: v._locations.tName,
        starttime: this.$fm.formatYMDHMS(v.time),
        scensetypeid: v._scenetype.value,
        online: true,
        sourceType: v.sourceType
      };
      evaluateApi.autoEvaluate(a).then((res) => {
        this.$emit('start', res.data);
      });
    },
    // è·³è½¬ä¸Šä¸€æ­¥
    lastStep() {
      this.$emit('update:modelValue', this.modelValue - 1);
    }
  }
};
</script>
src/views/analysis/evalution/components/precheck/components/CompCheckExemption.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
<template>
  <el-card shadow="never">
    <template #header>
      <div><el-text tag="b" size="large">条目豁免</el-text></div>
      <el-text size="small" type="info">自定义设置此次评估不参与计算的条目</el-text>
    </template>
    è±å…æ¡ç›®åŠŸèƒ½æš‚æœªå®Œæˆï¼Œè¯·ç›´æŽ¥ç‚¹å‡»ä¸‹ä¸€æ­¥
    <template #footer>
      <el-row justify="space-around">
        <el-button type="primary" size="default" @click="lastStep">上一步</el-button>
        <el-button type="primary" size="default" @click="nextStep">下一步</el-button>
      </el-row>
    </template>
  </el-card>
</template>
<script>
/**
 * è¯„估条目豁免设定
 */
export default {
  props: {
    // æ­¥éª¤ä¸‹æ ‡
    modelValue: Number
  },
  emits: ['update:modelValue', 'change'],
  data() {
    return {
      // è±å…æ¡ç›®
      exemptionItems: []
    };
  },
  methods: {
    // è·³è½¬ä¸‹ä¸€æ­¥
    nextStep() {
      this.$emit('change', this.exemptionItems);
      this.$emit('update:modelValue', this.modelValue + 1);
    },
    // è·³è½¬ä¸‹ä¸€æ­¥
    lastStep() {
      this.$emit('update:modelValue', this.modelValue - 1);
    }
  }
};
</script>
src/views/analysis/evalution/components/precheck/components/CompCheckSource.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,334 @@
<template>
  <div>
    <el-card shadow="never">
      <template #header>
        <div><el-text tag="b" size="large">数据源检查</el-text></div>
        <el-text size="small" type="info">检查评估所需数据源是否完整</el-text>
      </template>
      <FormCol>
        <template v-for="(v, i) in checkResults" :key="i">
          <el-row class="h-small" align="middle">
            <el-col :span="14">
              <el-row align="middle">
                <el-text
                  size="default"
                  :class="v.required ? 'required' : 'not-required'"
                  >*</el-text
                >
                <el-text size="default" class="m-l-4">{{ v.name }}</el-text>
              </el-row>
            </el-col>
            <el-col :span="5">
              <el-row align="middle">
                <el-space>
                  <template v-if="v.loading">
                    <el-icon class="is-loading"><Loading /></el-icon>
                    <el-text size="default" type="default">检查中...</el-text>
                  </template>
                  <template v-else-if="v.pass == true">
                    <el-icon color="var(--el-color-success)"><Check /></el-icon>
                    <el-text size="default" type="success">通过</el-text>
                  </template>
                  <template v-else-if="v.pass == false">
                    <el-icon color="var(--el-color-danger)"><Close /></el-icon>
                    <el-text size="default" type="danger">缺失</el-text>
                  </template>
                  <template v-else>
                    <el-icon color="var(--el-color-warning)"
                      ><Warning
                    /></el-icon>
                    <el-text size="default" type="warning">暂略过</el-text>
                  </template>
                </el-space>
              </el-row>
            </el-col>
            <el-col :span="5">
              <el-button
                v-show="!v.loading"
                :type="v.pass ? '' : 'danger'"
                size="small"
                @click="goto(v.path)"
                :disabled="v.path == ''"
              >
                {{ v.pass ? '去修改' : '去完善' }}
                <el-icon class="m-l-4"><Right /></el-icon>
              </el-button>
            </el-col>
          </el-row>
          <el-row align="middle" class="m-b-16">
            <el-text size="small" class="not-required">*</el-text>
            <el-text size="small" class="m-l-4 color-i">{{ v.des }}</el-text>
          </el-row>
        </template>
      </FormCol>
      <template #footer>
        <el-row justify="space-around">
          <el-button type="primary" size="default" @click="lastStep"
            >上一步</el-button
          >
          <el-button
            :disabled="!checkPass"
            type="primary"
            size="default"
            @click="nextStep"
            >下一步</el-button
          >
        </el-row>
      </template>
    </el-card>
    <el-dialog
      title="扬尘监测数据月度统计管理"
      v-model="dialog1"
      destroy-on-close
      width="90%"
    >
      <CompDataResultEdit :areaInfo="areaInfo"></CompDataResultEdit>
      <template #footer> </template>
    </el-dialog>
  </div>
</template>
<script>
import evaluateApi from '@/api/fysp/evaluateApi';
import taskApi from '@/api/fysp/taskApi';
import userMapApi from '@/api/fysp/userMapApi';
import problemApi from '@/api/fysp/problemApi';
import monitordataApi from '@/api/fysp/monitordataApi';
import complaintApi from '@/api/fytz/complaintApi';
import CompDataResultEdit from '../../CompDataResultEdit.vue';
/**
 * ç”Ÿæˆä¸€é¡¹æ•°æ®æºæ£€æŸ¥æ¡ç›®
 * @param {*} _name æ¡ç›®åç§°
 * @param {*} _path è·³è½¬é¡µé¢URL
 * @param {*} _fetch æ¡ç›®çš„网络请求函数
 * @param {*} _required æ˜¯å¦å¿…选
 */
function baseCheckItem(_name, _path, _fetch, _required) {
  return {
    required: _required,
    name: _name,
    loading: true,
    pass: false,
    path: _path,
    des: '',
    async fetch() {
      this.loading = true;
      setTimeout(async () => {
        if (typeof _fetch === 'function') {
          _fetch()
            .then((res) => {
              this.pass = res ? res.pass : undefined;
              this.des = res ? res.des : undefined;
            })
            .catch(() => {
              this.pass = false;
              this.des = '网络链接错误';
            })
            .finally(() => {
              this.loading = false;
            });
        } else {
          this.pass = undefined;
          this.des = undefined;
          this.loading = false;
        }
      }, 1000);
    }
  };
}
/**
 * è¯„估数据源完整性检查
 */
export default {
  components: {
    CompDataResultEdit
  },
  props: {
    // æ­¥éª¤ä¸‹æ ‡
    modelValue: Number
  },
  emits: ['update:modelValue', 'change'],
  data() {
    return {
      areaInfo: {},
      // æ•°æ®æºæ£€æŸ¥è®°å½•
      checkResults: [
        // åŒºåŸŸèŒƒå›´å†…的自动评估规则表是否存在
        baseCheckItem(
          '自动评估规则表',
          '',
          () => {
            const param = {
              taskTypeId: 99,
              ...this.areaInfo
            };
            return evaluateApi.fetchEvaluationRule(param).then((res) => {
              const pass = res.data.length > 0;
              let des = '';
              if (pass) {
                res.data.forEach((e) => {
                  if (des != '') {
                    des += '、';
                  }
                  des += `《${e.rulename}》`;
                });
              } else {
                des = '未找到相关评估规则表';
              }
              return { pass, des };
            });
          },
          true
        ),
        // åŒºåŸŸèŒƒå›´å†…的监管任务是否存在
        baseCheckItem('现场监管巡查总任务', '', () => {
          return taskApi.fetchTopTasks(this.areaInfo).then((res) => {
            const pass = res.data.length > 0;
            let des = '';
            if (pass) {
              res.data.forEach((e) => {
                if (des != '') {
                  des += '、';
                }
                des += e.name;
              });
            } else {
              des = '未找到相关巡查总任务';
            }
            return { pass, des };
          });
        }),
        // åŒºåŸŸèŒƒå›´å†…的监测数据是否存在、数据时间跨度是否完整、数据的初步分析是否完成
        baseCheckItem(
          '现场监测数据',
          () => {
            this.dialog1 = true;
          },
          () => {
            return monitordataApi
              .fetchDustDataResult(this.areaInfo)
              .then((res) => {
                const pass = res.data.length > 0;
                let des = '';
                if (pass) {
                  des = `找到月度统计共${res.data.length}条`;
                } else {
                  des = '未找到相关记录';
                }
                return { pass, des };
              });
          }
        ),
        // åŒºåŸŸèŒƒå›´å†…的每个监管点位与监测仪器的匹配记录是否存在,缺失情况等
        baseCheckItem('监管点位与监测点匹配', '', () => {
          return userMapApi.fetchDeviceMap(this.areaInfo).then((res) => {
            const pass = res.data.length > 0;
            let des = '';
            if (pass) {
              des = `找到匹配记录共${res.data.length}条`;
            } else {
              des = '未找到相关匹配记录';
            }
            return { pass, des };
          });
        }),
        // åŒºåŸŸèŒƒå›´å†…的监管问题配置表是否存在
        baseCheckItem('现场监管问题类型', '', () => {
          const param = {
            cityCode: this.areaInfo.citycode,
            districtCode: this.areaInfo.districtcode,
            sceneTypeId: this.areaInfo.scensetypeid
          };
          return problemApi.fetchProblemType(param).then((res) => {
            const pass = res.length > 0;
            let des = '';
            if (pass) {
              des = `找到问题类型共${res.length}条`;
            } else {
              des = '未找到相关问题类型';
            }
            return { pass, des };
          });
        }),
        // complaintApi.fetchComplaints();
        // åŒºåŸŸèŒƒå›´å†…的信访投诉记录是否存在,可随时补充
        baseCheckItem('信访投诉', ''),
        // complaintApi.fetchPunishment();
        // åŒºåŸŸèŒƒå›´å†…的行政处罚记录是否存在,可随时补充
        baseCheckItem('行政处罚', '')
      ],
      dialog1: false
    };
  },
  computed: {
    /**
     * åˆ¤æ–­æ•°æ®æºæ£€æŸ¥æ˜¯å¦é€šè¿‡
     * å…¨éƒ¨åŠ è½½å®ŒæˆåŽï¼Œå¿…è¦é¡¹å¿…é¡»é€šè¿‡ï¼Œå¯é€‰é¡¹éžå¿…é¡»é€šè¿‡
     */
    checkPass() {
      let res = true;
      this.checkResults.forEach((e) => {
        if (e.loading) {
          res = res && false;
        } else if (e.required) {
          res = res && e.pass;
        }
      });
      return res;
    }
  },
  methods: {
    // è·³è½¬ä¸‹ä¸€æ­¥
    nextStep() {
      this.$emit('change', this.checkResults);
      this.$emit('update:modelValue', this.modelValue + 1);
    },
    // è·³è½¬ä¸Šä¸€æ­¥
    lastStep() {
      this.$emit('update:modelValue', this.modelValue - 1);
    },
    // è·³è½¬æ£€æŸ¥é¡¹çš„链接
    goto(path) {
      if (typeof path === 'string' && path != '') {
        this.$router.push(path);
      } else if (typeof path === 'function') {
        path();
      }
    },
    // å¼€å§‹æ£€æŸ¥ä»»åŠ¡
    startCheck(value) {
      // const v = value.value;
      // this.areaInfo = {
      //   provincecode: v._locations.pCode,
      //   provincename: v._locations.pName,
      //   citycode: v._locations.cCode,
      //   cityname: v._locations.cName,
      //   districtcode: v._locations.dCode,
      //   districtname: v._locations.dName,
      //   towncode: v._locations.tCode,
      //   townname: v._locations.tName,
      //   starttime: this.$fm.formatYMDHMS(v.time),
      //   scensetypeid: v._scenetype.value,
      //   online: true,
      //   sourceType: v.sourceType
      // };
      this.areaInfo = value;
      this.checkResults.forEach((e) => {
        e.fetch();
      });
    }
  }
};
</script>
<style scoped>
.required {
  color: var(--el-color-danger);
}
.not-required {
  color: transparent;
}
</style>
src/views/monitor/DataDashboard.vue
@@ -1,27 +1,31 @@
<!-- 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"
      />
      <!-- è®¾å¤‡å®žæ—¶æ•°æ®åŒºåŸŸ -->
      <RealTimeData :current-device="currentDevice" :hourly-data="hourlyData" />
      <!-- åˆ†åŒºæ•°æ®æŽ’名区域 -->
      <DistrictRanking
        :selected-month="selectedMonth"
        :ranking-type="rankingType"
        :ranking-data="rankingData"
        :sorted-ranking-data="sortedRankingData"
        @month-change="handleMonthChange"
        @type-change="handleTypeChange"
      />
      <div class="grid-container">
        <div class="left-section">
          <!-- è®¾å¤‡åœ¨çº¿æƒ…况区域 -->
          <DeviceStatus
            :online-count="onlineCount"
            :offline-count="offlineCount"
            :normal-count="normalCount"
            :fault-count="faultCount"
          />
          <!-- åˆ†åŒºæ•°æ®æŽ’名区域 -->
          <DistrictRanking
            style="flex: 1"
            :selected-month="selectedMonth"
            :ranking-type="rankingType"
            :ranking-data="rankingData"
            :sorted-ranking-data="sortedRankingData"
            @month-change="handleMonthChange"
            @type-change="handleTypeChange"
          />
        </div>
        <div class="right-section">
          <!-- è®¾å¤‡å®žæ—¶æ•°æ®åŒºåŸŸ -->
          <RealTimeData style="flex: 1" :devices="devices" />
        </div>
      </div>
      <!-- åœ¨çº¿è®¾å¤‡å’Œåº—铺清单区域 -->
      <ShopList
@@ -58,8 +62,7 @@
      faultCount: 0,
      // è®¾å¤‡å®žæ—¶æ•°æ®
      currentDevice: null,
      hourlyData: [],
      devices: [],
      // åˆ†åŒºæ•°æ®æŽ’名
      selectedMonth: '2023-12',
@@ -147,45 +150,80 @@
    updateRealTimeData() {
      // æ¨¡æ‹Ÿæ•°æ® - å®žé™…应从API获取
      const devices = [
      this.devices = [
        {
          deviceId: 'DEV-001',
          supplier: '供应商A',
          æ²¹çƒŸæµ“度: (Math.random() * 2).toFixed(2),
          é£Žæœºç”µæµ: (Math.random() * 5 + 1).toFixed(2),
          å‡€åŒ–器电流: (Math.random() * 3 + 0.5).toFixed(2),
          status: '正常',
          monitorTime: new Date().toLocaleString(),
          smokeDensity: (Math.random() * 2).toFixed(2),
          fanCurrent: (Math.random() * 5 + 1).toFixed(2),
          purifierCurrent: (Math.random() * 3 + 0.5).toFixed(2),
          hourlyData: this.generateHourlyData(),
        },
        {
          deviceId: 'DEV-002',
          supplier: '供应商B',
          status: '正常',
          monitorTime: new Date().toLocaleString(),
          smokeDensity: (Math.random() * 2).toFixed(2),
          fanCurrent: (Math.random() * 5 + 1).toFixed(2),
          purifierCurrent: (Math.random() * 3 + 0.5).toFixed(2),
          hourlyData: this.generateHourlyData(),
        },
        {
          deviceId: 'DEV-003',
          supplier: '供应商C',
          status: '异常',
          monitorTime: new Date().toLocaleString(),
          smokeDensity: (Math.random() * 15 + 5).toFixed(2),
          fanCurrent: (Math.random() * 3 + 4).toFixed(2),
          purifierCurrent: (Math.random() * 2 + 2).toFixed(2),
          hourlyData: this.generateHourlyData(),
        },
        {
          deviceId: 'DEV-004',
          supplier: '供应商D',
          status: '异常',
          monitorTime: new Date().toLocaleString(),
          smokeDensity: (Math.random() * 15 + 5).toFixed(2),
          fanCurrent: (Math.random() * 3 + 4).toFixed(2),
          purifierCurrent: (Math.random() * 2 + 2).toFixed(2),
          hourlyData: this.generateHourlyData(),
        },
      ]
    },
      this.currentDevice = devices[0]
    generateHourlyData() {
      // ç”Ÿæˆæ¨¡æ‹Ÿçš„近一小时数据
      this.hourlyData = []
      const hourlyData = []
      for (let i = 59; i >= 0; i--) {
        const time = new Date()
        time.setMinutes(time.getMinutes() - i)
        this.hourlyData.push({
        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),
          smokeDensity: (Math.random() * 2).toFixed(2),
          fanCurrent: (Math.random() * 5 + 1).toFixed(2),
          purifierCurrent: (Math.random() * 3 + 0.5).toFixed(2),
        })
      }
      return hourlyData
    },
    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 },
        { 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 },
        { name: '高淳区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 1 },
      ]
      // æŽ’序
@@ -240,14 +278,35 @@
  padding: 20px;
}
.grid-container {
  display: flex;
  /* display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: min-content; */
  gap: 20px;
}
.left-section {
  flex: 2;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.right-section {
  width: 670px;
  display: flex;
  flex-direction: column;
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .el-row {
    flex-direction: column;
  .grid-container {
    grid-template-columns: 1fr;
  }
  .el-col {
    width: 100% !important;
  .left-section,
  .right-section {
    margin-bottom: 10px;
  }
}