餐饮油烟智能监测与监管一体化平台
riku
2026-03-19 64d3eae2852d17c3bbade662d3f7a7c47d681ad6
2026.3.19
已修改12个文件
已删除1个文件
已添加9个文件
4232 ■■■■■ 文件已修改
components.d.ts 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/search-option/FYOptionTime.vue 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/search-option/FYOptionTopTask.vue 80 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/constants/menu.js 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/sfc/ExceptionText.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/sfc/TimeSelect.vue 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/DataProduct.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/data-product/DataProduct.vue 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/data-product/components/BaseProdProcess.vue 316 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/data-product/components/ProdDownload.vue 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/data-product/components/ProdQueryOpt.vue 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/data-product/components/ProdQueryOptCompare.vue 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/data-product/components/ProdQueryOptWithMode.vue 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/data-product/prod-step-change.js 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/huanxincode/HuanxinCodeManage.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/ComplaintManage.vue 597 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/MonitorControl.vue 931 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/PunishmentManage.vue 590 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/task/components/CompMonitorPlan.vue 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/DataDashboard.vue 584 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/DataException.vue 157 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components.d.ts
@@ -21,11 +21,13 @@
    CompQuickSet: typeof import('./src/components/search-option/CompQuickSet.vue')['default']
    DeviceStatus: typeof import('./src/components/monitor/DeviceStatus.vue')['default']
    DistrictRanking: typeof import('./src/components/monitor/DistrictRanking.vue')['default']
    ElAffix: typeof import('element-plus/es')['ElAffix']
    ElAside: typeof import('element-plus/es')['ElAside']
    ElBadge: typeof import('element-plus/es')['ElBadge']
    ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
    ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
    ElButton: typeof import('element-plus/es')['ElButton']
    ElCalendar: typeof import('element-plus/es')['ElCalendar']
    ElCard: typeof import('element-plus/es')['ElCard']
    ElCascader: typeof import('element-plus/es')['ElCascader']
    ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
@@ -39,14 +41,18 @@
    ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
    ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
    ElDialog: typeof import('element-plus/es')['ElDialog']
    ElDivider: typeof import('element-plus/es')['ElDivider']
    ElDrawer: typeof import('element-plus/es')['ElDrawer']
    ElEmpty: typeof import('element-plus/es')['ElEmpty']
    ElForm: typeof import('element-plus/es')['ElForm']
    ElFormItem: typeof import('element-plus/es')['ElFormItem']
    ElFrom: typeof import('element-plus/es')['ElFrom']
    ElHeader: typeof import('element-plus/es')['ElHeader']
    ElIcon: typeof import('element-plus/es')['ElIcon']
    ElImage: typeof import('element-plus/es')['ElImage']
    ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
    ElInput: typeof import('element-plus/es')['ElInput']
    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
    ElLink: typeof import('element-plus/es')['ElLink']
    ElMain: typeof import('element-plus/es')['ElMain']
    ElMenu: typeof import('element-plus/es')['ElMenu']
@@ -55,12 +61,17 @@
    ElOption: typeof import('element-plus/es')['ElOption']
    ElPagination: typeof import('element-plus/es')['ElPagination']
    ElPopover: typeof import('element-plus/es')['ElPopover']
    ElProgress: typeof import('element-plus/es')['ElProgress']
    ElRadio: typeof import('element-plus/es')['ElRadio']
    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
    ElRow: typeof import('element-plus/es')['ElRow']
    ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
    ElSelect: typeof import('element-plus/es')['ElSelect']
    ElSpace: typeof import('element-plus/es')['ElSpace']
    ElStatistic: typeof import('element-plus/es')['ElStatistic']
    ElStep: typeof import('element-plus/es')['ElStep']
    ElSteps: typeof import('element-plus/es')['ElSteps']
    ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
    ElSwitch: typeof import('element-plus/es')['ElSwitch']
    ElTable: typeof import('element-plus/es')['ElTable']
@@ -70,6 +81,8 @@
    ElTag: typeof import('element-plus/es')['ElTag']
    ElText: typeof import('element-plus/es')['ElText']
    ElTooltip: typeof import('element-plus/es')['ElTooltip']
    ElTree: typeof import('element-plus/es')['ElTree']
    ElUpload: typeof import('element-plus/es')['ElUpload']
    FormCol: typeof import('./src/components/layout/FormCol.vue')['default']
    FYBgTaskCard: typeof import('./src/components/bg-task/FYBgTaskCard.vue')['default']
    FYBgTaskDialog: typeof import('./src/components/bg-task/FYBgTaskDialog.vue')['default']
@@ -96,6 +109,8 @@
    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']
    IEpArrowLeft: typeof import('~icons/ep/arrow-left')['default']
    IEpArrowRight: typeof import('~icons/ep/arrow-right')['default']
    IEpDownload: typeof import('~icons/ep/download')['default']
    IEpInfoFilled: typeof import('~icons/ep/info-filled')['default']
    ItemDevice: typeof import('./src/components/list-item/ItemDevice.vue')['default']
@@ -129,11 +144,13 @@
  const CompQuickSet: typeof import('./src/components/search-option/CompQuickSet.vue')['default']
  const DeviceStatus: typeof import('./src/components/monitor/DeviceStatus.vue')['default']
  const DistrictRanking: typeof import('./src/components/monitor/DistrictRanking.vue')['default']
  const ElAffix: typeof import('element-plus/es')['ElAffix']
  const ElAside: typeof import('element-plus/es')['ElAside']
  const ElBadge: typeof import('element-plus/es')['ElBadge']
  const ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
  const ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
  const ElButton: typeof import('element-plus/es')['ElButton']
  const ElCalendar: typeof import('element-plus/es')['ElCalendar']
  const ElCard: typeof import('element-plus/es')['ElCard']
  const ElCascader: typeof import('element-plus/es')['ElCascader']
  const ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
@@ -147,14 +164,18 @@
  const ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
  const ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
  const ElDialog: typeof import('element-plus/es')['ElDialog']
  const ElDivider: typeof import('element-plus/es')['ElDivider']
  const ElDrawer: typeof import('element-plus/es')['ElDrawer']
  const ElEmpty: typeof import('element-plus/es')['ElEmpty']
  const ElForm: typeof import('element-plus/es')['ElForm']
  const ElFormItem: typeof import('element-plus/es')['ElFormItem']
  const ElFrom: typeof import('element-plus/es')['ElFrom']
  const ElHeader: typeof import('element-plus/es')['ElHeader']
  const ElIcon: typeof import('element-plus/es')['ElIcon']
  const ElImage: typeof import('element-plus/es')['ElImage']
  const ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
  const ElInput: typeof import('element-plus/es')['ElInput']
  const ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
  const ElLink: typeof import('element-plus/es')['ElLink']
  const ElMain: typeof import('element-plus/es')['ElMain']
  const ElMenu: typeof import('element-plus/es')['ElMenu']
@@ -163,12 +184,17 @@
  const ElOption: typeof import('element-plus/es')['ElOption']
  const ElPagination: typeof import('element-plus/es')['ElPagination']
  const ElPopover: typeof import('element-plus/es')['ElPopover']
  const ElProgress: typeof import('element-plus/es')['ElProgress']
  const ElRadio: typeof import('element-plus/es')['ElRadio']
  const ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
  const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
  const ElRow: typeof import('element-plus/es')['ElRow']
  const ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
  const ElSelect: typeof import('element-plus/es')['ElSelect']
  const ElSpace: typeof import('element-plus/es')['ElSpace']
  const ElStatistic: typeof import('element-plus/es')['ElStatistic']
  const ElStep: typeof import('element-plus/es')['ElStep']
  const ElSteps: typeof import('element-plus/es')['ElSteps']
  const ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
  const ElSwitch: typeof import('element-plus/es')['ElSwitch']
  const ElTable: typeof import('element-plus/es')['ElTable']
@@ -178,6 +204,8 @@
  const ElTag: typeof import('element-plus/es')['ElTag']
  const ElText: typeof import('element-plus/es')['ElText']
  const ElTooltip: typeof import('element-plus/es')['ElTooltip']
  const ElTree: typeof import('element-plus/es')['ElTree']
  const ElUpload: typeof import('element-plus/es')['ElUpload']
  const FormCol: typeof import('./src/components/layout/FormCol.vue')['default']
  const FYBgTaskCard: typeof import('./src/components/bg-task/FYBgTaskCard.vue')['default']
  const FYBgTaskDialog: typeof import('./src/components/bg-task/FYBgTaskDialog.vue')['default']
@@ -204,6 +232,8 @@
  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']
  const IEpArrowLeft: typeof import('~icons/ep/arrow-left')['default']
  const IEpArrowRight: typeof import('~icons/ep/arrow-right')['default']
  const IEpDownload: typeof import('~icons/ep/download')['default']
  const IEpInfoFilled: typeof import('~icons/ep/info-filled')['default']
  const ItemDevice: typeof import('./src/components/list-item/ItemDevice.vue')['default']
src/components/search-option/FYOptionTime.vue
@@ -14,66 +14,76 @@
</template>
<script>
import dayjs from 'dayjs';
import dayjs from 'dayjs'
const MONTH = 'month';
const DATE = 'date';
const RANGE = 'datetimerange';
const RANGE2 = 'daterange';
const MONTH = 'month'
const DATE = 'date'
const RANGE = 'datetimerange'
const RANGE2 = 'daterange'
export default {
  props: {
    type: {
      type: String,
      default: MONTH
      default: MONTH,
    },
    // è¿”回结果
    value: Date || Array,
    // æ˜¯å¦é»˜è®¤è¿”回初始选项
    initValue: {
      type: Boolean,
      default: true
      default: true,
    },
    label: {
      type: String,
      default: '时间'
      default: '时间',
    },
    prop: {
      type: String,
      default: 'time'
    }
      default: 'time',
    },
  },
  emits: ['update:value', 'change'],
  data() {
    return {
      date: this.value
    };
      date: this.value,
    }
  },
  watch: {
    value: {
      handler(newVal, oldVal) {
        if (newVal != oldVal) {
          this.date = newVal
        }
      },
      immediate: true,
    },
  },
  computed: {},
  methods: {
    handleChange(value) {
      this.$emit('update:value', value);
      this.$emit('change', value);
    }
      this.$emit('update:value', value)
      this.$emit('change', value)
    },
  },
  mounted() {
    if (this.initValue) {
      switch (this.type) {
        case RANGE:
        case RANGE2:
          this.date = [dayjs().startOf('month').toDate(), dayjs().toDate()];
          break;
          this.date = [dayjs().startOf('month').toDate(), dayjs().toDate()]
          break
        case MONTH:
          this.date = dayjs().startOf('month').toDate();
          break;
          this.date = dayjs().startOf('month').toDate()
          break
        case DATE:
          this.date = dayjs().toDate();
          break;
          this.date = dayjs().toDate()
          break
        default:
          break;
          break
      }
      this.handleChange(this.date);
      this.handleChange(this.date)
    }
  },
  }
};
</script>
src/components/search-option/FYOptionTopTask.vue
@@ -6,76 +6,68 @@
      :placeholder="label"
      style="width: 260px"
    >
      <el-option
        v-for="s in filtedBeforeTask"
        :key="s.value"
        :label="s.label"
        :value="s.value"
      />
      <el-option v-for="s in filtedBeforeTask" :key="s.value" :label="s.label" :value="s.value" />
    </el-select>
  </el-form-item>
</template>
<script>
import taskApi from '@/api/fysp/taskApi';
import taskApi from '@/api/fysp/taskApi'
import dayjs from 'dayjs'
export default {
  props: {
    label: {
      type: String,
      default: '总任务'
      default: '总任务',
    },
    // è¿”回结果
    value: Object,
    // æ˜¯å¦é»˜è®¤è¿”回初始选项
    initValue: {
      type: Boolean,
      default: true
      default: true,
    },
    // form表单绑定属性名
    prop: {
      type: String,
      default: 'topTaskId'
      default: 'topTaskId',
    },
    // é€‰é¡¹ç­›é€‰æ¡ä»¶ï¼Œç­›é€‰æŸä»»åŠ¡ä¹‹å‰çš„ç›¸åŒè¡Œæ”¿åŒºåˆ’å†…çš„ä»»åŠ¡
    beforeTask: {
      type: Object,
      default: () => {
        return {};
      }
    }
        return {}
      },
    },
  },
  emits: ['update:value'],
  data() {
    return {
      selected: {},
      topTasks: []
    };
      topTasks: [],
    }
  },
  computed: {
    // é€‰æ‹©æ¡†ä¸­ä½¿ç”¨é¡¶å±‚任务id作为选项值
    formatedValue() {
      return this.value?.tguid;
      return this.value?.tguid
    },
    // æŸä»»åŠ¡ä¹‹å‰çš„ç›¸åŒè¡Œæ”¿åŒºåˆ’å†…çš„ä»»åŠ¡
    filtedBeforeTask() {
      const filteredTasks = this.topTasks.filter((t) => {
        return (
          (!this.beforeTask.provincecode ||
            this.beforeTask.provincecode == t.data.provincecode) &&
          (!this.beforeTask.citycode ||
            this.beforeTask.citycode == t.data.citycode) &&
          (!this.beforeTask.districtcode ||
            this.beforeTask.districtcode == t.data.districtcode) &&
          (!this.beforeTask.starttime ||
            t.data.starttime < this.beforeTask.starttime)
        );
      });
          (!this.beforeTask.provincecode || this.beforeTask.provincecode == t.data.provincecode) &&
          (!this.beforeTask.citycode || this.beforeTask.citycode == t.data.citycode) &&
          (!this.beforeTask.districtcode || this.beforeTask.districtcode == t.data.districtcode) &&
          (!this.beforeTask.starttime || t.data.starttime < this.beforeTask.starttime)
        )
      })
      if (filteredTasks.length > 0) {
        this.handleChange(filteredTasks[0]?.value);
        this.handleChange(filteredTasks[0]?.value)
      }
      return filteredTasks;
    }
      return filteredTasks
    },
  },
  methods: {
    //获取查询条件
@@ -85,25 +77,29 @@
          return {
            value: r.tguid,
            label: r.name,
            data: r
          };
        });
        this.topTasks = list;
        if (this.initValue) {
          this.handleChange(list[0].value);
            data: r,
        }
      });
        })
        this.topTasks = list.filter((e) => {
          return (
            e.data.districtname == '徐汇区' && dayjs(e.data.starttime).isBefore(dayjs('2023-12-31'))
          )
        })
        if (this.initValue) {
          this.handleChange(list[0].value)
        }
      })
    },
    //查询子任务统计信息
    handleChange(value) {
      const task = this.topTasks.find((t) => t.data.tguid == value);
      const param = task ? task.data : {};
      const task = this.topTasks.find((t) => t.data.tguid == value)
      const param = task ? task.data : {}
      this.$emit('update:value', param);
    }
      this.$emit('update:value', param)
    },
  },
  mounted() {
    this.getOptions();
    this.getOptions()
  },
  }
};
</script>
src/constants/menu.js
@@ -73,21 +73,35 @@
    icon: 'solar:people-nearby-line-duotone',
    name: '监管巡查',
    children: [
      {
        path: '/index/inspection/monitor-control',
        icon: 'solar:eye-scan-line-duotone',
        name: '监管监控',
      },
      // {
      //   path: '/index/inspection/monitor-control',
      //   icon: 'solar:eye-scan-line-duotone',
      //   name: '监管监控',
      // },
      {
        path: '/index/inspection/task-manage',
        icon: 'solar:file-text-line-duotone',
        name: '任务管理',
        name: '巡查任务',
      },
      {
        path: '/index/inspection/pro-check',
        icon: 'solar:check-square-line-duotone',
        name: '问题审核',
      },
      {
        path: '/index/analysis/data-product',
        icon: 'solar:checklist-minimalistic-line-duotone',
        name: '问题整改',
        name: '整改清单',
      },
      {
        path: '/index/inspection/punishment-manage',
        icon: 'solar:hand-shake-line-duotone',
        name: '行政处罚',
      },
      {
        path: '/index/inspection/complaint-manage',
        icon: 'solar:call-chat-line-duotone',
        name: '信访投诉',
      },
    ],
  },
src/router/index.js
@@ -110,6 +110,16 @@
              path: 'monitor-control',
              component: () => import('@/views/inspection/MonitorControl.vue'),
            },
            {
              name: 'punishment-manage',
              path: 'punishment-manage',
              component: () => import('@/views/inspection/PunishmentManage.vue'),
            },
            {
              name: 'complaint-manage',
              path: 'complaint-manage',
              component: () => import('@/views/inspection/ComplaintManage.vue'),
            },
          ],
        },
        {
@@ -129,7 +139,7 @@
            {
              name: 'data-product',
              path: 'data-product',
              component: () => import('@/views/analysis/DataProduct.vue'),
              component: () => import('@/views/analysis/data-product/DataProduct.vue'),
            },
          ],
        },
src/sfc/ExceptionText.vue
@@ -35,7 +35,9 @@
</script>
<template>
  <el-tag type="info">
  <el-link @click="requestExceptionData" class="text"><slot /></el-link>
  </el-tag>
</template>
<style lang="scss" scoped>
src/sfc/TimeSelect.vue
@@ -14,6 +14,57 @@
-->
<script>
import dayjs from 'dayjs'
// æ—¶é—´èŒƒå›´å¿«æ·é€‰é¡¹
const dayStart = dayjs('2023-08-01').startOf('date')
const dayEnd = dayStart.endOf('date')
const shortcuts = [
  {
    text: '今天',
    value: [dayStart.toDate(), dayEnd.toDate()],
  },
  {
    text: '本周',
    value: [dayStart.startOf('week').toDate(), dayEnd.endOf('week').toDate()],
  },
  {
    text: '上周',
    value: [dayStart.day(-7).toDate(), dayEnd.day(-1).toDate()],
  },
  {
    text: '本月',
    value: [dayStart.startOf('month').toDate(), dayEnd.endOf('month').toDate()],
  },
  {
    text: '上月',
    value: [
      dayStart.subtract(1, 'month').startOf('month').toDate(),
      dayEnd.subtract(1, 'month').endOf('month').toDate(),
    ],
  },
  {
    text: '本季度',
    value: [dayStart.startOf('quarter').toDate(), dayEnd.endOf('quarter').toDate()],
  },
  {
    text: '上季度',
    value: [
      dayStart.subtract(1, 'quarter').startOf('quarter').toDate(),
      dayEnd.subtract(1, 'quarter').endOf('quarter').toDate(),
    ],
  },
  {
    text: '去年',
    value: [
      dayStart.subtract(1, 'year').startOf('year').toDate(),
      dayEnd.subtract(1, 'year').endOf('year').toDate(),
    ],
  },
  {
    text: '今年',
    value: [dayStart.startOf('year').toDate(), dayEnd.endOf('year').toDate()],
  },
]
export default {
  emits: ['submitTime'],
  props: {
@@ -42,6 +93,8 @@
      ],
      // é€‰ä¸­çš„æ—¶é—´èŒƒå›´
      selectedRange: '',
      // æ—¶é—´èŒƒå›´å¿«æ·é€‰é¡¹
      shortcuts,
    }
  },
@@ -138,6 +191,7 @@
    <div v-show="showTimePicker" class="time-picker-container">
      <el-date-picker
        v-model="time"
        :shortcuts="shortcuts"
        type="daterange"
        range-separator="~"
        start-placeholder="开始时间"
@@ -153,6 +207,7 @@
    <span class="demonstration">时间:</span>
    <el-date-picker
      v-model="time"
      :shortcuts="shortcuts"
      type="daterange"
      range-separator="~"
      start-placeholder="Start date"
src/views/analysis/DataProduct.vue
ÎļþÒÑɾ³ý
src/views/analysis/data-product/DataProduct.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,116 @@
<template>
  <BaseProdProcess
    v-model:active="active"
    @onStep1="onStep1"
    @onStep2="onStep2"
    @onStep3="onStep3"
    :loading="loading"
  >
    <template #step1="{ onSearch }">
      <ProdQueryOptWithMode :loading="loading" @submit="onSearch"></ProdQueryOptWithMode>
    </template>
    <template #step2="{ contentHeight }">
      <el-table
        id="prod-inspection-table"
        :data="tableData"
        v-loading="loading"
        :height="contentHeight + 'px'"
        table-layout="fixed"
        :show-overflow-tooltip="true"
        size="small"
        border
      >
        <el-table-column fixed="left" prop="index" label="编号" width="50"> </el-table-column>
        <el-table-column
          fixed="left"
          prop="subTask.scensename"
          label="名称"
          :show-overflow-tooltip="true"
          min-width="200"
        >
        </el-table-column>
        <el-table-column
          prop="subTask.planstarttime"
          label="巡查时间"
          :formatter="timeFormat"
          width="90"
        />
        <!-- <el-table-column prop="provincename" label="省" width="90" />
        <el-table-column prop="cityname" label="市" width="90" />
        <el-table-column prop="districtname" label="区县" width="90" /> -->
        <el-table-column prop="subTask.townname" label="街道" width="80" />
        <el-table-column prop="problems.length" label="问题数" width="60" />
        <el-table-column label="问题摘要" width="300">
          <template #default="{ row }">
            <template v-for="(value, index) in row.problems" :key="value.guid">
              <br v-if="index > 0" />{{ index + 1 + '、' + value.problemname }}
            </template>
          </template>
        </el-table-column>
        <el-table-column prop="unChangeProblems.length" label="未整改数" width="60" />
        <el-table-column label="未整改问题" width="300">
          <template #default="{ row }">
            <template v-for="(value, index) in row.unChangeProblems" :key="value.guid">
              <br v-if="index > 0" />{{ index + 1 + '、' + value.problemname }}
            </template>
          </template>
        </el-table-column>
        <!-- <el-table-column
          prop="evaluate.updatedate"
          label="更新时间"
          width="140"
          :formatter="timeFormat"
        /> -->
      </el-table>
    </template>
  </BaseProdProcess>
</template>
<script setup>
import { ref, inject } from 'vue'
import dayjs from 'dayjs'
import BaseProdProcess from './components/BaseProdProcess.vue'
import dataprodbaseApi from '@/api/fysp/dataprodbaseApi.js'
import { conversionFromTable } from '@/utils/excel'
import { useProdStepChange } from './prod-step-change.js'
import ProdQueryOptWithMode from './components/ProdQueryOptWithMode.vue'
const { active, changeActive } = useProdStepChange()
const loading = ref(false)
const tableData = ref([])
function onStep1(opt) {
  loading.value = true
  dataprodbaseApi
    .fetchProdInspectionInfo(opt)
    .then((res) => {
      if (res.success) {
        tableData.value = res.data.map((item) => {
          return {
            ...item,
            unChangeProblems: item.problems.filter((p) => !p.ischanged),
          }
        })
      }
      changeActive()
    })
    .finally(() => {
      loading.value = false
    })
}
function onStep2() {
  changeActive()
}
function onStep3(val) {
  if (val.downloadType == '1') {
    loading.value = true
    conversionFromTable('prod-inspection-table', '整改清单')
    loading.value = false
  }
}
function timeFormat(row, column, cellValue, index) {
  return dayjs(cellValue).format('YYYY-MM-DD')
}
</script>
src/views/analysis/data-product/components/BaseProdProcess.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,316 @@
<template>
  <el-row>
    <!-- æ­¥éª¤1 æ•°æ®äº§å“ç”Ÿæˆé€‰é¡¹ -->
    <div :class="active == 1 ? 'prod-active' : 'prod-inactive'" ref="step1Ref">
      <transition name="el-fade-in" @after-leave="handleTransitionContentEnd(1)">
        <div v-show="showStep1Content">
          <template v-if="$slots.step1">
            <slot name="step1" :onSearch="onSearch"></slot>
          </template>
          <template v-else>
            <ProdQueryOpt :loading="loading" @submit="onSearch"> </ProdQueryOpt>
          </template>
        </div>
      </transition>
      <transition name="el-fade-in" @after-leave="handleTransitionThumbnailEnd(1)">
        <div
          v-show="showStep1Thumbnail"
          class="prod-thumbnail-wrapper"
          :style="{ height: viewHeight + 'px' }"
          @click="changeActive(1)"
        >
          <div class="prod-thumbnail">①修改选项</div>
        </div>
      </transition>
    </div>
    <!-- æ­¥éª¤2 æ•°æ®äº§å“ç»“果预览 -->
    <div :class="active == 2 ? 'prod-active' : 'prod-inactive'" ref="step2Ref">
      <transition name="el-fade-in" @after-leave="handleTransitionContentEnd(2)">
        <div v-show="showStep2Content">
          <div ref="titleRef" class="prod-title">
            <el-text tag="b" size="large">数据产品预览</el-text>
            <el-button type="primary" @click="$emit('onStep2')"> ä¸‹è½½æ•°æ®äº§å“ </el-button>
          </div>
          <slot name="step2" :contentHeight="contentHeight"></slot>
        </div>
      </transition>
      <transition name="el-fade-in" @after-leave="handleTransitionThumbnailEnd(2)">
        <div
          v-show="showStep2Thumbnail"
          class="prod-thumbnail-wrapper"
          :style="{ height: viewHeight + 'px' }"
          @click="changeActive(2)"
        >
          <div
            :class="
              'prod-thumbnail prod-thumbnail_middle ' +
              (active < 2 ? 'prod-thumbnail-disabled' : '')
            "
          >
            â‘¡æ•°æ®äº§å“é¢„览
          </div>
        </div>
      </transition>
    </div>
    <!-- æ­¥éª¤3 æ•°æ®äº§å“è¡¨å•下载 -->
    <div :class="active == 3 ? 'prod-active' : 'prod-inactive'" ref="step3Ref">
      <transition name="el-fade-in" @after-leave="handleTransitionContentEnd(3)">
        <div v-show="showStep3Content">
          <template v-if="$slots.step3">
            <slot name="step3" :onDownload="onDownload" :queryOpt="queryOpt"></slot>
          </template>
          <template v-else>
            <ProdDownload
              :loading="loading"
              :queryOpt="queryOpt"
              @submit="onDownload"
            ></ProdDownload>
          </template>
        </div>
      </transition>
      <transition name="el-fade-in" @after-leave="handleTransitionThumbnailEnd(3)">
        <div
          v-show="showStep3Thumbnail"
          class="prod-thumbnail-wrapper"
          :style="{ height: viewHeight + 'px' }"
          @click="changeActive(3)"
        >
          <div
            :class="
              'prod-thumbnail prod-thumbnail_end ' + (active < 3 ? 'prod-thumbnail-disabled' : '')
            "
          >
            â‘¢æ•°æ®äº§å“ä¸‹è½½
          </div>
        </div>
      </transition>
    </div>
  </el-row>
</template>
<script setup>
import { computed, inject, ref, watch, onMounted, onUnmounted } from 'vue'
import ProdQueryOpt from './ProdQueryOpt.vue'
import ProdDownload from './ProdDownload.vue'
const props = defineProps({
  active: {
    type: Number,
    default: 1,
  },
  loading: {
    type: Boolean,
    default: false,
  },
})
const emit = defineEmits(['update:active', 'onStep1', 'onStep2', 'onStep3'])
const contentMaxHeight = inject('contentMaxHeight')
const viewHeight = inject('viewHeight', contentMaxHeight.value)
const btnDisabled = ref(false)
const titleRef = ref(null)
const contentHeight = ref('50vh')
function calContentHeight() {
  // console.log(titleRef.value.offsetHeight);
  contentHeight.value = viewHeight - (titleRef.value?.offsetHeight || 0)
  // console.log(contentHeight.value);
}
// æ•°æ®äº§å“ç”Ÿæˆé€‰é¡¹
const queryOpt = ref({})
// æ­¥éª¤å¼•用
const step1Ref = ref(null)
const step2Ref = ref(null)
const step3Ref = ref(null)
// æŽ§åˆ¶æ˜¾ç¤º/隐藏的状态
const showStep1Content = ref(props.active === 1)
const showStep1Thumbnail = ref(props.active != 1)
const showStep2Content = ref(props.active === 2)
const showStep2Thumbnail = ref(props.active != 2)
const showStep3Content = ref(props.active === 3)
const showStep3Thumbnail = ref(props.active != 3)
// è®°å½•动画是否正在进行中
const isAnimating = ref({})
// ç›‘听active变化
watch(
  () => props.active,
  (newActive, oldActive) => {
    // æ ‡è®°åŠ¨ç”»å¼€å§‹
    isAnimating.value[oldActive] = true
    isAnimating.value[newActive] = true
    // å…ˆéšè—æ‰€æœ‰å†…容,等待动画结束后再显示正确的内容
    if (oldActive === 1) {
      showStep1Content.value = false
    } else if (oldActive === 2) {
      showStep2Content.value = false
    } else if (oldActive === 3) {
      showStep3Content.value = false
    }
    if (newActive === 1) {
      showStep1Thumbnail.value = false
    } else if (newActive === 2) {
      showStep2Thumbnail.value = false
    } else if (newActive === 3) {
      showStep3Thumbnail.value = false
    }
  },
)
// å¤„理动画结束事件
function handleTransitionThumbnailEnd(stepIndex) {
  // æ£€æŸ¥åŠ¨ç”»æ˜¯å¦ç¡®å®žç»“æŸï¼ˆé¿å…é‡å¤è§¦å‘ï¼‰
  if (isAnimating.value[stepIndex]) {
    isAnimating.value[stepIndex] = false
    // setTimeout(() => {
    // åŠ¨ç”»ç»“æŸåŽï¼Œæ›´æ–°æ˜¾ç¤ºçŠ¶æ€
    if (stepIndex === 1) {
      showStep1Content.value = props.active === 1
    } else if (stepIndex === 2) {
      showStep2Content.value = props.active === 2
    } else if (stepIndex === 3) {
      showStep3Content.value = props.active === 3
    }
    // }, 50);
  }
}
function handleTransitionContentEnd(stepIndex) {
  // æ£€æŸ¥åŠ¨ç”»æ˜¯å¦ç¡®å®žç»“æŸï¼ˆé¿å…é‡å¤è§¦å‘ï¼‰
  if (isAnimating.value[stepIndex]) {
    isAnimating.value[stepIndex] = false
    // setTimeout(() => {
    // åŠ¨ç”»ç»“æŸåŽï¼Œæ›´æ–°æ˜¾ç¤ºçŠ¶æ€
    if (stepIndex === 1) {
      showStep1Thumbnail.value = props.active != 1
    } else if (stepIndex === 2) {
      showStep2Thumbnail.value = props.active != 2
    } else if (stepIndex === 3) {
      showStep3Thumbnail.value = props.active != 3
    }
    // }, 50);
  }
}
function onSearch(opt) {
  queryOpt.value = opt
  emit('onStep1', opt)
}
function onDownload(val) {
  emit('onStep3', val)
}
function changeActive(index) {
  let isAnimate = false
  Object.values(isAnimating.value).forEach((item) => {
    isAnimate = isAnimate || item
  })
  if (!isAnimate && !btnDisabled.value && props.active >= index) {
    emit('update:active', index)
    btnDisabled.value = true
    setTimeout(() => {
      btnDisabled.value = false
    }, 500)
  }
  // emit('update:active', index);
}
let resizeObserver = null
onMounted(() => {
  if (titleRef.value) {
    resizeObserver = new ResizeObserver(() => {
      calContentHeight()
    })
    resizeObserver.observe(titleRef.value)
  }
})
// åœ¨ç»„件卸载时清理
onUnmounted(() => {
  if (resizeObserver && titleRef.value) {
    resizeObserver.unobserve(titleRef)
  }
})
</script>
<style scoped>
.prod-active {
  /* width: 66.667%; */
  width: 90%;
  transition:
    width 0.5s ease,
    box-shadow 0.3s ease;
  /* background-color: #409eff; */
  margin: 5px 0;
  border-radius: 4px;
  box-shadow:
    -3px 0 6px rgba(0, 0, 0, 0.1),
    3px 0 6px rgba(0, 0, 0, 0.1);
}
.prod-inactive {
  /* width: 16.667%; */
  width: 5%;
  transition: width 0.5s ease;
  /* background-color: #e4e7ed; */
  margin: 5px 0;
  border-radius: 4px;
}
.prod-title {
  padding: 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.prod-thumbnail-wrapper {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 0 2px;
}
.prod-thumbnail {
  height: 90%;
  width: 100%;
  background-color: #409eff;
  color: white;
  display: flex;
  justify-content: center;
  align-items: center;
  writing-mode: vertical-rl;
  text-orientation: upright;
  letter-spacing: 8px;
  font-size: 18px;
  font-weight: 600;
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
  cursor: pointer;
}
.prod-thumbnail_middle {
  border-radius: 0px;
}
.prod-thumbnail_end {
  border-top-left-radius: 0px;
  border-bottom-left-radius: 0px;
  border-top-right-radius: 4px;
  border-bottom-right-radius: 4px;
}
.prod-thumbnail-disabled {
  background-color: #e4e7ed;
  color: #c0c4cc;
  cursor: not-allowed;
}
</style>
src/views/analysis/data-product/components/ProdDownload.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,110 @@
<template>
  <el-card shadow="never">
    <template #header>
      <div><el-text tag="b" size="large">数据产品下载</el-text></div>
    </template>
    <el-form :inline="false" label-position="left" label-width="150px">
      <el-form-item label="区县">
        <el-text>{{ opts.districtName }}</el-text>
      </el-form-item>
      <el-form-item label="时间范围">
        <el-text>{{ opts.startTime }} è‡³ {{ opts.endTime }}</el-text>
      </el-form-item>
      <el-form-item label="场景类型">
        <el-text>{{ opts.sceneTypeName }}</el-text>
      </el-form-item>
      <el-form-item label="产品形式">
        <el-radio-group v-model="downloadType">
          <el-radio
            v-for="item in _downloadTypeOptions"
            :key="item.value"
            :value="item.value"
            :disabled="item.disabled"
          >
            {{ item.label }}
          </el-radio>
        </el-radio-group>
      </el-form-item>
    </el-form>
    <template #footer>
      <el-row justify="end">
        <el-button
          type="primary"
          size="default"
          :loading="loading"
          @click="submit"
          icon="Download"
          >下载</el-button
        >
      </el-row>
    </template>
  </el-card>
</template>
<script setup>
import { ref, computed } from 'vue';
import dayjs from 'dayjs';
import scene_1 from '@/assets/image/scene_1.png';
const props = defineProps({
  // æ•°æ®äº§å“ç”Ÿæˆé€‰é¡¹
  queryOpt: {
    type: Object,
    default: () => {}
  },
  loading: {
    type: Boolean,
    default: false
  },
  downloadTypeOptions: {
    type: Array,
    default: () => [
      {
        value: '1',
        label: 'Excel表单'
      },
      {
        value: '2',
        label: 'Word文档'
      }
    ]
  },
  // ä¸‹è½½ç±»åž‹æ˜¯å¦æœ‰æ•ˆ
  downloadTypeValid: {
    type: Array,
    default: () => ['1']
  },
  defaultDownloadType: {
    type: String,
    default: '1'
  }
});
const emit = defineEmits(['submit']);
const downloadType = ref(props.defaultDownloadType);
const opts = computed(() => {
  if (props.queryOpt instanceof Array && props.queryOpt.length > 0) {
    return props.queryOpt[0];
  } else {
    return props.queryOpt;
  }
});
const _downloadTypeOptions = computed(() => {
  return props.downloadTypeOptions.map((item) => ({
    ...item,
    disabled: !props.downloadTypeValid.includes(item.value)
  }));
});
const submit = () => {
  emit('submit', {
    downloadType: downloadType.value
  });
};
</script>
<style scoped>
/* .image {
  width: 200px;
  height: 200px;
} */
</style>
src/views/analysis/data-product/components/ProdQueryOpt.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
<template>
  <el-card shadow="never">
    <template #header>
      <div><el-text tag="b" size="large">产品生成选项</el-text></div>
    </template>
    <SearchBar
      v-show="active"
      ref="refSearchBar"
      :btn-show="false"
      :init="false"
      @on-submit="search"
    >
    </SearchBar>
    <template #footer>
      <el-row v-show="active" justify="end">
        <el-button
          type="primary"
          size="default"
          :loading="loading"
          @click="submit"
          >生成</el-button
        >
      </el-row>
    </template>
  </el-card>
</template>
<script setup>
import { ref, computed } from 'vue';
import dayjs from 'dayjs';
const props = defineProps({
  loading: {
    type: Boolean,
    default: false
  },
  active: {
    type: Boolean,
    default: true
  }
});
const emit = defineEmits(['submit']);
const refSearchBar = ref(null);
const submit = () => {
  refSearchBar.value.onSubmit();
};
const search = (options) => {
  const opt = {
    topTaskId: options.topTask.tguid,
    topTaskName: options.topTask.name,
    provinceCode: options.topTask.provincecode,
    provinceName: options.topTask.provincename,
    cityCode: options.topTask.citycode,
    cityName: options.topTask.cityname,
    districtCode: options.topTask.districtcode,
    districtName: options.topTask.districtname,
    townCode: options.topTask.towncode,
    townName: options.topTask.townname,
    startTime: dayjs(options.topTask.starttime).format('YYYY-MM-DD HH:mm:ss'),
    endTime: dayjs(options.topTask.endtime)
      .endOf('day')
      .format('YYYY-MM-DD HH:mm:ss'),
    sceneTypeId: options.sceneTypeId,
    sceneTypeName: options.sceneTypeName,
    needCache: true
  };
  emit('submit', opt);
};
</script>
src/views/analysis/data-product/components/ProdQueryOptCompare.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,114 @@
<template>
  <el-card shadow="never">
    <template #header>
      <div><el-text tag="b" size="large">产品生成选项</el-text></div>
    </template>
    <el-space fill>
      <el-alert type="info" show-icon :closable="false">
        <p>选择本期需要统计的总任务和场景类型</p>
      </el-alert>
      <el-form :inline="true" :model="formSearch">
        <FYOptionTopTask v-model:value="formSearch.topTask"></FYOptionTopTask>
        <FYOptionScene
          :allOption="false"
          :type="2"
          v-model:value="formSearch.scenetype"
        ></FYOptionScene>
      </el-form>
    </el-space>
    <el-space fill>
      <el-alert type="info" show-icon :closable="false">
        <p>选择需要进行对比的历史版本</p>
      </el-alert>
      <el-form :inline="true" :model="formSearch2">
        <FYOptionTopTask
          :beforeTask="formSearch.topTask"
          v-model:value="formSearch2.topTask"
        ></FYOptionTopTask>
      </el-form>
    </el-space>
    <template #footer>
      <el-row v-show="active" justify="end">
        <el-button
          type="primary"
          size="default"
          :loading="loading"
          @click="submit"
          >生成</el-button
        >
      </el-row>
    </template>
  </el-card>
</template>
<script setup>
import { ref, computed } from 'vue';
import dayjs from 'dayjs';
const props = defineProps({
  loading: {
    type: Boolean,
    default: false
  },
  active: {
    type: Boolean,
    default: true
  }
});
const emit = defineEmits(['submit']);
const formSearch = ref({
  topTask: {},
  scenetype: {}
});
const formSearch2 = ref({
  topTask: {}
});
const submit = () => {
  const opt1 = {
    topTaskId: formSearch.value.topTask.tguid,
    topTaskName: formSearch.value.topTask.name,
    provinceCode: formSearch.value.topTask.provincecode,
    provinceName: formSearch.value.topTask.provincename,
    cityCode: formSearch.value.topTask.citycode,
    cityName: formSearch.value.topTask.cityname,
    districtCode: formSearch.value.topTask.districtcode,
    districtName: formSearch.value.topTask.districtname,
    townCode: formSearch.value.topTask.towncode,
    townName: formSearch.value.topTask.townname,
    startTime: dayjs(formSearch.value.topTask.starttime).format(
      'YYYY-MM-DD HH:mm:ss'
    ),
    endTime: dayjs(formSearch.value.topTask.endtime)
      .endOf('day')
      .format('YYYY-MM-DD HH:mm:ss'),
    sceneTypeId: formSearch.value.scenetype.value,
    sceneTypeName: formSearch.value.scenetype.label,
    needCache: true
  };
  const opt2 = {
    topTaskId: formSearch2.value.topTask.tguid,
    topTaskName: formSearch2.value.topTask.name,
    provinceCode: formSearch2.value.topTask.provincecode,
    provinceName: formSearch2.value.topTask.provincename,
    cityCode: formSearch2.value.topTask.citycode,
    cityName: formSearch2.value.topTask.cityname,
    districtCode: formSearch2.value.topTask.districtcode,
    districtName: formSearch2.value.topTask.districtname,
    townCode: formSearch2.value.topTask.towncode,
    townName: formSearch2.value.topTask.townname,
    startTime: dayjs(formSearch2.value.topTask.starttime).format(
      'YYYY-MM-DD HH:mm:ss'
    ),
    endTime: dayjs(formSearch2.value.topTask.endtime)
      .endOf('day')
      .format('YYYY-MM-DD HH:mm:ss'),
    sceneTypeId: formSearch.value.scenetype.value,
    sceneTypeName: formSearch.value.scenetype.label,
    needCache: true
  };
  emit('submit', [opt1, opt2]);
};
</script>
src/views/analysis/data-product/components/ProdQueryOptWithMode.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,156 @@
<template>
  <el-card shadow="never">
    <template #header>
      <div><el-text tag="b" size="large">产品生成选项</el-text></div>
    </template>
    <el-switch
      v-model="mode"
      size="large"
      active-text="按所选时段统计"
      inactive-text="按总任务统计"
    />
    <!-- <SearchBar
      v-show="active && !mode"
      ref="refSearchBar"
      :btn-show="false"
      :init="false"
      @on-submit="search"
    >
    </SearchBar> -->
    <el-form :inline="true">
      <FYOptionScene
        :allOption="false"
        :type="2"
        :initValue="false"
        v-model:value="scenetype"
      ></FYOptionScene>
      <FYOptionTopTask v-show="!mode" v-model:value="topTask"></FYOptionTopTask>
      <!-- åŒºåŽ¿ -->
      <FYOptionLocation
        v-show="mode"
        :allOption="false"
        :level="3"
        :checkStrictly="false"
        v-model:value="locations"
      ></FYOptionLocation>
      <FYOptionTime
        v-show="mode"
        :initValue="false"
        type="daterange"
        v-model:value="timeRange"
        style="width: 300px"
      ></FYOptionTime>
    </el-form>
    <template #footer>
      <el-row v-show="active" justify="end">
        <el-button type="primary" size="default" :loading="loading" @click="search">生成</el-button>
      </el-row>
    </template>
  </el-card>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import dayjs from 'dayjs'
const props = defineProps({
  loading: {
    type: Boolean,
    default: false,
  },
  active: {
    type: Boolean,
    default: true,
  },
})
const emit = defineEmits(['submit'])
const mode = ref(true)
const scenetype = ref({
  label: '餐饮',
  value: '5',
})
const topTask = ref({})
const locations = ref({})
const timeRange = ref([])
watch(
  () => topTask.value,
  (newVal, oldVal) => {
    if (newVal?.provincecode) {
      locations.value.pCode = newVal.provincecode
      locations.value.pName = newVal.provincename
      locations.value.cCode = newVal.citycode
      locations.value.cName = newVal.cityname
      locations.value.dCode = newVal.districtcode
      locations.value.dName = newVal.districtname
      // locations.value.tCode = newVal.towncode
      // locations.value.tName = newVal.townname
      timeRange.value = [new Date(newVal.starttime), new Date(newVal.endtime)]
      console.log(timeRange.value)
    }
  },
  { deep: true },
)
const search = (options) => {
  const [st, et] = timeRange.value
  const startTime = dayjs(st).startOf('day').format('YYYY-MM-DD HH:mm:ss')
  const endTime = dayjs(et).endOf('day').format('YYYY-MM-DD HH:mm:ss')
  let opt = {
    sceneTypeId: scenetype.value.sceneTypeId,
    sceneTypeName: scenetype.value.sceneTypeName,
    needCache: true,
  }
  // æŒ‰æ‰€é€‰æ—¶æ®µç»Ÿè®¡
  if (mode.value) {
    opt = {
      provinceCode: locations.value.pCode,
      provinceName: locations.value.pName,
      cityCode: locations.value.cCode,
      cityName: locations.value.cName,
      districtCode: locations.value.dCode,
      districtName: locations.value.dName,
      townCode: locations.value.tCode,
      townName: locations.value.tName,
      startTime,
      endTime,
      ...opt,
    }
  }
  // æŒ‰æ€»ä»»åŠ¡ç»Ÿè®¡
  else {
    opt = {
      provinceCode: topTask.value.provincecode,
      provinceName: topTask.value.provincename,
      cityCode: topTask.value.citycode,
      cityName: topTask.value.cityname,
      districtCode: topTask.value.districtcode,
      districtName: topTask.value.districtname,
      townCode: topTask.value.towncode,
      townName: topTask.value.townname,
      ...opt,
    }
  }
  // const opt = {
  //   topTaskId: options.topTask.tguid,
  //   topTaskName: options.topTask.name,
  //   provinceCode: options.topTask.provincecode,
  //   provinceName: options.topTask.provincename,
  //   cityCode: options.topTask.citycode,
  //   cityName: options.topTask.cityname,
  //   districtCode: options.topTask.districtcode,
  //   districtName: options.topTask.districtname,
  //   townCode: options.topTask.towncode,
  //   townName: options.topTask.townname,
  //   startTime,
  //   endTime,
  //   sceneTypeId: options.sceneTypeId,
  //   sceneTypeName: options.sceneTypeName,
  //   needCache: true
  // };
  emit('submit', opt)
}
</script>
src/views/analysis/data-product/prod-step-change.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
import { ref } from 'vue';
/**
 * æ•°æ®äº§å“æ­¥éª¤åˆ‡æ¢
 */
export function useProdStepChange() {
  const active = ref(1);
  function changeActive() {
    active.value++;
    active.value = active.value > 3 ? 1 : active.value;
  }
  return {
    active,
    changeActive
  };
}
src/views/analysis/huanxincode/HuanxinCodeManage.vue
@@ -31,8 +31,8 @@
      <el-col :span="8">
        <el-card shadow="hover" class="dashboard-card green-card" @click="filterByCode('green')">
          <div class="card-content">
            <div class="card-title">绿码店铺数</div>
            <div class="card-value">{{ statistics.greenCount }}</div>
            <div class="card-title">绿码店铺</div>
            <div class="card-value">{{ statistics.greenCount }}<el-text>个</el-text></div>
            <div class="card-percentage">{{ statistics.greenPercentage }}%</div>
          </div>
        </el-card>
@@ -40,8 +40,8 @@
      <el-col :span="8">
        <el-card shadow="hover" class="dashboard-card yellow-card" @click="filterByCode('yellow')">
          <div class="card-content">
            <div class="card-title">黄码店铺数</div>
            <div class="card-value">{{ statistics.yellowCount }}</div>
            <div class="card-title">黄码店铺</div>
            <div class="card-value">{{ statistics.yellowCount }}<el-text>个</el-text></div>
            <div class="card-percentage">{{ statistics.yellowPercentage }}%</div>
          </div>
        </el-card>
@@ -49,8 +49,8 @@
      <el-col :span="8">
        <el-card shadow="hover" class="dashboard-card red-card" @click="filterByCode('red')">
          <div class="card-content">
            <div class="card-title">红码店铺数</div>
            <div class="card-value">{{ statistics.redCount }}</div>
            <div class="card-title">红码店铺</div>
            <div class="card-value">{{ statistics.redCount }}<el-text>个</el-text></div>
            <div class="card-percentage">{{ statistics.redPercentage }}%</div>
          </div>
        </el-card>
src/views/inspection/ComplaintManage.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,597 @@
<template>
  <div class="complaint-manage">
    <!-- é¤é¥®åº—铺信访投诉卡片 -->
    <el-card class="mb-4">
      <template #header>
        <div class="card-header">
          <span>餐饮店铺信访投诉</span>
          <div class="filter-group">
            <FYOptionTime
              class="m-r-8"
              :initValue="false"
              type="daterange"
              v-model:value="complaintDateRange"
              style="width: 300px; margin-bottom: 0px"
              :shortcuts="shortcuts"
            ></FYOptionTime>
            <el-button type="success" icon="Plus" @click="addComplaint">新增投诉</el-button>
            <el-button type="info" icon="Upload" @click="importComplaint">批量导入</el-button>
          </div>
        </div>
      </template>
      <!-- å›¾è¡¨å±•示 -->
      <div class="chart-container">
        <div class="chart-item">
          <div class="chart-header">
            <h3>投诉数趋势</h3>
            <div class="chart-summary">投诉总数: {{ complaintStats.totalCount }}</div>
          </div>
          <div ref="dailyComplaintChart" class="chart"></div>
        </div>
        <div class="chart-item">
          <h3>投诉来源分布</h3>
          <div ref="sourceComplaintChart" class="chart"></div>
        </div>
      </div>
      <!-- æŠ•诉记录表格 -->
      <el-table
        :data="filteredComplaintData"
        table-layout="fixed"
        :show-overflow-tooltip="true"
        height="400px"
        border
      >
        <el-table-column prop="shopName" label="投诉店铺" />
        <el-table-column prop="complaintReason" label="投诉原因" width="180" />
        <el-table-column prop="complaintRequest" label="投诉诉求" width="180" />
        <el-table-column prop="complaintTime" label="投诉时间" width="180" />
        <el-table-column prop="complaintSource" label="投诉来源" width="150" />
        <el-table-column prop="handlingDepartment" label="处理部门" width="150" />
        <el-table-column prop="complaintResult" label="投诉结果" width="150" />
        <el-table-column width="250">
          <template #header>
            <el-input v-model="complaintKeyword" placeholder="关键字搜索" style="width: 120px" />
          </template>
          <template #default="scope">
            <el-button size="small" type="primary" @click="editComplaint(scope.row)"
              >编辑</el-button
            >
            <el-button size="small" type="danger" @click="deleteComplaint(scope.row.id)"
              >删除</el-button
            >
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <div class="pagination-container">
        <el-pagination
          v-model:current-page="complaintPagination.currentPage"
          v-model:page-size="complaintPagination.pageSize"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          :total="complaintPagination.total"
          @size-change="handleComplaintSizeChange"
          @current-change="handleComplaintCurrentChange"
        />
      </div>
    </el-card>
    <!-- æ–°å¢ž/编辑投诉对话框 -->
    <el-dialog v-model="complaintDialogVisible" title="投诉信息" width="600px">
      <el-form :model="complaintForm" label-width="100px">
        <el-form-item label="投诉店铺">
          <el-input v-model="complaintForm.shopName" />
        </el-form-item>
        <el-form-item label="投诉原因">
          <el-input v-model="complaintForm.complaintReason" />
        </el-form-item>
        <el-form-item label="投诉诉求">
          <el-input v-model="complaintForm.complaintRequest" type="textarea" />
        </el-form-item>
        <el-form-item label="投诉时间">
          <el-date-picker v-model="complaintForm.complaintTime" type="datetime" />
        </el-form-item>
        <el-form-item label="投诉来源">
          <el-input v-model="complaintForm.complaintSource" />
        </el-form-item>
        <el-form-item label="处理部门">
          <el-input v-model="complaintForm.handlingDepartment" />
        </el-form-item>
        <el-form-item label="投诉结果">
          <el-input v-model="complaintForm.complaintResult" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="complaintDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="saveComplaint">保存</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- æŠ•诉批量导入对话框 -->
    <el-dialog v-model="complaintImportDialogVisible" title="投诉批量导入" width="600px">
      <div class="import-container">
        <p class="import-tip">请选择要导入的Excel文件</p>
        <el-upload
          class="upload-demo"
          action="#"
          drag
          :auto-upload="false"
          :on-change="handleComplaintFileChange"
          :file-list="complaintImportFileList"
          accept=".xlsx,.xls"
          :limit="1"
          :on-exceed="handleExceed"
        >
          <el-icon class="el-icon--upload"><upload-filled /></el-icon>
          <div class="el-upload__text">拖动文件或<em>点击上传</em></div>
          <template #tip>
            <div class="el-upload__tip">只能上传Excel文件,且不超过5MB</div>
          </template>
        </el-upload>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="complaintImportDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="confirmComplaintImport">导入</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
import * as echarts from 'echarts'
import dayjs from 'dayjs'
import { ElMessage } from 'element-plus'
// æ—¶é—´èŒƒå›´å¿«æ·é€‰é¡¹
const dayStart = dayjs('2023-08-01').startOf('date')
const dayEnd = dayStart.endOf('date')
const shortcuts = [
  {
    text: '今天',
    value: [dayStart.toDate(), dayEnd.toDate()],
  },
  {
    text: '本周',
    value: [dayStart.startOf('week').toDate(), dayEnd.endOf('week').toDate()],
  },
  {
    text: '上周',
    value: [dayStart.day(-7).toDate(), dayEnd.day(-1).toDate()],
  },
  {
    text: '本月',
    value: [dayStart.startOf('month').toDate(), dayEnd.endOf('month').toDate()],
  },
  {
    text: '上月',
    value: [
      dayStart.subtract(1, 'month').startOf('month').toDate(),
      dayEnd.subtract(1, 'month').endOf('month').toDate(),
    ],
  },
  {
    text: '本季度',
    value: [dayStart.startOf('quarter').toDate(), dayEnd.endOf('quarter').toDate()],
  },
  {
    text: '上季度',
    value: [
      dayStart.subtract(1, 'quarter').startOf('quarter').toDate(),
      dayEnd.subtract(1, 'quarter').endOf('quarter').toDate(),
    ],
  },
  {
    text: '去年',
    value: [
      dayStart.subtract(1, 'year').startOf('year').toDate(),
      dayEnd.subtract(1, 'year').endOf('year').toDate(),
    ],
  },
  {
    text: '今年',
    value: [dayStart.startOf('year').toDate(), dayEnd.endOf('year').toDate()],
  },
]
// å›¾è¡¨å¼•用
const dailyComplaintChart = ref(null)
const sourceComplaintChart = ref(null)
// ä¿¡è®¿æŠ•诉数据
const complaintDateRange = ref([dayStart.startOf('month').toDate(), dayEnd.endOf('month').toDate()])
const complaintKeyword = ref('')
const complaintStats = ref({
  totalCount: 85,
})
const complaintTableData = ref([])
const complaintPagination = ref({
  currentPage: 1,
  pageSize: 10,
  total: 0,
})
// è¿‡æ»¤åŽçš„æŠ•诉数据
const filteredComplaintData = computed(() => {
  if (!complaintKeyword.value) {
    return complaintTableData.value
  }
  const keyword = complaintKeyword.value.toLowerCase()
  return complaintTableData.value.filter((item) => {
    return Object.values(item).some((value) => {
      return String(value).toLowerCase().includes(keyword)
    })
  })
})
// å¯¹è¯æ¡†çŠ¶æ€
const complaintDialogVisible = ref(false)
const complaintImportDialogVisible = ref(false)
// å¯¼å…¥æ–‡ä»¶åˆ—表
const complaintImportFileList = ref([])
// è¡¨å•数据
const complaintForm = ref({
  id: '',
  shopName: '',
  complaintReason: '',
  complaintRequest: '',
  complaintTime: '',
  complaintSource: '',
  handlingDepartment: '',
  complaintResult: '',
})
const searchComplaint = () => {
  // æ¨¡æ‹Ÿæœç´¢æŠ•诉数据
  console.log('搜索投诉数据', {
    dateRange: complaintDateRange.value,
    keyword: complaintKeyword.value,
  })
  // è®¡ç®—时间范围
  const startTime = complaintDateRange.value[0]
  const endTime = complaintDateRange.value[1]
  const startDate = dayjs(startTime)
  const endDate = dayjs(endTime)
  const daysDiff = endDate.diff(startDate, 'day')
  const months = daysDiff / 30
  // ç”Ÿæˆæ–°çš„æ¨¡æ‹Ÿæ•°æ®
  const totalCount = Math.floor(20 * months) + Math.floor(Math.random() * 5)
  complaintStats.value = {
    totalCount,
  }
  // ç”Ÿæˆæ–°çš„æŠ•诉记录
  const newData = []
  const shopNames = [
    '鲜味馆',
    '烧烤达人',
    '味美餐厅',
    '香辣小龙虾',
    '川菜馆',
    '西餐厅',
    '日料店',
    '火锅店',
  ]
  const complaintReasons = ['油烟扰民', '夜间噪声', '异味污染', '卫生问题']
  const sources = ['12345热线', '居民投诉', '网络平台', '其他']
  const departments = ['徐汇区环保局', '长宁区环保局', '静安区环保局', '普陀区环保局']
  // const results = ['已处理', '处理中', '未处理']
  const results = ['已处理']
  for (let i = 0; i < totalCount; i++) {
    // ç”Ÿæˆåœ¨æ—¶é—´èŒƒå›´å†…的随机时间
    const randomDays = Math.floor(Math.random() * (daysDiff + 1))
    const randomTime = startDate
      .add(randomDays, 'day')
      .add(Math.floor(Math.random() * 24), 'hour')
      .add(Math.floor(Math.random() * 60), 'minute')
    newData.push({
      id: i + 1,
      shopName: shopNames[Math.floor(Math.random() * shopNames.length)],
      complaintReason: complaintReasons[Math.floor(Math.random() * complaintReasons.length)],
      complaintRequest:
        '要求整改' + complaintReasons[Math.floor(Math.random() * complaintReasons.length)],
      complaintTime: randomTime.format('YYYY-MM-DD HH:mm'),
      complaintSource: sources[Math.floor(Math.random() * sources.length)],
      handlingDepartment: departments[Math.floor(Math.random() * departments.length)],
      complaintResult: results[Math.floor(Math.random() * results.length)],
    })
  }
  complaintTableData.value = newData
  complaintPagination.value.total = newData.length
  // é‡æ–°åˆå§‹åŒ–图表以更新数据
  initCharts()
}
// ç›‘听投诉日期范围变化
watch(
  () => complaintDateRange.value,
  () => {
    searchComplaint()
  },
  { deep: true },
)
const addComplaint = () => {
  // é‡ç½®è¡¨å•
  complaintForm.value = {
    id: '',
    shopName: '',
    complaintReason: '',
    complaintRequest: '',
    complaintTime: '',
    complaintSource: '',
    handlingDepartment: '',
    complaintResult: '',
  }
  complaintDialogVisible.value = true
}
const editComplaint = (row) => {
  // å¡«å……表单
  complaintForm.value = { ...row }
  complaintDialogVisible.value = true
}
const saveComplaint = () => {
  // æ¨¡æ‹Ÿä¿å­˜æ•°æ®
  console.log('保存投诉数据', complaintForm.value)
  complaintDialogVisible.value = false
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„æ•°æ®ä¿å­˜é€»è¾‘
}
const deleteComplaint = (id) => {
  // æ¨¡æ‹Ÿåˆ é™¤æ•°æ®
  console.log('删除投诉数据', id)
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„æ•°æ®åˆ é™¤é€»è¾‘
}
const importComplaint = () => {
  // æ‰“开导入对话框
  complaintImportFileList.value = []
  complaintImportDialogVisible.value = true
}
const handleComplaintFileChange = (file, fileList) => {
  complaintImportFileList.value = fileList
  console.log('选择的投诉文件:', file)
}
const confirmComplaintImport = () => {
  if (complaintImportFileList.value.length === 0) {
    ElMessage.warning('请选择要导入的文件')
    return
  }
  const file = complaintImportFileList.value[0]
  console.log('开始导入投诉数据:', file.name)
  // é¢„留导入逻辑
  // è¿™é‡Œå°†å®žçŽ°å®žé™…çš„æ–‡ä»¶è§£æžå’Œæ•°æ®å¯¼å…¥
  complaintImportDialogVisible.value = false
  ElMessage.success('导入操作已触发,预留导入逻辑')
}
const handleExceed = (files, fileList) => {
  ElMessage.warning('只能上传一个文件')
}
const handleComplaintSizeChange = (size) => {
  complaintPagination.value.pageSize = size
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„åˆ†é¡µé€»è¾‘
}
const handleComplaintCurrentChange = (current) => {
  complaintPagination.value.currentPage = current
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„åˆ†é¡µé€»è¾‘
}
// åˆå§‹åŒ–图表
const initCharts = () => {
  // æ¯æ—¥æŠ•诉数量图
  if (dailyComplaintChart.value && complaintDateRange.value.length > 0) {
    const chart = echarts.init(dailyComplaintChart.value)
    // è®¡ç®—时间范围并生成x轴数据
    const startTime = complaintDateRange.value[0]
    const endTime = complaintDateRange.value[1]
    const startDate = dayjs(startTime)
    const endDate = dayjs(endTime)
    const daysDiff = endDate.diff(startDate, 'day')
    let xAxisData = []
    let seriesData = []
    // å¤„理投诉数据
    const complaintData = complaintTableData.value
    const dateFormat = daysDiff <= 30 ? 'MM/DD' : 'YYYY/MM'
    // ç”Ÿæˆæ—¥æœŸèŒƒå›´
    if (daysDiff <= 30) {
      // åŒä¸€ä¸ªæœˆå†…,按日显示
      for (let i = 0; i <= daysDiff; i++) {
        const date = startDate.add(i, 'day')
        xAxisData.push(date.format(dateFormat))
        // è®¡ç®—该日期的投诉数量
        const count = complaintData.filter((item) => {
          const itemDate = dayjs(item.complaintTime)
          return itemDate.format(dateFormat) === date.format(dateFormat)
        }).length
        seriesData.push(count)
      }
    } else {
      // è¶…过一个月,按月显示
      const startMonth = startDate.startOf('month')
      const endMonth = endDate.endOf('month')
      const monthsDiff = endMonth.diff(startMonth, 'month')
      for (let i = 0; i <= monthsDiff; i++) {
        const date = startMonth.add(i, 'month')
        xAxisData.push(date.format(dateFormat))
        // è®¡ç®—该月份的投诉数量
        const count = complaintData.filter((item) => {
          const itemDate = dayjs(item.complaintTime)
          return itemDate.format(dateFormat) === date.format(dateFormat)
        }).length
        seriesData.push(count)
      }
    }
    chart.setOption({
      xAxis: {
        type: 'category',
        data: xAxisData,
      },
      yAxis: {
        type: 'value',
      },
      series: [
        {
          data: seriesData,
          type: 'bar',
        },
      ],
    })
  }
  // æŠ•诉来源分布图
  if (sourceComplaintChart.value) {
    const chart = echarts.init(sourceComplaintChart.value)
    chart.setOption({
      series: [
        {
          type: 'pie',
          data: [
            { value: 40, name: '12345热线' },
            { value: 25, name: '居民投诉' },
            { value: 15, name: '网络平台' },
            { value: 5, name: '其他' },
          ],
          label: {
            show: true,
            formatter: '{b}: {d}%',
          },
        },
      ],
    })
  }
}
// ç›‘听窗口大小变化
const handleResize = () => {
  // é‡æ–°è°ƒæ•´å›¾è¡¨å¤§å°
  if (dailyComplaintChart.value) {
    echarts.init(dailyComplaintChart.value).resize()
  }
  if (sourceComplaintChart.value) {
    echarts.init(sourceComplaintChart.value).resize()
  }
}
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  searchComplaint()
  initCharts()
  window.addEventListener('resize', handleResize)
})
// ç»„件卸载时清理事件监听
onUnmounted(() => {
  cleanup()
})
// æ¸…理
const cleanup = () => {
  window.removeEventListener('resize', handleResize)
}
</script>
<style scoped>
.complaint-manage {
  padding: 20px;
  background-color: #f5f7fa;
  min-height: 100vh;
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.filter-group {
  display: flex;
  align-items: center;
}
.chart-container {
  display: flex;
  gap: 20px;
  margin-bottom: 30px;
}
.chart-item {
  flex: 1;
  background-color: #fff;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.chart-item h3 {
  margin-bottom: 15px;
  font-size: 16px;
  font-weight: 600;
  color: #303133;
}
.chart-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
}
.chart-summary {
  font-size: 14px;
  color: #606266;
}
.chart {
  height: 300px;
}
.pagination-container {
  margin-top: 20px;
  display: flex;
  justify-content: flex-end;
}
.import-container {
  padding: 20px;
}
.import-tip {
  margin-bottom: 20px;
  color: #606266;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
}
</style>
src/views/inspection/MonitorControl.vue
@@ -1,21 +1,18 @@
<template>
  <div class="monitor-control">
    <!-- æ€»è§ˆçŽ°åœºå·¡æŸ¥å¡ç‰‡ -->
    <el-card class="mb-4">
      <template #header>
    <!-- <template #header>
        <div class="card-header">
          <span>现场巡查总览</span>
          <div class="filter-group">
        <el-form :model="params" label-position="left" label-width="70px">
            <FYOptionTime
              :initValue="false"
              type="daterange"
              v-model:value="params.timeRange"
              style="width: 300px; margin-bottom: 0px"
            style="width: 300px"
              :shortcuts="shortcuts"
            ></FYOptionTime>
            <!-- åŒºåŽ¿ -->
            <FYOptionLocation
              class="m-l-8"
              :allOption="false"
              :level="3"
              :checkStrictly="false"
@@ -23,10 +20,11 @@
              v-model:value="params.locations"
              style="width: 300px; margin-bottom: 0px"
            ></FYOptionLocation>
        </el-form>
          </div>
        </div>
      </template>
    </template> -->
    <el-scrollbar>
      <!-- ç»Ÿè®¡æ•°æ®åŒºåŸŸ -->
      <div class="stats-sections">
        <!-- å·¦ä¾§ï¼šå·²å·¡æŸ¥åº—铺率、巡查点次、复查点次 -->
@@ -88,284 +86,8 @@
          <div ref="problemTypeChart" class="chart"></div>
        </div>
      </div>
    </el-scrollbar>
    </el-card>
    <!-- é¤é¥®åº—铺行政处罚卡片 -->
    <el-card class="mb-4">
      <template #header>
        <div class="card-header">
          <span>餐饮店铺行政处罚</span>
          <div class="filter-group">
            <FYOptionTime
              class="m-r-8"
              :initValue="false"
              type="daterange"
              v-model:value="punishmentDateRange"
              style="width: 300px; margin-bottom: 0px"
              :shortcuts="shortcuts"
            ></FYOptionTime>
            <el-button type="success" icon="Plus" @click="addPunishment">新增处罚</el-button>
            <el-button type="info" icon="Upload" @click="importPunishment">批量导入</el-button>
          </div>
        </div>
      </template>
      <!-- å›¾è¡¨å±•示 -->
      <div class="chart-container">
        <div class="chart-item">
          <div class="chart-header">
            <h3>处罚数趋势</h3>
            <div class="chart-summary">处罚总数: {{ punishmentStats.totalCount }}</div>
          </div>
          <div ref="dailyPunishmentChart" class="chart"></div>
        </div>
        <div class="chart-item">
          <h3>店铺类型处罚分布</h3>
          <div ref="shopTypePunishmentChart" class="chart"></div>
        </div>
      </div>
      <!-- å¤„罚记录表格 -->
      <el-table
        :data="filteredPunishmentData"
        table-layout="fixed"
        :show-overflow-tooltip="true"
        height="400px"
        border
      >
        <el-table-column prop="shopName" label="处罚店铺" />
        <el-table-column prop="punishmentItem" label="处罚事项" width="180" />
        <el-table-column prop="punishmentTime" label="处罚时间" width="180" />
        <el-table-column prop="punishmentReason" label="处罚理由" width="200" />
        <el-table-column prop="punishmentResult" label="处罚结果" width="150" />
        <el-table-column prop="punishmentDepartment" label="处罚部门" width="150" />
        <el-table-column width="250">
          <template #header>
            <el-input v-model="punishmentKeyword" placeholder="关键字搜索" style="width: 120px" />
          </template>
          <template #default="scope">
            <el-button size="small" type="primary" @click="editPunishment(scope.row)"
              >编辑</el-button
            >
            <el-button size="small" type="danger" @click="deletePunishment(scope.row.id)"
              >删除</el-button
            >
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <div class="pagination-container">
        <el-pagination
          v-model:current-page="punishmentPagination.currentPage"
          v-model:page-size="punishmentPagination.pageSize"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          :total="punishmentPagination.total"
          @size-change="handlePunishmentSizeChange"
          @current-change="handlePunishmentCurrentChange"
        />
      </div>
    </el-card>
    <!-- é¤é¥®åº—铺信访投诉卡片 -->
    <el-card class="mb-4">
      <template #header>
        <div class="card-header">
          <span>餐饮店铺信访投诉</span>
          <div class="filter-group">
            <FYOptionTime
              class="m-r-8"
              :initValue="false"
              type="daterange"
              v-model:value="complaintDateRange"
              style="width: 300px; margin-bottom: 0px"
              :shortcuts="shortcuts"
            ></FYOptionTime>
            <el-button type="success" icon="Plus" @click="addComplaint">新增投诉</el-button>
            <el-button type="info" icon="Upload" @click="importComplaint">批量导入</el-button>
          </div>
        </div>
      </template>
      <!-- å›¾è¡¨å±•示 -->
      <div class="chart-container">
        <div class="chart-item">
          <div class="chart-header">
            <h3>投诉数趋势</h3>
            <div class="chart-summary">投诉总数: {{ complaintStats.totalCount }}</div>
          </div>
          <div ref="dailyComplaintChart" class="chart"></div>
        </div>
        <div class="chart-item">
          <h3>投诉来源分布</h3>
          <div ref="sourceComplaintChart" class="chart"></div>
        </div>
      </div>
      <!-- æŠ•诉记录表格 -->
      <el-table
        :data="filteredComplaintData"
        table-layout="fixed"
        :show-overflow-tooltip="true"
        height="400px"
        border
      >
        <el-table-column prop="shopName" label="投诉店铺" />
        <el-table-column prop="complaintReason" label="投诉原因" width="180" />
        <el-table-column prop="complaintRequest" label="投诉诉求" width="180" />
        <el-table-column prop="complaintTime" label="投诉时间" width="180" />
        <el-table-column prop="complaintSource" label="投诉来源" width="150" />
        <el-table-column prop="handlingDepartment" label="处理部门" width="150" />
        <el-table-column prop="complaintResult" label="投诉结果" width="150" />
        <el-table-column width="250">
          <template #header>
            <el-input v-model="complaintKeyword" placeholder="关键字搜索" style="width: 120px" />
          </template>
          <template #default="scope">
            <el-button size="small" type="primary" @click="editComplaint(scope.row)"
              >编辑</el-button
            >
            <el-button size="small" type="danger" @click="deleteComplaint(scope.row.id)"
              >删除</el-button
            >
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <div class="pagination-container">
        <el-pagination
          v-model:current-page="complaintPagination.currentPage"
          v-model:page-size="complaintPagination.pageSize"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          :total="complaintPagination.total"
          @size-change="handleComplaintSizeChange"
          @current-change="handleComplaintCurrentChange"
        />
      </div>
    </el-card>
    <!-- æ–°å¢ž/编辑处罚对话框 -->
    <el-dialog v-model="punishmentDialogVisible" title="处罚信息" width="600px">
      <el-form :model="punishmentForm" label-width="100px">
        <el-form-item label="处罚店铺">
          <el-input v-model="punishmentForm.shopName" />
        </el-form-item>
        <el-form-item label="处罚事项">
          <el-input v-model="punishmentForm.punishmentItem" />
        </el-form-item>
        <el-form-item label="处罚时间">
          <el-date-picker v-model="punishmentForm.punishmentTime" type="datetime" />
        </el-form-item>
        <el-form-item label="处罚理由">
          <el-input v-model="punishmentForm.punishmentReason" type="textarea" />
        </el-form-item>
        <el-form-item label="处罚结果">
          <el-input v-model="punishmentForm.punishmentResult" />
        </el-form-item>
        <el-form-item label="处罚部门">
          <el-input v-model="punishmentForm.punishmentDepartment" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="punishmentDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="savePunishment">保存</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- æ–°å¢ž/编辑投诉对话框 -->
    <el-dialog v-model="complaintDialogVisible" title="投诉信息" width="600px">
      <el-form :model="complaintForm" label-width="100px">
        <el-form-item label="投诉店铺">
          <el-input v-model="complaintForm.shopName" />
        </el-form-item>
        <el-form-item label="投诉原因">
          <el-input v-model="complaintForm.complaintReason" />
        </el-form-item>
        <el-form-item label="投诉诉求">
          <el-input v-model="complaintForm.complaintRequest" type="textarea" />
        </el-form-item>
        <el-form-item label="投诉时间">
          <el-date-picker v-model="complaintForm.complaintTime" type="datetime" />
        </el-form-item>
        <el-form-item label="投诉来源">
          <el-input v-model="complaintForm.complaintSource" />
        </el-form-item>
        <el-form-item label="处理部门">
          <el-input v-model="complaintForm.handlingDepartment" />
        </el-form-item>
        <el-form-item label="投诉结果">
          <el-input v-model="complaintForm.complaintResult" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="complaintDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="saveComplaint">保存</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- å¤„罚批量导入对话框 -->
    <el-dialog v-model="punishmentImportDialogVisible" title="处罚批量导入" width="600px">
      <div class="import-container">
        <p class="import-tip">请选择要导入的Excel文件</p>
        <el-upload
          class="upload-demo"
          action="#"
          :auto-upload="false"
          :on-change="handlePunishmentFileChange"
          :file-list="punishmentImportFileList"
          accept=".xlsx,.xls"
          :limit="1"
          :on-exceed="handleExceed"
        >
          <el-button type="primary">选择文件</el-button>
          <template #tip>
            <div class="el-upload__tip">只能上传Excel文件,且不超过5MB</div>
          </template>
        </el-upload>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="punishmentImportDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="confirmPunishmentImport">导入</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- æŠ•诉批量导入对话框 -->
    <el-dialog v-model="complaintImportDialogVisible" title="投诉批量导入" width="600px">
      <div class="import-container">
        <p class="import-tip">请选择要导入的Excel文件</p>
        <el-upload
          class="upload-demo"
          action="#"
          :auto-upload="false"
          :on-change="handleComplaintFileChange"
          :file-list="complaintImportFileList"
          accept=".xlsx,.xls"
          :limit="1"
          :on-exceed="handleExceed"
        >
          <el-button type="primary">选择文件</el-button>
          <template #tip>
            <div class="el-upload__tip">只能上传Excel文件,且不超过5MB</div>
          </template>
        </el-upload>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="complaintImportDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="confirmComplaintImport">导入</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
@@ -464,137 +186,6 @@
const inspectionTrendChart = ref(null)
const problemTypeChart = ref(null)
const rectificationRateChart = ref(null)
const dailyPunishmentChart = ref(null)
const shopTypePunishmentChart = ref(null)
const dailyComplaintChart = ref(null)
const sourceComplaintChart = ref(null)
// è¡Œæ”¿å¤„罚数据
const punishmentDateRange = ref([
  dayStart.startOf('month').toDate(),
  dayEnd.endOf('month').toDate(),
])
const punishmentKeyword = ref('')
const punishmentStats = ref({
  totalCount: 120,
})
const punishmentTableData = ref([
  // {
  //   id: 1,
  //   shopName: '味美餐厅',
  //   punishmentItem: '油烟超标排放',
  //   punishmentTime: '2026-03-10 14:30',
  //   punishmentReason: '未安装油烟净化设备',
  //   punishmentResult: '罚款5000元',
  //   punishmentDepartment: '徐汇区环保局',
  // },
  // {
  //   id: 2,
  //   shopName: '香辣小龙虾',
  //   punishmentItem: '油烟超标排放',
  //   punishmentTime: '2026-03-08 10:15',
  //   punishmentReason: '油烟净化设备未正常运行',
  //   punishmentResult: '罚款3000元',
  //   punishmentDepartment: '徐汇区环保局',
  // },
])
const punishmentPagination = ref({
  currentPage: 1,
  pageSize: 10,
  total: 2,
})
// è¿‡æ»¤åŽçš„处罚数据
const filteredPunishmentData = computed(() => {
  if (!punishmentKeyword.value) {
    return punishmentTableData.value
  }
  const keyword = punishmentKeyword.value.toLowerCase()
  return punishmentTableData.value.filter((item) => {
    return Object.values(item).some((value) => {
      return String(value).toLowerCase().includes(keyword)
    })
  })
})
// ä¿¡è®¿æŠ•诉数据
const complaintDateRange = ref([dayStart.startOf('month').toDate(), dayEnd.endOf('month').toDate()])
const complaintKeyword = ref('')
const complaintStats = ref({
  totalCount: 85,
})
const complaintTableData = ref([
  // {
  //   id: 1,
  //   shopName: '鲜味馆',
  //   complaintReason: '油烟扰民',
  //   complaintRequest: '要求安装油烟净化设备',
  //   complaintTime: '2026-03-12 09:20',
  //   complaintSource: '12345热线',
  //   handlingDepartment: '徐汇区环保局',
  //   complaintResult: '已处理',
  // },
  // {
  //   id: 2,
  //   shopName: '烧烤达人',
  //   complaintReason: '夜间油烟污染',
  //   complaintRequest: '要求整改',
  //   complaintTime: '2026-03-10 22:30',
  //   complaintSource: '居民投诉',
  //   handlingDepartment: '徐汇区环保局',
  //   complaintResult: '处理中',
  // },
])
const complaintPagination = ref({
  currentPage: 1,
  pageSize: 10,
  total: 2,
})
// è¿‡æ»¤åŽçš„æŠ•诉数据
const filteredComplaintData = computed(() => {
  if (!complaintKeyword.value) {
    return complaintTableData.value
  }
  const keyword = complaintKeyword.value.toLowerCase()
  return complaintTableData.value.filter((item) => {
    return Object.values(item).some((value) => {
      return String(value).toLowerCase().includes(keyword)
    })
  })
})
// å¯¹è¯æ¡†çŠ¶æ€
const punishmentDialogVisible = ref(false)
const complaintDialogVisible = ref(false)
const punishmentImportDialogVisible = ref(false)
const complaintImportDialogVisible = ref(false)
// å¯¼å…¥æ–‡ä»¶åˆ—表
const punishmentImportFileList = ref([])
const complaintImportFileList = ref([])
// è¡¨å•数据
const punishmentForm = ref({
  id: '',
  shopName: '',
  punishmentItem: '',
  punishmentTime: '',
  punishmentReason: '',
  punishmentResult: '',
  punishmentDepartment: '',
})
const complaintForm = ref({
  id: '',
  shopName: '',
  complaintReason: '',
  complaintRequest: '',
  complaintTime: '',
  complaintSource: '',
  handlingDepartment: '',
  complaintResult: '',
})
const refreshInspectionData = () => {
  // æ¨¡æ‹Ÿåˆ·æ–°æ•°æ®
@@ -635,294 +226,8 @@
  initCharts()
}
const searchPunishment = () => {
  // æ¨¡æ‹Ÿæœç´¢å¤„罚数据
  console.log('搜索处罚数据', {
    dateRange: punishmentDateRange.value,
    keyword: punishmentKeyword.value,
  })
  // è®¡ç®—时间范围
  const startTime = punishmentDateRange.value[0]
  const endTime = punishmentDateRange.value[1]
  const startDate = dayjs(startTime)
  const endDate = dayjs(endTime)
  const daysDiff = endDate.diff(startDate, 'day')
  const months = daysDiff / 30
  // ç”Ÿæˆæ–°çš„æ¨¡æ‹Ÿæ•°æ®
  const totalCount = Math.floor(10 * months) + Math.floor(Math.random() * 3)
  punishmentStats.value = {
    totalCount,
  }
  // ç”Ÿæˆæ–°çš„处罚记录
  const newData = []
  const shopNames = [
    '味美餐厅',
    '香辣小龙虾',
    '鲜味馆',
    '烧烤达人',
    '川菜馆',
    '西餐厅',
    '日料店',
    '火锅店',
  ]
  const punishmentItems = ['油烟超标排放', '未安装油烟净化设备', '设备未正常运行', '噪声污染']
  const departments = ['徐汇区环保局', '长宁区环保局', '静安区环保局', '普陀区环保局']
  for (let i = 0; i < totalCount; i++) {
    // ç”Ÿæˆåœ¨æ—¶é—´èŒƒå›´å†…的随机时间
    const randomDays = Math.floor(Math.random() * (daysDiff + 1))
    const randomTime = startDate
      .add(randomDays, 'day')
      .add(Math.floor(Math.random() * 24), 'hour')
      .add(Math.floor(Math.random() * 60), 'minute')
    newData.push({
      id: i + 1,
      shopName: shopNames[Math.floor(Math.random() * shopNames.length)],
      punishmentItem: punishmentItems[Math.floor(Math.random() * punishmentItems.length)],
      punishmentTime: randomTime.format('YYYY-MM-DD HH:mm'),
      punishmentReason:
        punishmentItems[Math.floor(Math.random() * punishmentItems.length)] + '的违规行为',
      punishmentResult: '罚款' + (Math.floor(Math.random() * 5) + 1) * 1000 + '元',
      punishmentDepartment: departments[Math.floor(Math.random() * departments.length)],
    })
  }
  punishmentTableData.value = newData
  punishmentPagination.value.total = newData.length
  // é‡æ–°åˆå§‹åŒ–图表以更新数据
  initCharts()
}
const searchComplaint = () => {
  // æ¨¡æ‹Ÿæœç´¢æŠ•诉数据
  console.log('搜索投诉数据', {
    dateRange: complaintDateRange.value,
    keyword: complaintKeyword.value,
  })
  // è®¡ç®—时间范围
  const startTime = complaintDateRange.value[0]
  const endTime = complaintDateRange.value[1]
  const startDate = dayjs(startTime)
  const endDate = dayjs(endTime)
  const daysDiff = endDate.diff(startDate, 'day')
  const months = daysDiff / 30
  // ç”Ÿæˆæ–°çš„æ¨¡æ‹Ÿæ•°æ®
  const totalCount = Math.floor(20 * months) + Math.floor(Math.random() * 5)
  complaintStats.value = {
    totalCount,
  }
  // ç”Ÿæˆæ–°çš„æŠ•诉记录
  const newData = []
  const shopNames = [
    '鲜味馆',
    '烧烤达人',
    '味美餐厅',
    '香辣小龙虾',
    '川菜馆',
    '西餐厅',
    '日料店',
    '火锅店',
  ]
  const complaintReasons = ['油烟扰民', '夜间噪声', '异味污染', '卫生问题']
  const sources = ['12345热线', '居民投诉', '网络平台', '其他']
  const departments = ['徐汇区环保局', '长宁区环保局', '静安区环保局', '普陀区环保局']
  const results = ['已处理', '处理中', '未处理']
  for (let i = 0; i < totalCount; i++) {
    // ç”Ÿæˆåœ¨æ—¶é—´èŒƒå›´å†…的随机时间
    const randomDays = Math.floor(Math.random() * (daysDiff + 1))
    const randomTime = startDate
      .add(randomDays, 'day')
      .add(Math.floor(Math.random() * 24), 'hour')
      .add(Math.floor(Math.random() * 60), 'minute')
    newData.push({
      id: i + 1,
      shopName: shopNames[Math.floor(Math.random() * shopNames.length)],
      complaintReason: complaintReasons[Math.floor(Math.random() * complaintReasons.length)],
      complaintRequest:
        '要求整改' + complaintReasons[Math.floor(Math.random() * complaintReasons.length)],
      complaintTime: randomTime.format('YYYY-MM-DD HH:mm'),
      complaintSource: sources[Math.floor(Math.random() * sources.length)],
      handlingDepartment: departments[Math.floor(Math.random() * departments.length)],
      complaintResult: results[Math.floor(Math.random() * results.length)],
    })
  }
  complaintTableData.value = newData
  complaintPagination.value.total = newData.length
  // é‡æ–°åˆå§‹åŒ–图表以更新数据
  initCharts()
}
// ç›‘听处罚日期范围变化
watch(
  () => punishmentDateRange.value,
  () => {
    searchPunishment()
  },
  { deep: true },
)
// ç›‘听投诉日期范围变化
watch(
  () => complaintDateRange.value,
  () => {
    searchComplaint()
  },
  { deep: true },
)
const addPunishment = () => {
  // é‡ç½®è¡¨å•
  punishmentForm.value = {
    id: '',
    shopName: '',
    punishmentItem: '',
    punishmentTime: '',
    punishmentReason: '',
    punishmentResult: '',
    punishmentDepartment: '',
  }
  punishmentDialogVisible.value = true
}
const editPunishment = (row) => {
  // å¡«å……表单
  punishmentForm.value = { ...row }
  punishmentDialogVisible.value = true
}
const savePunishment = () => {
  // æ¨¡æ‹Ÿä¿å­˜æ•°æ®
  console.log('保存处罚数据', punishmentForm.value)
  punishmentDialogVisible.value = false
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„æ•°æ®ä¿å­˜é€»è¾‘
}
const deletePunishment = (id) => {
  // æ¨¡æ‹Ÿåˆ é™¤æ•°æ®
  console.log('删除处罚数据', id)
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„æ•°æ®åˆ é™¤é€»è¾‘
}
const importPunishment = () => {
  // æ‰“开导入对话框
  punishmentImportFileList.value = []
  punishmentImportDialogVisible.value = true
}
const handlePunishmentFileChange = (file, fileList) => {
  punishmentImportFileList.value = fileList
  console.log('选择的处罚文件:', file)
}
const confirmPunishmentImport = () => {
  if (punishmentImportFileList.value.length === 0) {
    ElMessage.warning('请选择要导入的文件')
    return
  }
  const file = punishmentImportFileList.value[0]
  console.log('开始导入处罚数据:', file.name)
  // é¢„留导入逻辑
  // è¿™é‡Œå°†å®žçŽ°å®žé™…çš„æ–‡ä»¶è§£æžå’Œæ•°æ®å¯¼å…¥
  punishmentImportDialogVisible.value = false
  ElMessage.success('导入操作已触发,预留导入逻辑')
}
const addComplaint = () => {
  // é‡ç½®è¡¨å•
  complaintForm.value = {
    id: '',
    shopName: '',
    complaintReason: '',
    complaintRequest: '',
    complaintTime: '',
    complaintSource: '',
    handlingDepartment: '',
    complaintResult: '',
  }
  complaintDialogVisible.value = true
}
const editComplaint = (row) => {
  // å¡«å……表单
  complaintForm.value = { ...row }
  complaintDialogVisible.value = true
}
const saveComplaint = () => {
  // æ¨¡æ‹Ÿä¿å­˜æ•°æ®
  console.log('保存投诉数据', complaintForm.value)
  complaintDialogVisible.value = false
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„æ•°æ®ä¿å­˜é€»è¾‘
}
const deleteComplaint = (id) => {
  // æ¨¡æ‹Ÿåˆ é™¤æ•°æ®
  console.log('删除投诉数据', id)
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„æ•°æ®åˆ é™¤é€»è¾‘
}
const importComplaint = () => {
  // æ‰“开导入对话框
  complaintImportFileList.value = []
  complaintImportDialogVisible.value = true
}
const handleComplaintFileChange = (file, fileList) => {
  complaintImportFileList.value = fileList
  console.log('选择的投诉文件:', file)
}
const confirmComplaintImport = () => {
  if (complaintImportFileList.value.length === 0) {
    ElMessage.warning('请选择要导入的文件')
    return
  }
  const file = complaintImportFileList.value[0]
  console.log('开始导入投诉数据:', file.name)
  // é¢„留导入逻辑
  // è¿™é‡Œå°†å®žçŽ°å®žé™…çš„æ–‡ä»¶è§£æžå’Œæ•°æ®å¯¼å…¥
  complaintImportDialogVisible.value = false
  ElMessage.success('导入操作已触发,预留导入逻辑')
}
const handleExceed = (files, fileList) => {
  ElMessage.warning('只能上传一个文件')
}
const handlePunishmentSizeChange = (size) => {
  punishmentPagination.value.pageSize = size
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„åˆ†é¡µé€»è¾‘
}
const handlePunishmentCurrentChange = (current) => {
  punishmentPagination.value.currentPage = current
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„åˆ†é¡µé€»è¾‘
}
const handleComplaintSizeChange = (size) => {
  complaintPagination.value.pageSize = size
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„åˆ†é¡µé€»è¾‘
}
const handleComplaintCurrentChange = (current) => {
  complaintPagination.value.currentPage = current
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„åˆ†é¡µé€»è¾‘
}
// åˆå§‹åŒ–图表
@@ -988,6 +293,8 @@
      series: [
        {
          type: 'pie',
          radius: ['20%', '45%'],
          center: ['50%', '45%'],
          data: [
            { value: 30, name: '油烟在线监测设备' },
            { value: 25, name: '油烟净化设施设备' },
@@ -997,182 +304,6 @@
            { value: 20, name: '空调和风机噪声' },
            { value: 18, name: '台账管理' },
            { value: 22, name: '信用承诺自评' },
          ],
          label: {
            show: true,
            formatter: '{b}: {d}%',
          },
        },
      ],
    })
  }
  // æ¯æ—¥å¤„罚数量图
  if (dailyPunishmentChart.value && punishmentDateRange.value.length > 0) {
    const chart = echarts.init(dailyPunishmentChart.value)
    // è®¡ç®—时间范围并生成x轴数据
    const startTime = punishmentDateRange.value[0]
    const endTime = punishmentDateRange.value[1]
    const startDate = dayjs(startTime)
    const endDate = dayjs(endTime)
    const daysDiff = endDate.diff(startDate, 'day')
    let xAxisData = []
    let seriesData = []
    // å¤„理处罚数据
    const punishmentData = punishmentTableData.value
    const dateFormat = daysDiff <= 30 ? 'MM/DD' : 'YYYY/MM'
    // ç”Ÿæˆæ—¥æœŸèŒƒå›´
    if (daysDiff <= 30) {
      // åŒä¸€ä¸ªæœˆå†…,按日显示
      for (let i = 0; i <= daysDiff; i++) {
        const date = startDate.add(i, 'day')
        xAxisData.push(date.format(dateFormat))
        // è®¡ç®—该日期的处罚数量
        const count = punishmentData.filter((item) => {
          const itemDate = dayjs(item.punishmentTime)
          return itemDate.format(dateFormat) === date.format(dateFormat)
        }).length
        seriesData.push(count)
      }
    } else {
      // è¶…过一个月,按月显示
      const startMonth = startDate.startOf('month')
      const endMonth = endDate.endOf('month')
      const monthsDiff = endMonth.diff(startMonth, 'month')
      for (let i = 0; i <= monthsDiff; i++) {
        const date = startMonth.add(i, 'month')
        xAxisData.push(date.format(dateFormat))
        // è®¡ç®—该月份的处罚数量
        const count = punishmentData.filter((item) => {
          const itemDate = dayjs(item.punishmentTime)
          return itemDate.format(dateFormat) === date.format(dateFormat)
        }).length
        seriesData.push(count)
      }
    }
    chart.setOption({
      xAxis: {
        type: 'category',
        data: xAxisData,
      },
      yAxis: {
        type: 'value',
      },
      series: [
        {
          data: seriesData,
          type: 'bar',
        },
      ],
    })
  }
  // åº—铺类型处罚分布图
  if (shopTypePunishmentChart.value) {
    const chart = echarts.init(shopTypePunishmentChart.value)
    chart.setOption({
      series: [
        {
          type: 'pie',
          data: [
            { value: 50, name: '中餐' },
            { value: 30, name: '烧烤' },
            { value: 20, name: '西餐' },
            { value: 15, name: '其他' },
          ],
          label: {
            show: true,
            formatter: '{b}: {d}%',
          },
        },
      ],
    })
  }
  // æ¯æ—¥æŠ•诉数量图
  if (dailyComplaintChart.value && complaintDateRange.value.length > 0) {
    const chart = echarts.init(dailyComplaintChart.value)
    // è®¡ç®—时间范围并生成x轴数据
    const startTime = complaintDateRange.value[0]
    const endTime = complaintDateRange.value[1]
    const startDate = dayjs(startTime)
    const endDate = dayjs(endTime)
    const daysDiff = endDate.diff(startDate, 'day')
    let xAxisData = []
    let seriesData = []
    // å¤„理投诉数据
    const complaintData = complaintTableData.value
    const dateFormat = daysDiff <= 30 ? 'MM/DD' : 'YYYY/MM'
    // ç”Ÿæˆæ—¥æœŸèŒƒå›´
    if (daysDiff <= 30) {
      // åŒä¸€ä¸ªæœˆå†…,按日显示
      for (let i = 0; i <= daysDiff; i++) {
        const date = startDate.add(i, 'day')
        xAxisData.push(date.format(dateFormat))
        // è®¡ç®—该日期的投诉数量
        const count = complaintData.filter((item) => {
          const itemDate = dayjs(item.complaintTime)
          return itemDate.format(dateFormat) === date.format(dateFormat)
        }).length
        seriesData.push(count)
      }
    } else {
      // è¶…过一个月,按月显示
      const startMonth = startDate.startOf('month')
      const endMonth = endDate.endOf('month')
      const monthsDiff = endMonth.diff(startMonth, 'month')
      for (let i = 0; i <= monthsDiff; i++) {
        const date = startMonth.add(i, 'month')
        xAxisData.push(date.format(dateFormat))
        // è®¡ç®—该月份的投诉数量
        const count = complaintData.filter((item) => {
          const itemDate = dayjs(item.complaintTime)
          return itemDate.format(dateFormat) === date.format(dateFormat)
        }).length
        seriesData.push(count)
      }
    }
    chart.setOption({
      xAxis: {
        type: 'category',
        data: xAxisData,
      },
      yAxis: {
        type: 'value',
      },
      series: [
        {
          data: seriesData,
          type: 'bar',
        },
      ],
    })
  }
  // æŠ•诉来源分布图
  if (sourceComplaintChart.value) {
    const chart = echarts.init(sourceComplaintChart.value)
    chart.setOption({
      series: [
        {
          type: 'pie',
          data: [
            { value: 40, name: '12345热线' },
            { value: 25, name: '居民投诉' },
            { value: 15, name: '网络平台' },
            { value: 5, name: '其他' },
          ],
          label: {
            show: true,
@@ -1230,18 +361,6 @@
  if (rectificationRateChart.value) {
    echarts.init(rectificationRateChart.value).resize()
  }
  if (dailyPunishmentChart.value) {
    echarts.init(dailyPunishmentChart.value).resize()
  }
  if (shopTypePunishmentChart.value) {
    echarts.init(shopTypePunishmentChart.value).resize()
  }
  if (dailyComplaintChart.value) {
    echarts.init(dailyComplaintChart.value).resize()
  }
  if (sourceComplaintChart.value) {
    echarts.init(sourceComplaintChart.value).resize()
  }
}
// ç›‘听params变化
@@ -1256,8 +375,6 @@
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  refreshInspectionData()
  searchPunishment()
  searchComplaint()
  initCharts()
  window.addEventListener('resize', handleResize)
})
@@ -1274,21 +391,23 @@
</script>
<style scoped>
.monitor-control {
  padding: 20px;
  background-color: #f5f7fa;
  min-height: 100vh;
.mb-4 {
  /* width: 600px; */
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
}
.filter-group {
  display: flex;
  align-items: center;
  flex-direction: column;
  align-items: flex-start;
  width: 100%;
  gap: 10px;
}
.mr-2 {
@@ -1297,6 +416,7 @@
.stats-sections {
  display: flex;
  flex-direction: column;
  gap: 20px;
  margin-bottom: 30px;
}
@@ -1304,9 +424,8 @@
.stats-section {
  flex: 1;
  background-color: #fff;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  /* border: 1px solid #e4e7ed; */
}
.stats-section h3 {
@@ -1318,8 +437,8 @@
.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-bottom: 20px;
}
@@ -1353,8 +472,8 @@
}
.chart-container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
  display: flex;
  flex-direction: column;
  gap: 20px;
  margin-bottom: 30px;
}
@@ -1387,7 +506,7 @@
.chart {
  width: 100%;
  height: 300px;
  height: 250px;
}
.pagination-container {
src/views/inspection/PunishmentManage.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,590 @@
<template>
  <div class="punishment-manage">
    <!-- é¤é¥®åº—铺行政处罚卡片 -->
    <el-card class="mb-4">
      <template #header>
        <div class="card-header">
          <span>餐饮店铺行政处罚</span>
          <div class="filter-group">
            <FYOptionTime
              class="m-r-8"
              :initValue="false"
              type="daterange"
              v-model:value="punishmentDateRange"
              style="width: 300px; margin-bottom: 0px"
              :shortcuts="shortcuts"
            ></FYOptionTime>
            <el-button type="success" icon="Plus" @click="addPunishment">新增处罚</el-button>
            <el-button type="info" icon="Upload" @click="importPunishment">批量导入</el-button>
          </div>
        </div>
      </template>
      <!-- å›¾è¡¨å±•示 -->
      <div class="chart-container">
        <div class="chart-item">
          <div class="chart-header">
            <h3>处罚数趋势</h3>
            <div class="chart-summary">处罚总数: {{ punishmentStats.totalCount }}</div>
          </div>
          <div ref="dailyPunishmentChart" class="chart"></div>
        </div>
        <div class="chart-item">
          <h3>店铺类型处罚分布</h3>
          <div ref="shopTypePunishmentChart" class="chart"></div>
        </div>
      </div>
      <!-- å¤„罚记录表格 -->
      <el-table
        :data="filteredPunishmentData"
        table-layout="fixed"
        :show-overflow-tooltip="true"
        height="400px"
        border
      >
        <el-table-column prop="shopName" label="处罚店铺" />
        <el-table-column prop="punishmentItem" label="处罚事项" width="180" />
        <el-table-column prop="punishmentTime" label="处罚时间" width="180" />
        <el-table-column prop="punishmentReason" label="处罚理由" width="200" />
        <el-table-column prop="punishmentResult" label="处罚结果" width="150" />
        <el-table-column prop="punishmentDepartment" label="处罚部门" width="150" />
        <el-table-column width="250">
          <template #header>
            <el-input v-model="punishmentKeyword" placeholder="关键字搜索" style="width: 120px" />
          </template>
          <template #default="scope">
            <el-button size="small" type="primary" @click="editPunishment(scope.row)"
              >编辑</el-button
            >
            <el-button size="small" type="danger" @click="deletePunishment(scope.row.id)"
              >删除</el-button
            >
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <div class="pagination-container">
        <el-pagination
          v-model:current-page="punishmentPagination.currentPage"
          v-model:page-size="punishmentPagination.pageSize"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          :total="punishmentPagination.total"
          @size-change="handlePunishmentSizeChange"
          @current-change="handlePunishmentCurrentChange"
        />
      </div>
    </el-card>
    <!-- æ–°å¢ž/编辑处罚对话框 -->
    <el-dialog v-model="punishmentDialogVisible" title="处罚信息" width="600px">
      <el-form :model="punishmentForm" label-width="100px">
        <el-form-item label="处罚店铺">
          <el-input v-model="punishmentForm.shopName" />
        </el-form-item>
        <el-form-item label="处罚事项">
          <el-input v-model="punishmentForm.punishmentItem" />
        </el-form-item>
        <el-form-item label="处罚时间">
          <el-date-picker v-model="punishmentForm.punishmentTime" type="datetime" />
        </el-form-item>
        <el-form-item label="处罚理由">
          <el-input v-model="punishmentForm.punishmentReason" type="textarea" />
        </el-form-item>
        <el-form-item label="处罚结果">
          <el-input v-model="punishmentForm.punishmentResult" />
        </el-form-item>
        <el-form-item label="处罚部门">
          <el-input v-model="punishmentForm.punishmentDepartment" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="punishmentDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="savePunishment">保存</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- å¤„罚批量导入对话框 -->
    <el-dialog v-model="punishmentImportDialogVisible" title="处罚批量导入" width="600px">
      <div class="import-container">
        <p class="import-tip">请选择要导入的Excel文件</p>
        <el-upload
          class="upload-demo"
          action="#"
          drag
          :auto-upload="false"
          :on-change="handlePunishmentFileChange"
          :file-list="punishmentImportFileList"
          accept=".xlsx,.xls"
          :limit="1"
          :on-exceed="handleExceed"
        >
          <el-icon class="el-icon--upload"><upload-filled /></el-icon>
          <div class="el-upload__text">拖动文件或<em>点击上传</em></div>
          <template #tip>
            <div class="el-upload__tip">只能上传Excel文件,且不超过5MB</div>
          </template>
        </el-upload>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="punishmentImportDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="confirmPunishmentImport">导入</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
import * as echarts from 'echarts'
import dayjs from 'dayjs'
import { ElMessage } from 'element-plus'
// æ—¶é—´èŒƒå›´å¿«æ·é€‰é¡¹
const dayStart = dayjs('2023-08-01').startOf('date')
const dayEnd = dayStart.endOf('date')
const shortcuts = [
  {
    text: '今天',
    value: [dayStart.toDate(), dayEnd.toDate()],
  },
  {
    text: '本周',
    value: [dayStart.startOf('week').toDate(), dayEnd.endOf('week').toDate()],
  },
  {
    text: '上周',
    value: [dayStart.day(-7).toDate(), dayEnd.day(-1).toDate()],
  },
  {
    text: '本月',
    value: [dayStart.startOf('month').toDate(), dayEnd.endOf('month').toDate()],
  },
  {
    text: '上月',
    value: [
      dayStart.subtract(1, 'month').startOf('month').toDate(),
      dayEnd.subtract(1, 'month').endOf('month').toDate(),
    ],
  },
  {
    text: '本季度',
    value: [dayStart.startOf('quarter').toDate(), dayEnd.endOf('quarter').toDate()],
  },
  {
    text: '上季度',
    value: [
      dayStart.subtract(1, 'quarter').startOf('quarter').toDate(),
      dayEnd.subtract(1, 'quarter').endOf('quarter').toDate(),
    ],
  },
  {
    text: '去年',
    value: [
      dayStart.subtract(1, 'year').startOf('year').toDate(),
      dayEnd.subtract(1, 'year').endOf('year').toDate(),
    ],
  },
  {
    text: '今年',
    value: [dayStart.startOf('year').toDate(), dayEnd.endOf('year').toDate()],
  },
]
// å›¾è¡¨å¼•用
const dailyPunishmentChart = ref(null)
const shopTypePunishmentChart = ref(null)
// è¡Œæ”¿å¤„罚数据
const punishmentDateRange = ref([
  dayStart.startOf('month').toDate(),
  dayEnd.endOf('month').toDate(),
])
const punishmentKeyword = ref('')
const punishmentStats = ref({
  totalCount: 120,
})
const punishmentTableData = ref([])
const punishmentPagination = ref({
  currentPage: 1,
  pageSize: 10,
  total: 0,
})
// è¿‡æ»¤åŽçš„处罚数据
const filteredPunishmentData = computed(() => {
  if (!punishmentKeyword.value) {
    return punishmentTableData.value
  }
  const keyword = punishmentKeyword.value.toLowerCase()
  return punishmentTableData.value.filter((item) => {
    return Object.values(item).some((value) => {
      return String(value).toLowerCase().includes(keyword)
    })
  })
})
// å¯¹è¯æ¡†çŠ¶æ€
const punishmentDialogVisible = ref(false)
const punishmentImportDialogVisible = ref(false)
// å¯¼å…¥æ–‡ä»¶åˆ—表
const punishmentImportFileList = ref([])
// è¡¨å•数据
const punishmentForm = ref({
  id: '',
  shopName: '',
  punishmentItem: '',
  punishmentTime: '',
  punishmentReason: '',
  punishmentResult: '',
  punishmentDepartment: '',
})
const searchPunishment = () => {
  // æ¨¡æ‹Ÿæœç´¢å¤„罚数据
  console.log('搜索处罚数据', {
    dateRange: punishmentDateRange.value,
    keyword: punishmentKeyword.value,
  })
  // è®¡ç®—时间范围
  const startTime = punishmentDateRange.value[0]
  const endTime = punishmentDateRange.value[1]
  const startDate = dayjs(startTime)
  const endDate = dayjs(endTime)
  const daysDiff = endDate.diff(startDate, 'day')
  const months = daysDiff / 30
  // ç”Ÿæˆæ–°çš„æ¨¡æ‹Ÿæ•°æ®
  const totalCount = Math.floor(10 * months) + Math.floor(Math.random() * 3)
  punishmentStats.value = {
    totalCount,
  }
  // ç”Ÿæˆæ–°çš„处罚记录
  const newData = []
  const shopNames = [
    '味美餐厅',
    '香辣小龙虾',
    '鲜味馆',
    '烧烤达人',
    '川菜馆',
    '西餐厅',
    '日料店',
    '火锅店',
  ]
  const punishmentItems = ['油烟超标排放', '未安装油烟净化设备', '设备未正常运行', '噪声污染']
  const departments = ['徐汇区环保局', '长宁区环保局', '静安区环保局', '普陀区环保局']
  for (let i = 0; i < totalCount; i++) {
    // ç”Ÿæˆåœ¨æ—¶é—´èŒƒå›´å†…的随机时间
    const randomDays = Math.floor(Math.random() * (daysDiff + 1))
    const randomTime = startDate
      .add(randomDays, 'day')
      .add(Math.floor(Math.random() * 24), 'hour')
      .add(Math.floor(Math.random() * 60), 'minute')
    newData.push({
      id: i + 1,
      shopName: shopNames[Math.floor(Math.random() * shopNames.length)],
      punishmentItem: punishmentItems[Math.floor(Math.random() * punishmentItems.length)],
      punishmentTime: randomTime.format('YYYY-MM-DD HH:mm'),
      punishmentReason:
        punishmentItems[Math.floor(Math.random() * punishmentItems.length)] + '的违规行为',
      punishmentResult: '罚款' + (Math.floor(Math.random() * 5) + 1) * 1000 + '元',
      punishmentDepartment: departments[Math.floor(Math.random() * departments.length)],
    })
  }
  punishmentTableData.value = newData
  punishmentPagination.value.total = newData.length
  // é‡æ–°åˆå§‹åŒ–图表以更新数据
  initCharts()
}
// ç›‘听处罚日期范围变化
watch(
  () => punishmentDateRange.value,
  () => {
    searchPunishment()
  },
  { deep: true },
)
const addPunishment = () => {
  // é‡ç½®è¡¨å•
  punishmentForm.value = {
    id: '',
    shopName: '',
    punishmentItem: '',
    punishmentTime: '',
    punishmentReason: '',
    punishmentResult: '',
    punishmentDepartment: '',
  }
  punishmentDialogVisible.value = true
}
const editPunishment = (row) => {
  // å¡«å……表单
  punishmentForm.value = { ...row }
  punishmentDialogVisible.value = true
}
const savePunishment = () => {
  // æ¨¡æ‹Ÿä¿å­˜æ•°æ®
  console.log('保存处罚数据', punishmentForm.value)
  punishmentDialogVisible.value = false
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„æ•°æ®ä¿å­˜é€»è¾‘
}
const deletePunishment = (id) => {
  // æ¨¡æ‹Ÿåˆ é™¤æ•°æ®
  console.log('删除处罚数据', id)
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„æ•°æ®åˆ é™¤é€»è¾‘
}
const importPunishment = () => {
  // æ‰“开导入对话框
  punishmentImportFileList.value = []
  punishmentImportDialogVisible.value = true
}
const handlePunishmentFileChange = (file, fileList) => {
  punishmentImportFileList.value = fileList
  console.log('选择的处罚文件:', file)
}
const confirmPunishmentImport = () => {
  if (punishmentImportFileList.value.length === 0) {
    ElMessage.warning('请选择要导入的文件')
    return
  }
  const file = punishmentImportFileList.value[0]
  console.log('开始导入处罚数据:', file.name)
  // é¢„留导入逻辑
  // è¿™é‡Œå°†å®žçŽ°å®žé™…çš„æ–‡ä»¶è§£æžå’Œæ•°æ®å¯¼å…¥
  punishmentImportDialogVisible.value = false
  ElMessage.success('导入操作已触发,预留导入逻辑')
}
const handleExceed = (files, fileList) => {
  ElMessage.warning('只能上传一个文件')
}
const handlePunishmentSizeChange = (size) => {
  punishmentPagination.value.pageSize = size
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„åˆ†é¡µé€»è¾‘
}
const handlePunishmentCurrentChange = (current) => {
  punishmentPagination.value.currentPage = current
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„åˆ†é¡µé€»è¾‘
}
// åˆå§‹åŒ–图表
const initCharts = () => {
  // æ¯æ—¥å¤„罚数量图
  if (dailyPunishmentChart.value && punishmentDateRange.value.length > 0) {
    const chart = echarts.init(dailyPunishmentChart.value)
    // è®¡ç®—时间范围并生成x轴数据
    const startTime = punishmentDateRange.value[0]
    const endTime = punishmentDateRange.value[1]
    const startDate = dayjs(startTime)
    const endDate = dayjs(endTime)
    const daysDiff = endDate.diff(startDate, 'day')
    let xAxisData = []
    let seriesData = []
    // å¤„理处罚数据
    const punishmentData = punishmentTableData.value
    const dateFormat = daysDiff <= 30 ? 'MM/DD' : 'YYYY/MM'
    // ç”Ÿæˆæ—¥æœŸèŒƒå›´
    if (daysDiff <= 30) {
      // åŒä¸€ä¸ªæœˆå†…,按日显示
      for (let i = 0; i <= daysDiff; i++) {
        const date = startDate.add(i, 'day')
        xAxisData.push(date.format(dateFormat))
        // è®¡ç®—该日期的处罚数量
        const count = punishmentData.filter((item) => {
          const itemDate = dayjs(item.punishmentTime)
          return itemDate.format(dateFormat) === date.format(dateFormat)
        }).length
        seriesData.push(count)
      }
    } else {
      // è¶…过一个月,按月显示
      const startMonth = startDate.startOf('month')
      const endMonth = endDate.endOf('month')
      const monthsDiff = endMonth.diff(startMonth, 'month')
      for (let i = 0; i <= monthsDiff; i++) {
        const date = startMonth.add(i, 'month')
        xAxisData.push(date.format(dateFormat))
        // è®¡ç®—该月份的处罚数量
        const count = punishmentData.filter((item) => {
          const itemDate = dayjs(item.punishmentTime)
          return itemDate.format(dateFormat) === date.format(dateFormat)
        }).length
        seriesData.push(count)
      }
    }
    chart.setOption({
      xAxis: {
        type: 'category',
        data: xAxisData,
      },
      yAxis: {
        type: 'value',
      },
      series: [
        {
          data: seriesData,
          type: 'bar',
        },
      ],
    })
  }
  // åº—铺类型处罚分布图
  if (shopTypePunishmentChart.value) {
    const chart = echarts.init(shopTypePunishmentChart.value)
    chart.setOption({
      series: [
        {
          type: 'pie',
          data: [
            { value: 50, name: '中餐' },
            { value: 30, name: '烧烤' },
            { value: 20, name: '西餐' },
            { value: 15, name: '其他' },
          ],
          label: {
            show: true,
            formatter: '{b}: {d}%',
          },
        },
      ],
    })
  }
}
// ç›‘听窗口大小变化
const handleResize = () => {
  // é‡æ–°è°ƒæ•´å›¾è¡¨å¤§å°
  if (dailyPunishmentChart.value) {
    echarts.init(dailyPunishmentChart.value).resize()
  }
  if (shopTypePunishmentChart.value) {
    echarts.init(shopTypePunishmentChart.value).resize()
  }
}
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  searchPunishment()
  initCharts()
  window.addEventListener('resize', handleResize)
})
// ç»„件卸载时清理事件监听
onUnmounted(() => {
  cleanup()
})
// æ¸…理
const cleanup = () => {
  window.removeEventListener('resize', handleResize)
}
</script>
<style scoped>
.punishment-manage {
  padding: 20px;
  background-color: #f5f7fa;
  min-height: 100vh;
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.filter-group {
  display: flex;
  align-items: center;
}
.chart-container {
  display: flex;
  gap: 20px;
  margin-bottom: 30px;
}
.chart-item {
  flex: 1;
  background-color: #fff;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.chart-item h3 {
  margin-bottom: 15px;
  font-size: 16px;
  font-weight: 600;
  color: #303133;
}
.chart-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
}
.chart-summary {
  font-size: 14px;
  color: #606266;
}
.chart {
  height: 300px;
}
.pagination-container {
  margin-top: 20px;
  display: flex;
  justify-content: flex-end;
}
.import-container {
  padding: 20px;
}
.import-tip {
  margin-bottom: 20px;
  color: #606266;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
}
</style>
src/views/inspection/task/components/CompMonitorPlan.vue
@@ -1,19 +1,13 @@
<template>
  <el-calendar
    v-model="dateValue"
    :range="dateRange"
    @update:model-value="onDateChange"
  >
  <el-calendar v-model="dateValue" :range="dateRange" @update:model-value="onDateChange">
    <template #header="{ date }">
      <div style="width: 100%">
        <el-row justify="space-between">
          <el-space>
            <el-tag type="default"
              >巡查量:{{
                `${taskStatistic.complete}/${taskStatistic.total}`
              }}</el-tag
              >巡查量:{{ `${taskStatistic.complete}/${taskStatistic.total}` }}</el-tag
            >
            <el-tag type="default"
            <!-- <el-tag type="default"
              >当日整改率:{{
                formatPercent(
                  taskStatistic.changedProblemNumOnTime /
@@ -28,18 +22,15 @@
                    taskStatistic.totalProblemNum
                )
              }}</el-tag
            >
            > -->
            <el-tag type="default"
              >综合整改率:{{
                formatPercent(
                  taskStatistic.changedProblemNum /
                    taskStatistic.totalProblemNum
                )
              >整改率:{{
                formatPercent(taskStatistic.changedProblemNum / taskStatistic.totalProblemNum)
              }}</el-tag
            >
            <!-- <el-tag type="default">整改:{{ taskStatistic.changed }}</el-tag> -->
          </el-space>
          <el-space>
          <!-- <el-space>
            <el-text>聚焦用户:</el-text>
            <el-select
              v-model="selectedUsers"
@@ -56,24 +47,15 @@
                :value="user.userName"
              />
            </el-select>
          </el-space>
          </el-space> -->
        </el-row>
        <el-row class="m-t-4">
        <!-- <el-row class="m-t-4">
          <el-space wrap>
            <el-tag
              type="default"
              v-for="user in taskStatistic.progressPerUser"
              :key="user.userId"
            >
              <!-- {{
                `${user.userName}:巡查量 ${
                  user.completeTaskNum
                },即时整改率 ${formatPercent(
                  user.changedProblemNumOnTime / user.totalProblemNum
                )},平均耗时 ${
                  user.avgInspectionTime ? user.avgInspectionTime : '--'
                }`
              }} -->
                {{
                `${user.userName}:${
                  user.completeTaskNum
@@ -85,17 +67,14 @@
              }}
            </el-tag>
          </el-space>
        </el-row>
        </el-row> -->
      </div>
    </template>
    <template #date-cell="{ data }">
      <div :class="calendarDayClz(data.day)">
        <div style="background-color: #f8f4f4">{{ getDay(data.day) }}</div>
        <template v-if="computeDayTask(data.day)">
          <el-row
            v-if="computeDayTask(data.day).totalTaskNum > 0"
            justify="space-between"
          >
          <el-row v-if="computeDayTask(data.day).totalTaskNum > 0" justify="space-between">
            <el-space direction="vertical">
              <el-text size="small" tag="b">巡查量</el-text>
              <el-text size="small"
@@ -108,7 +87,7 @@
              <el-text size="small">{{
                formatPercent(
                  computeDayTask(data.day).changedProblemNum /
                    computeDayTask(data.day).totalProblemNum
                    computeDayTask(data.day).totalProblemNum,
                )
              }}</el-text>
            </el-space>
@@ -123,9 +102,7 @@
              <el-text
                title="巡查人员"
                size="small"
                :type="
                  selectedUsers.includes(item.userName) ? 'primary' : 'info'
                "
                :type="selectedUsers.includes(item.userName) ? 'primary' : 'info'"
                :tag="selectedUsers.includes(item.userName) ? 'b' : 'span'"
                >{{ item.userName }}</el-text
              >
@@ -136,9 +113,7 @@
                title="巡查量"
                size="small"
                style="text-align: center; flex: 1"
                :type="
                  selectedUsers.includes(item.userName) ? 'primary' : 'info'
                "
                :type="selectedUsers.includes(item.userName) ? 'primary' : 'info'"
                :tag="selectedUsers.includes(item.userName) ? 'b' : 'span'"
                >{{ item.completeTaskNum }}</el-text
              >
@@ -146,27 +121,17 @@
                title="当日整改率"
                size="small"
                style="text-align: center; flex: 1"
                :type="
                  selectedUsers.includes(item.userName) ? 'primary' : 'info'
                "
                :type="selectedUsers.includes(item.userName) ? 'primary' : 'info'"
                :tag="selectedUsers.includes(item.userName) ? 'b' : 'span'"
                >{{
                  formatPercent(
                    item.changedProblemNumOnTime / item.totalProblemNum
                  )
                }}</el-text
                >{{ formatPercent(item.changedProblemNumOnTime / item.totalProblemNum) }}</el-text
              >
              <el-text
                title="平均耗时"
                size="small"
                style="text-align: center; flex: 1"
                :type="
                  selectedUsers.includes(item.userName) ? 'primary' : 'info'
                "
                :type="selectedUsers.includes(item.userName) ? 'primary' : 'info'"
                :tag="selectedUsers.includes(item.userName) ? 'b' : 'span'"
                >{{
                  timeUtil.formatSecondsToChinese(item.avgInspectionTime)
                }}</el-text
                >{{ timeUtil.formatSecondsToChinese(item.avgInspectionTime) }}</el-text
              >
              <!-- </el-space> -->
            </el-row>
@@ -177,52 +142,43 @@
  </el-calendar>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
import taskApi from '@/api/fysp/taskApi';
import dayjs from 'dayjs';
import timeUtil from '@/utils/time-util';
import { ref, computed, onMounted, watch } from 'vue'
import taskApi from '@/api/fysp/taskApi'
import dayjs from 'dayjs'
import timeUtil from '@/utils/time-util'
const props = defineProps({
  task: {
    type: Object,
    default: () => {}
    default: () => {},
  },
  dayTaskList: {
    type: Array,
    default: () => []
  }
});
const emit = defineEmits(['dateChange']);
    default: () => [],
  },
})
const emit = defineEmits(['dateChange'])
// é€‰ä¸­æ—¥æœŸ
const selectedUsers = ref([]);
const selectedUsers = ref([])
// const dateValue = ref(new Date());
const dateValue = ref();
const dateValue = ref()
// æ—¥åŽ†èŒƒå›´
const startDay = computed(() => dayjs(props.task.starttime));
const endDay = computed(() => dayjs(props.task.endtime));
const dateRange = computed(() => [
  startDay.value.toDate(),
  endDay.value.toDate()
]);
const startDay = computed(() => dayjs(props.task.starttime))
const endDay = computed(() => dayjs(props.task.endtime))
const dateRange = computed(() => [startDay.value.toDate(), endDay.value.toDate()])
// æ—¥æœŸæ˜¯å¦åœ¨ä»»åŠ¡èŒƒå›´å†…
function isDayEnable(day) {
  const _day = dayjs(day);
  return (
    _day.isSameOrAfter(startDay.value, 'day') &&
    _day.isSameOrBefore(endDay.value, 'day')
  );
  const _day = dayjs(day)
  return _day.isSameOrAfter(startDay.value, 'day') && _day.isSameOrBefore(endDay.value, 'day')
}
/********************** æ—¥æœŸæ ·å¼ *********************************/
function calendarDayClz(day) {
  return (
    'calendar-day ' +
    (isDayEnable(day) ? 'calendar-day-enable' : 'calendar-day-disable')
  );
  return 'calendar-day ' + (isDayEnable(day) ? 'calendar-day-enable' : 'calendar-day-disable')
}
function getDay(day) {
  return day.split('-').splice(1, 2).join('-');
  return day.split('-').splice(1, 2).join('-')
}
/********************** ä»»åŠ¡æ•°æ® *********************************/
@@ -231,11 +187,11 @@
  () => props.dayTaskList,
  (nV, oV) => {
    if (nV && dateValue.value) {
      onDateChange(dateValue.value);
      onDateChange(dateValue.value)
    }
  },
  { immediate: false }
);
  { immediate: false },
)
// // èŽ·å–æ—¥ä»»åŠ¡ç»Ÿè®¡ä¿¡æ¯
// const dayTaskLoading = ref(false);
@@ -255,26 +211,26 @@
// }
// æ—¥ä»»åŠ¡æ•°æ®å±•ç¤º
const compMap = new Map();
const compMap = new Map()
function computeDayTask(day) {
  const key = props.task.tguid + day;
  const key = props.task.tguid + day
  if (compMap.has(key)) {
    return compMap.get(key).value;
    return compMap.get(key).value
  }
  const result = computed(() => {
    return props.dayTaskList.find((v) => {
      return dayjs(v.date).isSame(dayjs(day));
    });
  });
  compMap.set(key, result);
  return result.value;
      return dayjs(v.date).isSame(dayjs(day))
    })
  })
  compMap.set(key, result)
  return result.value
}
function onDateChange(e) {
  if (isDayEnable(e)) {
    const day = dayjs(e).format('YYYY-MM-DD');
    const t = computeDayTask(day);
    emit('dateChange', t, day);
    const day = dayjs(e).format('YYYY-MM-DD')
    const t = computeDayTask(day)
    emit('dateChange', t, day)
  }
}
@@ -288,15 +244,15 @@
    changedProblemNum: 0,
    totalProblemNum: 0,
    changedProblemNumOnTime: 0,
    efficientChangedProNum: 0
  };
  const userMap = new Map();
    efficientChangedProNum: 0,
  }
  const userMap = new Map()
  props.dayTaskList.forEach((e) => {
    res.total += e.totalTaskNum;
    res.complete += e.completeTaskNum;
    res.changed += e.changedTaskNum;
    res.changedProblemNum += e.changedProblemNum;
    res.totalProblemNum += e.totalProblemNum;
    res.total += e.totalTaskNum
    res.complete += e.completeTaskNum
    res.changed += e.changedTaskNum
    res.changedProblemNum += e.changedProblemNum
    res.totalProblemNum += e.totalProblemNum
    e.progressPerUser.forEach((user) => {
      if (!userMap.has(user.userId)) {
        userMap.set(user.userId, {
@@ -305,32 +261,30 @@
          changedProblemNumOnTime: 0,
          totalProblemNum: 0,
          totalInspectionTime: 0,
          dayTaskNum: 0
        });
          dayTaskNum: 0,
        })
      }
      res.changedProblemNumOnTime += user.changedProblemNumOnTime
      res.efficientChangedProNum += user.efficientChangedProNum
      const userItem = userMap.get(user.userId);
      userItem.completeTaskNum += user.completeTaskNum;
      userItem.changedProblemNumOnTime += user.changedProblemNumOnTime;
      userItem.totalProblemNum += user.totalProblemNum;
      userItem.totalInspectionTime += user.avgInspectionTime ?? 0;
      userItem.dayTaskNum++;
    });
  });
      const userItem = userMap.get(user.userId)
      userItem.completeTaskNum += user.completeTaskNum
      userItem.changedProblemNumOnTime += user.changedProblemNumOnTime
      userItem.totalProblemNum += user.totalProblemNum
      userItem.totalInspectionTime += user.avgInspectionTime ?? 0
      userItem.dayTaskNum++
    })
  })
  res.progressPerUser = Array.from(userMap.values()).map((user) => ({
    ...user,
    completeTaskNum: Math.round(user.completeTaskNum * 100) / 100,
    avgInspectionTime: timeUtil.formatSecondsToChinese(
      user.totalInspectionTime / user.dayTaskNum
    )
  }));
  return res;
});
    avgInspectionTime: timeUtil.formatSecondsToChinese(user.totalInspectionTime / user.dayTaskNum),
  }))
  return res
})
const formatPercent = (num) => {
  return isNaN(num) ? '0%' : parseInt(num * 100) + '%';
};
  return isNaN(num) ? '0%' : parseInt(num * 100) + '%'
}
/********************** åˆå§‹åŒ– *********************************/
// watch(
src/views/monitor/DataDashboard.vue
@@ -4,7 +4,7 @@
    <div class="top-cards">
      <!-- æ—¶é—´å‘¨æœŸé€‰é¡¹å¡ç‰‡ -->
      <div class="time-period-card">
        <div class="card-title">时间选择</div>
        <!-- <div class="card-title">时间选择</div> -->
        <div class="time-controls">
          <div class="time-tab-container">
            <div
@@ -57,10 +57,10 @@
        </div>
      </div>
      <div class="cards-container">
        <!-- è¶…标数 -->
        <!-- æµ“度预警 -->
        <div class="metric-card">
          <div class="card-header">
            <div class="card-title">{{ getPeriodLabel() }}超标数</div>
            <div class="card-title">{{ getPeriodLabel() }}浓度预警</div>
            <div class="card-icon warning-icon">
              <svg
                width="20"
@@ -86,7 +86,7 @@
              </svg>
            </div>
          </div>
          <div class="card-value">{{ metrics.overStandardCount }}</div>
          <div class="card-value">{{ metrics.overStandardCount }}<el-text>次</el-text></div>
          <div class="card-trend">
            <span
              class="trend-arrow"
@@ -99,10 +99,35 @@
          </div>
        </div>
        <!-- åœ¨çº¿çއ -->
        <!-- è®¾å¤‡åœ¨çº¿çއ -->
        <el-popover placement="right-start" title="设备监控" width="400" trigger="click">
          <div class="popover-content">
            <div class="overview-items-container">
              <div class="overview-item">
                <div class="overview-label">餐饮店铺总数</div>
                <div class="overview-value">{{ overview.totalShops }}</div>
              </div>
              <div class="overview-item">
                <div class="overview-label">在线设备数</div>
                <div class="overview-value">{{ overview.onlineDevices }}</div>
              </div>
              <div class="overview-item">
                <div class="overview-label">离线设备数</div>
                <div class="overview-value">{{ overview.offlineDevices }}</div>
              </div>
            </div>
            <!-- è®¾å¤‡çŠ¶æ€é¥¼å›¾ -->
            <div class="device-status-chart">
              <canvas id="deviceStatusChart"></canvas>
            </div>
          </div>
          <template #reference>
        <div class="metric-card">
          <div class="card-header">
            <div class="card-title">在线率</div>
                <div class="card-title">设备在线率</div>
            <div class="card-icon online-icon">
              <svg
                width="20"
@@ -139,12 +164,32 @@
            <span class="trend-text">{{ Math.abs(metrics.onlineRateTrend) }}%</span>
            <span class="trend-label">{{ getCompareLabel() }}</span>
          </div>
              <div class="view-details">
                <span>详情</span>
                <svg
                  width="12"
                  height="12"
                  viewBox="0 0 24 24"
                  fill="none"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path
                    d="M9 18L15 12L9 6"
                    stroke="currentColor"
                    stroke-width="2"
                    stroke-linecap="round"
                    stroke-linejoin="round"
                  />
                </svg>
        </div>
            </div>
          </template>
        </el-popover>
        <!-- å‡€åŒ–器运行效率 -->
        <!-- çŽ¯ä¿¡ç ç»¿ç çŽ‡ -->
        <div class="metric-card">
          <div class="card-header">
            <div class="card-title">净化器运行效率</div>
            <div class="card-title">环信码绿码率</div>
            <div class="card-icon efficiency-icon">
              <svg
                width="20"
@@ -187,170 +232,8 @@
        </div>
        <!-- å·¡æŸ¥ç‚¹æ¬¡ -->
        <div class="metric-card">
          <div class="card-header">
            <div class="card-title">巡查点次</div>
            <div class="card-icon task-icon">
              <svg
                width="20"
                height="20"
                viewBox="0 0 24 24"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path
                  d="M22 11.08V12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C15.7376 2 19.0503 4.16113 20.7748 7.33007"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
                <path
                  d="M22 4L12 14.01L9 11.01"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
              </svg>
            </div>
          </div>
          <div class="card-value">{{ metrics.inspectionPoints }}</div>
          <div class="card-trend">
            <span
              class="trend-arrow"
              :class="{
                up: metrics.inspectionPointsTrend > 0,
                down: metrics.inspectionPointsTrend < 0,
              }"
            >
              {{ metrics.inspectionPointsTrend > 0 ? '↑' : '↓' }}
            </span>
            <span class="trend-text">{{ Math.abs(metrics.inspectionPointsTrend) }}</span>
            <span class="trend-label">{{ getCompareLabel() }}</span>
          </div>
        </div>
      </div>
    </div>
    <!-- ä¸»è¦å†…容区 -->
    <div class="main-content">
      <!-- ä¸­éƒ¨GIS地图区 -->
      <div class="map-section">
        <div id="map" class="map-container">
          <BaseMap :showSatellite="true"></BaseMap>
        </div>
        <!-- åœ°å›¾ç‚¹ä½å¼¹çª— -->
        <el-dialog v-model="dialogVisible" title="企业实时数据" width="400px">
          <div class="dialog-content">
            <el-descriptions :column="1" border>
              <el-descriptions-item label="企业名称">{{
                selectedPoint.enterpriseName
              }}</el-descriptions-item>
              <el-descriptions-item label="设备编号">{{
                selectedPoint.deviceId
              }}</el-descriptions-item>
              <el-descriptions-item label="油烟浓度"
                >{{ selectedPoint.oilSmokeConcentration }} mg/m³</el-descriptions-item
              >
              <el-descriptions-item label="颗粒物"
                >{{ selectedPoint.particulateMatter }} mg/m³</el-descriptions-item
              >
              <el-descriptions-item label="非甲烷总烃"
                >{{ selectedPoint.nonMethaneHydrocarbon }} mg/m³</el-descriptions-item
              >
              <el-descriptions-item label="监测时间">{{
                selectedPoint.monitoringTime
              }}</el-descriptions-item>
              <el-descriptions-item label="超标情况">
                <el-tag :type="selectedPoint.isOverStandard ? 'danger' : 'success'">
                  {{ selectedPoint.isOverStandard ? '超标' : '正常' }}
                </el-tag>
              </el-descriptions-item>
            </el-descriptions>
          </div>
          <template #footer>
            <span class="dialog-footer">
              <el-button @click="dialogVisible = false">关闭</el-button>
              <el-button type="primary" @click="viewDetails">查看详情</el-button>
            </span>
          </template>
        </el-dialog>
      </div>
    </div>
    <!-- å³ä¾§å®žæ—¶ç›‘测总览区 -->
    <div class="overview-section">
      <div class="section-header">
        <h3>设备监控</h3>
        <!-- <span class="view-more">查看更多</span> -->
      </div>
      <div class="overview-items-container">
        <div class="overview-item">
          <div class="overview-label">餐饮店铺总数</div>
          <div class="overview-value">{{ overview.totalShops }}</div>
        </div>
        <div class="overview-item">
          <div class="overview-label">在线设备数</div>
          <div class="overview-value">{{ overview.onlineDevices }}</div>
        </div>
        <div class="overview-item">
          <div class="overview-label">离线设备数</div>
          <div class="overview-value">{{ overview.offlineDevices }}</div>
        </div>
      </div>
      <!-- è®¾å¤‡çŠ¶æ€é¥¼å›¾ -->
      <div class="device-status-chart">
        <canvas id="deviceStatusChart"></canvas>
      </div>
    </div>
    <!-- åœ°å›¾å›¾ä¾‹ -->
    <div class="map-legend">
      <div class="legend-header">
        <h4>图例</h4>
      </div>
      <div class="legend-items">
        <div class="legend-item">
          <img src="@/assets/exceed.png" alt="油烟浓度超标" class="legend-icon" />
          <span class="legend-text">油烟浓度超标</span>
        </div>
        <div class="legend-item">
          <img src="@/assets/exception.png" alt="供电异常" class="legend-icon" />
          <span class="legend-text">供电异常</span>
        </div>
        <div class="legend-item">
          <img src="@/assets/offline.png" alt="设备或网络异常" class="legend-icon" />
          <span class="legend-text">设备或网络异常</span>
        </div>
        <div class="legend-item">
          <img
            src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSI1IiB5PSI4IiB3aWR0aD0iMjIiIGhlaWdodD0iMTYiIHJ4PSIzIiBmaWxsPSIjNTJjNDFhIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiLz48cGF0aCBkPSJNNSA4IFEgMTYgMyAyNyA4IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9IiMzODllMGQiLz48cGF0aCBkPSJNNSAyNCBRIDE2IDI5IDI3IDI0IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9IiM2NjY2NjYiLz48cmVjdCB4PSI4IiB5PSIxMSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjEwIiByeD0iMiIgZmlsbD0id2hpdGUiLz48cGF0aCBkPSJNMTIgMTQgTCAyMSAxNCIgc3Ryb2tlPSIjNTJjNDFhIiBzdHJva2Utd2lkdGg9IjEuNSIvPjxwYXRoIGQ9Ik0xMiAxNyBMIDE4IDE3IiBzdHJva2U9IiM1MmM0MWEiIHN0cm9rZS13aWR0aD0iMS41Ii8+PHBhdGggZD0iTTEyIDIwIEwgMTUgMjAiIHN0cm9rZT0iIzUyYzQxYSIgc3Ryb2tlLXdpZHRoPSIxLjUiLz48bGluZSB4MT0iMTYiIHkxPSI4IiB4Mj0iMTYiIHkyPSIzIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEuNSIvPjxjaXJjbGUgY3g9IjE2IiBjeT0iMyIgcj0iMS41IiBmaWxsPSJ3aGl0ZSIvPjxjaXJjbGUgY3g9IjI3IiBjeT0iMTYiIHI9IjMiIGZpbGw9IiNmZmZmZmYiLz48Y2lyY2xlIGN4PSIyNyIgY3k9IjE2IiByPSIxLjUiIGZpbGw9IiM1MmM0MWEiLz48bGluZSB4MT0iNSIgeTE9IjEzIiB4Mj0iNiIgeTI9IjEzIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEuNSIvPjxsaW5lIHgxPSI1IiB5MT0iMTkiIHgyPSI2IiB5Mj0iMTkiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS41Ii8+PC9zdmc+"
            alt="在线状态"
            class="legend-icon"
          />
          <span class="legend-text">在线状态</span>
        </div>
        <div class="legend-item">
          <img
            src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSI1IiB5PSI4IiB3aWR0aD0iMjIiIGhlaWdodD0iMTYiIHJ4PSIzIiBmaWxsPSIjOGM4YzhjIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiLz48cGF0aCBkPSJNNSA4IFEgMTYgMyAyNyA4IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9IiM2NjY2NjYiLz48cGF0aCBkPSJNNSAyNCBRIDE2IDI5IDI3IDI0IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9IiM2NjY2NjYiLz48cmVjdCB4PSI4IiB5PSIxMSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjEwIiByeD0iMiIgZmlsbD0id2hpdGUiLz48bGluZSB4MT0iMTEiIHkxPSIxMiIgeDI9IjIxIiB5Mj0iMjIiIHN0cm9rZT0iIzhjOGM4YyIgc3Ryb2tlLXdpZHRoPSIyIi8+PGxpbmUgeDE9IjExIiB5MT0iMjIiIHgyPSIyMSIgeTI9IjEyIiBzdHJva2U9IiM4YzhjOGMiIHN0cm9rZS13aWR0aD0iMiIvPjxsaW5lIHgxPSIxNiIgeTE9IjgiIHgyPSIxNiIgeTI9IjMiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS41Ii8+PGNpcmNsZSBjeD0iMTYiIGN5PSIzIiByPSIxLjUiIGZpbGw9IndoaXRlIi8+PGNpcmNsZSBjeD0iMjciIGN5PSIxNiIgcj0iMyIgZmlsbD0iI2ZmZmZmZiIvPjxjaXJjbGUgY3g9IjI3IiBjeT0iMTYiIHI9IjEuNSIgZmlsbD0iIzhjOGM4YyIvPjxsaW5lIHgxPSI1IiB5MT0iMTMiIHgyPSI2IiB5Mj0iMTMiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS41Ii8+PGxpbmUgeDE9IjUiIHkxPSIxOSIgeDI9IjYiIHkyPSIxOSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiLz48L3N2Zz4="
            alt="离线状态"
            class="legend-icon"
          />
          <span class="legend-text">离线状态</span>
        </div>
      </div>
    </div>
    <!-- å·¡æŸ¥æƒ…况统计卡片 -->
    <el-scrollbar class="inspection-section">
      <div class="section-header">
        <h3>巡查汇总</h3>
      </div>
        <el-popover placement="right-start" title="现场巡查统计" width="350" trigger="click">
          <div class="inspection-popover-content">
      <!-- å·¡æŸ¥é‡ç»Ÿè®¡ -->
      <div class="inspection-metrics">
        <div class="inspection-metric-item">
@@ -373,12 +256,12 @@
      <!-- é—®é¢˜æ•´æ”¹æƒ…况 -->
      <div class="inspection-chart-container">
        <div class="section-header"><h3>整改汇总</h3></div>
              <div class="section-header"><h3>问题整改</h3></div>
        <canvas id="rectificationChart"></canvas>
      </div>
      <!-- é—®é¢˜å®¡æ ¸æƒ…况 -->
      <div class="inspection-table-container">
            <div class="inspection-table-container" style="display: none">
        <div class="section-header"><h3>审核汇总</h3></div>
        <div class="inspection-metric-label">问题审核</div>
        <div class="inspection-table">
@@ -429,45 +312,141 @@
              {{ inspectionStats.fullyReviewedRectifiedShops }}
            </div>
          </div>
          <!-- <div class="table-row">
            <div class="table-cell">无问题店铺数量</div>
            <div class="table-cell value">{{ inspectionStats.noProblemShops }}</div>
          </div>
          <div class="table-row">
            <div class="table-cell">问题未审核店铺数量</div>
            <div class="table-cell value">{{ inspectionStats.unreviewedProblemShops }}</div>
          </div>
          <div class="table-row">
            <div class="table-cell">问题部分审核店铺数量</div>
            <div class="table-cell value">
              {{ inspectionStats.partiallyReviewedProblemShops }}
            </div>
          </div>
          <div class="table-row">
            <div class="table-cell">问题全部审核店铺数量</div>
            <div class="table-cell value">{{ inspectionStats.fullyReviewedProblemShops }}</div>
          </div>
          <div class="table-row">
            <div class="table-cell">未整改店铺数</div>
            <div class="table-cell value">{{ inspectionStats.unrectifiedShops }}</div>
          </div>
          <div class="table-row">
            <div class="table-cell">整改未审核店铺数</div>
            <div class="table-cell value">{{ inspectionStats.unreviewedRectifiedShops }}</div>
          </div>
          <div class="table-row">
            <div class="table-cell">整改部分审核店铺数</div>
            <div class="table-cell value">
              {{ inspectionStats.partiallyReviewedRectifiedShops }}
          <template #reference>
            <div class="metric-card">
              <div class="card-header">
                <div class="card-title">巡查点次</div>
                <div class="card-icon task-icon">
                  <svg
                    width="20"
                    height="20"
                    viewBox="0 0 24 24"
                    fill="none"
                    xmlns="http://www.w3.org/2000/svg"
                  >
                    <path
                      d="M22 11.08V12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C15.7376 2 19.0503 4.16113 20.7748 7.33007"
                      stroke="currentColor"
                      stroke-width="2"
                      stroke-linecap="round"
                      stroke-linejoin="round"
                    />
                    <path
                      d="M22 4L12 14.01L9 11.01"
                      stroke="currentColor"
                      stroke-width="2"
                      stroke-linecap="round"
                      stroke-linejoin="round"
                    />
                  </svg>
            </div>
          </div>
          <div class="table-row">
            <div class="table-cell">整改全部审核店铺数</div>
            <div class="table-cell value">{{ inspectionStats.fullyReviewedRectifiedShops }}</div>
          </div> -->
              <div class="card-value">{{ metrics.inspectionPoints }}</div>
              <div class="card-trend">
                <span
                  class="trend-arrow"
                  :class="{
                    up: metrics.inspectionPointsTrend > 0,
                    down: metrics.inspectionPointsTrend < 0,
                  }"
                >
                  {{ metrics.inspectionPointsTrend > 0 ? '↑' : '↓' }}
                </span>
                <span class="trend-text">{{ Math.abs(metrics.inspectionPointsTrend) }}</span>
                <span class="trend-label">{{ getCompareLabel() }}</span>
              </div>
              <div class="view-details">
                <span>详情</span>
                <svg
                  width="12"
                  height="12"
                  viewBox="0 0 24 24"
                  fill="none"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path
                    d="M9 18L15 12L9 6"
                    stroke="currentColor"
                    stroke-width="2"
                    stroke-linecap="round"
                    stroke-linejoin="round"
                  />
                </svg>
        </div>
      </div>
    </el-scrollbar>
          </template>
        </el-popover>
      </div>
    </div>
    <!-- ä¸»è¦å†…容区 -->
    <div class="main-content">
      <!-- ä¸­éƒ¨GIS地图区 -->
      <div class="map-section">
        <div id="map" class="map-container">
          <BaseMap :showSatellite="true"></BaseMap>
        </div>
      </div>
    </div>
    <div class="monitor-control-container">
      <el-button size="large" @click="toggleMonitorControl" class="push-btn">
        <div style="display: flex; flex-direction: column">
          <el-icon>
            <ArrowRight v-if="isMonitorControlExpanded" />
            <ArrowLeft v-else />
          </el-icon>
          <div>现</div>
          <div>场</div>
          <div>å·¡</div>
          <div>查</div>
        </div>
      </el-button>
      <MonitorControl
        v-if="isMonitorControlExpanded"
        :class="{ 'monitor-control': true, collapsed: !isMonitorControlExpanded }"
        style="height: calc(90vh - 40px)"
      />
    </div>
    <!-- åœ°å›¾å›¾ä¾‹ -->
    <div class="map-legend">
      <div class="legend-header">
        <h4>图例</h4>
      </div>
      <div class="legend-items">
        <div class="legend-item">
          <img src="@/assets/exceed.png" alt="油烟浓度超标" class="legend-icon" />
          <span class="legend-text">油烟浓度超标</span>
        </div>
        <div class="legend-item">
          <img src="@/assets/exception.png" alt="供电异常" class="legend-icon" />
          <span class="legend-text">供电异常</span>
        </div>
        <div class="legend-item">
          <img src="@/assets/offline.png" alt="设备或网络异常" class="legend-icon" />
          <span class="legend-text">设备或网络异常</span>
        </div>
        <div class="legend-item">
          <img
            src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSI1IiB5PSI4IiB3aWR0aD0iMjIiIGhlaWdodD0iMTYiIHJ4PSIzIiBmaWxsPSIjNTJjNDFhIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiLz48cGF0aCBkPSJNNSA4IFEgMTYgMyAyNyA4IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9IiMzODllMGQiLz48cGF0aCBkPSJNNSAyNCBRIDE2IDI5IDI3IDI0IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9IiM2NjY2NjYiLz48cmVjdCB4PSI4IiB5PSIxMSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjEwIiByeD0iMiIgZmlsbD0id2hpdGUiLz48cGF0aCBkPSJNMTIgMTQgTCAyMSAxNCIgc3Ryb2tlPSIjNTJjNDFhIiBzdHJva2Utd2lkdGg9IjEuNSIvPjxwYXRoIGQ9Ik0xMiAxNyBMIDE4IDE3IiBzdHJva2U9IiM1MmM0MWEiIHN0cm9rZS13aWR0aD0iMS41Ii8+PHBhdGggZD0iTTEyIDIwIEwgMTUgMjAiIHN0cm9rZT0iIzUyYzQxYSIgc3Ryb2tlLXdpZHRoPSIxLjUiLz48bGluZSB4MT0iMTYiIHkxPSI4IiB4Mj0iMTYiIHkyPSIzIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEuNSIvPjxjaXJjbGUgY3g9IjE2IiBjeT0iMyIgcj0iMS41IiBmaWxsPSJ3aGl0ZSIvPjxjaXJjbGUgY3g9IjI3IiBjeT0iMTYiIHI9IjMiIGZpbGw9IiNmZmZmZmYiLz48Y2lyY2xlIGN4PSIyNyIgY3k9IjE2IiByPSIxLjUiIGZpbGw9IiM1MmM0MWEiLz48bGluZSB4MT0iNSIgeTE9IjEzIiB4Mj0iNiIgeTI9IjEzIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEuNSIvPjxsaW5lIHgxPSI1IiB5MT0iMTkiIHgyPSI2IiB5Mj0iMTkiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS41Ii8+PC9zdmc+"
            alt="在线状态"
            class="legend-icon"
          />
          <span class="legend-text">在线状态</span>
        </div>
        <div class="legend-item">
          <img
            src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSI1IiB5PSI4IiB3aWR0aD0iMjIiIGhlaWdodD0iMTYiIHJ4PSIzIiBmaWxsPSIjOGM4YzhjIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiLz48cGF0aCBkPSJNNSA4IFEgMTYgMyAyNyA4IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9IiM2NjY2NjYiLz48cGF0aCBkPSJNNSAyNCBRIDE2IDI5IDI3IDI0IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9IiM2NjY2NjYiLz48cmVjdCB4PSI4IiB5PSIxMSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjEwIiByeD0iMiIgZmlsbD0id2hpdGUiLz48bGluZSB4MT0iMTEiIHkxPSIxMiIgeDI9IjIxIiB5Mj0iMjIiIHN0cm9rZT0iIzhjOGM4YyIgc3Ryb2tlLXdpZHRoPSIyIi8+PGxpbmUgeDE9IjExIiB5MT0iMjIiIHgyPSIyMSIgeTI9IjEyIiBzdHJva2U9IiM4YzhjOGMiIHN0cm9rZS13aWR0aD0iMiIvPjxsaW5lIHgxPSIxNiIgeTE9IjgiIHgyPSIxNiIgeTI9IjMiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS41Ii8+PGNpcmNsZSBjeD0iMTYiIGN5PSIzIiByPSIxLjUiIGZpbGw9IndoaXRlIi8+PGNpcmNsZSBjeD0iMjciIGN5PSIxNiIgcj0iMyIgZmlsbD0iI2ZmZmZmZiIvPjxjaXJjbGUgY3g9IjI3IiBjeT0iMTYiIHI9IjEuNSIgZmlsbD0iIzhjOGM4YyIvPjxsaW5lIHgxPSI1IiB5MT0iMTMiIHgyPSI2IiB5Mj0iMTMiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS41Ii8+PGxpbmUgeDE9IjUiIHkxPSIxOSIgeDI9IjYiIHkyPSIxOSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiLz48L3N2Zz4="
            alt="离线状态"
            class="legend-icon"
          />
          <span class="legend-text">离线状态</span>
        </div>
      </div>
    </div>
  </div>
</template>
@@ -477,19 +456,22 @@
import districtSearch from '@/utils/map/districtsearch.js'
import marks from '@/utils/map/marks.js'
import { generateTestShops } from '@/debug/debugdata'
import MonitorControl from '@/views/inspection/MonitorControl.vue'
export default {
  name: 'DataDashboard',
  components: {
    MonitorControl,
  },
  data() {
    return {
      activeTime: 'day',
      currentDate: new Date(),
      currentDate: new Date('2023-08-01'),
      timeTabs: [
        { label: '日', value: 'day' },
        { label: '周', value: 'week' },
        { label: '月', value: 'month' },
      ],
      dialogVisible: false,
      selectedPoint: {
        enterpriseName: '',
        deviceId: '',
@@ -504,8 +486,8 @@
        overStandardTrend: 5,
        onlineRate: 92,
        onlineRateTrend: 2,
        purifierEfficiency: 85,
        purifierEfficiencyTrend: -3,
        purifierEfficiency: 95,
        purifierEfficiencyTrend: 2,
        inspectionPoints: 350,
        inspectionPointsTrend: 50,
      },
@@ -536,19 +518,20 @@
      },
      map: null,
      refreshTimer: null,
      isMonitorControlExpanded: true,
    }
  },
  computed: {
    currentTimeDisplay() {
      const date = this.currentDate
      let weekStart = new Date(date)
      let weekEnd = new Date(date)
      switch (this.activeTime) {
        case 'day':
          return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
        case 'week':
          // ç®€å•计算周显示,实际项目中可能需要更复杂的周计算逻辑
          let weekStart = new Date(date)
          weekStart.setDate(date.getDate() - date.getDay() + 1)
          let weekEnd = new Date(date)
          weekEnd.setDate(date.getDate() + (7 - date.getDay()))
          return `${weekStart.getFullYear()}-${String(weekStart.getMonth() + 1).padStart(2, '0')}-${String(weekStart.getDate()).padStart(2, '0')} ~ ${weekEnd.getFullYear()}-${String(weekEnd.getMonth() + 1).padStart(2, '0')}-${String(weekEnd.getDate()).padStart(2, '0')}`
        case 'month':
@@ -562,7 +545,7 @@
    this.initMap()
    this.initDeviceStatusChart()
    this.initRectificationChart()
    this.startAutoRefresh()
    // this.startAutoRefresh()
  },
  beforeUnmount() {
    if (this.refreshTimer) {
@@ -570,6 +553,9 @@
    }
  },
  methods: {
    toggleMonitorControl() {
      this.isMonitorControlExpanded = !this.isMonitorControlExpanded
    },
    handleTimeChange(tab) {
      this.activeTime = tab.value
      // æ¨¡æ‹Ÿåˆ‡æ¢æ—¶é—´å‘¨æœŸåŽçš„æ•°æ®æ›´æ–°
@@ -631,12 +617,18 @@
      // è¿™é‡Œåº”该根据选择的时间周期从接口获取数据
      // æ¨¡æ‹Ÿæ•°æ®æ›´æ–°
      setTimeout(() => {
        const m = Math.floor(Math.random() * 50) + 150
        this.overview = {
          totalShops: 245,
          onlineDevices: m,
          offlineDevices: 245 - m,
        }
        this.metrics = {
          overStandardCount: Math.floor(Math.random() * 30),
          overStandardTrend: Math.floor(Math.random() * 20) - 10,
          onlineRate: Math.floor(Math.random() * 20) + 80,
          onlineRate: ((this.overview.onlineDevices / this.overview.totalShops) * 100).toFixed(0),
          onlineRateTrend: Math.floor(Math.random() * 10) - 5,
          purifierEfficiency: Math.floor(Math.random() * 30) + 70,
          purifierEfficiency: Math.floor(Math.random() * 20) + 80,
          purifierEfficiencyTrend: Math.floor(Math.random() * 10) - 5,
          inspectionPoints: Math.floor(Math.random() * 100) + 300,
          inspectionPointsTrend: Math.floor(Math.random() * 100) - 50,
@@ -662,6 +654,7 @@
        }
        // æ›´æ–°å›¾è¡¨
        this.initDeviceStatusChart()
        this.initRectificationChart()
      }, 300)
    },
@@ -779,6 +772,7 @@
            left: '3%',
            right: '4%',
            bottom: '3%',
            top: '5%',
            containLabel: true,
          },
          xAxis: {
@@ -878,6 +872,34 @@
  /* padding: 16px; */
  border-radius: 8px;
  /* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); */
}
/* ç›‘控控制卡片 */
.monitor-control {
  /* position: absolute; */
  width: 500px;
  transition: all 0.3s ease;
  /* top: 0px; */
  /* right: 0px; */
  /* z-index: 10; */
}
.push-btn {
  z-index: 1;
  width: 2.5rem;
  height: initial;
  margin: initial;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* background-color: white; */
  /* border-color: white; */
  /* border-top: 1px solid;
  border-left: 1px solid;
  border-bottom: 1px solid; */
  border-top-right-radius: 0px;
  border-bottom-right-radius: 0px;
  /* box-shadow: var(--el-box-shadow-light); */
}
/* æ—¶é—´å‘¨æœŸå¡ç‰‡ */
@@ -993,6 +1015,7 @@
.metric-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  cursor: pointer;
}
.card-header {
@@ -1024,7 +1047,7 @@
}
.efficiency-icon {
  color: #722ed1;
  color: #52c41a;
}
.task-icon {
@@ -1133,19 +1156,63 @@
}
/* å³ä¾§å®žæ—¶ç›‘测总览区 */
.overview-section {
  position: absolute;
  bottom: 4px;
  left: 4px;
  width: 320px;
  background-color: rgba(255, 255, 255, 0.9);
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  padding: 20px;
.popover-content {
  padding: 10px;
}
.overview-items-container {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-bottom: 16px;
  border-bottom: 1px solid #f0f0f0;
  margin-bottom: 20px;
}
.overview-item {
  display: flex;
  flex-direction: column;
  z-index: 10;
  max-height: calc(100vh - 220px);
  align-items: center;
  flex: 1;
  text-align: center;
}
.overview-label {
  font-size: 12px;
  color: #86909c;
  font-weight: 500;
  margin-bottom: 8px;
}
.overview-value {
  font-size: 24px;
  font-weight: bold;
  color: #262626;
}
.device-status-chart {
  flex: 1;
  min-height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 16px;
}
.view-details {
  position: absolute;
  bottom: 12px;
  right: 16px;
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  color: #1890ff;
  cursor: pointer;
}
.view-details:hover {
  text-decoration: underline;
}
.overview-items-container {
@@ -1177,30 +1244,51 @@
  color: #262626;
}
.device-status-chart {
  flex: 1;
  min-height: 100px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 16px;
/* å·¡æŸ¥æƒ…况统计 */
.inspection-popover-content {
  padding: 10px;
  max-height: 400px;
  overflow-y: auto;
}
/* å·¡æŸ¥æƒ…况统计 */
.inspection-section {
.monitor-control-container {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 320px;
  background-color: rgba(255, 255, 255, 0.9);
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  padding: 20px;
  display: flex;
  flex-direction: column;
  z-index: 10;
  max-height: calc(70vh);
  border-top: 1px solid #f0f0f0;
  transition: all 0.3s ease;
  /* background-color: rgba(255, 255, 255, 0.9); */
  display: flex;
  border-radius: 8px;
  /* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); */
  overflow: hidden;
}
.monitor-control-container.collapsed {
  width: 60px;
}
.monitor-control-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 15px;
  border-bottom: 1px solid #e8e8e8;
  height: 40px;
  position: relative;
}
.monitor-control-header span {
  font-weight: 600;
  color: #333;
  writing-mode: vertical-rl;
  text-orientation: mixed;
  letter-spacing: 2px;
  white-space: nowrap;
}
.collapse-btn {
  /* transform: translateY(-50%); */
}
.inspection-metrics {
@@ -1304,12 +1392,12 @@
.map-legend {
  position: absolute;
  bottom: 4px;
  right: 4px;
  width: 200px;
  left: 4px;
  /* width: 200px; */
  background-color: rgba(255, 255, 255, 0.9);
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  padding: 16px;
  padding: 4px;
  z-index: 10;
}
src/views/monitor/DataException.vue
@@ -2,9 +2,16 @@
  <div class="data-exception-container">
    <!-- æœç´¢åŒºåŸŸ -->
    <div ref="h1" class="search-container">
      <!-- <div class="search-header">
        <h3>查询表格</h3>
      </div> -->
      <el-collapse v-model="activeSearchNames" @change="handleSearchCollapseChange">
        <el-collapse-item name="1">
          <template #title>
            <div class="search-header">
              <h3>查询条件</h3>
              <span v-if="!isSearchExpanded" class="search-summary">
                {{ getSearchSummary() }}
              </span>
            </div>
          </template>
      <el-row>
        <div class="search-form">
          <div class="form-row">
@@ -42,20 +49,18 @@
      <div class="summary-info">
        <span>{{ beginTime }} â€”— {{ endTime }} æ²¹çƒŸç›‘测异常信息汇总</span>
      </div>
        </el-collapse-item>
      </el-collapse>
    </div>
    <!-- å¼‚常分析 -->
    <div class="analysis-container">
      <el-collapse v-model="activeNames">
        <el-collapse-item name="1">
          <template #title>
            <div class="collapse-title">
              <h4 class="collapse-header">异常分析</h4>
      <el-space>
        <h4 class="collapse-header">监测预警</h4>
              <el-icon class="header-icon">
                <i-ep-info-filled />
              </el-icon>
            </div>
          </template>
      </el-space>
          <el-card class="analysis-card">
            <el-row :gutter="24">
              <el-col :span="8">
@@ -78,7 +83,8 @@
                  </div>
                  <hr class="item-divider" />
                  <div class="item-shops">
                    <el-scrollbar max-height="80px">
                <el-scrollbar :height="scrollbarHeight">
                  <el-space wrap>
                      <ExceptionText
                        v-for="(item, index) in exception0"
                        :key="item"
@@ -89,8 +95,9 @@
                        @submit-exception-data="getAbnormalDataByClick"
                      >
                        {{ item.diName }}
                        <span v-if="index < exception0.length - 1" class="text-blank">,</span>
                      <span v-if="index < exception0.length - 1" class="text-blank"></span>
                      </ExceptionText>
                  </el-space>
                    </el-scrollbar>
                  </div>
                </div>
@@ -115,7 +122,8 @@
                  </div>
                  <hr class="item-divider" />
                  <div class="item-shops">
                    <el-scrollbar max-height="80px">
                <el-scrollbar :height="scrollbarHeight">
                  <el-space wrap>
                      <ExceptionText
                        v-for="(item, index) in exception1"
                        :key="item"
@@ -126,8 +134,9 @@
                        @submit-exception-data="getAbnormalDataByClick"
                      >
                        {{ item.diName }}
                        <span v-if="index < exception1.length - 1" class="text-blank">,</span>
                      <span v-if="index < exception1.length - 1" class="text-blank"></span>
                      </ExceptionText>
                  </el-space>
                    </el-scrollbar>
                  </div>
                </div>
@@ -150,7 +159,8 @@
                  </div>
                  <hr class="item-divider" />
                  <div class="item-shops">
                    <el-scrollbar max-height="80px">
                <el-scrollbar :height="scrollbarHeight">
                  <el-space wrap>
                      <ExceptionText
                        v-for="(item, index) in exception2"
                        :key="item"
@@ -161,21 +171,26 @@
                        @submit-exception-data="getAbnormalDataByClick"
                      >
                        {{ item.diName }}
                        <span v-if="index < exception2.length - 1" class="text-blank">,</span>
                      <span v-if="index < exception2.length - 1" class="text-blank"></span>
                      </ExceptionText>
                  </el-space>
                    </el-scrollbar>
                  </div>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </el-collapse-item>
      </el-collapse>
    </div>
    <!-- å¼‚常数据表格 -->
    <div class="table-container">
      <el-collapse v-model="activeNames">
        <el-collapse-item name="1">
          <template #title>
            <div class="collapse-title">
      <h4 class="table-title">异常数据</h4>
            </div>
          </template>
      <el-card v-show="!isNoData">
        <el-table
          ref="tableH"
@@ -183,64 +198,64 @@
          :data="displayData"
          style="width: 100%"
          border
          :height="tableHeight"
          :cell-class-name="tableCellClassName"
              :show-overflow-tooltip="true"
        >
          <el-table-column prop="diName" label="店铺名称" align="center">
            <template #default="{ row }">
                <!-- <template #default="{ row }">
              <el-tooltip effect="dark" :content="row.diName">
                <div class="cell ellipsis">{{ row.diName }}</div>
              </el-tooltip>
            </template>
                </template> -->
          </el-table-column>
          <el-table-column prop="devId" label="设备编号" align="center">
            <template #default="{ row }">
                <!-- <template #default="{ row }">
              <el-tooltip effect="dark" :content="row.devId">
                <div class="cell ellipsis">{{ row.devId }}</div>
              </el-tooltip>
            </template>
                </template> -->
          </el-table-column>
          <el-table-column prop="diSupplier" label="供应商" align="center">
            <template #default="{ row }">
                <!-- <template #default="{ row }">
              <el-tooltip effect="dark" :content="row.diSupplier">
                <div class="cell ellipsis">{{ row.diSupplier }}</div>
              </el-tooltip>
            </template>
                </template> -->
          </el-table-column>
          <el-table-column prop="exception" label="异常分类" align="center">
            <template #default="{ row }">
              <el-table-column prop="exception" label="异常分类" align="center" width="90">
                <!-- <template #default="{ row }">
              <el-tooltip effect="dark" :content="row.exception">
                <div class="cell ellipsis">{{ row.exception }}</div>
              </el-tooltip>
            </template>
                </template> -->
          </el-table-column>
          <el-table-column label="异常类型" align="center">
              <el-table-column label="异常类型" align="center" width="120">
            <template #default="{ row }">
              <span v-if="row.exceptionType == '0'">油烟数据超标</span>
              <span v-else-if="row.exceptionType == '1'">疑似供电异常</span>
              <span v-else-if="row.exceptionType == '2'">掉线</span>
            </template>
          </el-table-column>
          <el-table-column prop="region" label="地区" align="center">
            <template #default="{ row }">
              <el-table-column prop="region" label="地区" align="center" width="80">
                <!-- <template #default="{ row }">
              <el-tooltip effect="dark" :content="row.region">
                <div class="cell ellipsis">{{ row.region }}</div>
              </el-tooltip>
            </template>
                </template> -->
          </el-table-column>
          <el-table-column prop="beginTime" label="开始时间" align="center">
            <template #default="{ row }">
                <!-- <template #default="{ row }">
              <el-tooltip effect="dark" :content="row.beginTime">
                <div class="cell ellipsis">{{ row.beginTime }}</div>
              </el-tooltip>
            </template>
                </template> -->
          </el-table-column>
          <el-table-column prop="endTime" label="结束时间" align="center">
            <template #default="{ row }">
                <!-- <template #default="{ row }">
              <el-tooltip effect="dark" :content="row.endTime">
                <div class="cell ellipsis">{{ row.endTime }}</div>
              </el-tooltip>
            </template>
                </template> -->
          </el-table-column>
          <el-table-column label="操作" align="center" width="120">
            <template #default="{ row }">
@@ -260,6 +275,8 @@
        </div>
      </el-card>
      <el-empty v-show="isNoData" :image-size="200" />
        </el-collapse-item>
      </el-collapse>
    </div>
    <!-- å¯¹è¯æ¡† -->
@@ -448,7 +465,11 @@
      selectedRowIndex: -1,
      // é»˜è®¤é€‰æ‹©çš„æŠ˜å é¢æ¿ç¼–号
      activeNames: ['1'],
      activeNames: [],
      // æœç´¢åŒºåŸŸæŠ˜å çŠ¶æ€
      activeSearchNames: [],
      // æœç´¢åŒºåŸŸæ˜¯å¦å±•å¼€
      isSearchExpanded: false,
      // å¼‚常时的表格
      abnormalTb: [],
      // å¼‚常的起止时间
@@ -462,6 +483,8 @@
      exception1: [],
      // ä¿å­˜ç€å¼‚常类型2对应的店铺名称和设备编号
      exception2: [],
      // å¼‚常店铺滚动区域高度
      scrollbarHeight: 250,
      // åŠ è½½åŠ¨ç”»
      loading: false,
      // æŠ½å±‰åŠ è½½åŠ¨ç”»
@@ -534,7 +557,6 @@
        this.isNextCantouch = false
      }
    },
    // å½“选择的时间发生变化时,异常分析部分的异常店铺数量同步变化
    beginTime() {
      this.getShopNames()
@@ -573,6 +595,37 @@
    window.addEventListener('resize', this.updateChart)
  },
  methods: {
    // å¤„理搜索区域折叠变化
    handleSearchCollapseChange(val) {
      this.isSearchExpanded = val.length > 0
    },
    // èŽ·å–æœç´¢æ¡ä»¶æ‘˜è¦
    getSearchSummary() {
      let summary =
        '时间: ' +
        (this.beginTime ? this.beginTime.substring(0, 10) : '全部') +
        ' è‡³ ' +
        (this.endTime ? this.endTime.substring(0, 10) : '全部')
      summary +=
        ' | åº—铺: ' +
        (this.deviceId[1]
          ? this.deviceInfo.find((item) => item.diCode === this.deviceId[1])?.diName || '已选择'
          : '全部')
      if (this.exceptionValue && this.exceptionValue.length > 0) {
        const exceptionTypes = {
          0: '油烟浓度超标',
          1: '供电异常',
          2: '设备或网络异常',
        }
        const selectedTypes = this.exceptionValue
          .map((val) => exceptionTypes[val] || val)
          .join(', ')
        summary += ' | å¼‚常类型: ' + selectedTypes
      } else {
        summary += ' | å¼‚常类型: å…¨éƒ¨'
      }
      return summary
    },
    // åŠŸèƒ½ï¼šå¯¹è¯æ¡†è¡¨æ ¼åºå·é€’å¢ž
    // æ—¶é—´ï¼š2023-8-17
    indexMethod(index) {
@@ -886,6 +939,9 @@
        // ç§»é™¤ç©ºæ•°æ®çŠ¶æ€
        this.isNoData = false
        this.handleCurrentChange(1)
        // ç‚¹å‡»æŸ¥è¯¢åŽæŠ˜å æœç´¢åŒºåŸŸ
        this.activeSearchNames = []
        this.isSearchExpanded = false
      })
    },
    handleSizeChange(val) {
@@ -1391,6 +1447,15 @@
      this.total = this.abnormalData.length
      // é»˜è®¤æ˜¾ç¤ºç¬¬ä¸€é¡µ
      this.handleCurrentChange(1)
      this.activeNames = ['1']
      // æ»šåŠ¨åˆ°å¼‚å¸¸æ•°æ®è¡¨æ ¼ä½ç½®
      this.$nextTick(() => {
        if (this.$refs.tableH) {
          setTimeout(() => {
            this.$refs.tableH.$el.scrollIntoView({ behavior: 'smooth' })
          }, 200)
        }
      })
    },
    // æ ¹æ®å¼‚常类型返回店铺名称和设备编号
@@ -1509,7 +1574,10 @@
}
.search-header {
  margin-bottom: 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
}
.search-header h3 {
@@ -1517,6 +1585,16 @@
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
.search-summary {
  font-size: 14px;
  color: #666;
  flex: 1;
  margin-left: 20px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.search-form {
@@ -1596,7 +1674,6 @@
}
.analysis-item {
  height: 180px;
  display: flex;
  flex-direction: column;
}