餐饮油烟智能监测与监管一体化平台
riku
2026-03-12 723be8e0896fbf7e9456a5defb44911a3d0cbc27
2026.3.12
已修改12个文件
已添加14个文件
6861 ■■■■ 文件已修改
components.d.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
index.html 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/layout/FormCol.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/constants/menu.js 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/sfc/ExceptionTypeLineChart.vue 74 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/sfc/TimeSelect.vue 177 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/style/base.scss 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/style/layout.js 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/map/districtsearch.js 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/map/index.js 98 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/scenenew/UserEdit.vue 191 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/scenenew/UserInfo.vue 226 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/scenenew/components/CompCompanyInfo.vue 205 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/scenenew/components/CompDeviceInfo.vue 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/scenenew/components/CompHazardousWasteFile.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/scenenew/components/CompHazardousWasteRecord.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/scenenew/components/CompSceneInfo.vue 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/scenenew/components/CompUserInfo.vue 250 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/scenenew/components/CompUserInfoAddDrawer.vue 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/DataDashboard.vue 1064 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/DataDashboard_old2.vue 313 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/DataDashboard_old3.vue 730 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/DataException.vue 1058 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/DataException_old.vue 1690 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/DataHistory.vue 56 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components.d.ts
@@ -27,10 +27,12 @@
    ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
    ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
    ElButton: typeof import('element-plus/es')['ElButton']
    ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
    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']
    ElCheckboxButton: typeof import('element-plus/es')['ElCheckboxButton']
    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
    ElCol: typeof import('element-plus/es')['ElCol']
    ElCollapse: typeof import('element-plus/es')['ElCollapse']
@@ -43,6 +45,9 @@
    ElDialog: typeof import('element-plus/es')['ElDialog']
    ElDivider: typeof import('element-plus/es')['ElDivider']
    ElDrawer: typeof import('element-plus/es')['ElDrawer']
    ElDropdown: typeof import('element-plus/es')['ElDropdown']
    ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
    ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
    ElEmpty: typeof import('element-plus/es')['ElEmpty']
    ElForm: typeof import('element-plus/es')['ElForm']
    ElFormItem: typeof import('element-plus/es')['ElFormItem']
@@ -58,6 +63,7 @@
    ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
    ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
    ElOption: typeof import('element-plus/es')['ElOption']
    ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
    ElPagination: typeof import('element-plus/es')['ElPagination']
    ElPopover: typeof import('element-plus/es')['ElPopover']
    ElRadio: typeof import('element-plus/es')['ElRadio']
@@ -81,6 +87,7 @@
    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']
    FYDownloadTableButton: typeof import('./src/components/button/FYDownloadTableButton.vue')['default']
    FYForm: typeof import('./src/components/form/FYForm.vue')['default']
    FYImageSelectDialog: typeof import('./src/components/FYImageSelectDialog.vue')['default']
@@ -103,8 +110,11 @@
    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']
    IEpDataLine: typeof import('~icons/ep/data-line')['default']
    IEpDownload: typeof import('~icons/ep/download')['default']
    IEpGrid: typeof import('~icons/ep/grid')['default']
    IEpInfoFilled: typeof import('~icons/ep/info-filled')['default']
    IEpWarning: typeof import('~icons/ep/warning')['default']
    ItemDevice: typeof import('./src/components/list-item/ItemDevice.vue')['default']
    ItemMonitorObj: typeof import('./src/components/list-item/ItemMonitorObj.vue')['default']
    ItemScene: typeof import('./src/components/list-item/ItemScene.vue')['default']
@@ -142,10 +152,12 @@
  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 ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
  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']
  const ElCheckboxButton: typeof import('element-plus/es')['ElCheckboxButton']
  const ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
  const ElCol: typeof import('element-plus/es')['ElCol']
  const ElCollapse: typeof import('element-plus/es')['ElCollapse']
@@ -158,6 +170,9 @@
  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 ElDropdown: typeof import('element-plus/es')['ElDropdown']
  const ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
  const ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
  const ElEmpty: typeof import('element-plus/es')['ElEmpty']
  const ElForm: typeof import('element-plus/es')['ElForm']
  const ElFormItem: typeof import('element-plus/es')['ElFormItem']
@@ -173,6 +188,7 @@
  const ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
  const ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
  const ElOption: typeof import('element-plus/es')['ElOption']
  const ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
  const ElPagination: typeof import('element-plus/es')['ElPagination']
  const ElPopover: typeof import('element-plus/es')['ElPopover']
  const ElRadio: typeof import('element-plus/es')['ElRadio']
@@ -196,6 +212,7 @@
  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 FYDownloadTableButton: typeof import('./src/components/button/FYDownloadTableButton.vue')['default']
  const FYForm: typeof import('./src/components/form/FYForm.vue')['default']
  const FYImageSelectDialog: typeof import('./src/components/FYImageSelectDialog.vue')['default']
@@ -218,8 +235,11 @@
  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 IEpDataLine: typeof import('~icons/ep/data-line')['default']
  const IEpDownload: typeof import('~icons/ep/download')['default']
  const IEpGrid: typeof import('~icons/ep/grid')['default']
  const IEpInfoFilled: typeof import('~icons/ep/info-filled')['default']
  const IEpWarning: typeof import('~icons/ep/warning')['default']
  const ItemDevice: typeof import('./src/components/list-item/ItemDevice.vue')['default']
  const ItemMonitorObj: typeof import('./src/components/list-item/ItemMonitorObj.vue')['default']
  const ItemScene: typeof import('./src/components/list-item/ItemScene.vue')['default']
index.html
@@ -8,6 +8,11 @@
  </head>
  <body>
    <div id="app"></div>
    <script type="text/javascript">
      window._AMapSecurityConfig = {
        serviceHost: 'https://fyami.com.cn:449/_AMapService',
      }
    </script>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
src/components/layout/FormCol.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
<template>
  <el-col :xs="cols.xs" :sm="cols.sm" :md="cols.md" :lg="cols.lg" :xl="cols.xl">
    <slot></slot>
  </el-col>
</template>
<script>
import { formResponsiveCols } from '@/style/layout'
export default {
  data() {
    return {
      cols: formResponsiveCols(),
    }
  },
}
</script>
<style scoped></style>
src/constants/menu.js
@@ -6,49 +6,59 @@
      {
        path: '/index/monitor/data-dashboard',
        icon: 'solar:window-frame-line-duotone',
        name: '数据看板',
        name: '污染态势',
      },
      {
        path: '/index/monitor/data-exception',
        icon: 'solar:siren-line-duotone',
        name: '异常分析',
        name: '监测预警',
      },
      {
        path: '/index/monitor/data-analysis-all',
        icon: 'solar:presentation-graph-line-duotone',
        name: '要素分析',
        children: [
          {
            path: '/index/monitor/data-analysis-all',
            icon: 'solar:structure-line-duotone',
            name: '全要素分析',
          },
          {
            icon: 'solar:round-graph-line-duotone',
            name: '分要素分析',
            children: [
              {
                path: '/index/monitor/data-analysis-concentration',
                icon: 'solar:graph-new-line-duotone',
                name: '油烟浓度',
              },
              {
                path: '/index/monitor/data-analysis-online-rate',
                icon: 'solar:graph-new-line-duotone',
                name: '在线率',
              },
              {
                path: '/index/monitor/data-analysis-open-rate',
                icon: 'solar:graph-new-line-duotone',
                name: '开启率',
              },
              {
                path: '/index/monitor/data-analysis-over-standard-rate',
                icon: 'solar:graph-new-line-duotone',
                name: '超标率',
              },
            ],
          },
        ],
        name: '数据分析',
      },
      // {
      //   icon: 'solar:presentation-graph-line-duotone',
      //   name: '数据分析',
      //   children: [
      //     {
      //       path: '/index/monitor/data-analysis-all',
      //       icon: 'solar:structure-line-duotone',
      //       name: '全要素分析',
      //     },
      //     {
      //       icon: 'solar:round-graph-line-duotone',
      //       name: '分要素分析',
      //       children: [
      //         {
      //           path: '/index/monitor/data-analysis-concentration',
      //           icon: 'solar:graph-new-line-duotone',
      //           name: '油烟浓度',
      //         },
      //         {
      //           path: '/index/monitor/data-analysis-online-rate',
      //           icon: 'solar:graph-new-line-duotone',
      //           name: '在线率',
      //         },
      //         {
      //           path: '/index/monitor/data-analysis-open-rate',
      //           icon: 'solar:graph-new-line-duotone',
      //           name: '开启率',
      //         },
      //         {
      //           path: '/index/monitor/data-analysis-over-standard-rate',
      //           icon: 'solar:graph-new-line-duotone',
      //           name: '超标率',
      //         },
      //       ],
      //     },
      //   ],
      // },
      {
        path: '/index/monitor/data-history',
        icon: 'solar:graph-new-line-duotone',
        name: '历史数据',
      },
    ],
  },
src/router/index.js
@@ -77,7 +77,13 @@
            {
              name: 'scene-info',
              path: 'scene-info',
              component: () => import('@/views/inspection/scene/SceneInfo.vue'),
              component: () => import('@/views/inspection/scenenew/UserInfo.vue'),
            },
            {
              //账户编辑
              name: 'scene-info-edit',
              path: 'scene-info-edit/:userId',
              component: () => import('@/views/inspection/scenenew/UserEdit.vue'),
            },
            {
              name: 'pro-check',
src/sfc/ExceptionTypeLineChart.vue
@@ -1,5 +1,5 @@
<!-- å¼‚常情况的 æŠ˜çº¿å›¾ç»„ä»¶
子组件有基本的样式
子组件有基本的样式
使用同一个图形实例,接受父组件传入的折线图option
**父组件
 <ExceptionTypeLineChart
@@ -9,66 +9,65 @@
      ></ExceptionTypeLineChart>
 -->
<template>
  <div  id="main" class="line-chart"></div>
  <div id="main" class="line-chart"></div>
</template>
<script>
import * as echarts from 'echarts';
import * as echarts from 'echarts'
export default {
  props: {
    option:{
      type:Object,
      default(){
    option: {
      type: Object,
      default() {
        return {}
      }
      },
    },
    isOpenDialog:{
      type:Boolean
    isOpenDialog: {
      type: Boolean,
    },
  },
  data() {
    return {
      chart: null
    };
      chart: null,
    }
  },
  mounted() {
     // èŽ·å–é¡µé¢å®½åº¦çš„ä¸€åŠ
    this.initChart();
    // èŽ·å–é¡µé¢å®½åº¦çš„ä¸€åŠ
    this.initChart()
    this.chart.clear
    this.chart.setOption(this.option,true)
    window.addEventListener('resize', this.resizeChart);
    this.chart.setOption(this.option, true)
    window.addEventListener('resize', this.resizeChart)
  },
  watch: {
    option(){
    option() {
      // this.chart.dispose;
      // this.initChart();
      // this.chart.clear
      // ä¸ä¸Žä¹‹å‰çš„option进行合并
      this.chart.setOption(this.option,true)
      this.chart.setOption(this.option, true)
    },
    isOpenDialog(){
      window.addEventListener('resize', this.resizeChart);
    isOpenDialog() {
      window.addEventListener('resize', this.resizeChart)
    },
  },
  beforeUnmount() {
    if (this.chart) {
      this.chart.dispose;
      this.chart.dispose
    }
  },
  methods: {
    initChart() {
      // åˆ›å»ºecharts实例
      this.chart = echarts.init(document.getElementById('main'));
      this.chart = echarts.init(document.getElementById('main'))
      // å®šä¹‰å›¾è¡¨çš„配置项和数据
      const option = {
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
          containLabel: true,
        },
        tooltip: {},
        toolbox: {
@@ -79,8 +78,8 @@
            //   yAxisIndex: 'none'
            // },
            // ä¿å­˜ä¸ºå›¾ç‰‡
            saveAsImage: {}
          }
            saveAsImage: {},
          },
        },
        xAxis: {
          type: 'time',
@@ -93,31 +92,30 @@
          {
            name: '油烟浓度',
            type: 'line',
            data: []
          }
        ]
      };
            data: [],
          },
        ],
      }
      // ä½¿ç”¨åˆšæŒ‡å®šçš„配置项和数据显示图表
      this.chart.setOption(option, true);
      this.chart.setOption(option, true)
    },
    // è·Ÿé¡µé¢å“åº”式变化
    resizeChart() {
      this.$nextTick(() => {
        if (this.chart) {
          this.chart.resize();
          this.chart.resize()
        }
      });
      })
      // this.chart.resize();
    }
  }
};
    },
  },
}
</script>
<style>
.line-chart {
  width:920px;
  width: 100%;
  height: 300px;
  margin-bottom: 20px;
  /* margin-left: 10px; */
src/sfc/TimeSelect.vue
@@ -1,8 +1,8 @@
<!-- æ—¥æœŸæ—¶é—´é€‰æ‹©å™¨ç»„ä»¶
<!-- æ—¥æœŸæ—¶é—´é€‰æ‹©å™¨ç»„ä»¶
  ä¼šå°†åˆå§‹é»˜è®¤æ—¶é—´ï¼ˆä¸€å‘¨å‰ï¼‰å’Œæ”¹å˜çš„æ—¶é—´é€šè¿‡äº‹ä»¶â€˜submitTime’传递给父组件
  åˆå§‹æ¸²æŸ“时就将时间传递给父组件:
  **
  **
  åœ¨çˆ¶ç»„件中设置
  <TimeSelect @submit-time="giveTime"></TimeSelect>
   giveTime(val) {
@@ -16,63 +16,192 @@
import dayjs from 'dayjs'
export default {
  emits: ['submitTime'],
  props: {
    // æŽ§åˆ¶æ˜¯å¦å¯ç”¨æ–°çš„æ•ˆæžœï¼ˆå¿«æ·æŒ‰é’® + è¯¦æƒ…按钮)
    useNewStyle: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      //保存开始和结束时间
      // éšä¾¿è®¾ç½®åˆå§‹å€¼ ï¼Œmounted时再设正确的,目的是改变时间了触发change
      time: ['2023-06-01 12:00:00', '2023-06-20 16:00:00']
    };
      time: ['2023-06-01 12:00:00', '2023-06-20 16:00:00'],
      // æŽ§åˆ¶æ—¶é—´é€‰æ‹©å™¨çš„æ˜¾ç¤º/隐藏(仅在新样式下使用)
      showTimePicker: false,
      // æ—¶é—´èŒƒå›´é€‰é¡¹
      timeRanges: [
        { value: 'today', label: '今日' },
        { value: 'yesterday', label: '昨日' },
        { value: 'thisWeek', label: '本周' },
        { value: 'lastWeek', label: '上周' },
        { value: 'thisMonth', label: '本月' },
        { value: 'lastMonth', label: '上月' },
      ],
      // é€‰ä¸­çš„æ—¶é—´èŒƒå›´
      selectedRange: '',
    }
  },
  // å°†åˆå§‹é»˜è®¤å¼€å§‹å’Œç»“束时间传递给父组件
  mounted() {
    this,this.initOneWeekAgoTime()
    this.$emit('submitTime', this.time);
    if (this.useNewStyle) {
      // æ–°æ ·å¼ä¸‹é»˜è®¤é€‰ä¸­ä»Šæ—¥
      this.selectedRange = 'today'
      this.selectTimeRange('today')
    } else {
      // åŽŸæœ‰æ ·å¼ä¸‹ä½¿ç”¨ä¸€å‘¨å‰çš„é»˜è®¤æ—¶é—´
      this.initOneWeekAgoTime()
      this.$emit('submitTime', this.time)
    }
  },
  methods:{
    initOneWeekAgoTime(){
  methods: {
    initOneWeekAgoTime() {
      // ç»™æ—¶é—´é€‰æ‹©å™¨è®¾ç½®é»˜è®¤æ—¶é—´ä¸ºä¸€å‘¨å‰
    this.time[0] = dayjs().subtract(4, 'week').format('YYYY-MM-DD HH:mm:ss');
    this.time[1] = dayjs().format('YYYY-MM-DD HH:mm:ss');
    }
  }
};
      this.time[0] = dayjs().subtract(4, 'week').format('YYYY-MM-DD HH:mm:ss')
      this.time[1] = dayjs().format('YYYY-MM-DD HH:mm:ss')
    },
    // å¿«æ·æ—¶æ®µé€‰æ‹©
    selectTimeRange(range) {
      let now = dayjs()
      let start, end
      switch (range) {
        case 'today':
          start = now.startOf('day').format('YYYY-MM-DD HH:mm:ss')
          end = now.endOf('day').format('YYYY-MM-DD HH:mm:ss')
          break
        case 'yesterday':
          start = now.subtract(1, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss')
          end = now.subtract(1, 'day').endOf('day').format('YYYY-MM-DD HH:mm:ss')
          break
        case 'thisWeek':
          start = now.startOf('week').format('YYYY-MM-DD HH:mm:ss')
          end = now.endOf('week').format('YYYY-MM-DD HH:mm:ss')
          break
        case 'lastWeek':
          start = now.subtract(1, 'week').startOf('week').format('YYYY-MM-DD HH:mm:ss')
          end = now.subtract(1, 'week').endOf('week').format('YYYY-MM-DD HH:mm:ss')
          break
        case 'thisMonth':
          start = now.startOf('month').format('YYYY-MM-DD HH:mm:ss')
          end = now.endOf('month').format('YYYY-MM-DD HH:mm:ss')
          break
        case 'lastMonth':
          start = now.subtract(1, 'month').startOf('month').format('YYYY-MM-DD HH:mm:ss')
          end = now.subtract(1, 'month').endOf('month').format('YYYY-MM-DD HH:mm:ss')
          break
      }
      this.time = [start, end]
      this.$emit('submitTime', this.time)
    },
    // å¤„理时间范围选择变化
    handleRangeChange(val) {
      if (val) {
        this.selectTimeRange(val)
      }
    },
    // åˆ‡æ¢æ—¶é—´é€‰æ‹©å™¨çš„æ˜¾ç¤º/隐藏
    toggleTimePicker() {
      this.showTimePicker = !this.showTimePicker
    },
  },
}
</script>
<template>
  <!-- æ—¥æœŸæ—¶é—´é€‰æ‹©å™¨ -->
  <div class="block">
    <span class="demonstration">起止时间:</span>
  <!-- æ—¶é—´é€‰æ‹©ç»„ä»¶ -->
  <div v-if="useNewStyle" class="time-select-container">
    <el-row align="middle" class="m-b-8">
      <span class="demonstration">时间:</span>
      <!-- å¿«æ·æ—¶æ®µé€‰æ‹©æŒ‰é’® -->
      <div class="quick-time-buttons">
        <el-radio-group v-model="selectedRange" @change="handleRangeChange">
          <el-radio-button v-for="range in timeRanges" :key="range.value" :label="range.value">
            {{ range.label }}
          </el-radio-button>
        </el-radio-group>
        <el-button size="small" type="primary" @click="toggleTimePicker">时间详情</el-button>
      </div>
    </el-row>
    <!-- æ—¶é—´é€‰æ‹©å™¨ï¼ˆé»˜è®¤éšè—ï¼‰ -->
    <div v-show="showTimePicker" class="time-picker-container">
      <el-date-picker
        v-model="time"
        type="daterange"
        range-separator="~"
        start-placeholder="开始时间"
        end-placeholder="结束时间"
        @change="$emit('submitTime', time)"
        class="pick-date"
      />
    </div>
  </div>
  <!-- åŽŸæœ‰æ ·å¼ -->
  <div v-else class="block">
    <span class="demonstration">时间:</span>
    <el-date-picker
      v-model="time"
      type="datetimerange"
      type="daterange"
      range-separator="~"
      start-placeholder="Start date"
      end-placeholder="End date"
      @change="$emit('submitTime', time)"
      class="pick-date"
    />
  </div>
</template>
<style>
.time-select-container {
  /* width: 100%; */
}
.time-label {
  margin-bottom: 8px;
}
.quick-time-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}
.time-picker-container {
  display: flex;
  align-items: center;
  white-space: nowrap;
}
.demonstration {
  margin-left: 30px;
  margin-top: 5px;
  margin-right: 10px;
  font-weight: bold;
  white-space: nowrap;
}
.pick-date {
  width: 100%;
}
.block {
  display: flex;
  justify-content: center;
  /* width: 50%; */
  white-space: nowrap;
}
.pick-date {
  width: 100%;
}
.block .demonstration {
  margin-left: 30px;
  margin-top: 5px;
}
</style>
src/style/base.scss
@@ -5,3 +5,102 @@
body {
  margin: 0;
}
/************************************** font size **************************************/
$fontsize: (
  s: var(--el-font-size-small),
  b: var(--el-font-size-base),
  m: var(--el-font-size-medium),
  l: var(--el-font-size-large),
);
@each $dName, $dValue in $fontsize {
  .f-#{$dName} {
    font-size: #{$dValue};
  }
}
/************************************** color **************************************/
$colors: (
  p: var(--el-color-primary),
  s: var(--el-color-success),
  w: var(--el-color-warning),
  d: var(--el-color-danger),
  e: var(--el-color-error),
  i: var(--el-color-info),
);
@each $dName, $dValue in $colors {
  .color-#{$dName} {
    color: #{$dValue};
  }
  .b-color-#{$dName} {
    background-color: #{$dValue};
  }
}
/************************************** å†…外边距 **************************************/
$direction: (
  l: 'left',
  t: 'top',
  r: 'right',
  b: 'bottom',
);
$size: (4, 8, 10, 16);
@each $dName, $dValue in $direction {
  @each $i in $size {
    .p-#{$dName}-#{$i} {
      padding-#{$dValue}: #{$i}px;
    }
    .m-#{$dName}-#{$i} {
      margin-#{$dValue}: #{$i}px;
    }
  }
}
@each $i in $size {
  .p-#{$i} {
    padding: #{$i}px;
  }
  .p-v-#{$i} {
    padding: #{$i}px 0;
  }
  .p-h-#{$i} {
    padding: 0 #{$i}px;
  }
  .m-v-#{$i} {
    margin: #{$i}px 0;
  }
  .m-h-#{$i} {
    margin: 0 #{$i}px;
  }
}
/************************************** å®½é«˜ **************************************/
$csize: (
  small: var(--el-component-size-small),
  default: var(--el-component-size-default),
  large: var(--el-component-size-large),
);
$ws: (20px, 40px, 60px, 100px, 150px, 250px, 300px);
@each $name, $value in $csize {
  .w-#{$name} {
    width: #{$value};
  }
  .h-#{$name} {
    height: #{$value};
  }
}
@each $i in $ws {
  .w-#{$i} {
    width: #{$i};
  }
  .h-#{$i} {
    height: #{$i};
  }
}
//
.cursor-p {
  cursor: pointer;
}
src/style/layout.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,70 @@
// è¡¨å•的响应式栅格布局
function formResponsiveCols() {
  return {
    xs: {
      span: 24,
      offset: 0,
      push: 0,
      pull: 0
    },
    sm: {
      span: 24,
      offset: 0,
      push: 0,
      pull: 0
    },
    md: {
      span: 20,
      offset: 2,
      push: 0,
      pull: 0
    },
    lg: {
      span: 18,
      offset: 3,
      push: 0,
      pull: 0
    },
    xl: {
      span: 14,
      offset: 5,
      push: 0,
      pull: 0
    }
  };
}
const cardResponsiveCols = {
  xs: {
    span: 24,
    offset: 0,
    push: 0,
    pull: 0
  },
  sm: {
    span: 24,
    offset: 0,
    push: 0,
    pull: 0
  },
  md: {
    span: 24,
    offset: 0,
    push: 0,
    pull: 0
  },
  lg: {
    span: 12,
    offset: 0,
    push: 0,
    pull: 0
  },
  xl: {
    span: 8,
    offset: 0,
    push: 0,
    pull: 0
  }
};
export { formResponsiveCols, cardResponsiveCols };
src/utils/map/districtsearch.js
@@ -1,53 +1,83 @@
import { map, AMap, onMapMounted } from './index';
import { map, AMap, onMapMounted } from './index'
// è¡Œæ”¿åŒºåˆ’缓存
var districtPolygonMap = new Map();
var districtPolygonMap = new Map()
// å½“前显示的区县
var activeDistrict = undefined;
var activeDistrict = undefined
export default {
  // ç»˜åˆ¶åŒºåŽ¿è¾¹ç•Œ
  drawDistrict(districtName, isNew) {
    if(!districtName) return;
    if (!districtName) return
    onMapMounted(() => {
      if (!isNew && districtPolygonMap.has(districtName)) {
        const districtPolygon = districtPolygonMap.get(districtName);
        map.add(districtPolygon);
        map.setFitView(districtPolygon);
        activeDistrict = districtPolygon;
        const { polygon, mask } = districtPolygonMap.get(districtName)
        map.add(polygon)
        map.setFitView(polygon)
        map.setMask(mask)
        activeDistrict = polygon
      } else {
        var district = new AMap.DistrictSearch({
          extensions: 'all', //返回行政区边界坐标等具体信息
          level: 'district' //设置查询行政区级别为区
        });
          level: 'district', //设置查询行政区级别为区
        })
        district.search(districtName, function (status, result) {
          var bounds = result.districtList[0].boundaries; //获取边界信息
          if (bounds) {
          if (status === 'complete') {
            var bounds = result.districtList[0].boundaries //获取边界信息
            var mask = []
            var polygon = []
            for (var i = 0; i < bounds.length; i++) {
              mask.push([bounds[i]])
              //生成行政区划 polygon
              const districtPolygon = new AMap.Polygon({
                map: map, //显示该覆盖物的地图对象
                strokeWeight: 1, //轮廓线宽度
                strokeWeight: 2, //轮廓线宽度
                path: bounds[i], //多边形轮廓线的节点坐标数组
                fillOpacity: 0.4, //多边形填充透明度
                fillOpacity: 0, //多边形填充透明度
                fillColor: '#0077ff',
                strokeColor: '#CC66CC' //线条颜色
              });
                strokeColor: '#99ffff', //线条颜色
              })
              districtPolygonMap.set(districtName, districtPolygon);
              activeDistrict = districtPolygon;
              map.setFitView(districtPolygon, true);
              polygon.push(districtPolygon)
            }
            activeDistrict = polygon
            districtPolygonMap.set(districtName, { polygon, mask })
            map.setFitView(polygon, true)
            map.setMask(mask)
          }
        });
        })
      }
    });
    })
  },
  removeDistrict() {
    onMapMounted(() => {
      if (activeDistrict) {
        map.remove(activeDistrict);
        activeDistrict = undefined;
        map.remove(activeDistrict)
        activeDistrict = undefined
      }
    })
  }
};
  },
  districtLayer(districtName) {
    onMapMounted(() => {
      //2、创建省市简易行政区图层
      var distProvince = new AMap.DistrictLayer.Province({
        zIndex: 10, //设置图层层级
        zooms: [2, 15], //设置图层显示范围
        adcode: districtName, //设置行政区 adcode
        depth: 2, //设置数据显示层级,0:显示国家面,1:显示省级,当国家为中国时设置depth为2的可以显示市一级
      })
      // 3、设置行政区图层样式
      distProvince.setStyles({
        'stroke-width': 2, //描边线宽
        fill: function (data) {
          //设置区域填充颜色,可根据回调信息返回区域信息设置不同填充色
          //回调返回区域信息数据,字段包括 SOC(国家代码)、NAME_ENG(英文名称)、NAME_CHN(中文名称)等
          //国家代码名称说明参考 https://a.amap.com/jsapi_demos/static/demo-center/js/soc-list.json
          return '#ffffffe7'
        },
      })
      //4、将简易行政区图层添加到地图
      map.add(distProvince)
    })
  },
}
src/utils/map/index.js
@@ -1,28 +1,28 @@
import AMapLoader from '@amap/amap-jsapi-loader';
import AMapLoader from '@amap/amap-jsapi-loader'
var mapInitDone = false;
var onMapMountedEvents = [];
var mapInitDone = false
var onMapMountedEvents = []
var AMap;
var AMap
// åœ°å›¾å¯¹è±¡
var map;
var map
// å«æ˜Ÿå›¾å±‚
var satellite;
var satellite
// é¼ æ ‡ç»˜å›¾
var mouseTool;
var mouseTool
// 3D图层
var object3Dlayer;
var object3Dlayer
// åœ°å›¾æ‹–动状态
var isDragging = false;
var isDragging = false
// åœ°å›¾åŠ è½½å®Œæˆè§¦å‘
function onMapMounted(...events) {
  if (mapInitDone) {
    events.forEach((e) => {
      e();
    });
      e()
    })
  } else {
    onMapMountedEvents = onMapMountedEvents.concat(events);
    onMapMountedEvents = onMapMountedEvents.concat(events)
  }
}
@@ -36,50 +36,51 @@
      'AMap.ControlBar',
      'AMap.ToolBar',
      'AMap.Scale',
      'AMap.DistrictSearch'
      'AMap.DistrictSearch',
      // 'AMap.DragRoute',
      // 'AMap.MouseTool',
      // 'AMap.PolygonEditor'
    ]
    ],
  })
    .then((_AMap) => {
      AMap = _AMap;
      _initMap(id);
      mapInitDone = true;
      AMap = _AMap
      _initMap(id)
      mapInitDone = true
      onMapMountedEvents.forEach((e) => {
        e();
      });
      onMapMountedEvents = [];
      console.log('-------------------map init done');
        e()
      })
      onMapMountedEvents = []
      console.log('-------------------map init done')
    })
    .catch((e) => {
      console.log(e);
    });
      console.log(e)
    })
}
function destroyMap() {
  map?.destroy();
  map = null;
  mapInitDone = false;
  map?.destroy()
  map = null
  mapInitDone = false
}
function _initMap(elementId) {
  map = new AMap.Map(elementId, {
    // mapStyle: 'amap://styles/e1e78509de64ddcd2efb4cb34c6fae2a',
    features: ['bg', 'road'],
    pitch: 45, // åœ°å›¾ä¿¯ä»°è§’度,有效范围 0 åº¦- 83 åº¦
    viewMode: '2D', // åœ°å›¾æ¨¡å¼
    pitch: 30, // åœ°å›¾ä¿¯ä»°è§’度,有效范围 0 åº¦- 83 åº¦
    viewMode: '3D', // åœ°å›¾æ¨¡å¼
    resizeEnable: true,
    center: [121.6039283, 31.25295567],
    zooms: [2, 26],
    zoom: 14
  });
    zoom: 14,
  })
  // map = new AMap.Map(elementId);
  // æ·»åŠ å«æ˜Ÿåœ°å›¾
  satellite = new AMap.TileLayer.Satellite();
  satellite.hide();
  map.add([satellite]);
  satellite = new AMap.TileLayer.Satellite()
  const roadNet = new AMap.TileLayer.RoadNet()
  // satellite.hide()
  map.add([satellite, roadNet])
  // _initMouseTool();
  // _init3DLayer();
@@ -88,36 +89,27 @@
// é¼ æ ‡ç»˜å›¾åˆå§‹åŒ–
function _initMouseTool() {
  mouseTool = new AMap.MouseTool(map);
  mouseTool = new AMap.MouseTool(map)
}
// 3D图层初始化
function _init3DLayer() {
  object3Dlayer = new AMap.Object3DLayer();
  map.add(object3Dlayer);
  object3Dlayer = new AMap.Object3DLayer()
  map.add(object3Dlayer)
}
// è®¾ç½®åœ°å›¾æ‹–拽监听事件
function _initDragEvent() {
  let dragEndEvent;
  let dragEndEvent
  map.on('dragstart', () => {
    clearTimeout(dragEndEvent);
    isDragging = true;
  });
    clearTimeout(dragEndEvent)
    isDragging = true
  })
  map.on('dragend', function () {
    dragEndEvent = setTimeout(() => {
      isDragging = false;
    }, 8000);
  });
      isDragging = false
    }, 8000)
  })
}
export {
  createMap,
  destroyMap,
  onMapMounted,
  map,
  AMap,
  mouseTool,
  object3Dlayer,
  isDragging
};
export { createMap, destroyMap, onMapMounted, map, AMap, mouseTool, object3Dlayer, isDragging }
src/views/inspection/scenenew/UserEdit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,191 @@
<template>
  <el-page-header @back="onBack">
    <template #content>
      <span> è´¦æˆ·ä¿¡æ¯ç¼–辑 </span>
    </template>
  </el-page-header>
  <el-divider />
  <el-tabs v-model="activeName" class="p-h-8" @tab-click="handleClick">
    <el-tab-pane label="基本信息" name="first">
      <FormCol>
        <div class="sub-title">账户信息</div>
        <CompUserInfo :model="formUser" />
      </FormCol>
      <el-divider />
      <FormCol>
        <div class="sub-title">店铺信息</div>
        <CompSceneInfo :form-info="formSceneInfo" />
      </FormCol>
      <el-divider />
      <FormCol>
        <div class="sub-title">企业信息</div>
        <CompCompanyInfo :form-info="formCompanyInfo" />
      </FormCol>
    </el-tab-pane>
    <el-tab-pane label="设备管理" name="second">
      <FormCol>
        <el-row class="sub-title" justify="space-between">
          <div>监测设备信息</div>
          <el-button type="success" @click="addDevice">新增设备</el-button>
        </el-row>
        <!-- æ–°å¢žè®¾å¤‡è¡¨å• -->
        <CompDeviceInfo
          v-if="showAddDevice"
          :form-info="newDeviceForm"
          :create="true"
          @onSubmit="handleDeviceSubmit"
          @onCancel="handleDeviceCancel"
        />
        <!-- è®¾å¤‡åˆ—表 -->
        <div v-if="formDeviceList.length === 0 && !showAddDevice" class="empty-device">
          <el-empty description="暂无设备信息" />
        </div>
        <CompDeviceInfo
          v-else-if="formDeviceList.length > 0"
          v-for="(item, index) in formDeviceList"
          :key="index"
          :form-info="item"
        />
      </FormCol>
    </el-tab-pane>
    <el-tab-pane label="危废排污" name="third">
      <FormCol>
        <div class="sub-title">危废排污清单</div>
        <CompHazardousWasteFile :form-info="formHazardousWasteFile" />
      </FormCol>
      <FormCol>
        <div class="sub-title">危废排污记录</div>
        <CompHazardousWasteRecord :form-info="formHazardousWasteRecord" />
      </FormCol>
    </el-tab-pane>
    <el-tab-pane label="行政处罚" name="fourth">
      <FormCol>
        <div class="sub-title">行政处罚表</div>
        <!-- <CompPunishment :form-info="formProblem" /> -->
      </FormCol>
    </el-tab-pane>
    <el-tab-pane label="信访投诉" name="fifth">
      <FormCol>
        <div class="sub-title">信访投诉</div>
        <!-- <CompLaint :form-info="formLaint" /> -->
      </FormCol>
    </el-tab-pane>
    <el-tab-pane label="巡查问题表" name="sixth">
      <FormCol>
        <div class="sub-title">巡查问题表</div>
        <!-- <CompProblem :form-info="formProblem" /> -->
      </FormCol>
    </el-tab-pane>
  </el-tabs>
  <!-- <ComBaseInformation v-model="drawer"></ComBaseInformation> -->
</template>
<script>
import userApi from '@/api/fytz/userApi'
import CompUserInfo from './components/CompUserInfo.vue'
import CompSceneInfo from './components/CompSceneInfo.vue'
import CompCompanyInfo from './components/CompCompanyInfo.vue'
import CompDeviceInfo from './components/CompDeviceInfo.vue'
import CompHazardousWasteFile from './components/CompHazardousWasteFile.vue'
import CompHazardousWasteRecord from './components/CompHazardousWasteRecord.vue'
export default {
  components: {
    // ComBaseInformation,
    // CompLaint,
    CompUserInfo,
    CompSceneInfo,
    CompCompanyInfo,
    CompDeviceInfo,
    CompHazardousWasteFile,
    CompHazardousWasteRecord,
    // CompPanyInfo,
    // CompFumePurifyDevice,
    // CompHazardousWasteFile,
    // CompHazardousWasteRecord,
    // CompProblem,
    // CompPunishment,
    // CompRestaurantBaseInfo,
    // CompVehicleBaseInfo,
    // CompUserInfos,
  },
  data() {
    return {
      drawer: false,
      formUser: {},
      formSceneInfo: {},
      formCompanyInfo: {},
      // formSubScene: {},
      formLaint: {},
      formDeviceList: [],
      formPanyInfo: {},
      formFumePurifyDevice: {},
      formHazardousWasteFile: {},
      formHazardousWasteRecord: {},
      formProblem: {},
      formPunishment: {},
      formRestaurantBaseInfo: {},
      activeName: 'first',
      scroll: '',
      showAddDevice: false,
      newDeviceForm: {},
    }
  },
  beforeRouteEnter(to, from, next) {
    userApi.fetchUserBaseInfo(to.params.userId).then((res) => {
      next((vm) => {
        if (res.userInfo) {
          vm.formUser = res.userInfo
        } else {
          vm.formUser = {
            guid: to.params.userId,
          }
        }
      })
    })
  },
  methods: {
    handleClick(tab) {
      console.log('tab', tab)
    },
    // å›žé€€é¡µé¢
    onBack() {
      this.$router.back()
    },
    // æ–°å¢žè®¾å¤‡
    addDevice() {
      this.showAddDevice = true
      this.newDeviceForm = {}
    },
    // è®¾å¤‡æäº¤
    handleDeviceSubmit(formData) {
      // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„API调用
      this.formDeviceList.push(formData.value)
      this.showAddDevice = false
      this.newDeviceForm = {}
    },
    // è®¾å¤‡å–消
    handleDeviceCancel() {
      this.showAddDevice = false
      this.newDeviceForm = {}
    },
  },
}
</script>
<style scoped>
.sub-title {
  font-size: var(--el--font--size--large);
  margin-bottom: 30px;
  margin-top: 30px;
  margin-left: 20px;
}
.empty-device {
  padding: 40px 0;
  text-align: center;
}
</style>
src/views/inspection/scenenew/UserInfo.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,226 @@
<template>
  <FYTable @search="onSearch" :row-class-name="tableRowClassName">
    <template #options>
      <FYOptionLocation
        :allOption="true"
        :level="5"
        v-model:value="formSearch._locations"
      ></FYOptionLocation>
      <FYOptionText
        label="场景名称"
        placeholder="输入名称关键字"
        v-model:value="formSearch.searchText"
      ></FYOptionText>
      <FYOptionScene
        :allOption="true"
        :type="1"
        :initValue="false"
        v-model:value="formSearch.scensetype"
      ></FYOptionScene>
      <FYOptionOnlineStatus
        :allOption="true"
        v-model:value="formSearch.online"
      ></FYOptionOnlineStatus>
    </template>
    <template #table-column>
      <el-table-column
        fixed="left"
        prop="userInfo.realname"
        label="名称"
        :show-overflow-tooltip="true"
        width="400"
      >
      </el-table-column>
      <el-table-column prop="userInfo.acountname" label="账号" width="110" />
      <el-table-column prop="sceneTypeName" label="类型" width="100" />
      <el-table-column prop="biProvinceName" label="省" width="80" />
      <el-table-column prop="biCityName" label="市" width="80" />
      <!-- <el-table-column prop="districtname" label="区县" width="90" /> -->
      <el-table-column prop="userInfo.extension1" label="区县" width="80" />
      <el-table-column prop="biTownName" label="街道" width="110" />
      <el-table-column prop="biArea" label="集中区" width="110" />
      <el-table-column prop="biManagementCompany" label="物业" min-width="110" />
      <el-table-column prop="userInfo.isenable" label="状态" width="90">
        <template #default="{ row }">
          {{ row.userInfo.isenable ? '上线中' : '已下线' }}
        </template>
      </el-table-column>
      <el-table-column prop="userInfo.usertype" label="用户类型" width="90" />
      <el-table-column fixed="right" align="right" label="操作" width="190">
        <template #header>
          <el-button icon="DocumentAdd" size="default" type="success" @click="drawer = true"
            >新增用户</el-button
          >
        </template>
        <template #default="{ row }">
          <el-space>
            <el-button
              :loading="row.loading2"
              :type="row.userInfo.isenable != '0' ? 'danger' : 'primary'"
              size="small"
              @click="itemActive(row)"
              >{{ row.userInfo.isenable != '0' ? '下线' : '上线' }}</el-button
            >
            <el-button-group>
              <el-button type="primary" :loading="row.loading1" size="small" @click="editRow(row)"
                >查看</el-button
              >
              <el-dropdown @command="handleCommand" trigger="click">
                <el-button
                  type="primary"
                  :loading="row.downloadLoading"
                  size="small"
                  :icon="row.downloadLoading ? '' : 'ArrowDown'"
                ></el-button>
                <template #dropdown>
                  <el-dropdown-menu>
                    <el-dropdown-item icon="Download" :command="{ c: 1, p: row }"
                      >下载环信码</el-dropdown-item
                    >
                  </el-dropdown-menu>
                </template>
              </el-dropdown>
            </el-button-group>
            <!-- <el-dropdown
              split-button
              :loading="row.loading1"
              size="small"
              type="primary"
              @click="editRow(row)"
              @command="handleCommand"
              trigger="click"
            >
              æŸ¥çœ‹
              <template #dropdown>
                <el-dropdown-menu>
                  <el-dropdown-item icon="Download" :command="{ c: 1, p: row }"
                    >下载环信码</el-dropdown-item
                  >
                </el-dropdown-menu>
              </template>
            </el-dropdown> -->
          </el-space>
        </template>
      </el-table-column>
    </template>
  </FYTable>
  <CompUserInfoAddDrawer v-model="drawer"></CompUserInfoAddDrawer>
</template>
<script>
import userApi from '@/api/fytz/userApi'
import creditApi from '@/api/fytz/creditApi'
import { useLoadingStore } from '@/stores/loadingStore'
import { mapStores } from 'pinia'
import { useMessageBoxTip } from '@/composables/messageBox'
import CompUserInfoAddDrawer from './components/CompUserInfoAddDrawer.vue'
export default {
  components: {
    CompUserInfoAddDrawer,
  },
  data() {
    return {
      // æŸ¥è¯¢æ¡ä»¶
      formSearch: {
        _locations: {},
        searchText: '',
        scensetype: {
          label: '餐饮',
          value: '1',
        },
        online: {},
      },
      // æ–°å¢žç”¨æˆ·å¼¹å‡ºæ¡†
      drawer: false,
    }
  },
  computed: {
    ...mapStores(useLoadingStore),
  },
  methods: {
    onSearch(page, func) {
      const f = this.formSearch
      const area = {}
      // è¡Œæ”¿åŒºåˆ’
      area.provinceCode = f._locations.pCode
      area.provinceName = f._locations.pName
      if (area.provinceCode == null) {
        area.provinceCode = null
        area.provinceName = null
      }
      area.cityCode = f._locations.cCode
      area.cityName = f._locations.cName
      area.districtCode = f._locations.dCode
      area.districtName = f._locations.dName
      area.townCode = f._locations.tCode
      area.townName = f._locations.tName
      // åœºæ™¯ç±»åž‹
      area.sceneTypes = []
      f.scensetype.value == null ? (area.sceneTypes = []) : (area.sceneTypes = [f.scensetype.value])
      // ä¸Šä¸‹çº¿çŠ¶æ€
      area.online = f.online.value
      // å…³é”®å­—
      area.searchText = f.searchText
      userApi.fetchUser(page.currentPage, page.pageSize, area).then((res) => {
        if (res) {
          func({
            data: res.data,
            total: res.head.totalCount,
          })
        }
      })
    },
    editRow(row) {
      row.loading1 = true
      this.loadingStore.pushLoading(() => (row.loading1 = false))
      this.$router.push(`scene-info-edit/${row.userInfo.guid}`)
    },
    itemActive(row) {
      const param = {
        guid: row.userInfo.guid,
        isenable: !row.userInfo.isenable,
      }
      const msg = row.userInfo.isenable ? '下线' : '上线'
      useMessageBoxTip({
        confirmMsg: `确认${msg}该场景?`,
        confirmTitle: msg,
        onConfirm: async () => {
          row.loading2 = true
          return userApi
            .updateUserInfo(param)
            .then((res) => {
              if (res.success) {
                row.userInfo.isenable = param.isenable
              }
            })
            .finally(() => {
              row.loading2 = false
            })
        },
      })
    },
    tableRowClassName({ row }) {
      return row.userInfo.isenable ? 'online-row' : 'offline-row'
    },
    handleCommand(e) {
      const userId = e.p.userInfo.guid
      switch (e.c) {
        // ä¸‹è½½çŽ¯ä¿¡ç 
        case 1:
          e.p.downloadLoading = true
          creditApi.downloadCode(userId).finally(() => {
            e.p.downloadLoading = false
          })
          break
        default:
          break
      }
    },
  },
}
</script>
<style></style>
src/views/inspection/scenenew/components/CompCompanyInfo.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,205 @@
<template>
  <el-form
    :inline="false"
    :model="formObj"
    ref="formRef"
    :rules="rules"
    label-position="right"
    label-width="150px"
  >
    <el-form-item label="单位名称" prop="companyName">
      <el-input clearable v-model="formObj.companyName" placeholder="单位名称" />
    </el-form-item>
    <el-form-item label="主要从事业务" prop="businessScope">
      <el-input
        clearable
        v-model="formObj.businessScope"
        placeholder="主要从事业务"
        type="textarea"
        rows="2"
      />
    </el-form-item>
    <el-form-item label="所属集团" prop="group">
      <el-input clearable v-model="formObj.group" placeholder="所属集团" />
    </el-form-item>
    <el-form-item label="省份名称" prop="province">
      <el-input clearable v-model="formObj.province" placeholder="省份名称" />
    </el-form-item>
    <el-form-item label="地市名称" prop="city">
      <el-input clearable v-model="formObj.city" placeholder="地市名称" />
    </el-form-item>
    <el-form-item label="区县名称" prop="district">
      <el-input clearable v-model="formObj.district" placeholder="区县名称" />
    </el-form-item>
    <el-form-item label="街镇名称" prop="town">
      <el-input clearable v-model="formObj.town" placeholder="街镇名称" />
    </el-form-item>
    <el-form-item label="所在工业区" prop="industrialZone">
      <el-input clearable v-model="formObj.industrialZone" placeholder="所在工业区" />
    </el-form-item>
    <el-form-item label="单位地址" prop="address">
      <el-input
        clearable
        v-model="formObj.address"
        placeholder="单位地址"
        type="textarea"
        rows="2"
      />
    </el-form-item>
    <el-form-item label="组织机构代码" prop="orgCode">
      <el-input clearable v-model="formObj.orgCode" placeholder="组织机构代码" />
    </el-form-item>
    <el-form-item label="法人" prop="legalPerson">
      <el-input clearable v-model="formObj.legalPerson" placeholder="法人" />
    </el-form-item>
    <el-form-item label="行业类别" prop="industryCategory">
      <el-input clearable v-model="formObj.industryCategory" placeholder="行业类别" />
    </el-form-item>
    <el-form-item label="行业代码" prop="industryCode">
      <el-input clearable v-model="formObj.industryCode" placeholder="行业代码" />
    </el-form-item>
    <el-form-item label="登记注册类型" prop="registrationType">
      <el-input clearable v-model="formObj.registrationType" placeholder="登记注册类型" />
    </el-form-item>
    <el-form-item label="注册资本(万元)" prop="registeredCapital">
      <el-input
        clearable
        type="number"
        v-model="formObj.registeredCapital"
        placeholder="注册资本(万元)"
      />
    </el-form-item>
    <el-form-item label="建厂年月" prop="establishmentDate">
      <el-input clearable v-model="formObj.establishmentDate" placeholder="建厂年月" />
    </el-form-item>
    <el-form-item label="最新改扩建年月" prop="latestExpansionDate">
      <el-input clearable v-model="formObj.latestExpansionDate" placeholder="最新改扩建年月" />
    </el-form-item>
    <el-form-item label="职工人数" prop="employeeCount">
      <el-input clearable type="number" v-model="formObj.employeeCount" placeholder="职工人数" />
    </el-form-item>
    <el-form-item label="企业规模" prop="companyScale">
      <el-input clearable v-model="formObj.companyScale" placeholder="企业规模" />
    </el-form-item>
    <el-form-item label="历次环评审批年月" prop="环评ApprovalDate">
      <el-input clearable v-model="formObj.环评ApprovalDate" placeholder="历次环评审批年月" />
    </el-form-item>
    <el-form-item label="排污许可证编号" prop="pollutantLicenseNumber">
      <el-input clearable v-model="formObj.pollutantLicenseNumber" placeholder="排污许可证编号" />
    </el-form-item>
    <el-form-item label="排污权交易文件" prop="emissionRightFile">
      <el-input clearable v-model="formObj.emissionRightFile" placeholder="排污权交易文件" />
    </el-form-item>
    <el-form-item label="邮政编码" prop="postalCode">
      <el-input clearable v-model="formObj.postalCode" placeholder="邮政编码" />
    </el-form-item>
    <el-form-item label="联系人姓名" prop="contactName">
      <el-input clearable v-model="formObj.contactName" placeholder="联系人姓名" />
    </el-form-item>
    <el-form-item label="联系电话" prop="telephone">
      <el-input clearable type="tel" v-model="formObj.telephone" placeholder="联系电话">
        <template #prepend>
          <el-icon><Iphone /></el-icon>
        </template>
      </el-input>
    </el-form-item>
    <el-form-item label="联系微信号" prop="wechat">
      <el-input clearable v-model="formObj.wechat" placeholder="联系微信号" />
    </el-form-item>
    <el-form-item label="传真" prop="fax">
      <el-input clearable v-model="formObj.fax" placeholder="传真" />
    </el-form-item>
    <el-form-item label="电子邮箱" prop="email">
      <el-input clearable type="email" v-model="formObj.email" placeholder="电子邮箱" />
    </el-form-item>
    <el-form-item>
      <el-button :disabled="!edit" type="primary" @click="onSubmit" :loading="updateLoading"
        >提交</el-button
      >
      <el-button :disabled="!edit" @click="onReset">重置</el-button>
    </el-form-item>
  </el-form>
</template>
<script setup>
import { defineProps, defineEmits, reactive, ref, watch } from 'vue'
import { Iphone } from '@element-plus/icons-vue'
import { useFormConfirm } from '@/composables/formConfirm'
const props = defineProps({
  //企业信息
  formInfo: Object,
  //是创建或者更新
  create: Boolean,
})
const emit = defineEmits(['onSubmit', 'onCancel'])
const { formObj, formRef, edit, onSubmit, onReset } = useFormConfirm({
  submit: {
    do: submit,
  },
  cancel: {
    do: cancel,
  },
})
const updateLoading = ref(false)
const rules = reactive({
  companyName: [
    {
      required: true,
      message: '单位名称不能为空',
      trigger: 'blur',
    },
  ],
  address: [
    {
      required: true,
      message: '单位地址不能为空',
      trigger: 'blur',
    },
  ],
  contactName: [
    {
      required: true,
      message: '联系人姓名不能为空',
      trigger: 'blur',
    },
  ],
  telephone: [
    {
      required: true,
      message: '联系电话不能为空',
      trigger: 'blur',
    },
  ],
})
// ä¼ä¸šä¿¡æ¯æ ¼å¼åŒ–
function parseCompanyInfo(s) {
  return s
}
function updateCompany() {
  updateLoading.value = true
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„API调用
  setTimeout(() => {
    updateLoading.value = false
  }, 1000)
}
watch(
  () => props.formInfo,
  (nValue) => {
    formObj.value = parseCompanyInfo(nValue)
  },
)
function submit() {
  emit('onSubmit', formObj)
  return updateCompany()
}
function cancel() {
  emit('onCancel')
}
</script>
src/views/inspection/scenenew/components/CompDeviceInfo.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,159 @@
<template>
  <el-form
    :inline="false"
    :model="formObj"
    ref="formRef"
    :rules="rules"
    label-position="right"
    label-width="150px"
  >
    <el-form-item label="设备名称" prop="deviceName">
      <el-input clearable v-model="formObj.deviceName" placeholder="设备名称" />
    </el-form-item>
    <el-form-item label="设备型号" prop="deviceModel">
      <el-input clearable v-model="formObj.deviceModel" placeholder="设备型号" />
    </el-form-item>
    <el-form-item label="设备编号" prop="deviceCode">
      <el-input clearable v-model="formObj.deviceCode" placeholder="设备编号" />
    </el-form-item>
    <el-form-item label="生产厂家" prop="manufacturer">
      <el-input clearable v-model="formObj.manufacturer" placeholder="生产厂家" />
    </el-form-item>
    <el-form-item label="安装日期" prop="installationDate">
      <el-input clearable v-model="formObj.installationDate" placeholder="安装日期" />
    </el-form-item>
    <el-form-item label="设备状态" prop="status">
      <el-select v-model="formObj.status" placeholder="设备状态">
        <el-option label="正常" value="normal" />
        <el-option label="异常" value="abnormal" />
        <el-option label="维护中" value="maintenance" />
        <el-option label="停用" value="disabled" />
      </el-select>
    </el-form-item>
    <el-form-item label="设备位置" prop="location">
      <el-input clearable v-model="formObj.location" placeholder="设备位置" />
    </el-form-item>
    <el-form-item label="维护周期" prop="maintenanceCycle">
      <el-input clearable v-model="formObj.maintenanceCycle" placeholder="维护周期" />
    </el-form-item>
    <el-form-item label="上次维护日期" prop="lastMaintenanceDate">
      <el-input clearable v-model="formObj.lastMaintenanceDate" placeholder="上次维护日期" />
    </el-form-item>
    <el-form-item label="负责人" prop="responsiblePerson">
      <el-input clearable v-model="formObj.responsiblePerson" placeholder="负责人" />
    </el-form-item>
    <el-form-item label="联系电话" prop="contactPhone">
      <el-input clearable type="tel" v-model="formObj.contactPhone" placeholder="联系电话">
        <template #prepend>
          <el-icon><Iphone /></el-icon>
        </template>
      </el-input>
    </el-form-item>
    <el-form-item label="设备描述" prop="description">
      <el-input
        clearable
        v-model="formObj.description"
        placeholder="设备描述"
        type="textarea"
        rows="2"
      />
    </el-form-item>
    <el-form-item>
      <el-button :disabled="!edit" type="primary" @click="onSubmit" :loading="updateLoading"
        >提交</el-button
      >
      <el-button :disabled="!edit" @click="onReset">重置</el-button>
      <el-button v-if="create" @click="cancel">取消</el-button>
    </el-form-item>
  </el-form>
</template>
<script setup>
import { defineProps, defineEmits, reactive, ref, watch } from 'vue'
import { Iphone } from '@element-plus/icons-vue'
import { useFormConfirm } from '@/composables/formConfirm'
const props = defineProps({
  //设备信息
  formInfo: Object,
  //是创建或者更新
  create: Boolean,
})
const emit = defineEmits(['onSubmit', 'onCancel'])
const { formObj, formRef, edit, onSubmit, onReset } = useFormConfirm({
  submit: {
    do: submit,
  },
  cancel: {
    do: cancel,
  },
})
const updateLoading = ref(false)
const rules = reactive({
  deviceName: [
    {
      required: true,
      message: '设备名称不能为空',
      trigger: 'blur',
    },
  ],
  deviceModel: [
    {
      required: true,
      message: '设备型号不能为空',
      trigger: 'blur',
    },
  ],
  deviceCode: [
    {
      required: true,
      message: '设备编号不能为空',
      trigger: 'blur',
    },
  ],
  manufacturer: [
    {
      required: true,
      message: '生产厂家不能为空',
      trigger: 'blur',
    },
  ],
  status: [
    {
      required: true,
      message: '设备状态不能为空',
      trigger: 'change',
    },
  ],
})
// è®¾å¤‡ä¿¡æ¯æ ¼å¼åŒ–
function parseDeviceInfo(s) {
  return s
}
function updateDevice() {
  updateLoading.value = true
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„API调用
  setTimeout(() => {
    updateLoading.value = false
  }, 1000)
}
watch(
  () => props.formInfo,
  (nValue) => {
    formObj.value = parseDeviceInfo(nValue)
  },
)
function submit() {
  emit('onSubmit', formObj)
  return updateDevice()
}
function cancel() {
  emit('onCancel')
}
</script>
src/views/inspection/scenenew/components/CompHazardousWasteFile.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<template>s</template>
src/views/inspection/scenenew/components/CompHazardousWasteRecord.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<template>s</template>
src/views/inspection/scenenew/components/CompSceneInfo.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,132 @@
<template>
  <el-form
    :inline="false"
    :model="formObj"
    ref="formRef"
    :rules="rules"
    label-position="right"
    label-width="150px"
  >
    <el-form-item label="店铺名称" prop="shopName">
      <el-input clearable v-model="formObj.shopName" placeholder="店铺名称" />
    </el-form-item>
    <el-form-item label="所属公司" prop="company">
      <el-input clearable v-model="formObj.company" placeholder="所属公司" />
    </el-form-item>
    <el-form-item label="集中区" prop="centralArea">
      <el-input clearable v-model="formObj.centralArea" placeholder="集中区" />
    </el-form-item>
    <el-form-item label="物业公司" prop="propertyCompany">
      <el-input clearable v-model="formObj.propertyCompany" placeholder="物业公司" />
    </el-form-item>
    <el-form-item label="联系人" prop="contactName">
      <el-input clearable v-model="formObj.contactName" placeholder="联系人" />
    </el-form-item>
    <el-form-item label="联系电话" prop="telephone">
      <el-input clearable type="tel" v-model="formObj.telephone" placeholder="联系电话">
        <template #prepend>
          <el-icon><Iphone /></el-icon>
        </template>
      </el-input>
    </el-form-item>
    <el-form-item label="地址" prop="address">
      <el-input clearable v-model="formObj.address" placeholder="地址" type="textarea" rows="2" />
    </el-form-item>
    <el-form-item>
      <el-button :disabled="!edit" type="primary" @click="onSubmit" :loading="updateLoading"
        >提交</el-button
      >
      <el-button :disabled="!edit" @click="onReset">重置</el-button>
    </el-form-item>
  </el-form>
</template>
<script setup>
import { defineProps, defineEmits, reactive, ref, watch } from 'vue'
import { Iphone } from '@element-plus/icons-vue'
import { useFormConfirm } from '@/composables/formConfirm'
const props = defineProps({
  //基本信息
  formInfo: Object,
  //是创建或者更新
  create: Boolean,
})
const emit = defineEmits(['onSubmit', 'onCancel'])
const { formObj, formRef, edit, onSubmit, onReset } = useFormConfirm({
  submit: {
    do: submit,
  },
  cancel: {
    do: cancel,
  },
})
const updateLoading = ref(false)
const rules = reactive({
  shopName: [
    {
      required: true,
      message: '店铺名称不能为空',
      trigger: 'blur',
    },
  ],
  company: [
    {
      required: true,
      message: '所属公司不能为空',
      trigger: 'blur',
    },
  ],
  contactName: [
    {
      required: true,
      message: '联系人不能为空',
      trigger: 'blur',
    },
  ],
  telephone: [
    {
      required: true,
      message: '联系电话不能为空',
      trigger: 'blur',
    },
  ],
  address: [
    {
      required: true,
      message: '地址不能为空',
      trigger: 'blur',
    },
  ],
})
// åœºæ™¯åŸºæœ¬ä¿¡æ¯æ ¼å¼åŒ–
function parseSceneBaseInfo(s) {
  return s
}
function updateScene() {
  updateLoading.value = true
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„API调用
  setTimeout(() => {
    updateLoading.value = false
  }, 1000)
}
watch(
  () => props.formInfo,
  (nValue) => {
    formObj.value = parseSceneBaseInfo(nValue)
  },
)
function submit() {
  emit('onSubmit', formObj)
  return updateScene()
}
function cancel() {
  emit('onCancel')
}
</script>
src/views/inspection/scenenew/components/CompUserInfo.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,250 @@
<template>
  <FYForm
    :form-info="formInfo"
    :rules="rules"
    :doClear="active"
    :useCancel="create"
    :useReset="!create"
    @update:isEdit="(v) => $emit('updateEdit', v)"
    @submit="submit"
    @cancel="$emit('onCancel')"
  >
    <template #form-item="{ formObj }">
      <!-- <el-form-item label="头像url" prop="HeadIconUrl">
        <el-input
          clearable
          v-model="formObj.HeadIconUrl"
          placeholder="头像url"
        />
      </el-form-item> -->
      <el-form-item label="账户名" prop="acountname">
        <el-input clearable v-model="formObj.acountname" placeholder="账户名" />
      </el-form-item>
      <el-form-item label="用户昵称" prop="realname">
        <el-input clearable v-model="formObj.realname" placeholder="用户昵称" />
      </el-form-item>
      <el-form-item label="密码" prop="password">
        <el-col :span="18">
          <el-input
            :disabled="true"
            clearable
            type="password"
            v-model="formObj.password"
            placeholder="默认密码123456"
          />
        </el-col>
        <el-col :span="6" v-if="!create">
          <el-row justify="end">
            <el-button type="danger" @click="onResetPw" :loading="pwLoading">重置密码</el-button>
          </el-row>
        </el-col>
      </el-form-item>
      <FYOptionUserType
        prop="_usertype"
        :allOption="false"
        :initValue="false"
        v-model:value="formObj._usertype"
      ></FYOptionUserType>
      <!-- <el-form-item label="所属企业" prop="departmentname">
        <el-input
          clearable
          v-model="formObj.departmentname"
          placeholder="所属企业"
          disabled
        />
      </el-form-item> -->
      <el-form-item label="是否可用" prop="isenable">
        <el-switch v-model="formObj.isenable" />
        <span style="margin-left: 16px">{{ formObj.isenable ? '可用' : '不可用' }}</span>
      </el-form-item>
      <el-form-item label="工号" prop="workno">
        <el-input clearable v-model="formObj.workno" placeholder="工号" />
      </el-form-item>
      <el-form-item label="手机" prop="telephone">
        <el-input clearable type="tel" v-model="formObj.telephone" placeholder="手机" />
      </el-form-item>
      <FYOptionLocation
        v-if="create"
        prop="_locations"
        :allOption="false"
        :level="5"
        :checkStrictly="true"
        :initValue="false"
        v-model:value="formObj._locations"
      ></FYOptionLocation>
      <FYOptionScene
        prop="_scenetype"
        :allOption="false"
        :type="1"
        :initValue="false"
        v-model:value="formObj._scenetype"
      ></FYOptionScene>
    </template>
  </FYForm>
</template>
<script setup>
import { defineProps, defineEmits, reactive, ref, watch, computed } from 'vue';
import { getSceneName } from '@/enum/scene';
import userApi from '@/api/fytz/userApi';
import { useMessageBoxTip } from '@/composables/messageBox';
const props = defineProps({
  //基本信息
  model: {
    type: Object
    // default: () => {
    //   return { isenable: true };
    // }
  },
  create: {
    type: Boolean,
    default: false
  },
  active: {
    type: Boolean,
    default: false
  }
});
const formInfo = ref({ isenable: true });
watch(
  () => props.model,
  (nValue) => {
    formInfo.value = parseUserInfo(nValue);
  }
);
const emit = defineEmits(['onSubmit', 'onCancel', 'updateEdit']);
const rules = reactive({
  acountname: [
    {
      required: true,
      message: '账户名不能为空',
      trigger: 'blur'
    }
  ],
  realname: [
    {
      required: true,
      message: '用户昵称不能为空',
      trigger: 'blur'
    }
  ]
  // password: [
  //   {
  //     required: props.create,
  //     message: '密码不能为空',
  //     trigger: 'blur',
  //   },
  // ],
});
// ç”¨æˆ·åŸºæœ¬ä¿¡æ¯æ ¼å¼åŒ–
function parseUserInfo(s) {
  if (s.usertype && s.usertypeid) {
    s._usertype = {
      label: s.usertype,
      value: s.usertypeid + ''
    };
  }
  if (s.extension2) {
    s._scenetype = getSceneName(s.extension2, 1);
  }
  s._locations = {};
  return s;
}
function parseUserInfoReverse(v) {
  // è¡Œæ”¿åŒºåˆ’信息填充
  const a = v._locations;
  v.extension1 = a.dName;
  // ç”¨æˆ·ç±»åž‹ä¿¡æ¯å¡«å……
  const b = v._usertype;
  v.usertypeid = b.value;
  v.usertype = b.label;
  // åœºæ™¯ç±»åž‹ä¿¡æ¯å¡«å……
  const c = v._scenetype;
  v.extension2 = c.value;
  return v;
}
function createUser(v, success, fail) {
  const l = v._locations;
  const params = {
    userInfo: v,
    baseInfo: {
      biProvinceCode: l.pCode,
      biProvinceName: l.pName,
      biCityCode: l.cCode,
      biCityName: l.cName,
      biDistrictCode: l.dCode,
      biDistrictName: l.dName,
      biTownCode: l.tCode,
      biTownName: l.tName,
      biAreaCode: l.aCode,
      biArea: l.aName
      // biManagementCompanyId:
      // biManagementCompany:
      // biContact
      // biTelephone
      // biAddress
    }
  };
  return userApi
    .createUser(params)
    .then(() => {
      emit('onSubmit', params);
      success();
    })
    .catch((err) => {
      fail(err);
    });
}
function updateUser(v, success, fail) {
  return userApi
    .updateUserInfo(v)
    .then(() => {
      emit('onSubmit', v);
      if (success) success();
    })
    .catch((err) => {
      if (fail) fail(err);
    });
}
function submit(v, success, fail) {
  parseUserInfoReverse(v.value);
  return props.create ? createUser(v.value, success, fail) : updateUser(v.value, success, fail);
  // parseUserInfoReverse(props.formInfo);
  // return props.create
  //   ? createUser(props.formInfo, success, fail)
  //   : updateUser(props.formInfo, success, fail);
}
// é‡ç½®å¯†ç 
const pwLoading = ref(false);
function onResetPw() {
  useMessageBoxTip({
    confirmMsg: '是否重置该场景密码?',
    confirmTitle: '重置密码',
    onConfirm: async () => {
      pwLoading.value = true;
      return userApi.resetPassword(formInfo.value.guid).finally(() => {
        pwLoading.value = false;
      });
    }
  });
}
</script>
src/views/inspection/scenenew/components/CompUserInfoAddDrawer.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
<template>
  <el-drawer
    title="新增用户"
    direction="rtl"
    :model-value="modelValue"
    @open="updateDrawer(true)"
    @close="updateDrawer(false)"
    :before-close="onDrawerClose"
  >
    <CompUserInfo
      :create="true"
      :active="modelValue"
      @updateEdit="(v) => (drawerEdit = v)"
      @on-submit="updateDrawer(false)"
      @on-cancel="updateDrawer(false)"
    ></CompUserInfo>
  </el-drawer>
</template>
<script>
import CompUserInfo from './CompUserInfo.vue';
import { useMessageBox } from '@/composables/messageBox';
export default {
  components: { CompUserInfo },
  props: ['modelValue'],
  emits: ['update:modelValue'],
  data() {
    return {
      drawerEdit: false
    };
  },
  methods: {
    updateDrawer(status) {
      this.$emit('update:modelValue', status);
    },
    onDrawerClose(done) {
      if (this.drawerEdit) {
        // å¼¹å‡ºç¡®è®¤æ¡†
        useMessageBox({
          confirmMsg: '是否放弃已编辑的内容?',
          confirmTitle: '关闭弹出框',
          onConfirm: () => {
            done();
          }
        });
      } else {
        // ç›´æŽ¥å…³é—­
        done();
      }
    },
  }
};
</script>
src/views/monitor/DataDashboard.vue
@@ -1,313 +1,865 @@
<template>
  <el-container class="data-dashboard">
    <el-main>
      <div class="grid-container">
        <div class="left-section">
          <!-- è®¾å¤‡åœ¨çº¿æƒ…况区域 -->
          <DeviceStatus
            :online-count="onlineCount"
            :offline-count="offlineCount"
            :normal-count="normalCount"
            :fault-count="faultCount"
          />
          <!-- åˆ†åŒºæ•°æ®æŽ’名区域 -->
          <DistrictRanking
            style="flex: 1"
            :selected-month="selectedMonth"
            :ranking-type="rankingType"
            :ranking-data="rankingData"
            :sorted-ranking-data="sortedRankingData"
            @month-change="handleMonthChange"
            @type-change="handleTypeChange"
          />
  <div class="data-dashboard">
    <!-- é¡¶éƒ¨æŒ‡æ ‡å¡ç‰‡åŒº -->
    <div class="top-cards">
      <div class="cards-container">
        <!-- æ—¶é—´å‘¨æœŸé€‰é¡¹å¡ç‰‡ -->
        <div class="time-period-card">
          <div class="card-title">时间周期</div>
          <div class="time-tab-container">
            <div
              v-for="tab in timeTabs"
              :key="tab.value"
              class="time-tab"
              :class="{ active: activeTime === tab.value }"
              @click="handleTimeChange(tab)"
            >
              {{ tab.label }}
            </div>
          </div>
        </div>
        <div class="right-section">
          <!-- è®¾å¤‡å®žæ—¶æ•°æ®åŒºåŸŸ -->
          <RealTimeData style="flex: 1" :devices="devices" />
        <!-- è¶…标数 -->
        <div class="metric-card">
          <div class="card-header">
            <div class="card-title">{{ getPeriodLabel() }}超标数</div>
            <div class="card-icon warning-icon">
              <svg
                width="20"
                height="20"
                viewBox="0 0 24 24"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path d="M12 9V13" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
                <path
                  d="M12 17.5V17"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                />
                <path
                  d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
              </svg>
            </div>
          </div>
          <div class="card-value">{{ metrics.overStandardCount }}</div>
          <div class="card-trend">
            <span
              class="trend-arrow"
              :class="{ up: metrics.overStandardTrend > 0, down: metrics.overStandardTrend < 0 }"
            >
              {{ metrics.overStandardTrend > 0 ? '↑' : '↓' }}
            </span>
            <span class="trend-text">{{ Math.abs(metrics.overStandardTrend) }}%</span>
            <span class="trend-label">{{ getCompareLabel() }}</span>
          </div>
        </div>
        <!-- åœ¨çº¿çއ -->
        <div class="metric-card">
          <div class="card-header">
            <div class="card-title">在线率</div>
            <div class="card-icon online-icon">
              <svg
                width="20"
                height="20"
                viewBox="0 0 24 24"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path
                  d="M9 12L11 14L15 10"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
                <path
                  d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
              </svg>
            </div>
          </div>
          <div class="card-value">{{ metrics.onlineRate }}%</div>
          <div class="card-trend">
            <span
              class="trend-arrow"
              :class="{ up: metrics.onlineRateTrend > 0, down: metrics.onlineRateTrend < 0 }"
            >
              {{ metrics.onlineRateTrend > 0 ? '↑' : '↓' }}
            </span>
            <span class="trend-text">{{ Math.abs(metrics.onlineRateTrend) }}%</span>
            <span class="trend-label">{{ getCompareLabel() }}</span>
          </div>
        </div>
        <!-- å‡€åŒ–器运行效率 -->
        <div class="metric-card">
          <div class="card-header">
            <div class="card-title">净化器运行效率</div>
            <div class="card-icon efficiency-icon">
              <svg
                width="20"
                height="20"
                viewBox="0 0 24 24"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path
                  d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2Z"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
                <path
                  d="M12 6V12L16 14"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
              </svg>
            </div>
          </div>
          <div class="card-value">{{ metrics.purifierEfficiency }}%</div>
          <div class="card-trend">
            <span
              class="trend-arrow"
              :class="{
                up: metrics.purifierEfficiencyTrend > 0,
                down: metrics.purifierEfficiencyTrend < 0,
              }"
            >
              {{ metrics.purifierEfficiencyTrend > 0 ? '↑' : '↓' }}
            </span>
            <span class="trend-text">{{ Math.abs(metrics.purifierEfficiencyTrend) }}%</span>
            <span class="trend-label">{{ getCompareLabel() }}</span>
          </div>
        </div>
        <!-- ä»»åŠ¡å®ŒæˆçŽ‡ -->
        <div class="metric-card">
          <div class="card-header">
            <div class="card-title">任务完成率</div>
            <div class="card-icon task-icon">
              <svg
                width="20"
                height="20"
                viewBox="0 0 24 24"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path
                  d="M22 11.08V12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C15.7376 2 19.0503 4.16113 20.7748 7.33007"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
                <path
                  d="M22 4L12 14.01L9 11.01"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
              </svg>
            </div>
          </div>
          <div class="card-value">{{ metrics.taskCompletionRate }}%</div>
          <div class="card-trend">
            <span
              class="trend-arrow"
              :class="{
                up: metrics.taskCompletionRateTrend > 0,
                down: metrics.taskCompletionRateTrend < 0,
              }"
            >
              {{ metrics.taskCompletionRateTrend > 0 ? '↑' : '↓' }}
            </span>
            <span class="trend-text">{{ Math.abs(metrics.taskCompletionRateTrend) }}%</span>
            <span class="trend-label">{{ getCompareLabel() }}</span>
          </div>
        </div>
      </div>
    </div>
      <!-- åœ¨çº¿è®¾å¤‡å’Œåº—铺清单区域 -->
      <ShopList
        :shops="shops"
        :shop-types="shopTypes"
        :districts="districts"
        :filter="filter"
        @filter-change="handleFilterChange"
      />
    </el-main>
  </el-container>
    <!-- ä¸»è¦å†…容区 -->
    <div class="main-content">
      <!-- ä¸­éƒ¨GIS地图区 -->
      <div class="map-section">
        <div id="map" class="map-container">
          <BaseMap></BaseMap>
        </div>
        <!-- åœ°å›¾ç‚¹ä½å¼¹çª— -->
        <el-dialog v-model="dialogVisible" title="企业实时数据" width="400px">
          <div class="dialog-content">
            <el-descriptions :column="1" border>
              <el-descriptions-item label="企业名称">{{
                selectedPoint.enterpriseName
              }}</el-descriptions-item>
              <el-descriptions-item label="设备编号">{{
                selectedPoint.deviceId
              }}</el-descriptions-item>
              <el-descriptions-item label="油烟浓度"
                >{{ selectedPoint.oilSmokeConcentration }} mg/m³</el-descriptions-item
              >
              <el-descriptions-item label="颗粒物"
                >{{ selectedPoint.particulateMatter }} mg/m³</el-descriptions-item
              >
              <el-descriptions-item label="非甲烷总烃"
                >{{ selectedPoint.nonMethaneHydrocarbon }} mg/m³</el-descriptions-item
              >
              <el-descriptions-item label="监测时间">{{
                selectedPoint.monitoringTime
              }}</el-descriptions-item>
              <el-descriptions-item label="超标情况">
                <el-tag :type="selectedPoint.isOverStandard ? 'danger' : 'success'">
                  {{ selectedPoint.isOverStandard ? '超标' : '正常' }}
                </el-tag>
              </el-descriptions-item>
            </el-descriptions>
          </div>
          <template #footer>
            <span class="dialog-footer">
              <el-button @click="dialogVisible = false">关闭</el-button>
              <el-button type="primary" @click="viewDetails">查看详情</el-button>
            </span>
          </template>
        </el-dialog>
      </div>
      <!-- å³ä¾§å®žæ—¶ç›‘测总览区 -->
      <div class="overview-section">
        <div class="section-header">
          <h3>实时监测总览</h3>
          <span class="view-more">查看更多</span>
        </div>
        <div class="overview-items-container">
          <div class="overview-item">
            <div class="overview-label">餐饮店铺总数</div>
            <div class="overview-value">{{ overview.totalShops }}</div>
          </div>
          <div class="overview-item">
            <div class="overview-label">在线设备数</div>
            <div class="overview-value">{{ overview.onlineDevices }}</div>
          </div>
          <div class="overview-item">
            <div class="overview-label">离线设备数</div>
            <div class="overview-value">{{ overview.offlineDevices }}</div>
          </div>
        </div>
        <!-- è®¾å¤‡çŠ¶æ€é¥¼å›¾ -->
        <div class="device-status-chart">
          <canvas id="deviceStatusChart"></canvas>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import DeviceStatus from '@/components/monitor/DeviceStatus.vue'
import RealTimeData from '@/components/monitor/RealTimeData.vue'
import DistrictRanking from '@/components/monitor/DistrictRanking.vue'
import ShopList from '@/components/monitor/ShopList.vue'
import * as echarts from 'echarts'
import { onMapMounted, map, AMap } from '@/utils/map/index'
import districtSearch from '@/utils/map/districtsearch.js'
export default {
  name: 'DataDashboard',
  components: {
    DeviceStatus,
    RealTimeData,
    DistrictRanking,
    ShopList,
  },
  data() {
    return {
      // è®¾å¤‡åœ¨çº¿æƒ…况数据
      onlineCount: 0,
      offlineCount: 0,
      normalCount: 0,
      faultCount: 0,
      // è®¾å¤‡å®žæ—¶æ•°æ®
      devices: [],
      // åˆ†åŒºæ•°æ®æŽ’名
      selectedMonth: '2023-12',
      rankingType: 'hourly',
      rankingData: [],
      sortedRankingData: [],
      // ç­›é€‰æ¡ä»¶
      filter: {
        district: '',
        shopType: '',
        status: '',
      },
      shopTypes: ['中餐', '西餐', '快餐', '火锅', '烧烤'],
      districts: ['东城区', '西城区', '朝阳区', '海淀区', '丰台区'],
      // åº—铺数据
      shops: [
        {
          id: 1,
          name: '张三餐厅',
          deviceCount: 2,
          status: '在线',
          district: '东城区',
          type: '中餐',
        },
        {
          id: 2,
          name: '李四饭店',
          deviceCount: 1,
          status: '离线',
          district: '西城区',
          type: '西餐',
        },
        {
          id: 3,
          name: '王五小吃',
          deviceCount: 3,
          status: '在线',
          district: '朝阳区',
          type: '快餐',
        },
        {
          id: 4,
          name: '赵六火锅',
          deviceCount: 2,
          status: '在线',
          district: '海淀区',
          type: '火锅',
        },
        {
          id: 5,
          name: '钱七烧烤',
          deviceCount: 1,
          status: '离线',
          district: '丰台区',
          type: '烧烤',
        },
      activeTime: 'day',
      timeTabs: [
        { label: '日', value: 'day' },
        { label: '周', value: 'week' },
        { label: '月', value: 'month' },
      ],
      dialogVisible: false,
      selectedPoint: {
        enterpriseName: '',
        deviceId: '',
        oilSmokeConcentration: 0,
        particulateMatter: 0,
        nonMethaneHydrocarbon: 0,
        monitoringTime: '',
        isOverStandard: false,
      },
      metrics: {
        overStandardCount: 12,
        overStandardTrend: 5,
        onlineRate: 92,
        onlineRateTrend: 2,
        purifierEfficiency: 85,
        purifierEfficiencyTrend: -3,
        taskCompletionRate: 78,
        taskCompletionRateTrend: 10,
      },
      overview: {
        totalShops: 245,
        onlineDevices: 220,
        offlineDevices: 25,
      },
      map: null,
      refreshTimer: null,
    }
  },
  mounted() {
    this.initData()
    this.startRealTimeUpdate()
    this.initMap()
    this.initDeviceStatusChart()
    this.startAutoRefresh()
  },
  beforeUnmount() {
    if (this.refreshTimer) {
      clearInterval(this.refreshTimer)
    }
  },
  methods: {
    initData() {
      // åˆå§‹åŒ–设备在线情况数据
      this.updateDeviceStatus()
      // åˆå§‹åŒ–实时数据
      this.updateRealTimeData()
      // åˆå§‹åŒ–分区数据排名
      this.updateRankingData()
    handleTimeChange(tab) {
      this.activeTime = tab.value
      // æ¨¡æ‹Ÿåˆ‡æ¢æ—¶é—´å‘¨æœŸåŽçš„æ•°æ®æ›´æ–°
      this.updateMetrics()
    },
    updateDeviceStatus() {
      // æ¨¡æ‹Ÿæ•°æ® - å®žé™…应从API获取
      this.onlineCount = Math.floor(Math.random() * 50) + 50
      this.offlineCount = Math.floor(Math.random() * 20)
      this.normalCount = this.onlineCount
      this.faultCount = this.offlineCount
    getPeriodLabel() {
      switch (this.activeTime) {
        case 'day':
          return '今日'
        case 'week':
          return '本周'
        case 'month':
          return '本月'
        default:
          return '今日'
      }
    },
    updateRealTimeData() {
      // æ¨¡æ‹Ÿæ•°æ® - å®žé™…应从API获取
      this.devices = [
        {
          deviceId: 'DEV-001',
          supplier: '供应商A',
          status: '正常',
          monitorTime: new Date().toLocaleString(),
          smokeDensity: (Math.random() * 2).toFixed(2),
          fanCurrent: (Math.random() * 5 + 1).toFixed(2),
          purifierCurrent: (Math.random() * 3 + 0.5).toFixed(2),
          hourlyData: this.generateHourlyData(),
        },
        {
          deviceId: 'DEV-002',
          supplier: '供应商B',
          status: '正常',
          monitorTime: new Date().toLocaleString(),
          smokeDensity: (Math.random() * 2).toFixed(2),
          fanCurrent: (Math.random() * 5 + 1).toFixed(2),
          purifierCurrent: (Math.random() * 3 + 0.5).toFixed(2),
          hourlyData: this.generateHourlyData(),
        },
        {
          deviceId: 'DEV-003',
          supplier: '供应商C',
          status: '异常',
          monitorTime: new Date().toLocaleString(),
          smokeDensity: (Math.random() * 15 + 5).toFixed(2),
          fanCurrent: (Math.random() * 3 + 4).toFixed(2),
          purifierCurrent: (Math.random() * 2 + 2).toFixed(2),
          hourlyData: this.generateHourlyData(),
        },
        {
          deviceId: 'DEV-004',
          supplier: '供应商D',
          status: '异常',
          monitorTime: new Date().toLocaleString(),
          smokeDensity: (Math.random() * 15 + 5).toFixed(2),
          fanCurrent: (Math.random() * 3 + 4).toFixed(2),
          purifierCurrent: (Math.random() * 2 + 2).toFixed(2),
          hourlyData: this.generateHourlyData(),
        },
      ]
    getCompareLabel() {
      switch (this.activeTime) {
        case 'day':
          return '较昨日'
        case 'week':
          return '较上周'
        case 'month':
          return '较上月'
        default:
          return '较昨日'
      }
    },
    updateMetrics() {
      // è¿™é‡Œåº”该根据选择的时间周期从接口获取数据
      // æ¨¡æ‹Ÿæ•°æ®æ›´æ–°
      setTimeout(() => {
        this.metrics = {
          overStandardCount: Math.floor(Math.random() * 30),
          overStandardTrend: Math.floor(Math.random() * 20) - 10,
          onlineRate: Math.floor(Math.random() * 20) + 80,
          onlineRateTrend: Math.floor(Math.random() * 10) - 5,
          purifierEfficiency: Math.floor(Math.random() * 30) + 70,
          purifierEfficiencyTrend: Math.floor(Math.random() * 10) - 5,
          taskCompletionRate: Math.floor(Math.random() * 40) + 60,
          taskCompletionRateTrend: Math.floor(Math.random() * 15) - 7,
        }
      }, 300)
    },
    initMap() {
      // setTimeout(() => {
      districtSearch.removeDistrict()
      districtSearch.drawDistrict('上海市')
      // districtSearch.districtLayer('310106')
      // }, 2000)
    },
    initDeviceStatusChart() {
      const chartDom = document.getElementById('deviceStatusChart')
      if (chartDom) {
        const chart = echarts.init(chartDom)
        const option = {
          tooltip: {
            trigger: 'item',
            formatter: '{b}: {c} ({d}%)',
            backgroundColor: 'rgba(255, 255, 255, 0.95)',
            borderColor: '#e8e8e8',
            borderWidth: 1,
            textStyle: {
              color: '#333',
            },
          },
          legend: {
            bottom: '0%',
            left: 'center',
            textStyle: {
              color: '#86909c',
              fontSize: 12,
            },
          },
          series: [
            {
              name: '设备状态',
              type: 'pie',
              radius: ['50%', '75%'],
              center: ['50%', '45%'],
              avoidLabelOverlap: false,
              itemStyle: {
                borderRadius: 8,
                borderColor: '#ffffff',
                borderWidth: 2,
                shadowBlur: 5,
                shadowOffsetX: 0,
                shadowColor: 'rgba(0, 0, 0, 0.1)',
              },
              label: {
                show: true,
                position: 'center',
                formatter: '{d}%',
                fontSize: 18,
                fontWeight: 'bold',
                color: '#262626',
              },
              labelLine: {
                show: false,
              },
              data: [
                {
                  value: this.overview.onlineDevices,
                  name: '在线',
                  itemStyle: {
                    color: '#1890ff',
                  },
                },
                {
                  value: this.overview.offlineDevices,
                  name: '离线',
                  itemStyle: {
                    color: '#f5222d',
                  },
                },
              ],
            },
          ],
        }
        chart.setOption(option)
    generateHourlyData() {
      // ç”Ÿæˆæ¨¡æ‹Ÿçš„近一小时数据
      const hourlyData = []
      for (let i = 59; i >= 0; i--) {
        const time = new Date()
        time.setMinutes(time.getMinutes() - i)
        hourlyData.push({
          time: time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
          smokeDensity: (Math.random() * 2).toFixed(2),
          fanCurrent: (Math.random() * 5 + 1).toFixed(2),
          purifierCurrent: (Math.random() * 3 + 0.5).toFixed(2),
        // å“åº”式调整
        window.addEventListener('resize', () => {
          chart.resize()
        })
      }
      return hourlyData
    },
    updateRankingData() {
      // æ¨¡æ‹Ÿæ•°æ® - å®žé™…应从API获取
      this.rankingData = [
        { name: '玄武区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 0 },
        { name: '秦淮区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: -1 },
        { name: '建邺区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 1 },
        { name: '鼓楼区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 0 },
        { name: '浦口区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: -2 },
        { name: '栖霞区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 2 },
        { name: '雨花台区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 0 },
        { name: '江宁区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 1 },
        { name: '六合区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: -1 },
        { name: '溧水区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 0 },
        { name: '高淳区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 1 },
      ]
      // æŽ’序
      this.sortedRankingData = [...this.rankingData].sort(
        (a, b) => parseFloat(b.value) - parseFloat(a.value),
      )
    },
    startRealTimeUpdate() {
      // æ¯30秒更新一次数据
      this.realTimeInterval = setInterval(() => {
        this.updateDeviceStatus()
        this.updateRealTimeData()
    startAutoRefresh() {
      // æ¯30秒自动刷新数据
      this.refreshTimer = setInterval(() => {
        this.updateMetrics()
        // è¿™é‡Œåº”该同时更新地图点位数据
      }, 30000)
    },
    handleMonthChange(val) {
      this.selectedMonth = val
      this.updateRankingData()
    },
    handleTypeChange(val) {
      this.rankingType = val
      this.updateRankingData()
    },
    handleFilterChange(val) {
      this.filter = { ...val }
    viewDetails() {
      // è·³è½¬åˆ°ä¼ä¸šç›‘控详情页
      this.$router.push('/monitor/enterprise-detail')
    },
  },
}
</script>
<style scoped>
/* å…¨å±€æ ·å¼ */
.data-dashboard {
  min-height: 100vh;
  width: 100%;
  height: calc(100vh - 60px);
  background-color: #f5f7fa;
  color: #333;
  box-sizing: border-box;
  font-family:
    -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  position: relative;
}
.el-header {
/* é¡¶éƒ¨æŒ‡æ ‡å¡ç‰‡åŒº */
.top-cards {
  position: absolute;
  top: 24px;
  left: 24px;
  z-index: 10;
  margin-bottom: 24px;
}
.cards-container {
  display: grid;
  grid-template-columns: 280px;
  grid-template-rows: auto repeat(4, auto);
  gap: 16px;
  background-color: rgba(255, 255, 255, 0.9);
  padding: 16px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* æ—¶é—´å‘¨æœŸå¡ç‰‡ */
.time-period-card {
  background-color: #ffffff;
  border-radius: 8px;
  padding: 20px;
  background-color: #f5f7fa;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.el-header h1 {
  font-size: 24px;
.time-period-card .card-title {
  font-size: 14px;
  color: #86909c;
  font-weight: 500;
  margin-bottom: 16px;
  text-align: center;
}
.time-tab-container {
  display: flex;
  flex-direction: row;
  gap: 8px;
  width: 100%;
  justify-content: center;
}
.time-tab {
  padding: 2px 4px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  transition: all 0.3s ease;
  color: #4e5969;
  text-align: center;
  border: 1px solid #e8e8e8;
  background-color: #fafafa;
}
.time-tab.active {
  background-color: #1890ff;
  color: #ffffff;
  border-color: #1890ff;
  box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);
}
.time-tab:hover:not(.active) {
  color: #1890ff;
  border-color: #e6f7ff;
  background-color: #e6f7ff;
}
/* æŒ‡æ ‡å¡ç‰‡ */
.metric-card {
  background-color: #ffffff;
  border-radius: 8px;
  padding: 20px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
  transition: all 0.3s ease;
  position: relative;
  overflow: hidden;
}
.metric-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
}
.card-title {
  font-size: 14px;
  color: #86909c;
  font-weight: 500;
}
.card-icon {
  color: #1890ff;
  display: flex;
  align-items: center;
  justify-content: center;
}
.warning-icon {
  color: #fa8c16;
}
.online-icon {
  color: #52c41a;
}
.efficiency-icon {
  color: #722ed1;
}
.task-icon {
  color: #1890ff;
}
.card-value {
  font-size: 32px;
  font-weight: bold;
  margin: 12px 0;
  color: #262626;
  line-height: 1.2;
}
.card-trend {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  padding-top: 12px;
  border-top: 1px solid #f0f0f0;
}
.trend-arrow {
  font-size: 14px;
  font-weight: bold;
}
.trend-arrow.up {
  color: #52c41a;
}
.trend-arrow.down {
  color: #f5222d;
}
.trend-text {
  color: #262626;
  font-weight: 500;
}
.trend-label {
  color: #86909c;
  font-size: 12px;
}
/* ä¸»è¦å†…容区 */
.main-content {
  width: 100%;
  height: 100%;
  position: relative;
}
/* ä¸­éƒ¨GIS地图区 */
.map-section {
  width: 100%;
  height: 100%;
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.section-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
  color: #262626;
}
.section-header h3 {
  font-size: 16px;
  font-weight: 600;
  color: #262626;
  margin: 0;
}
.view-more {
  font-size: 12px;
  color: #1890ff;
  cursor: pointer;
  display: flex;
  align-items: center;
}
.view-more:hover {
  text-decoration: underline;
}
.map-container {
  flex: 1;
  position: relative;
  overflow: hidden;
  background-color: #fafafa;
}
.map-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #86909c;
  font-size: 16px;
}
/* å³ä¾§å®žæ—¶ç›‘测总览区 */
.overview-section {
  position: absolute;
  top: 200px;
  right: 24px;
  width: 320px;
  background-color: rgba(255, 255, 255, 0.9);
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  padding: 20px;
  display: flex;
  flex-direction: column;
  z-index: 10;
  max-height: calc(100vh - 220px);
}
.overview-items-container {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 24px;
  padding-bottom: 16px;
  border-bottom: 1px solid #f0f0f0;
}
.overview-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  flex: 1;
  text-align: center;
}
.overview-label {
  font-size: 12px;
  color: #86909c;
  font-weight: 500;
  margin-bottom: 8px;
}
.overview-value {
  font-size: 24px;
  font-weight: bold;
  color: #262626;
}
.device-status-chart {
  flex: 1;
  margin-top: 16px;
  min-height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* å¼¹çª—样式 */
.dialog-content {
  color: #333;
}
.el-main {
  padding: 20px;
.dialog-footer {
  text-align: right;
}
.grid-container {
  display: flex;
  /* display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: min-content; */
  gap: 20px;
/* é—ªçƒæ•ˆæžœ */
@keyframes blink {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}
.left-section {
  flex: 2;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.right-section {
  width: 670px;
  display: flex;
  flex-direction: column;
.blink {
  animation: blink 1s infinite;
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .grid-container {
    grid-template-columns: 1fr;
@media (max-width: 1200px) {
  .top-cards {
    position: relative;
    margin-bottom: 24px;
  }
  .left-section,
  .right-section {
    margin-bottom: 10px;
  .cards-container {
    grid-template-columns: repeat(2, 1fr);
    grid-template-rows: auto auto;
    background-color: #ffffff;
  }
  .main-content {
    height: calc(100vh - 300px);
  }
  .overview-section {
    position: relative;
    top: 0;
    right: 0;
    width: 100%;
    max-height: none;
    height: 300px;
    margin-top: 16px;
    background-color: #ffffff;
  }
}
@media (max-width: 768px) {
  .data-dashboard {
    padding: 16px;
  }
  .top-cards {
    left: 16px;
  }
  .cards-container {
    grid-template-columns: 1fr;
    grid-template-rows: auto repeat(4, auto);
  }
  .time-period-card {
    order: -1;
  }
  .time-tab-container {
    flex-direction: row;
  }
  .time-tab {
    flex: 1;
    padding: 8px 0;
  }
  .overview-section {
    right: 16px;
  }
  .overview-items-container {
    flex-direction: column;
    gap: 16px;
  }
  .overview-item {
    flex-direction: row;
    justify-content: space-between;
    width: 100%;
    text-align: left;
  }
}
</style>
src/views/monitor/DataDashboard_old2.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,313 @@
<template>
  <el-container class="data-dashboard">
    <el-main>
      <div class="grid-container">
        <div class="left-section">
          <!-- è®¾å¤‡åœ¨çº¿æƒ…况区域 -->
          <DeviceStatus
            :online-count="onlineCount"
            :offline-count="offlineCount"
            :normal-count="normalCount"
            :fault-count="faultCount"
          />
          <!-- åˆ†åŒºæ•°æ®æŽ’名区域 -->
          <DistrictRanking
            style="flex: 1"
            :selected-month="selectedMonth"
            :ranking-type="rankingType"
            :ranking-data="rankingData"
            :sorted-ranking-data="sortedRankingData"
            @month-change="handleMonthChange"
            @type-change="handleTypeChange"
          />
        </div>
        <div class="right-section">
          <!-- è®¾å¤‡å®žæ—¶æ•°æ®åŒºåŸŸ -->
          <RealTimeData style="flex: 1" :devices="devices" />
        </div>
      </div>
      <!-- åœ¨çº¿è®¾å¤‡å’Œåº—铺清单区域 -->
      <ShopList
        :shops="shops"
        :shop-types="shopTypes"
        :districts="districts"
        :filter="filter"
        @filter-change="handleFilterChange"
      />
    </el-main>
  </el-container>
</template>
<script>
import DeviceStatus from '@/components/monitor/DeviceStatus.vue'
import RealTimeData from '@/components/monitor/RealTimeData.vue'
import DistrictRanking from '@/components/monitor/DistrictRanking.vue'
import ShopList from '@/components/monitor/ShopList.vue'
export default {
  name: 'DataDashboard',
  components: {
    DeviceStatus,
    RealTimeData,
    DistrictRanking,
    ShopList,
  },
  data() {
    return {
      // è®¾å¤‡åœ¨çº¿æƒ…况数据
      onlineCount: 0,
      offlineCount: 0,
      normalCount: 0,
      faultCount: 0,
      // è®¾å¤‡å®žæ—¶æ•°æ®
      devices: [],
      // åˆ†åŒºæ•°æ®æŽ’名
      selectedMonth: '2023-12',
      rankingType: 'hourly',
      rankingData: [],
      sortedRankingData: [],
      // ç­›é€‰æ¡ä»¶
      filter: {
        district: '',
        shopType: '',
        status: '',
      },
      shopTypes: ['中餐', '西餐', '快餐', '火锅', '烧烤'],
      districts: ['东城区', '西城区', '朝阳区', '海淀区', '丰台区'],
      // åº—铺数据
      shops: [
        {
          id: 1,
          name: '张三餐厅',
          deviceCount: 2,
          status: '在线',
          district: '东城区',
          type: '中餐',
        },
        {
          id: 2,
          name: '李四饭店',
          deviceCount: 1,
          status: '离线',
          district: '西城区',
          type: '西餐',
        },
        {
          id: 3,
          name: '王五小吃',
          deviceCount: 3,
          status: '在线',
          district: '朝阳区',
          type: '快餐',
        },
        {
          id: 4,
          name: '赵六火锅',
          deviceCount: 2,
          status: '在线',
          district: '海淀区',
          type: '火锅',
        },
        {
          id: 5,
          name: '钱七烧烤',
          deviceCount: 1,
          status: '离线',
          district: '丰台区',
          type: '烧烤',
        },
      ],
    }
  },
  mounted() {
    this.initData()
    this.startRealTimeUpdate()
  },
  methods: {
    initData() {
      // åˆå§‹åŒ–设备在线情况数据
      this.updateDeviceStatus()
      // åˆå§‹åŒ–实时数据
      this.updateRealTimeData()
      // åˆå§‹åŒ–分区数据排名
      this.updateRankingData()
    },
    updateDeviceStatus() {
      // æ¨¡æ‹Ÿæ•°æ® - å®žé™…应从API获取
      this.onlineCount = Math.floor(Math.random() * 50) + 50
      this.offlineCount = Math.floor(Math.random() * 20)
      this.normalCount = this.onlineCount
      this.faultCount = this.offlineCount
    },
    updateRealTimeData() {
      // æ¨¡æ‹Ÿæ•°æ® - å®žé™…应从API获取
      this.devices = [
        {
          deviceId: 'DEV-001',
          supplier: '供应商A',
          status: '正常',
          monitorTime: new Date().toLocaleString(),
          smokeDensity: (Math.random() * 2).toFixed(2),
          fanCurrent: (Math.random() * 5 + 1).toFixed(2),
          purifierCurrent: (Math.random() * 3 + 0.5).toFixed(2),
          hourlyData: this.generateHourlyData(),
        },
        {
          deviceId: 'DEV-002',
          supplier: '供应商B',
          status: '正常',
          monitorTime: new Date().toLocaleString(),
          smokeDensity: (Math.random() * 2).toFixed(2),
          fanCurrent: (Math.random() * 5 + 1).toFixed(2),
          purifierCurrent: (Math.random() * 3 + 0.5).toFixed(2),
          hourlyData: this.generateHourlyData(),
        },
        {
          deviceId: 'DEV-003',
          supplier: '供应商C',
          status: '异常',
          monitorTime: new Date().toLocaleString(),
          smokeDensity: (Math.random() * 15 + 5).toFixed(2),
          fanCurrent: (Math.random() * 3 + 4).toFixed(2),
          purifierCurrent: (Math.random() * 2 + 2).toFixed(2),
          hourlyData: this.generateHourlyData(),
        },
        {
          deviceId: 'DEV-004',
          supplier: '供应商D',
          status: '异常',
          monitorTime: new Date().toLocaleString(),
          smokeDensity: (Math.random() * 15 + 5).toFixed(2),
          fanCurrent: (Math.random() * 3 + 4).toFixed(2),
          purifierCurrent: (Math.random() * 2 + 2).toFixed(2),
          hourlyData: this.generateHourlyData(),
        },
      ]
    },
    generateHourlyData() {
      // ç”Ÿæˆæ¨¡æ‹Ÿçš„近一小时数据
      const hourlyData = []
      for (let i = 59; i >= 0; i--) {
        const time = new Date()
        time.setMinutes(time.getMinutes() - i)
        hourlyData.push({
          time: time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
          smokeDensity: (Math.random() * 2).toFixed(2),
          fanCurrent: (Math.random() * 5 + 1).toFixed(2),
          purifierCurrent: (Math.random() * 3 + 0.5).toFixed(2),
        })
      }
      return hourlyData
    },
    updateRankingData() {
      // æ¨¡æ‹Ÿæ•°æ® - å®žé™…应从API获取
      this.rankingData = [
        { name: '玄武区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 0 },
        { name: '秦淮区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: -1 },
        { name: '建邺区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 1 },
        { name: '鼓楼区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 0 },
        { name: '浦口区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: -2 },
        { name: '栖霞区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 2 },
        { name: '雨花台区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 0 },
        { name: '江宁区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 1 },
        { name: '六合区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: -1 },
        { name: '溧水区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 0 },
        { name: '高淳区', value: (Math.random() * 1.5 + 0.5).toFixed(2), rankChange: 1 },
      ]
      // æŽ’序
      this.sortedRankingData = [...this.rankingData].sort(
        (a, b) => parseFloat(b.value) - parseFloat(a.value),
      )
    },
    startRealTimeUpdate() {
      // æ¯30秒更新一次数据
      this.realTimeInterval = setInterval(() => {
        this.updateDeviceStatus()
        this.updateRealTimeData()
      }, 30000)
    },
    handleMonthChange(val) {
      this.selectedMonth = val
      this.updateRankingData()
    },
    handleTypeChange(val) {
      this.rankingType = val
      this.updateRankingData()
    },
    handleFilterChange(val) {
      this.filter = { ...val }
    },
  },
}
</script>
<style scoped>
.data-dashboard {
  min-height: 100vh;
  background-color: #f5f7fa;
}
.el-header {
  padding: 20px;
  background-color: #f5f7fa;
}
.el-header h1 {
  font-size: 24px;
  margin: 0;
  color: #333;
}
.el-main {
  padding: 20px;
}
.grid-container {
  display: flex;
  /* display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: min-content; */
  gap: 20px;
}
.left-section {
  flex: 2;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.right-section {
  width: 670px;
  display: flex;
  flex-direction: column;
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .grid-container {
    grid-template-columns: 1fr;
  }
  .left-section,
  .right-section {
    margin-bottom: 10px;
  }
}
</style>
src/views/monitor/DataDashboard_old3.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,730 @@
<template>
  <div class="data-dashboard">
    <!-- é¡¶éƒ¨æŒ‡æ ‡å¡ç‰‡åŒº -->
    <div class="top-cards">
      <div class="cards-container">
        <!-- æ—¶é—´å‘¨æœŸé€‰é¡¹å¡ç‰‡ -->
        <div class="time-period-card">
          <div class="card-title">时间周期</div>
          <div class="time-tab-container">
            <div
              v-for="tab in timeTabs"
              :key="tab.value"
              class="time-tab"
              :class="{ active: activeTime === tab.value }"
              @click="handleTimeChange(tab)"
            >
              {{ tab.label }}
            </div>
          </div>
        </div>
        <!-- è¶…标数 -->
        <div class="metric-card">
          <div class="card-header">
            <div class="card-title">{{ getPeriodLabel() }}超标数</div>
            <div class="card-icon warning-icon">⚠</div>
          </div>
          <div class="card-value">{{ metrics.overStandardCount }}</div>
          <div class="card-trend">
            <span
              class="trend-arrow"
              :class="{ up: metrics.overStandardTrend > 0, down: metrics.overStandardTrend < 0 }"
            >
              {{ metrics.overStandardTrend > 0 ? '↑' : '↓' }}
            </span>
            <span class="trend-text">{{ Math.abs(metrics.overStandardTrend) }}%</span>
            <span class="trend-label">{{ getCompareLabel() }}</span>
          </div>
        </div>
        <!-- åœ¨çº¿çއ -->
        <div class="metric-card">
          <div class="card-header">
            <div class="card-title">{{ getPeriodLabel() }}在线率</div>
            <div class="card-icon online-icon">🟢</div>
          </div>
          <div class="card-value">{{ metrics.onlineRate }}%</div>
          <div class="card-trend">
            <span
              class="trend-arrow"
              :class="{ up: metrics.onlineRateTrend > 0, down: metrics.onlineRateTrend < 0 }"
            >
              {{ metrics.onlineRateTrend > 0 ? '↑' : '↓' }}
            </span>
            <span class="trend-text">{{ Math.abs(metrics.onlineRateTrend) }}%</span>
            <span class="trend-label">{{ getCompareLabel() }}</span>
          </div>
        </div>
        <!-- å‡€åŒ–器运行效率 -->
        <div class="metric-card">
          <div class="card-header">
            <div class="card-title">{{ getPeriodLabel() }}净化器运行效率</div>
            <div class="card-icon efficiency-icon">⚙</div>
          </div>
          <div class="card-value">{{ metrics.purifierEfficiency }}%</div>
          <div class="card-trend">
            <span
              class="trend-arrow"
              :class="{
                up: metrics.purifierEfficiencyTrend > 0,
                down: metrics.purifierEfficiencyTrend < 0,
              }"
            >
              {{ metrics.purifierEfficiencyTrend > 0 ? '↑' : '↓' }}
            </span>
            <span class="trend-text">{{ Math.abs(metrics.purifierEfficiencyTrend) }}%</span>
            <span class="trend-label">{{ getCompareLabel() }}</span>
          </div>
        </div>
        <!-- ä»»åŠ¡å®ŒæˆçŽ‡ -->
        <div class="metric-card">
          <div class="card-header">
            <div class="card-title">{{ getPeriodLabel() }}任务完成率</div>
            <div class="card-icon task-icon">✅</div>
          </div>
          <div class="card-value">{{ metrics.taskCompletionRate }}%</div>
          <div class="card-trend">
            <span
              class="trend-arrow"
              :class="{
                up: metrics.taskCompletionRateTrend > 0,
                down: metrics.taskCompletionRateTrend < 0,
              }"
            >
              {{ metrics.taskCompletionRateTrend > 0 ? '↑' : '↓' }}
            </span>
            <span class="trend-text">{{ Math.abs(metrics.taskCompletionRateTrend) }}%</span>
            <span class="trend-label">{{ getCompareLabel() }}</span>
          </div>
        </div>
      </div>
    </div>
    <!-- ä¸­éƒ¨GIS地图区 -->
    <div class="map-section">
      <div id="map" class="map-container"></div>
      <!-- åœ°å›¾ç‚¹ä½å¼¹çª— -->
      <el-dialog v-model="dialogVisible" title="企业实时数据" width="400px">
        <div class="dialog-content">
          <el-descriptions :column="1" border>
            <el-descriptions-item label="企业名称">{{
              selectedPoint.enterpriseName
            }}</el-descriptions-item>
            <el-descriptions-item label="设备编号">{{
              selectedPoint.deviceId
            }}</el-descriptions-item>
            <el-descriptions-item label="油烟浓度"
              >{{ selectedPoint.oilSmokeConcentration }} mg/m³</el-descriptions-item
            >
            <el-descriptions-item label="颗粒物"
              >{{ selectedPoint.particulateMatter }} mg/m³</el-descriptions-item
            >
            <el-descriptions-item label="非甲烷总烃"
              >{{ selectedPoint.nonMethaneHydrocarbon }} mg/m³</el-descriptions-item
            >
            <el-descriptions-item label="监测时间">{{
              selectedPoint.monitoringTime
            }}</el-descriptions-item>
            <el-descriptions-item label="超标情况">
              <el-tag :type="selectedPoint.isOverStandard ? 'danger' : 'success'">
                {{ selectedPoint.isOverStandard ? '超标' : '正常' }}
              </el-tag>
            </el-descriptions-item>
          </el-descriptions>
        </div>
        <template #footer>
          <span class="dialog-footer">
            <el-button @click="dialogVisible = false">关闭</el-button>
            <el-button type="primary" @click="viewDetails">查看详情</el-button>
          </span>
        </template>
      </el-dialog>
    </div>
    <!-- å³ä¾§å®žæ—¶ç›‘测总览区 -->
    <div class="overview-section">
      <div class="overview-card">
        <div class="overview-title">实时监测总览</div>
        <div class="overview-items-container">
          <div class="overview-item">
            <div class="overview-label">餐饮店铺总数</div>
            <div class="overview-value">{{ overview.totalShops }}</div>
          </div>
          <div class="overview-item">
            <div class="overview-label">在线设备数</div>
            <div class="overview-value">{{ overview.onlineDevices }}</div>
          </div>
          <div class="overview-item">
            <div class="overview-label">离线设备数</div>
            <div class="overview-value">{{ overview.offlineDevices }}</div>
          </div>
        </div>
        <!-- è®¾å¤‡çŠ¶æ€é¥¼å›¾ -->
        <div class="device-status-chart">
          <canvas id="deviceStatusChart"></canvas>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import * as echarts from 'echarts'
export default {
  name: 'DataDashboard',
  data() {
    return {
      activeTime: 'day',
      timeTabs: [
        { label: '日', value: 'day' },
        { label: '周', value: 'week' },
        { label: '月', value: 'month' },
      ],
      dialogVisible: false,
      selectedPoint: {
        enterpriseName: '',
        deviceId: '',
        oilSmokeConcentration: 0,
        particulateMatter: 0,
        nonMethaneHydrocarbon: 0,
        monitoringTime: '',
        isOverStandard: false,
      },
      metrics: {
        overStandardCount: 12,
        overStandardTrend: 5,
        onlineRate: 92,
        onlineRateTrend: 2,
        purifierEfficiency: 85,
        purifierEfficiencyTrend: -3,
        taskCompletionRate: 78,
        taskCompletionRateTrend: 10,
      },
      overview: {
        totalShops: 245,
        onlineDevices: 220,
        offlineDevices: 25,
      },
      map: null,
      refreshTimer: null,
    }
  },
  mounted() {
    this.initMap()
    this.initDeviceStatusChart()
    this.startAutoRefresh()
  },
  beforeUnmount() {
    if (this.refreshTimer) {
      clearInterval(this.refreshTimer)
    }
  },
  methods: {
    handleTimeChange(tab) {
      this.activeTime = tab.value
      // æ¨¡æ‹Ÿåˆ‡æ¢æ—¶é—´å‘¨æœŸåŽçš„æ•°æ®æ›´æ–°
      this.updateMetrics()
    },
    getPeriodLabel() {
      switch (this.activeTime) {
        case 'day':
          return '今日'
        case 'week':
          return '本周'
        case 'month':
          return '本月'
        default:
          return '今日'
      }
    },
    getCompareLabel() {
      switch (this.activeTime) {
        case 'day':
          return '较昨日'
        case 'week':
          return '较上周'
        case 'month':
          return '较上月'
        default:
          return '较昨日'
      }
    },
    updateMetrics() {
      // è¿™é‡Œåº”该根据选择的时间周期从接口获取数据
      // æ¨¡æ‹Ÿæ•°æ®æ›´æ–°
      setTimeout(() => {
        this.metrics = {
          overStandardCount: Math.floor(Math.random() * 30),
          overStandardTrend: Math.floor(Math.random() * 20) - 10,
          onlineRate: Math.floor(Math.random() * 20) + 80,
          onlineRateTrend: Math.floor(Math.random() * 10) - 5,
          purifierEfficiency: Math.floor(Math.random() * 30) + 70,
          purifierEfficiencyTrend: Math.floor(Math.random() * 10) - 5,
          taskCompletionRate: Math.floor(Math.random() * 40) + 60,
          taskCompletionRateTrend: Math.floor(Math.random() * 15) - 7,
        }
      }, 300)
    },
    initMap() {
      // è¿™é‡Œåº”该初始化真实的GIS地图
      // æ¨¡æ‹Ÿåœ°å›¾åˆå§‹åŒ–
      const mapElement = document.getElementById('map')
      if (mapElement) {
        mapElement.innerHTML = '<div class="map-placeholder">GIS地图加载中...</div>'
        // å®žé™…项目中这里应该使用真实的地图API,如高德地图、百度地图等
      }
    },
    initDeviceStatusChart() {
      const chartDom = document.getElementById('deviceStatusChart')
      if (chartDom) {
        const chart = echarts.init(chartDom)
        const option = {
          tooltip: {
            trigger: 'item',
            formatter: '{b}: {c} ({d}%)',
            backgroundColor: 'rgba(255, 255, 255, 0.95)',
            borderColor: '#ebeef5',
            borderWidth: 1,
            textStyle: {
              color: '#303133',
            },
          },
          legend: {
            bottom: '0%',
            left: 'center',
            textStyle: {
              color: '#606266',
              fontSize: 12,
            },
          },
          series: [
            {
              name: '设备状态',
              type: 'pie',
              radius: ['40%', '65%'],
              center: ['50%', '45%'],
              avoidLabelOverlap: false,
              itemStyle: {
                borderRadius: 8,
                borderColor: '#ffffff',
                borderWidth: 2,
                shadowBlur: 10,
                shadowOffsetX: 0,
                shadowColor: 'rgba(0, 0, 0, 0.1)',
              },
              label: {
                show: true,
                position: 'outside',
                formatter: '{b}: {d}%',
                fontSize: 12,
                color: '#606266',
              },
              labelLine: {
                show: true,
                length: 10,
                length2: 20,
                lineStyle: {
                  color: '#606266',
                  width: 1,
                },
              },
              data: [
                {
                  value: this.overview.onlineDevices,
                  name: '在线',
                  itemStyle: {
                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                      { offset: 0, color: '#67c23a' },
                      { offset: 1, color: '#409eff' },
                    ]),
                  },
                },
                {
                  value: this.overview.offlineDevices,
                  name: '离线',
                  itemStyle: {
                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                      { offset: 0, color: '#f56c6c' },
                      { offset: 1, color: '#e6a23c' },
                    ]),
                  },
                },
              ],
            },
          ],
        }
        chart.setOption(option)
        // å“åº”式调整
        window.addEventListener('resize', () => {
          chart.resize()
        })
      }
    },
    startAutoRefresh() {
      // æ¯30秒自动刷新数据
      this.refreshTimer = setInterval(() => {
        this.updateMetrics()
        // è¿™é‡Œåº”该同时更新地图点位数据
      }, 30000)
    },
    viewDetails() {
      // è·³è½¬åˆ°ä¼ä¸šç›‘控详情页
      this.$router.push('/monitor/enterprise-detail')
    },
  },
}
</script>
<style scoped>
.data-dashboard {
  width: 100%;
  height: 100vh;
  background-color: #f5f7fa;
  color: #303133;
  padding: 20px;
  box-sizing: border-box;
  display: grid;
  grid-template-rows: auto 1fr;
  grid-template-columns: 1fr 300px;
  grid-template-areas:
    'top overview'
    'map overview';
  gap: 20px;
}
.top-cards {
  grid-area: top;
}
.time-period-card {
  background-color: #ffffff;
  border: 1px solid #ebeef5;
  border-radius: 12px;
  padding: 24px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.time-period-card .card-title {
  font-size: 14px;
  color: #909399;
  font-weight: 500;
  margin-bottom: 16px;
  text-align: center;
}
.time-tab-container {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
}
.time-tab {
  padding: 2px 4px;
  border-radius: 8px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  transition: all 0.3s ease;
  color: #606266;
  text-align: center;
  border: 1px solid #ebeef5;
  background-color: #f9f9f9;
}
.time-tab.active {
  background-color: #409eff;
  color: #ffffff;
  border-color: #409eff;
  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
}
.time-tab:hover:not(.active) {
  color: #409eff;
  border-color: #c6e2ff;
  background-color: #ecf5ff;
}
.time-tab.active {
  background-color: #409eff;
  color: #ffffff;
  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
}
.time-tab:hover:not(.active) {
  color: #409eff;
  background-color: rgba(64, 158, 255, 0.1);
}
.cards-container {
  display: grid;
  grid-template-columns: 120px repeat(4, 1fr);
  gap: 20px;
}
.metric-card {
  background-color: #ffffff;
  border: 1px solid #ebeef5;
  border-radius: 12px;
  padding: 24px;
  transition: all 0.3s ease;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
  position: relative;
  overflow: hidden;
}
.metric-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 4px;
  height: 100%;
  background-color: #409eff;
}
.metric-card:nth-child(1)::before {
  background-color: #f56c6c;
}
.metric-card:nth-child(2)::before {
  background-color: #67c23a;
}
.metric-card:nth-child(3)::before {
  background-color: #e6a23c;
}
.metric-card:nth-child(4)::before {
  background-color: #909399;
}
.metric-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12);
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
}
.card-title {
  font-size: 14px;
  color: #909399;
  font-weight: 500;
}
.card-icon {
  font-size: 20px;
}
.card-value {
  font-size: 36px;
  font-weight: bold;
  margin: 16px 0;
  color: #303133;
  line-height: 1.2;
}
.card-trend {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  padding-top: 12px;
  border-top: 1px solid #f0f2f5;
}
.trend-arrow {
  font-size: 16px;
  font-weight: bold;
}
.trend-arrow.up {
  color: #67c23a;
}
.trend-arrow.down {
  color: #f56c6c;
}
.trend-text {
  color: #606266;
  font-weight: 500;
}
.trend-label {
  color: #909399;
  font-size: 12px;
}
.map-section {
  grid-area: map;
  background-color: #ffffff;
  border: 1px solid #ebeef5;
  border-radius: 8px;
  position: relative;
  overflow: hidden;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.map-container {
  width: 100%;
  height: 100%;
  position: relative;
}
.map-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #909399;
  font-size: 18px;
}
.overview-section {
  grid-area: overview;
}
.overview-card {
  background-color: #ffffff;
  border: 1px solid #ebeef5;
  border-radius: 8px;
  padding: 20px 0px;
  display: flex;
  flex-direction: column;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.overview-title {
  font-size: 16px;
  font-weight: bold;
  margin-bottom: 20px;
  text-align: center;
  color: #303133;
}
.overview-items-container {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
}
.overview-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  flex: 1;
  text-align: center;
  padding: 12px 0;
}
.overview-item:last-child {
  border-right: none;
}
.overview-label {
  font-size: 14px;
  color: #606266;
  font-weight: 500;
  margin-bottom: 8px;
}
.overview-value {
  font-size: 24px;
  font-weight: bold;
  color: #303133;
}
.device-status-chart {
  flex: 1;
  margin-top: 20px;
  min-height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
  /* background-color: #c6e2ff; */
}
.dialog-content {
  color: #333;
}
.dialog-footer {
  text-align: right;
}
/* é—ªçƒæ•ˆæžœ */
@keyframes blink {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}
.blink {
  animation: blink 1s infinite;
}
/* å“åº”式设计 */
@media (max-width: 1200px) {
  .data-dashboard {
    grid-template-columns: 1fr;
    grid-template-areas:
      'top'
      'map'
      'overview';
    height: auto;
  }
  .cards-container {
    grid-template-columns: 160px repeat(2, 1fr);
  }
  .overview-section {
    height: 300px;
  }
}
@media (max-width: 768px) {
  .cards-container {
    grid-template-columns: 1fr;
    grid-template-rows: auto repeat(4, auto);
  }
  .time-period-card {
    order: -1;
  }
  .time-tab-container {
    flex-direction: row;
  }
  .time-tab {
    flex: 1;
    padding: 8px 0;
  }
}
</style>
src/views/monitor/DataException.vue
@@ -1,92 +1,84 @@
<template>
  <el-row ref="h1">
    <el-col>
      <!-- èœå•读标题 -->
      <div ref="h1" class="header-container">
        <span class="describe-info">店铺名选择:</span>
        <!-- åº—铺名  çº§è” -->
        <ShopNameAndID @submit-id="(n) => (deviceId[1] = n)"></ShopNameAndID>
        <!-- å¼‚常类型选择 -->
        <ExceptionType @submitExceptionType="(val) => (exceptionValue = val)"> </ExceptionType>
        <TimeSelect @submit-time="giveTime"></TimeSelect>
  <div class="data-exception-container">
    <!-- æœç´¢åŒºåŸŸ -->
    <div ref="h1" class="search-container">
      <!-- <div class="search-header">
        <h3>查询表格</h3>
      </div> -->
      <el-row>
        <div class="search-form">
          <div class="form-row">
            <div class="form-item">
              <span class="form-label">店铺设备:</span>
              <ShopNameAndID @submit-id="(n) => (deviceId[1] = n)"></ShopNameAndID>
            </div>
            <div class="form-item">
              <ExceptionType @submitExceptionType="(val) => (exceptionValue = val)">
              </ExceptionType>
            </div>
          </div>
          <div class="form-row">
            <div class="form-item full-width">
              <TimeSelect @submit-time="giveTime" :useNewStyle="true"></TimeSelect>
            </div>
          </div>
        </div>
        <div class="form-actions">
          <el-button type="primary" :loading="button.queryButton" @click="showTable"
            >查询</el-button
          >
          <el-tooltip
            class="box-item"
            effect="dark"
            content="点击可导出Excel文件"
            placement="top-start"
          >
            <el-icon class="iconExcel clickable" title="导出Excel文件" @click="exportDom">
              <i-ep-Download />
            </el-icon>
          </el-tooltip>
        </div>
      </el-row>
      <div class="summary-info">
        <span>{{ beginTime }} â€”— {{ endTime }} æ²¹çƒŸç›‘测异常信息汇总</span>
      </div>
      <div ref="h2" style="display: flex; margin-top: 2px; justify-content: right">
        <el-button
          type="primary"
          plain
          style="margin-left: 20px"
          :loading="button.queryButton"
          @click="showTable"
          >查询</el-button
        >
    </div>
        <el-tooltip
          class="box-item"
          effect="dark"
          content="点击可导出Excel文件"
          placement="top-start"
        >
          <!-- åšæˆå‡½æ•°js文件 -->
          <el-icon class="iconExcel clickable" title="导出Excel文件" @click="exportDom">
            <i-ep-Download />
            <!-- å¯¼å‡ºä¸ºExcel -->
          </el-icon>
        </el-tooltip>
      </div>
      <div style="display: flex; justify-content: right; margin-right: 40px">
        <span class="collapse-header-text">
          é™å®‰åŒº {{ beginTime }} â€”— {{ endTime }} æ²¹çƒŸç›‘测异常信息汇总</span
        >
      </div>
      <br />
      <el-collapse ref="h3" v-model="activeNames">
    <!-- å¼‚常分析 -->
    <div class="analysis-container">
      <el-collapse v-model="activeNames">
        <el-collapse-item name="1">
          <template #title>
            <el-tooltip class="box-item" effect="dark" content="点击可折叠" placement="right-start">
            <div class="collapse-title">
              <h4 class="collapse-header">异常分析</h4>
              <el-icon class="header-icon">
                <i-ep-info-filled />
              </el-icon>
            </el-tooltip>
            <el-tooltip class="box-item" effect="dark" content="点击可折叠" placement="right-start">
            </el-tooltip>
            </div>
          </template>
          <el-card class="box-card">
            <el-row :gutter="25">
          <el-card class="analysis-card">
            <el-row :gutter="24">
              <el-col :span="8">
                <div style="display: flex">
                  <img
                    src="@/assets/exceed.jpg"
                    style="width: 25px; height: 25px; margin-top: 5px"
                  />
                  <span
                    style="font-size: 16px; font-weight: bold; margin-top: 4px; margin-left: 4px"
                    >油烟浓度超标</span
                  >
                </div>
                <div class="box-card-label">
                  <el-scrollbar>
                    <span class="box-card-label">异常店铺占比:</span>
                    <span style="font-size: 20px">{{ exception0.length }} /{{ shopsTotal }} </span>
                    <span style="font-size: 17px">
                      ({{ ((exception0.length / shopsTotal) * 100).toFixed(1) }}%)</span
                    >
                    {{ shopsTotal }}
                    <span class="right-text">
                <div class="analysis-item">
                  <div class="item-header">
                    <img src="@/assets/exceed.jpg" class="item-icon" />
                    <span class="item-title">油烟浓度超标</span>
                  </div>
                  <div class="item-content">
                    <div class="item-stats">
                      <span class="stats-label">异常店铺占比:</span>
                      <span class="stats-value">{{ exception0.length }} /{{ shopsTotal }}</span>
                      <span class="stats-percent"
                        >({{ ((exception0.length / shopsTotal) * 100).toFixed(1) }}%)</span
                      >
                    </div>
                    <div class="item-percent">
                      å¼‚常数占比:{{ ((exception0Num / exceptionAllNum) * 100).toFixed(1) }}%
                    </span>
                  </el-scrollbar>
                </div>
                <hr />
                <div class="box-card-butcontainer">
                  <el-card class="sub-box-card">
                    <el-scrollbar max-height="70px">
                    </div>
                  </div>
                  <hr class="item-divider" />
                  <div class="item-shops">
                    <el-scrollbar max-height="80px">
                      <ExceptionText
                        v-for="(item, index) in exception0"
                        :key="item"
@@ -100,39 +92,30 @@
                        <span v-if="index < exception0.length - 1" class="text-blank">,</span>
                      </ExceptionText>
                    </el-scrollbar>
                  </el-card>
                  </div>
                </div>
              </el-col>
              <el-col :span="8">
                <div style="display: flex">
                  <img
                    src="@/assets/exception.jpg"
                    style="width: 25px; height: 25px; margin-top: 5px"
                  />
                  <span
                    style="font-size: 16px; font-weight: bold; margin-top: 5px; margin-left: 4px"
                    >供电异常</span
                  >
                </div>
                <div class="box-card-label">
                  <el-scrollbar>
                    <span class="box-card-label">异常店铺占比:</span>
                    <span style="font-size: 20px">{{ exception1.length }} /{{ shopsTotal }}</span>
                    <span style="font-size: 17px">
                      ({{ ((exception1.length / shopsTotal) * 100).toFixed(1) }}%)</span
                    >
                    <span class="right-text">
                <div class="analysis-item">
                  <div class="item-header">
                    <img src="@/assets/exception.jpg" class="item-icon" />
                    <span class="item-title">供电异常</span>
                  </div>
                  <div class="item-content">
                    <div class="item-stats">
                      <span class="stats-label">异常店铺占比:</span>
                      <span class="stats-value">{{ exception1.length }} /{{ shopsTotal }}</span>
                      <span class="stats-percent"
                        >({{ ((exception1.length / shopsTotal) * 100).toFixed(1) }}%)</span
                      >
                    </div>
                    <div class="item-percent">
                      å¼‚常数占比:{{ ((exception1Num / exceptionAllNum) * 100).toFixed(1) }}%
                    </span>
                  </el-scrollbar>
                </div>
                <hr />
                <div>
                  <el-card class="sub-box-card">
                    <el-scrollbar max-height="70px">
                    </div>
                  </div>
                  <hr class="item-divider" />
                  <div class="item-shops">
                    <el-scrollbar max-height="80px">
                      <ExceptionText
                        v-for="(item, index) in exception1"
                        :key="item"
@@ -146,35 +129,28 @@
                        <span v-if="index < exception1.length - 1" class="text-blank">,</span>
                      </ExceptionText>
                    </el-scrollbar>
                  </el-card>
                  </div>
                </div>
              </el-col>
              <el-col :span="8">
                <div style="display: flex">
                  <img
                    src="@/assets/offline.jpg"
                    style="width: 25px; height: 25px; margin-top: 5px"
                  />
                  <span
                    style="font-size: 16px; font-weight: bold; margin-top: 5px; margin-left: 4px"
                    >设备或网络异常</span
                  >
                </div>
                <div class="box-card-label">
                  <el-scrollbar>
                    <span class="box-card-label">异常店铺占比:</span>
                    <span style="font-size: 20px">{{ exception2.length }} /{{ shopsTotal }}</span>
                    <span style="font-size: 17px">
                      ({{ ((exception2.length / shopsTotal) * 100).toFixed(1) }}%)</span
                    >
                    <span class="right-text"> å¼‚常数占比:{{ connectException }}% </span>
                  </el-scrollbar>
                </div>
                <hr />
                <div>
                  <el-card class="sub-box-card">
                    <el-scrollbar max-height="70px">
                <div class="analysis-item">
                  <div class="item-header">
                    <img src="@/assets/offline.jpg" class="item-icon" />
                    <span class="item-title">设备或网络异常</span>
                  </div>
                  <div class="item-content">
                    <div class="item-stats">
                      <span class="stats-label">异常店铺占比:</span>
                      <span class="stats-value">{{ exception2.length }} /{{ shopsTotal }}</span>
                      <span class="stats-percent"
                        >({{ ((exception2.length / shopsTotal) * 100).toFixed(1) }}%)</span
                      >
                    </div>
                    <div class="item-percent">异常数占比:{{ connectException }}%</div>
                  </div>
                  <hr class="item-divider" />
                  <div class="item-shops">
                    <el-scrollbar max-height="80px">
                      <ExceptionText
                        v-for="(item, index) in exception2"
                        :key="item"
@@ -188,212 +164,198 @@
                        <span v-if="index < exception2.length - 1" class="text-blank">,</span>
                      </ExceptionText>
                    </el-scrollbar>
                  </el-card>
                  </div>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </el-collapse-item>
      </el-collapse>
    </div>
      <h4 class="table-text">异常数据</h4>
    </el-col>
  </el-row>
  <el-card class="table-page" v-show="!isNoData">
    <el-table
      ref="tableH"
      size="small"
      v-loading="loading"
      :data="displayData"
      style="width: 100%"
      border
      :height="tableHeight"
      :cell-class-name="tableCellClassName"
    >
      <el-table-column prop="diName" label="店铺名称" align="center">
        <template #default="{ row }">
          <el-tooltip effect="dark" :content="row.diName">
            <div class="cell ellipsis">{{ row.diName }}</div>
          </el-tooltip>
        </template>
      </el-table-column>
      <el-table-column prop="devId" label="设备编号" align="center">
        <template #default="{ row }">
          <el-tooltip effect="dark" :content="row.devId">
            <div class="cell ellipsis">{{ row.devId }}</div>
          </el-tooltip>
        </template>
      </el-table-column>
      <el-table-column prop="diSupplier" label="供应商" align="center">
        <template #default="{ row }">
          <el-tooltip effect="dark" :content="row.diSupplier">
            <div class="cell ellipsis">{{ row.diSupplier }}</div>
          </el-tooltip>
        </template>
      </el-table-column>
      <el-table-column prop="exception" label="异常分类" align="center">
        <template #default="{ row }">
          <el-tooltip effect="dark" :content="row.exception">
            <div class="cell ellipsis">{{ row.exception }}</div>
          </el-tooltip>
        </template>
      </el-table-column>
      <el-table-column label="异常类型" align="center">
        <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-tooltip effect="dark" :content="row.region">
            <div class="cell ellipsis">{{ row.region }}</div>
          </el-tooltip>
        </template>
      </el-table-column>
      <el-table-column prop="beginTime" label="开始时间" align="center">
        <template #default="{ row }">
          <el-tooltip effect="dark" :content="row.beginTime">
            <div class="cell ellipsis">{{ row.beginTime }}</div>
          </el-tooltip>
        </template>
      </el-table-column>
      <el-table-column prop="endTime" label="结束时间" align="center">
        <template #default="{ row }">
          <el-tooltip effect="dark" :content="row.endTime">
            <div class="cell ellipsis">{{ row.endTime }}</div>
          </el-tooltip>
        </template>
      </el-table-column>
      <el-table-column label="操作" align="center">
        <template #default="{ row }">
          <el-button type="primary" class="table-button" @click="showDrawer(row)"
            >查看详情</el-button
          >
        </template>
      </el-table-column>
    </el-table>
    <el-pagination
      ref="h4"
      background
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      :total="total"
      :page-size="pageSize"
      layout="total,prev, pager, next, jumper"
    />
  </el-card>
  <el-empty v-show="isNoData" :image-size="200" />
  <!-- å¯¹è¯æ¡† -->
  <div>
    <el-dialog v-model="centerDialogVisible" draggable align-center>
      <template #header>
        <div style="font-size: 17px">
          åº—铺名:{{ rowShopName }}
          <span style="margin-left: 40px">异常类型:</span>
          <span v-if="rowExceptionType == '0'">油烟数据超标</span>
          <span v-else-if="rowExceptionType == '1'">供电异常</span>
          <span v-else-if="rowExceptionType == '2'">掉线</span>
          <div style="margin-top: 10px">
            å¼‚常时间段:{{ rowBeginTime }} ~
            {{ rowEndTime }}
          </div>
        </div>
        <div class="dialog-button-position">
          <el-button
            type="danger"
            :loading="button.preButton"
            :disabled="isPreCantouch || banTouch"
            @click="getPreviousRowData"
            >上条异常</el-button
          >
          <el-button
            type="danger"
            :loading="button.afterButton"
            :disabled="isNextCantouch || banTouch"
            @click="getNextRowData"
            >下条异常</el-button
          >
        </div>
      </template>
      <!-- è¶…标数据时 -->
      <!-- æŠ˜çº¿å›¾ -->
      <!-- æŽ‰çº¿ -->
      <!-- <div
          ref="ref"
          v-show="isOfflineShow"
          style="
            width: 100%;
            height: 300px;
            /* min-width: 100px; */
            margin-bottom: 20px;
            margin-left: 10px;
            min-width: 350px;
          "
        ></div> -->
      <ExceptionTypeLineChart
        :option="option"
        :is-open-dialog="centerDialogVisible"
        v-loading="chartLoading"
      ></ExceptionTypeLineChart>
      <!--  -->
      <div style="margin-top: 40px; margin-bottom: 5px; border: 1px">
        <el-table :data="exceedingData" height="360" border style="margin-top: 25px">
          <el-table-column
            type="index"
            label="序号"
            width="60px"
            align="center"
            fixed
            :index="indexMethod"
          ></el-table-column>
          <el-table-column fixed prop="diName" label="店铺名称" show-overflow-tooltip />
          <el-table-column
            prop="mvStatCode"
            label="设备编号"
            align="center"
            show-overflow-tooltip
          />
          <el-table-column prop="diSupplier" label="供应商" align="center" show-overflow-tooltip />
          <el-table-column
            prop="mvDataTime"
            label="采集时间"
            align="center"
            show-overflow-tooltip
          />
          <el-table-column
            prop="mvFumeConcentration2"
            label="油烟浓度(mg/m³)"
            align="center"
            show-overflow-tooltip
          />
    <!-- å¼‚常数据表格 -->
    <div class="table-container">
      <h4 class="table-title">异常数据</h4>
      <el-card v-show="!isNoData">
        <el-table
          ref="tableH"
          v-loading="loading"
          :data="displayData"
          style="width: 100%"
          border
          :height="tableHeight"
          :cell-class-name="tableCellClassName"
        >
          <el-table-column prop="diName" label="店铺名称" align="center">
            <template #default="{ row }">
              <el-tooltip effect="dark" :content="row.diName">
                <div class="cell ellipsis">{{ row.diName }}</div>
              </el-tooltip>
            </template>
          </el-table-column>
          <el-table-column prop="devId" label="设备编号" align="center">
            <template #default="{ row }">
              <el-tooltip effect="dark" :content="row.devId">
                <div class="cell ellipsis">{{ row.devId }}</div>
              </el-tooltip>
            </template>
          </el-table-column>
          <el-table-column prop="diSupplier" label="供应商" align="center">
            <template #default="{ row }">
              <el-tooltip effect="dark" :content="row.diSupplier">
                <div class="cell ellipsis">{{ row.diSupplier }}</div>
              </el-tooltip>
            </template>
          </el-table-column>
          <el-table-column prop="exception" label="异常分类" align="center">
            <template #default="{ row }">
              <el-tooltip effect="dark" :content="row.exception">
                <div class="cell ellipsis">{{ row.exception }}</div>
              </el-tooltip>
            </template>
          </el-table-column>
          <el-table-column label="异常类型" align="center">
            <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-tooltip effect="dark" :content="row.region">
                <div class="cell ellipsis">{{ row.region }}</div>
              </el-tooltip>
            </template>
          </el-table-column>
          <el-table-column prop="beginTime" label="开始时间" align="center">
            <template #default="{ row }">
              <el-tooltip effect="dark" :content="row.beginTime">
                <div class="cell ellipsis">{{ row.beginTime }}</div>
              </el-tooltip>
            </template>
          </el-table-column>
          <el-table-column prop="endTime" label="结束时间" align="center">
            <template #default="{ row }">
              <el-tooltip effect="dark" :content="row.endTime">
                <div class="cell ellipsis">{{ row.endTime }}</div>
              </el-tooltip>
            </template>
          </el-table-column>
          <el-table-column label="操作" align="center" width="120">
            <template #default="{ row }">
              <el-button type="primary" size="small" @click="showDrawer(row)">查看</el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
        <div class="pagination-container">
          <el-pagination
            ref="h4"
            @size-change="handleSizeChange"
            @current-change="handleCurrentChange"
            :total="total"
            :page-size="pageSize"
            layout="total, prev, pager, next, jumper"
          />
        </div>
      </el-card>
      <el-empty v-show="isNoData" :image-size="200" />
    </div>
      <el-tag type="success" class="mx-1" effect="dark" round
        ><span class="table-line-lable" v-show="rowExceptionType == '0'">异常记录: </span>
        <span v-show="rowExceptionType == '1' || rowExceptionType == '2'">缺失数据:</span>
        <span class="table-line-num">{{ exceptionTotal }}条</span>
        <span v-show="rowExceptionType === '1' || rowExceptionType === '2'"> (逻辑计算)</span>
      </el-tag>
    </el-dialog>
    <!-- å¯¹è¯æ¡† -->
    <div>
      <el-dialog v-model="centerDialogVisible" draggable align-center class="detail-dialog">
        <template #header>
          <div class="dialog-header">
            <div class="dialog-title">
              <span>店铺名:{{ rowShopName }}</span>
              <span class="dialog-info"
                >异常类型:
                <span v-if="rowExceptionType == '0'">油烟数据超标</span>
                <span v-else-if="rowExceptionType == '1'">供电异常</span>
                <span v-else-if="rowExceptionType == '2'">掉线</span>
              </span>
              <div class="dialog-time">异常时间段:{{ rowBeginTime }} ~ {{ rowEndTime }}</div>
            </div>
            <div class="dialog-actions">
              <el-button
                type="primary"
                :loading="button.preButton"
                :disabled="isPreCantouch || banTouch"
                @click="getPreviousRowData"
                >上条异常</el-button
              >
              <el-button
                type="primary"
                :loading="button.afterButton"
                :disabled="isNextCantouch || banTouch"
                @click="getNextRowData"
                >下条异常</el-button
              >
            </div>
          </div>
        </template>
        <ExceptionTypeLineChart
          :option="option"
          :is-open-dialog="centerDialogVisible"
          v-loading="chartLoading"
        ></ExceptionTypeLineChart>
        <div class="dialog-table-container">
          <el-table :data="exceedingData" height="360" border style="margin-top: 20px">
            <el-table-column
              type="index"
              label="序号"
              width="60px"
              align="center"
              fixed
              :index="indexMethod"
            ></el-table-column>
            <el-table-column fixed prop="diName" label="店铺名称" show-overflow-tooltip />
            <el-table-column
              prop="mvStatCode"
              label="设备编号"
              align="center"
              show-overflow-tooltip
            />
            <el-table-column
              prop="diSupplier"
              label="供应商"
              align="center"
              show-overflow-tooltip
            />
            <el-table-column
              prop="mvDataTime"
              label="采集时间"
              align="center"
              show-overflow-tooltip
            />
            <el-table-column
              prop="mvFumeConcentration2"
              label="油烟浓度(mg/m³)"
              align="center"
              show-overflow-tooltip
            />
          </el-table>
        </div>
        <div class="dialog-footer">
          <el-tag type="success" effect="dark" round
            ><span v-show="rowExceptionType == '0'">异常记录:</span>
            <span v-show="rowExceptionType == '1' || rowExceptionType == '2'">缺失数据:</span>
            <span class="table-line-num">{{ exceptionTotal }}条</span>
            <span v-show="rowExceptionType === '1' || rowExceptionType === '2'"> (逻辑计算)</span>
          </el-tag>
        </div>
      </el-dialog>
    </div>
  </div>
</template>
<script>
import { defineAsyncComponent } from 'vue'
import { ElMessage } from 'element-plus'
import ExceptionType from '@/sfc/ExceptionType.vue'
import TimeSelect from '@/sfc/TimeSelect.vue'
import ExceptionText from '@/sfc/ExceptionText.vue'
@@ -632,7 +594,7 @@
    //功能: åŠ¨æ€è®¡ç®—è¡¨æ ¼é«˜åº¦
    calcTableHeight() {
      const h1 = this.$refs.h1.$el.offsetHeight
      const h1 = this.$refs.h1.offsetHeight
      const h2 = this.$refs.h4.$el.offsetHeight
      this.tableHeight = `calc(100vh - ${h1}px - ${h2}px - 45px - var(--el-main-padding) * 2 - var(--el-card-padding))`
    },
@@ -1530,12 +1492,241 @@
</script>
<style scoped>
.header-container {
  display: flex;
  margin-left: 20px;
  /* flex-wrap: wrap;
    align-items: center; */
/* å…¨å±€å®¹å™¨ */
.data-exception-container {
  padding: 20px;
  background-color: #f5f7fa;
  min-height: 100vh;
}
/* æœç´¢åŒºåŸŸ */
.search-container {
  background-color: white;
  border-radius: 8px;
  padding: 20px;
  margin-bottom: 20px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.search-header {
  margin-bottom: 20px;
}
.search-header h3 {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
.search-form {
  display: flex;
  flex-direction: column;
  gap: 16px;
  flex: 1;
}
.form-row {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  flex-wrap: wrap;
}
.form-item {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 200px;
}
.form-item.full-width {
  flex: 100%;
}
.form-label {
  font-weight: bolder;
  white-space: nowrap;
}
.form-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 12px;
}
.summary-info {
  text-align: right;
  font-size: 14px;
  color: #999;
  margin-top: 8px;
}
/* å¼‚常分析 */
.analysis-container {
  background-color: white;
  border-radius: 8px;
  padding: 20px;
  margin-bottom: 20px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.collapse-title {
  display: flex;
  align-items: center;
  gap: 8px;
}
.collapse-header {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
.header-icon {
  color: #1890ff;
}
.analysis-card {
  border-radius: 8px;
  overflow: hidden;
}
.analysis-item {
  height: 180px;
  display: flex;
  flex-direction: column;
}
.item-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
}
.item-icon {
  width: 24px;
  height: 24px;
}
.item-title {
  font-size: 14px;
  font-weight: 600;
  color: #333;
}
.item-content {
  flex: 1;
  margin-bottom: 12px;
}
.item-stats {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 4px;
}
.stats-label {
  font-size: 13px;
  color: #666;
}
.stats-value {
  font-size: 18px;
  font-weight: 600;
  color: #333;
}
.stats-percent {
  font-size: 13px;
  color: #999;
}
.item-percent {
  font-size: 13px;
  color: #666;
  margin-top: 4px;
}
.item-divider {
  margin: 10px 0;
  border: 0.5px solid #f0f0f0;
}
.item-shops {
  flex: 1;
}
/* è¡¨æ ¼åŒºåŸŸ */
.table-container {
  background-color: white;
  border-radius: 8px;
  padding: 20px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.table-title {
  margin: 0 0 16px 0;
  font-size: 16px;
  font-weight: 600;
  color: #333;
}
.pagination-container {
  margin-top: 16px;
  display: flex;
  justify-content: flex-end;
}
/* å¯¹è¯æ¡† */
.detail-dialog {
  border-radius: 8px;
  overflow: hidden;
}
.dialog-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  width: 100%;
}
.dialog-title {
  font-size: 16px;
  color: #333;
}
.dialog-info {
  margin-left: 24px;
  color: #666;
}
.dialog-time {
  margin-top: 8px;
  font-size: 14px;
  color: #666;
}
.dialog-actions {
  display: flex;
  gap: 8px;
}
.dialog-table-container {
  margin-top: 20px;
}
.dialog-footer {
  margin-top: 20px;
  text-align: right;
}
/* é€šç”¨æ ·å¼ */
.ellipsis {
  white-space: nowrap;
  overflow: hidden;
@@ -1543,148 +1734,127 @@
}
.iconExcel {
  font-size: 25px;
  margin-left: 20px;
  bottom: -6px;
  font-size: 20px;
  cursor: pointer;
  color: #1890ff;
}
/* å¯é¼ æ ‡ç®­å¤´å˜ä¸ºå¯ç‚¹å‡»çŠ¶æ€ */
.clickable {
  cursor: pointer;
}
.card-header {
  margin: 0;
}
body {
  margin: 0;
}
.exception-divider-rowline {
  margin: 10px 0px;
}
/* å¼‚常分析数据与按钮 */
.exception-container {
  display: flex;
}
.example-showcase .el-loading-mask {
  z-index: 9;
}
.scrollbar-demo-item {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 20px;
  margin: 10px;
  text-align: center;
  border-radius: 4px;
  background: var(--el-color-primary-light-9);
  color: var(--el-color-primary);
}
.collapse-header {
  margin-left: 5px;
  font-size: 18px;
}
.collapse-header-text {
  margin-top: 5px;
  font-size: 14px;
  color: gray;
}
.box-card-label {
  font-size: 14px;
  white-space: nowrap;
}
.right-text {
  /* float :right; */
  /* text-align: right; */
  margin-left: 80px;
}
:deep().el-card {
  border-radius: 9px;
}
/* â€˜æŸ¥çœ‹è¯¦æƒ…’ çš„弹出框高度调整 */
:deep().el-dialog {
  height: 98%;
  /* ä¸å‡ºçŽ°æ»šåŠ¨æ¡ */
  overflow-y: hidden;
  border-radius: 9px;
}
.table-page {
  margin-left: 20px;
}
.table-text {
  font-size: 18px;
  margin: 5px 0px 10px 20px;
}
.text-blank {
  margin-right: 10px;
  color: #000000;
}
/* åº—铺名选择文本 */
.describe-info {
  margin-top: 5px;
  font-weight: bold;
  white-space: nowrap;
}
/* æ—¶é—´é€‰æ‹©æ–‡æœ¬ */
.describe-time-text {
  margin-left: 30px;
  margin-top: 5px;
  font-weight: bold;
}
/* å¼‚常表格下标签中的数组 */
.table-line-num {
  font-weight: bold;
  color: black;
}
.button_info.el-button_inner {
  text-align: left;
/* è¡¨æ ¼è¡Œæ ·å¼ */
:deep().el-table__row .exceeding-row {
  background-color: #fff1f0;
  color: #cf1322;
}
:deep().el-table__row .abnormal-power-supply {
  background-color: #fffbe6;
  color: #d48806;
}
:deep().el-table__row .disconnect {
  background-color: #e6f7ff;
  color: #1890ff;
}
/* è¡¨æ ¼æ ·å¼ */
:deep().el-table {
  border-radius: 8px;
  overflow: hidden;
}
:deep().el-table th {
  background-color: #fafafa;
  font-weight: 600;
}
/* æŒ‰é’®æ ·å¼ */
:deep().el-button--primary {
  background-color: #1890ff;
  border-color: #1890ff;
}
:deep().el-button--primary:hover {
  background-color: #40a9ff;
  border-color: #40a9ff;
}
/* å¡ç‰‡æ ·å¼ */
:deep().el-card {
  border-radius: 8px;
  border: 1px solid #f0f0f0;
}
/* å“åº”式调整 */
@media (max-width: 1200px) {
  .search-form {
    flex-direction: column;
    align-items: flex-start;
  }
  .form-actions {
    margin-left: 0;
    margin-top: 12px;
  }
}
/* åŽŸæœ‰æ ·å¼å…¼å®¹ */
.header-container {
  display: flex;
  margin-left: 0;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px;
}
.describe-info {
  margin-top: 0;
  font-weight: 500;
  white-space: nowrap;
  color: #666;
}
.table-page {
  margin-left: 0;
}
.table-text {
  font-size: 16px;
  margin: 0 0 16px 0;
  font-weight: 600;
  color: #333;
}
.el-collapse {
  margin-left: 20px;
  margin-left: 0;
}
:deep().el-collapse .el-collapse-item__content {
  padding-bottom: 0px;
}
.box-card {
  height: 190px;
  height: auto;
  min-height: 190px;
}
.sub-box-card {
  height: 100px;
  height: auto;
  min-height: 100px;
  border: 0px;
}
.mx-1 {
  margin-bottom: 0px;
}
.dialog-button-position {
  display: flex;
  justify-content: right;
  margin-bottom: 10px;
}
:deep().el-table__row .exceeding-row {
  background-color: #f53f3f;
}
:deep().el-table__row .abnormal-power-supply {
  background-color: #fdf4bf;
}
:deep().el-table__row .disconnect {
  background-color: #f7ba1e;
}
.el-table {
  color: #000000;
}
/* è¡¨æ ¼ä¸­çš„æŒ‰é’®å®½åº¦é“ºæ»¡ */
.table-button {
  width: 100%;
}
</style>
src/views/monitor/DataException_old.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1690 @@
<template>
  <el-row ref="h1">
    <el-col>
      <!-- èœå•读标题 -->
      <div ref="h1" class="header-container">
        <span class="describe-info">店铺名选择:</span>
        <!-- åº—铺名  çº§è” -->
        <ShopNameAndID @submit-id="(n) => (deviceId[1] = n)"></ShopNameAndID>
        <!-- å¼‚常类型选择 -->
        <ExceptionType @submitExceptionType="(val) => (exceptionValue = val)"> </ExceptionType>
        <TimeSelect @submit-time="giveTime"></TimeSelect>
      </div>
      <div ref="h2" style="display: flex; margin-top: 2px; justify-content: right">
        <el-button
          type="primary"
          plain
          style="margin-left: 20px"
          :loading="button.queryButton"
          @click="showTable"
          >查询</el-button
        >
        <el-tooltip
          class="box-item"
          effect="dark"
          content="点击可导出Excel文件"
          placement="top-start"
        >
          <!-- åšæˆå‡½æ•°js文件 -->
          <el-icon class="iconExcel clickable" title="导出Excel文件" @click="exportDom">
            <i-ep-Download />
            <!-- å¯¼å‡ºä¸ºExcel -->
          </el-icon>
        </el-tooltip>
      </div>
      <div style="display: flex; justify-content: right; margin-right: 40px">
        <span class="collapse-header-text">
          é™å®‰åŒº {{ beginTime }} â€”— {{ endTime }} æ²¹çƒŸç›‘测异常信息汇总</span
        >
      </div>
      <br />
      <el-collapse ref="h3" v-model="activeNames">
        <el-collapse-item name="1">
          <template #title>
            <el-tooltip class="box-item" effect="dark" content="点击可折叠" placement="right-start">
              <h4 class="collapse-header">异常分析</h4>
              <el-icon class="header-icon">
                <i-ep-info-filled />
              </el-icon>
            </el-tooltip>
            <el-tooltip class="box-item" effect="dark" content="点击可折叠" placement="right-start">
            </el-tooltip>
          </template>
          <el-card class="box-card">
            <el-row :gutter="25">
              <el-col :span="8">
                <div style="display: flex">
                  <img
                    src="@/assets/exceed.jpg"
                    style="width: 25px; height: 25px; margin-top: 5px"
                  />
                  <span
                    style="font-size: 16px; font-weight: bold; margin-top: 4px; margin-left: 4px"
                    >油烟浓度超标</span
                  >
                </div>
                <div class="box-card-label">
                  <el-scrollbar>
                    <span class="box-card-label">异常店铺占比:</span>
                    <span style="font-size: 20px">{{ exception0.length }} /{{ shopsTotal }} </span>
                    <span style="font-size: 17px">
                      ({{ ((exception0.length / shopsTotal) * 100).toFixed(1) }}%)</span
                    >
                    {{ shopsTotal }}
                    <span class="right-text">
                      å¼‚常数占比:{{ ((exception0Num / exceptionAllNum) * 100).toFixed(1) }}%
                    </span>
                  </el-scrollbar>
                </div>
                <hr />
                <div class="box-card-butcontainer">
                  <el-card class="sub-box-card">
                    <el-scrollbar max-height="70px">
                      <ExceptionText
                        v-for="(item, index) in exception0"
                        :key="item"
                        :devId="item.devId"
                        exception-value="0"
                        :begin-time="beginTime"
                        :end-time="endTime"
                        @submit-exception-data="getAbnormalDataByClick"
                      >
                        {{ item.diName }}
                        <span v-if="index < exception0.length - 1" class="text-blank">,</span>
                      </ExceptionText>
                    </el-scrollbar>
                  </el-card>
                </div>
              </el-col>
              <el-col :span="8">
                <div style="display: flex">
                  <img
                    src="@/assets/exception.jpg"
                    style="width: 25px; height: 25px; margin-top: 5px"
                  />
                  <span
                    style="font-size: 16px; font-weight: bold; margin-top: 5px; margin-left: 4px"
                    >供电异常</span
                  >
                </div>
                <div class="box-card-label">
                  <el-scrollbar>
                    <span class="box-card-label">异常店铺占比:</span>
                    <span style="font-size: 20px">{{ exception1.length }} /{{ shopsTotal }}</span>
                    <span style="font-size: 17px">
                      ({{ ((exception1.length / shopsTotal) * 100).toFixed(1) }}%)</span
                    >
                    <span class="right-text">
                      å¼‚常数占比:{{ ((exception1Num / exceptionAllNum) * 100).toFixed(1) }}%
                    </span>
                  </el-scrollbar>
                </div>
                <hr />
                <div>
                  <el-card class="sub-box-card">
                    <el-scrollbar max-height="70px">
                      <ExceptionText
                        v-for="(item, index) in exception1"
                        :key="item"
                        :devId="item.devId"
                        exception-value="1"
                        :begin-time="beginTime"
                        :end-time="endTime"
                        @submit-exception-data="getAbnormalDataByClick"
                      >
                        {{ item.diName }}
                        <span v-if="index < exception1.length - 1" class="text-blank">,</span>
                      </ExceptionText>
                    </el-scrollbar>
                  </el-card>
                </div>
              </el-col>
              <el-col :span="8">
                <div style="display: flex">
                  <img
                    src="@/assets/offline.jpg"
                    style="width: 25px; height: 25px; margin-top: 5px"
                  />
                  <span
                    style="font-size: 16px; font-weight: bold; margin-top: 5px; margin-left: 4px"
                    >设备或网络异常</span
                  >
                </div>
                <div class="box-card-label">
                  <el-scrollbar>
                    <span class="box-card-label">异常店铺占比:</span>
                    <span style="font-size: 20px">{{ exception2.length }} /{{ shopsTotal }}</span>
                    <span style="font-size: 17px">
                      ({{ ((exception2.length / shopsTotal) * 100).toFixed(1) }}%)</span
                    >
                    <span class="right-text"> å¼‚常数占比:{{ connectException }}% </span>
                  </el-scrollbar>
                </div>
                <hr />
                <div>
                  <el-card class="sub-box-card">
                    <el-scrollbar max-height="70px">
                      <ExceptionText
                        v-for="(item, index) in exception2"
                        :key="item"
                        :devId="item.devId"
                        exception-value="2"
                        :begin-time="beginTime"
                        :end-time="endTime"
                        @submit-exception-data="getAbnormalDataByClick"
                      >
                        {{ item.diName }}
                        <span v-if="index < exception2.length - 1" class="text-blank">,</span>
                      </ExceptionText>
                    </el-scrollbar>
                  </el-card>
                </div>
              </el-col>
            </el-row>
          </el-card>
        </el-collapse-item>
      </el-collapse>
      <h4 class="table-text">异常数据</h4>
    </el-col>
  </el-row>
  <el-card class="table-page" v-show="!isNoData">
    <el-table
      ref="tableH"
      size="small"
      v-loading="loading"
      :data="displayData"
      style="width: 100%"
      border
      :height="tableHeight"
      :cell-class-name="tableCellClassName"
    >
      <el-table-column prop="diName" label="店铺名称" align="center">
        <template #default="{ row }">
          <el-tooltip effect="dark" :content="row.diName">
            <div class="cell ellipsis">{{ row.diName }}</div>
          </el-tooltip>
        </template>
      </el-table-column>
      <el-table-column prop="devId" label="设备编号" align="center">
        <template #default="{ row }">
          <el-tooltip effect="dark" :content="row.devId">
            <div class="cell ellipsis">{{ row.devId }}</div>
          </el-tooltip>
        </template>
      </el-table-column>
      <el-table-column prop="diSupplier" label="供应商" align="center">
        <template #default="{ row }">
          <el-tooltip effect="dark" :content="row.diSupplier">
            <div class="cell ellipsis">{{ row.diSupplier }}</div>
          </el-tooltip>
        </template>
      </el-table-column>
      <el-table-column prop="exception" label="异常分类" align="center">
        <template #default="{ row }">
          <el-tooltip effect="dark" :content="row.exception">
            <div class="cell ellipsis">{{ row.exception }}</div>
          </el-tooltip>
        </template>
      </el-table-column>
      <el-table-column label="异常类型" align="center">
        <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-tooltip effect="dark" :content="row.region">
            <div class="cell ellipsis">{{ row.region }}</div>
          </el-tooltip>
        </template>
      </el-table-column>
      <el-table-column prop="beginTime" label="开始时间" align="center">
        <template #default="{ row }">
          <el-tooltip effect="dark" :content="row.beginTime">
            <div class="cell ellipsis">{{ row.beginTime }}</div>
          </el-tooltip>
        </template>
      </el-table-column>
      <el-table-column prop="endTime" label="结束时间" align="center">
        <template #default="{ row }">
          <el-tooltip effect="dark" :content="row.endTime">
            <div class="cell ellipsis">{{ row.endTime }}</div>
          </el-tooltip>
        </template>
      </el-table-column>
      <el-table-column label="操作" align="center">
        <template #default="{ row }">
          <el-button type="primary" class="table-button" @click="showDrawer(row)"
            >查看详情</el-button
          >
        </template>
      </el-table-column>
    </el-table>
    <el-pagination
      ref="h4"
      background
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      :total="total"
      :page-size="pageSize"
      layout="total,prev, pager, next, jumper"
    />
  </el-card>
  <el-empty v-show="isNoData" :image-size="200" />
  <!-- å¯¹è¯æ¡† -->
  <div>
    <el-dialog v-model="centerDialogVisible" draggable align-center>
      <template #header>
        <div style="font-size: 17px">
          åº—铺名:{{ rowShopName }}
          <span style="margin-left: 40px">异常类型:</span>
          <span v-if="rowExceptionType == '0'">油烟数据超标</span>
          <span v-else-if="rowExceptionType == '1'">供电异常</span>
          <span v-else-if="rowExceptionType == '2'">掉线</span>
          <div style="margin-top: 10px">
            å¼‚常时间段:{{ rowBeginTime }} ~
            {{ rowEndTime }}
          </div>
        </div>
        <div class="dialog-button-position">
          <el-button
            type="danger"
            :loading="button.preButton"
            :disabled="isPreCantouch || banTouch"
            @click="getPreviousRowData"
            >上条异常</el-button
          >
          <el-button
            type="danger"
            :loading="button.afterButton"
            :disabled="isNextCantouch || banTouch"
            @click="getNextRowData"
            >下条异常</el-button
          >
        </div>
      </template>
      <!-- è¶…标数据时 -->
      <!-- æŠ˜çº¿å›¾ -->
      <!-- æŽ‰çº¿ -->
      <!-- <div
          ref="ref"
          v-show="isOfflineShow"
          style="
            width: 100%;
            height: 300px;
            /* min-width: 100px; */
            margin-bottom: 20px;
            margin-left: 10px;
            min-width: 350px;
          "
        ></div> -->
      <ExceptionTypeLineChart
        :option="option"
        :is-open-dialog="centerDialogVisible"
        v-loading="chartLoading"
      ></ExceptionTypeLineChart>
      <!--  -->
      <div style="margin-top: 40px; margin-bottom: 5px; border: 1px">
        <el-table :data="exceedingData" height="360" border style="margin-top: 25px">
          <el-table-column
            type="index"
            label="序号"
            width="60px"
            align="center"
            fixed
            :index="indexMethod"
          ></el-table-column>
          <el-table-column fixed prop="diName" label="店铺名称" show-overflow-tooltip />
          <el-table-column
            prop="mvStatCode"
            label="设备编号"
            align="center"
            show-overflow-tooltip
          />
          <el-table-column prop="diSupplier" label="供应商" align="center" show-overflow-tooltip />
          <el-table-column
            prop="mvDataTime"
            label="采集时间"
            align="center"
            show-overflow-tooltip
          />
          <el-table-column
            prop="mvFumeConcentration2"
            label="油烟浓度(mg/m³)"
            align="center"
            show-overflow-tooltip
          />
        </el-table>
      </div>
      <el-tag type="success" class="mx-1" effect="dark" round
        ><span class="table-line-lable" v-show="rowExceptionType == '0'">异常记录: </span>
        <span v-show="rowExceptionType == '1' || rowExceptionType == '2'">缺失数据:</span>
        <span class="table-line-num">{{ exceptionTotal }}条</span>
        <span v-show="rowExceptionType === '1' || rowExceptionType === '2'"> (逻辑计算)</span>
      </el-tag>
    </el-dialog>
  </div>
</template>
<script>
import { defineAsyncComponent } from 'vue'
import ExceptionType from '@/sfc/ExceptionType.vue'
import TimeSelect from '@/sfc/TimeSelect.vue'
import ExceptionText from '@/sfc/ExceptionText.vue'
import * as XLSX from 'xlsx/xlsx.mjs'
import dayjs from 'dayjs'
import axiosInstanceInstance from '@/utils/request.js'
const ShopNameAndID = defineAsyncComponent(() => import('@/sfc/ShopNameAndID.vue'))
//  å¼‚常图形异步组件
const ExceptionTypeLineChart = defineAsyncComponent(
  () => import('@/sfc/ExceptionTypeLineChart.vue'),
)
export default {
  name: 'TablePage',
  components: {
    ExceptionType,
    TimeSelect,
    ShopNameAndID,
    ExceptionText,
    ExceptionTypeLineChart,
  },
  data() {
    return {
      exception0Num: 0,
      exception1Num: 0,
      exception2Num: 0,
      // æŠ˜çº¿å›¾åŠ è½½ä¸­
      chartLoading: false,
      button: {
        // æŸ¥è¯¢æŒ‰é’®
        queryButton: false,
        // ä¸Šä¸€æ¡æŒ‰é’®
        preButton: false,
        // ä¸‹ä¸€æ¡æŒ‰é’®
        afterButton: false,
        //
        banTouch: 0,
      },
      // å¼‚常折线图的配置
      option: {},
      // æŠ˜çº¿å›¾å±•示
      isChartShow: false,
      // table元素
      tableRef: null,
      // å¼‚常表格数据
      tableHeight: 300,
      // ç©ºæ•°æ®çŠ¶æ€
      isNoData: false,
      // å¼¹å‡ºæ¡†ä¸­è¡¨æ ¼æ¡æ•°
      exceptionTotal: 0,
      // æ— æ•°æ®æ—¶çš„æ—¶é—´æ•°ç»„,元素相差10分钟
      // abnormalTimeTenMinute: [],
      // åº—铺总数
      shopsTotal: 0,
      // â€™ä¸Šä¸€æ¡â€˜æŒ‰é’®æ˜¯å¦å¯ä»¥è¢«ç‚¹å‡»çŠ¶æ€
      isPreCantouch: false,
      // â€™ä¸‹ä¸€æ¡â€˜æŒ‰é’®æ˜¯å¦å¯ä»¥è¢«ç‚¹å‡»çŠ¶æ€
      isNextCantouch: false,
      // å¯¹è¯æ¡†æ˜¯å¦å±•示
      centerDialogVisible: false,
      // æŠ½å±‰å¤´éƒ¨ä¿¡æ¯
      // æŠ˜çº¿å›¾å¯¹åº”的当前表格行数据
      // åº—铺名
      rowShopName: '',
      // å¼‚常类型
      rowExceptionType: '',
      // å¼‚常开始时间
      rowBeginTime: '',
      // å¼‚常结束时间
      rowEndTime: '',
      // å¼‚常的设备编号
      rowMvStatCode: '',
      // ä¾›åº”商
      rowDiSupplier: '',
      // è¡¨æ ¼çš„一行数据
      rowTable: [],
      //拼接的所有数据
      allExceptionTimeData: [],
      // æ— æ•°æ®æ—¶å¢žåŠ çš„å‰30分钟数据
      beforeData: [],
      // æ— æ•°æ®æ—¶å¢žåŠ çš„åŽ40分钟数据
      afterData: [],
      // -1表示未选择表格的行
      selectedRowIndex: -1,
      // é»˜è®¤é€‰æ‹©çš„æŠ˜å é¢æ¿ç¼–号
      activeNames: ['1'],
      // å¼‚常时的表格
      abnormalTb: [],
      // å¼‚常的起止时间
      abnormalBt: '',
      abnormalEt: '',
      // æ˜¯å¦å±•示时间轴  å¦
      isAbnormal: false,
      // ä¿å­˜ç€å¼‚常类型0对应的店铺名称和设备编号
      exception0: [],
      // ä¿å­˜ç€å¼‚常类型1对应的店铺名称和设备编号
      exception1: [],
      // ä¿å­˜ç€å¼‚常类型2对应的店铺名称和设备编号
      exception2: [],
      // åŠ è½½åŠ¨ç”»
      loading: false,
      // æŠ½å±‰åŠ è½½åŠ¨ç”»
      loadingDrawer: true,
      // åˆ†é¡µå±•示数据
      // å¼‚常表的数据
      displayData: [],
      // å­˜æ”¾åŽç«¯è¿”回的json数据
      jsonData: [],
      // åˆ†é¡µçš„起始索引
      startIndex: 0,
      // å½“前页
      currentPage: 1,
      // æ¯é¡µæ¡æ•°
      pageSize: 10,
      total: 0,
      // é€‰æ‹©åº—铺名
      deviceId: [],
      deviceInfo: [],
      // æ—¶é—´é€‰æ‹©å™¨å¼€å§‹æ—¶é—´
      beginTime: '',
      // æ—¶é—´é€‰æ‹©å™¨ç»“束时间
      endTime: '',
      // å¼‚常表数据
      abnormalData: [],
      // å¼¹å‡ºçš„对话框中的异常表格数据
      exceedingData: [],
      drawerVisible: false,
      // è¡¨æ ¼çš„一行数据
      drawerData: {},
      // æŠ½å±‰æ–¹å‘,从右向左打开
      drawerDirection: 'rtl',
      optionsTime: [
        // æ—¶é—´é¢—粒度
        {
          value: '10',
          label: '10分钟数据',
          disabled: true,
        },
      ],
      // åº—铺名 çº§è”选择器
      optionsShop: [],
      // å¼‚常类型选择器
      exceptionValue: [],
    }
  },
  // ç›‘听  åˆ¤æ–­æŒ‰é’®æ˜¯å¦å¯ç‚¹å‡»
  watch: {
    selectedRowIndex(newVaue) {
      // å¤„于表格的最后一条数据 è®¾ç½®â€˜ä¸Šä¸€æ¡â€™æŒ‰é’®ä¸å¯ç‚¹
      if (newVaue === this.displayData.length - 1) {
        this.isPreCantouch = true
        //用户先点了第一条,pre为true,然后点击最后一条,next为true。此时两个按钮都被封锁
        if (this.isNextCantouch == true) {
          this.isNextCantouch = false
        }
      }
      // å¤„于表格第一条数据 è®¾ç½®â€˜ä¸‹ä¸€æ¡â€™æŒ‰é’®ä¸å¯ç‚¹
      else if (newVaue === 0) {
        this.isNextCantouch = true
        //用户先点了表格最后一条,next为true,然后点击第一条,pre为true。此时两个按钮都被封锁
        if (this.isPreCantouch == true) {
          this.isPreCantouch = false
        }
      }
      // å¤„于表格的中间行 å°†æŒ‰é’®è®¾ç½®ä¸ºå¯ç‚¹å‡»çŠ¶æ€
      else {
        this.isPreCantouch = false
        this.isNextCantouch = false
      }
    },
    // å½“选择的时间发生变化时,异常分析部分的异常店铺数量同步变化
    beginTime() {
      this.getShopNames()
    },
    endTime() {
      this.getShopNames()
    },
    centerDialogVisible() {
      window.addEventListener('resize', this.updateChart)
    },
  },
  computed: {
    exceptionAllNum() {
      let sum = this.exception0Num + this.exception1Num + this.exception2Num
      return sum == 0 ? 1 : sum
    },
    connectException() {
      let sum = this.exception0Num + this.exception1Num + this.exception2Num
      if (sum == 0) {
        return 0
      } else {
        return (100 - (this.exception0Num / sum) * 100 - (this.exception1Num / sum) * 100).toFixed(
          1,
        )
      }
    },
  },
  mounted() {
    // ä»ŽæŽ¥å£èŽ·å–åº—é“ºåç§° ç»™çº§è”下拉框
    this.getDeviceInfo()
    // å±•示最近7天数据
    this.getRecentSevenDays()
    // æ ¹æ®å¼‚常类型返回店铺名称和设备编号 æ¸²æŸ“异常分析部分对应的店铺名
    this.getShopNames()
    this.calcTableHeight()
    window.addEventListener('resize', this.updateChart)
  },
  methods: {
    // åŠŸèƒ½ï¼šå¯¹è¯æ¡†è¡¨æ ¼åºå·é€’å¢ž
    // æ—¶é—´ï¼š2023-8-17
    indexMethod(index) {
      return index + 1
    },
    // åŠŸèƒ½ï¼šæ”¹å˜è¡¨æ ¼æŸä¸ªå•å…ƒæ ¼çš„é¢œè‰²
    tableCellClassName({ row, column, rowIndex, columnIndex }) {
      if (columnIndex == 4) {
        if (row.exceptionType == '0') {
          return 'exceeding-row'
        } else if (row.exceptionType == '1') {
          return 'abnormal-power-supply'
        } else if (row.exceptionType == '2') {
          return 'disconnect'
        }
      }
    },
    //功能: åŠ¨æ€è®¡ç®—è¡¨æ ¼é«˜åº¦
    calcTableHeight() {
      const h1 = this.$refs.h1.$el.offsetHeight
      const h2 = this.$refs.h4.$el.offsetHeight
      this.tableHeight = `calc(100vh - ${h1}px - ${h2}px - 45px - var(--el-main-padding) * 2 - var(--el-card-padding))`
    },
    //功能: æ—¶é—´æ˜¯å¦è¶…过10分钟
    isTimeDifferenceGreaterThan10Minutes(dateString1, dateString2) {
      const date1 = new Date(dateString1)
      const date2 = new Date(dateString2)
      // è®¡ç®—两个日期的时间差(毫秒)
      const timeDifferenceMs = Math.abs(date1 - date2)
      // è½¬æ¢ä¸ºåˆ†é’Ÿ
      const timeDifferenceMinutes = Math.floor(timeDifferenceMs / (1000 * 60))
      // åˆ¤æ–­æ—¶é—´å·®æ˜¯å¦å¤§äºŽ10分钟
      return timeDifferenceMinutes > 10
    },
    // ä»¥10分钟为间隔返回时间字符串数组
    generateTimePoints(timePoints, yAxisData) {
      let updatedTimePoints = []
      let yAxisDataAdressed = []
      for (let i = 0; i < timePoints.length; i++) {
        updatedTimePoints.push(timePoints[i])
        yAxisDataAdressed.push(yAxisData[i])
        if (i < timePoints.length - 1) {
          let current = timePoints[i]
          let next = timePoints[i + 1]
          while (this.isTimeDifferenceGreaterThan10Minutes(current, next)) {
            current = dayjs(current).add(10, 'minute').format('YYYY-MM-DD HH:mm:ss')
            updatedTimePoints.push(current)
            yAxisDataAdressed.push(null)
          }
        }
      }
      let obj = {}
      obj['time'] = updatedTimePoints
      obj['data'] = yAxisDataAdressed
      return obj
    },
    isExceedOneMonth(dateStr1, dateStr2) {
      // è¶…过一个月,返回True,否则返回False
      // å°†æ—¥æœŸå­—符串转为日期对象
      const date1 = new Date(dateStr1)
      const date2 = new Date(dateStr2)
      // èŽ·å–ä¸¤ä¸ªæ—¥æœŸçš„å¹´ã€æœˆã€æ—¥
      const year1 = date1.getFullYear()
      const month1 = date1.getMonth()
      const day1 = date1.getDate()
      const year2 = date2.getFullYear()
      const month2 = date2.getMonth()
      const day2 = date2.getDate()
      // åˆ¤æ–­ä¸¤ä¸ªæ—¥æœŸæ˜¯å¦ç›¸å·®ä¸€ä¸ªæœˆ
      if (year1 === year2) {
        // å¹´ä»½ç›¸ç­‰ï¼Œæ¯”较月份差值
        if (Math.abs(month1 - month2) === 1) {
          // æœˆä»½å·®å€¼ä¸º1,还需要判断具体日期
          if ((month1 < month2 && day1 < day2) || (month1 > month2 && day1 > day2)) {
            return true
          }
        }
      } else if (Math.abs(year1 - year2) === 1) {
        // å¹´ä»½å·®å€¼ä¸º1,比较月份和日期
        if (
          (year1 < year2 && month1 === 11 && month2 === 0 && day1 < day2) ||
          (year1 > year2 && month1 === 0 && month2 === 11 && day1 > day2)
        ) {
          return true
        }
      }
      // é»˜è®¤è¿”回false,表示两个日期字符串不相差一个月
      return false
    },
    // åˆšæ‰“开卡片时第一个图形不会自动伸缩 å½“点击上/下一条时会自动伸缩
    // å›¾å½¢å“åº”式变化
    // updateChart() {
    //   this.$nextTick(() => {
    //     if (this.chart1) {
    //       this.chart1.resize();
    //     }
    //     if (this.chart2) {
    //       this.chart2.resize();
    //     }
    //   });
    // },
    // ä»Žæ—¶é—´é€‰æ‹©å™¨ç»„件拿到开始和结束时间
    giveTime(val) {
      //将中国标准时间转为指定格式(该组件返回的标准时间的格式,所以必须的加这个函数)
      this.beginTime = dayjs(val[0]).format('YYYY-MM-DD HH:mm:ss')
      this.endTime = dayjs(val[1]).format('YYYY-MM-DD HH:mm:ss')
    },
    // å‚数:异常的开始和结束时间。返回时间数组,从开始时间的后10分钟到结束时间为止。
    // æ¯”如12:00:00-13:00:00 æ‰€ä»¥è¿”回的数组元素是 12:10:00 ,12:20:00,12:30:00....13:00:00
    descTenTime(begin, end) {
      let time = []
      if (begin == end) {
        time.push(begin)
        return time
      }
      // ä¿ç•™ç»“æžœ 00 10 20 30
      let temp = dayjs(begin).add(10, 'minute').format('YYYY-MM-DD HH:mm:ss')
      while (temp != end) {
        time.push(temp)
        temp = dayjs(temp).add(10, 'minute').format('YYYY-MM-DD HH:mm:ss')
      }
      // åŠ ä¸Šå¼‚å¸¸çš„ç»“æŸæ—¶é—´
      time.push(temp)
      return time
    },
    // ä¿å­˜å½“前选择的行所有信息
    setinfo(index) {
      this.rowShopName = this.displayData[index].diName
      this.rowExceptionType = this.displayData[index].exceptionType
      this.rowBeginTime = this.displayData[index].beginTime
      this.rowEndTime = this.displayData[index].endTime
      this.rowMvStatCode = this.displayData[index].devId
      this.rowDiSupplier = this.displayData[index].diSupplier
    },
    //功能: ä¾›ç”µå¼‚常和掉线时的表格数据
    setExceptionData() {
      // æ— æ•°æ®æ—¶çš„æ—¶é—´æ•°ç»„ æ—¶é—´ç›¸å·®10分钟
      const abnormalTimeTenMinute = this.descTenTime(this.rowBeginTime, this.rowEndTime)
      // åŽ»é™¤ä¾›ç”µå¼‚å¸¸å’ŒæŽ‰çº¿åŒºé—´çš„ç¬¬ä¸€ä¸ªæœ‰å…ƒç´ çš„å€¼
      this.exceedingData = []
      for (let i = 0; i < abnormalTimeTenMinute.length; i++) {
        this.exceedingData.push({
          mvStatCode: this.rowMvStatCode,
          diName: this.rowShopName,
          diSupplier: this.rowDiSupplier,
          mvDataTime: abnormalTimeTenMinute[i],
          mvFumeConcentration2: '',
        })
      }
      // ä¿å­˜æ— æ•°æ®æ—¶è¡¨æ ¼æ¡æ•°
      this.exceptionTotal = abnormalTimeTenMinute.length
    },
    // ç‚¹å‡»è¡¨æ ¼çš„行时
    selectTableRow(row) {
      // èŽ·å–å½“å‰è¡Œçš„ç´¢å¼•
      this.selectedRowIndex = this.displayData.indexOf(row)
      // è¿›å…¥æŠ½å±‰é¡µé¢æ›´æ–°å¤´éƒ¨æ•°æ®
      this.setinfo(this.selectedRowIndex)
    },
    // èŽ·å–èŽ·å–è¡¨æ ¼ä¸‹ä¸€è¡Œæ•°æ®
    getNextRowData() {
      // ä¸æ˜¯è¡¨æ ¼çš„第一行
      if (this.selectedRowIndex !== 0) {
        // ç‚¹å‡»è¿‡ç¨‹ä¸­ é”ä½ä¸Šä¸‹æ¡æŒ‰é’®  åœ¨è®¾ç½®å®Œå›¾å½¢é…ç½®é¡¹åŽè§£é”
        this.banTouch = 1
        //得到上一行数据索引
        this.selectedRowIndex = this.selectedRowIndex - 1
        //请求数据 æ”¹å˜exceedingData
        this.setinfo(this.selectedRowIndex)
        let params = {}
        if (this.drawerData.devId) {
          params['devId'] = this.displayData[this.selectedRowIndex].devId
        }
        if (this.drawerData.beginTime) {
          params['beginTime'] = this.displayData[this.selectedRowIndex].beginTime
        }
        if (this.drawerData.endTime) {
          params['endTime'] = this.displayData[this.selectedRowIndex].endTime
        }
        this.button.afterButton = true
        axiosInstanceInstance.get('/fume/exceed', { params: params }).then((response) => {
          // ä¿å­˜è¿”回的超标数据
          this.exceedingData = response.data.data
          this.drawChartTest()
          this.exceptionTotal = this.exceedingData.length
          this.button.afterButton = false
        })
      }
    },
    // èŽ·å–èŽ·å–è¡¨æ ¼ä¸‹ä¸€è¡Œæ•°æ®
    getPreviousRowData() {
      // ä¸æ˜¯è¡¨æ ¼çš„第一行
      if (this.selectedRowIndex < this.displayData.length - 1) {
        // ç‚¹å‡»è¿‡ç¨‹ä¸­ é”ä½ä¸Šä¸‹æ¡æŒ‰é’®  åœ¨è®¾ç½®å®Œå›¾å½¢é…ç½®é¡¹åŽè§£é”
        this.banTouch = 1
        //得到上一行数据索引
        this.selectedRowIndex = this.selectedRowIndex + 1
        //请求数据 æ”¹å˜exceedingData
        this.setinfo(this.selectedRowIndex)
        let params = {}
        if (this.drawerData.devId) {
          params['devId'] = this.displayData[this.selectedRowIndex].devId
        }
        if (this.drawerData.beginTime) {
          params['beginTime'] = this.displayData[this.selectedRowIndex].beginTime
        }
        if (this.drawerData.endTime) {
          params['endTime'] = this.displayData[this.selectedRowIndex].endTime
        }
        this.button.preButton = true
        axiosInstanceInstance.get('/fume/exceed', { params: params }).then((response) => {
          // ä¿å­˜è¿”回的超标数据
          this.exceedingData = response.data.data
          this.drawChartTest()
          this.exceptionTotal = this.exceedingData.length
          this.button.preButton = false
        })
      }
    },
    // â€˜æŸ¥çœ‹è¯¦æƒ…’ å¼¹å‡ºæ¡†éƒ¨åˆ†
    showDrawer(row) {
      // è®¡ç®—当前行的索引
      this.selectTableRow(row)
      this.rowTable = row
      // è¡¨æ ¼çš„行数据以对象形式给drawerData
      this.drawerData = row
      this.centerDialogVisible = true
      // æ ¹æ®è¡Œæ•°æ®è¯·æ±‚详细超标数据渲染折线图
      let params = {}
      if (this.drawerData.devId) {
        params['devId'] = this.drawerData.devId
      }
      if (this.drawerData.beginTime) {
        params['beginTime'] = this.drawerData.beginTime
      }
      if (this.drawerData.endTime) {
        params['endTime'] = this.drawerData.endTime
      }
      axiosInstanceInstance.get('/fume/exceed', { params: params }).then((response) => {
        // ä¿å­˜è¿”回的超标数据
        this.exceedingData = response.data.data
        this.drawChartTest()
        this.exceptionTotal = this.exceedingData.length
      })
    },
    // ç”¨æˆ·æ ¹æ®è¾“入的条件查询
    showTable() {
      if (this.isExceedOneMonth(this.beginTime, this.endTime)) {
        alert('时间跨度不能超过一个月')
        return
      }
      let params = {}
      if (this.deviceId[1]) {
        params['devId'] = this.deviceId[1]
      }
      if (this.exceptionValue.length != 0) {
        params['exceptionValue'] = this.exceptionValue.join()
      }
      if (this.beginTime) {
        params['beginTime'] = this.beginTime
      }
      if (this.endTime) {
        params['endTime'] = this.endTime
      }
      this.loading = true
      this.button.queryButton = true
      axiosInstanceInstance.get('/fume/abnormalthree', { params: params }).then((response) => {
        this.abnormalData = response.data.data
        this.total = this.abnormalData.length
        this.loading = false
        this.button.queryButton = false
        if (response.data.data.length == 0) {
          ElMessage('该时段无数据')
          this.isNoData = true
          return
        }
        // ç§»é™¤ç©ºæ•°æ®çŠ¶æ€
        this.isNoData = false
        this.handleCurrentChange(1)
      })
    },
    handleSizeChange(val) {
      this.pageSize = val
      // æ”¹å˜æ¯é¡µæ˜¾ç¤ºæ•°ç›®æ—¶è·³åˆ°ç¬¬ä¸€é¡µ
      this.handleCurrentChange(1)
    },
    handleCurrentChange(val) {
      const startIndex = (val - 1) * this.pageSize
      const endIndex = startIndex + this.pageSize
      this.displayData = this.abnormalData.slice(startIndex, endIndex)
    },
    //相差多少个十分钟  è®¡ç®—中并不包括开始时间,但包括结束时间。
    diffTenMinutesNum(beginNormal, endNormal) {
      // å°†å¼€å§‹æ—¶é—´å’Œç»“束时间转换为dayjs对象
      const start = dayjs(beginNormal)
      const end = dayjs(endNormal)
      // è®¡ç®—结束时间减去开始时间中间相差多少个十分钟
      const diffInMinutes = end.diff(start, 'minute')
      const diffInTenMinutes = Math.floor(diffInMinutes / 10)
      return diffInTenMinutes
    },
    // å‚数:异常的开始时间,异常的结束时间。
    // åŠŸèƒ½ï¼šè¿”å›žå¼€å§‹æ—¶é—´çš„å‰30分钟的时间点,结束时间后40分钟的时间点
    before30AndAfter40(begin, end) {
      let time = []
      const before30MinBegin = dayjs(begin).subtract(30, 'minute').format('YYYY-MM-DD HH:mm:ss')
      // åŽä¸€æ®µçš„开始时间
      const after10MinBegin = dayjs(end).add(10, 'minute').format('YYYY-MM-DD HH:mm:ss')
      // å¾€åŽ40分钟
      const after40MinEnd = dayjs(end).add(40, 'minute').format('YYYY-MM-DD HH:mm:ss')
      time.push(before30MinBegin)
      time.push(after10MinBegin)
      time.push(after40MinEnd)
      return time
    },
    // å‚数:设备编号, å¼€å§‹æ—¶é—´ï¼Œ ç»“束时间
    // åŠŸèƒ½ï¼šè¿”å›žæŸè®¾å¤‡åœ¨è¯¥æ—¶æ®µåŽ†å²æ•°æ®çš„get请求参数。
    requestGetParms(devnum, begin, end) {
      return {
        devId: devnum,
        beginTime: begin,
        endTime: end,
      }
    },
    // å‚数:对象数组(该对象中的属性不能是引用类型,否则拷贝的值还是会相互影响)
    // åŠŸèƒ½ï¼šæ‹·è´è¯¥å¯¹è±¡æ•°ç»„ã€‚
    shallowCopyList(itemIsObjOfList) {
      let tempList = []
      itemIsObjOfList.forEach((item) => {
        tempList.push({ ...item })
      })
      return tempList
    },
    // å‚数:添加首尾时间数据的异常数据数组(元素为对象)
    // åŠŸèƒ½ï¼šå¯¹ä¸­é—´å¼‚å¸¸åŒºé—´æ—¶é—´å’Œå€¼è¿›è¡Œè¡¥å……ï¼Œè¿”å›žå¤„ç†åŽçš„ç»“æžœ
    // è¯¦ç»†æè¿°ï¼šéåŽ†æ•°ç»„ï¼Œå½“å‘çŽ°æ•°ç»„å…ƒç´ ä¸ºç©ºæ—¶ï¼Œè®¾ç½®è¯¥å…ƒç´ çš„æ—¶é—´ä¸ºä¸Šä¸€ä¸ªå…ƒç´ æ—¶é—´çš„åŽ10分钟,并把浓度值设置为null(上个元素的时间一定不为空,无需再去判断上个元素为空的情况)。
    addTenMinutes(exceptionDataArr) {
      // xè½´ æ—¥æœŸæ—¶é—´
      let dateList = []
      // yè½´ è¶…标油烟浓度
      let fumeExceeding = []
      let obj = {}
      for (let i = 0; i < exceptionDataArr.length; i++) {
        if (exceptionDataArr[i] == null) {
          //x轴日期。元素为null时, è®¾ç½®è¯¥å…ƒç´ çš„æ—¶é—´ä¸ºå‰ä¸€å…ƒç´ çš„æ—¶é—´åŽ10分钟
          dateList.push(
            dayjs(dateList[dateList.length - 1])
              .add(10, 'minute')
              .format('YYYY-MM-DD HH:mm:ss'),
          )
          // è¶…标油烟浓度
          fumeExceeding.push(null)
        } else {
          //x轴日期
          dateList.push(exceptionDataArr[i].mvDataTime)
          // è¶…标油烟浓度
          fumeExceeding.push(exceptionDataArr[i].mvFumeConcentration2)
        }
      }
      obj['dateList'] = dateList
      obj['fumeExceeding'] = fumeExceeding
      return obj
    },
    // å‚数:加上前后区间的异常数据,时间字符串
    // åŠŸèƒ½ï¼šåˆ¤æ–­data中是否有该日期时间,存在返回该时间对应的浓度值,否则返回-1
    findTimeInExceptionData(data, time) {
      for (let i = 0; i < data.length; i++) {
        if (data[i] == null) {
          continue
        }
        if (data[i]['mvDataTime'] == time) {
          return data[i]['mvFumeConcentration2']
        }
      }
      return -1
    },
    // å‚æ•°:前区间的开始时间, åŽåŒºé—´çš„结束时间, åŠ ä¸Šå‰åŽåŒºé—´çš„æ€»æ—¶é—´æ®µçš„å¼‚å¸¸æ•°æ®çš„å¯¹è±¡æ•°ç»„
    // åŠŸèƒ½ï¼šæ ¹æ®å¼€å§‹å’Œç»“æŸæ—¶é—´,返回以10分钟为间隔的时间和对应的值
    keepContinuousByEachTenMinutes(intervalStarTime, intervalEndTime, headAndTailExceptionData) {
      let xAxis = []
      let yAxis = []
      let obj = {}
      let current = intervalStarTime
      let tail = dayjs(intervalEndTime).add(10, 'minute').format('YYYY-MM-DD HH:mm:ss')
      while (current != tail) {
        let value = this.findTimeInExceptionData(headAndTailExceptionData, current)
        if (value != -1) {
          xAxis.push(current)
          yAxis.push(value)
        } else {
          xAxis.push(current)
          yAxis.push(null)
        }
        current = dayjs(current).add(10, 'minute').format('YYYY-MM-DD HH:mm:ss')
      }
      obj['xAxis'] = xAxis
      obj['yAxis'] = yAxis
      return obj
    },
    // å‚数:超标数据前面区间的数据
    // åŠŸèƒ½ï¼šè¿”å›žé™¤åŽ»æœ€åŽä¸€ä¸ªå…ƒç´ çš„æ•°ç»„
    removeLastItemOfBeforeData(beforeDataOfExceeding) {
      let tempList = []
      if (beforeDataOfExceeding.length == 1) {
        return tempList
      } else {
        for (let i = 0; i < beforeDataOfExceeding.length - 1; i++) {
          tempList.push({ ...beforeDataOfExceeding[i] })
        }
        return tempList
      }
    },
    // è®¾ç½®option
    // å‚数:x轴时间, y轴油烟浓度, å¼‚常类别(0代表超标,1代表供电异常和掉线), å¼‚常开始时间,异常结束时间,异常开始时间在整个区间的索引下标,异常结束时间在整个区间的索引下标
    setOption(
      xData,
      yData,
      exceptionCategory,
      exceptionBeginTime,
      exceptionEndTime,
      beginIndex,
      endIndex,
    ) {
      this.option = {}
      // è¶…æ ‡
      if (exceptionCategory == 0) {
        this.option = {
          tooltip: {},
          toolbox: {
            // å·¥å…·æ 
            feature: {
              //     dataZoom: {
              //   yAxisIndex: 'none'
              // },
              // ä¿å­˜ä¸ºå›¾ç‰‡
              saveAsImage: {},
            },
          },
          xAxis: {
            type: 'category',
            data: xData,
            name: '时间',
            axisLabel: {
              formatter: function (value) {
                return value.slice(11, -3)
              },
            },
          },
          yAxis: {
            type: 'value',
            name: 'mg/m³',
          },
          series: [
            {
              name: '油烟浓度',
              type: 'line',
              data: yData.map((item) => {
                if (item >= 1) {
                  return {
                    value: item,
                    itemStyle: {
                      color: 'red',
                    },
                  }
                }
                return item
              }),
              // å˜æ¢æŒ‡å®šæ—¶é—´åŒºé—´çš„背景颜色
              markArea: {
                itemStyle: {
                  color: 'rgba(255, 173, 177, 0.4)',
                },
                data: [
                  [
                    {
                      name: '超标时间段',
                      xAxis: exceptionBeginTime,
                    },
                    {
                      xAxis: exceptionEndTime,
                    },
                  ],
                ],
              },
              markLine: {
                symbol: 'none',
                itemStyle: {
                  // åŸºçº¿å…¬å…±æ ·å¼
                  normal: {
                    lineStyle: {
                      type: 'dashed',
                    },
                    label: {
                      show: true,
                      position: 'end',
                      formatter: '{b}',
                    },
                  },
                },
                data: [
                  {
                    name: '超标',
                    type: 'average',
                    yAxis: 1,
                    lineStyle: {
                      // color: '#ff0000'
                      color: 'red',
                    },
                  },
                ],
              },
            },
          ],
          // æŒ‡å®šæ—¶é—´åŒºé—´çš„线段变颜色
          visualMap: {
            show: false,
            dimension: 0,
            pieces: [
              {
                lte: beginIndex,
                color: 'green',
              },
              {
                gt: beginIndex,
                lte: endIndex,
                color: 'red',
              },
              {
                gt: endIndex,
                lte: xData.length - 1,
                color: 'green',
              },
            ],
          },
        }
      }
      // ä¾›ç”µå¼‚常和掉线
      else if (exceptionCategory == 1) {
        this.option = {
          tooltip: {},
          toolbox: {
            // å·¥å…·æ 
            feature: {
              // dataZoom: {
              //   // åŒºåŸŸç¼©æ”¾
              //   yAxisIndex: 'none'
              // },
              // ä¿å­˜ä¸ºå›¾ç‰‡
              saveAsImage: {},
            },
          },
          xAxis: {
            type: 'category',
            data: xData,
            name: '时间',
            axisLabel: {
              formatter: function (value) {
                return value.slice(11, -3)
              },
            },
          },
          yAxis: {
            type: 'value',
            name: 'mg/m³',
          },
          series: [
            {
              name: '油烟数据',
              type: 'line',
              data: yData,
              markLine: {
                silent: true,
                data: [
                  // æ ‡æ³¨æ— æ•°æ®æ—¶é—´æ®µçš„æ•ˆæžœï¼Œå°†è¿™ä¸ªæ—¶é—´æ®µçš„æ•°è½´éƒ¨åˆ†å˜ä¸ºçº¢è‰²
                  {
                    name: '无数据',
                    xAxis: exceptionBeginTime,
                  },
                  {
                    xAxis: exceptionEndTime,
                  },
                ],
                lineStyle: {
                  color: 'red',
                },
              },
            },
          ],
        }
      }
      this.banTouch = 0
    },
    // åŠŸèƒ½ï¼šç‚¹å‡» â€˜æŸ¥çœ‹è¯¦æƒ…’, â€˜ä¸‹ä¸€æ¡â€™æŒ‰é’®æ—¶ä¼š å…ˆé€»è¾‘计算。最后展示图形
    drawChartTest() {
      this.beforeData = []
      this.afterData = []
      this.allExceptionTimeData = []
      //异常的开始时间 ç»“束时间
      let exceptionBeginTime = this.rowBeginTime
      let exceptionEndTime = this.rowEndTime
      // beforeAndAfterTime[0]:前30分钟的时间点
      // beforeAndAfterTime[1]:后10分钟的时间点
      // beforeAndAfterTime[2]:后40分钟的时间点
      let beforeAndAfterTime = this.before30AndAfter40(exceptionBeginTime, exceptionEndTime)
      // æž„造异常时间前的区间数据请求参数
      let paramsBefore = this.requestGetParms(
        this.displayData[this.selectedRowIndex].devId,
        beforeAndAfterTime[0],
        this.displayData[this.selectedRowIndex].beginTime,
      )
      // æž„造异常时间后的区间数据请求参数
      let paramsAfter = this.requestGetParms(
        this.displayData[this.selectedRowIndex].devId,
        beforeAndAfterTime[1],
        beforeAndAfterTime[2],
      )
      // æŠ˜çº¿å›¾åŠ è½½ä¸­æ•ˆæžœ
      this.chartLoading = true
      // è¯·æ±‚前半段
      axiosInstanceInstance.get('/fume/history', { params: paramsBefore }).then((result1) => {
        this.beforeData = result1.data.data
        // è¯·æ±‚后半段
        axiosInstanceInstance.get('/fume/history', { params: paramsAfter }).then((result2) => {
          this.afterData = result2.data.data
          //保存异常区间的值
          let tempArr = []
          // ä¿å­˜å¼‚常区间前后的值
          let before = []
          let after = []
          // åˆ¤æ–­æ˜¯å¦æ˜¯ä¾›ç”µå¼‚常或掉线
          if (this.rowExceptionType === '1' || this.rowExceptionType === '2') {
            // é‡æž„表格 ç¼ºå¤±å¼‚常数据自动填充
            this.setExceptionData()
            //相差几个10分钟
            const TenMinuteNum = this.diffTenMinutesNum(exceptionBeginTime, exceptionEndTime)
            //用null填充中异常无数据的时间
            for (let i = 0; i < TenMinuteNum; i++) {
              tempArr.push(null)
            }
            before = this.shallowCopyList(this.beforeData)
            after = this.shallowCopyList(this.afterData)
            // after = this.afterData
          }
          // è¶…æ ‡
          else {
            let beforeTemp = this.removeLastItemOfBeforeData(this.beforeData)
            // å‰åŽåŒºé—´åªæ˜¾ç¤ºè·ç¦»è¶…标区间时间最近的浓度小于1的时间点
            for (let i = beforeTemp.length - 1; i >= 0; i--) {
              if (beforeTemp[i].mvFumeConcentration2 >= 1) {
                break
              }
              if (beforeTemp[i].mvFumeConcentration2 < 1) {
                before.unshift(this.beforeData[i])
              }
            }
            for (let i = 0; i < this.afterData.length; i++) {
              if (this.afterData[i].mvFumeConcentration2 >= 1) {
                break
              }
              if (this.afterData[i].mvFumeConcentration2 < 1) {
                after.unshift(this.afterData[i])
              }
            }
            tempArr = this.shallowCopyList(this.exceedingData)
          }
          // å°†å‰åŽåŒºé—´æ•°æ® ä¸Ž å¼‚常区间数据 åˆå¹¶
          this.allExceptionTimeData = [...before, ...tempArr, ...after]
          // x轴日期时间
          let dateList
          // yè½´ è¶…标油烟浓度
          let fumeExceeding
          let timeAndValue
          // ä»Žæ·»åŠ äº†é¦–ä½åŒºé—´çš„å¼€å§‹å’Œç»“æŸæ—¶é—´è¿›è¡ŒéåŽ† ä¿è¯æ—¶é—´ä»¥10分钟为间隔
          timeAndValue = this.keepContinuousByEachTenMinutes(
            beforeAndAfterTime[0],
            beforeAndAfterTime[2],
            this.allExceptionTimeData,
          )
          dateList = timeAndValue['xAxis']
          fumeExceeding = timeAndValue['yAxis']
          // æå–异常起始时间点在整个区间内的数据索引
          let startIndex = dateList.findIndex((item) => item === exceptionBeginTime)
          let endIndex = dateList.findIndex((item) => item === exceptionEndTime)
          // ä¾›ç”µå¼‚常和掉线情况 è¶…标情况
          if (this.rowExceptionType === '1' || this.rowExceptionType === '2') {
            this.setOption(
              dateList,
              fumeExceeding,
              1,
              exceptionBeginTime,
              exceptionEndTime,
              startIndex,
              endIndex,
            )
          } else {
            // è¶…标情况
            this.setOption(
              dateList,
              fumeExceeding,
              0,
              exceptionBeginTime,
              exceptionEndTime,
              startIndex,
              endIndex,
            )
          }
          this.chartLoading = false
        })
      })
    },
    getDeviceInfo() {
      // çº§è”下拉框数据 ä»ŽæŽ¥å£ä¸­åŠ¨æ€èŽ·å–
      axiosInstanceInstance.get('/fume/device').then((result) => {
        this.deviceInfo = result.data.data
        // èŽ·å–åˆ°æ€»çš„åº—é“ºæ•°é‡
        this.shopsTotal = result.data.data.length
        this.deviceInfo.forEach((item) => {
          this.optionsShop[this.optionsShop.length] = {
            value: item.diName,
            label: item.diName,
            children: [
              {
                value: item.diCode,
                label: item.diCode,
              },
            ],
          }
        })
      })
    },
    exportDom() {
      // å¯¼å‡ºä¸ºExcel文件
      const fields = ['devId', 'exceptionType', 'region', 'beginTime', 'endTime']
      const itemsFormatted = this.abnormalData.map((item) => {
        const newItem = {}
        fields.forEach((field) => {
          newItem[field] = item[field]
        })
        return newItem
      })
      // åˆ›å»ºxlsx对象
      const xls = XLSX.utils.json_to_sheet(itemsFormatted)
      // ç¼–辑表头行       ä¿®æ”¹è¡¨å¤´
      xls['A1'].v = '设备编号'
      xls['B1'].v = '异常类型'
      xls['C1'].v = '地区'
      xls['D1'].v = '开始时间'
      xls['E1'].v = '结束时间'
      // åˆ›å»ºworkbook,并把sheet添加进去
      const wb = XLSX.utils.book_new()
      XLSX.utils.book_append_sheet(wb, xls, 'Sheet1')
      // å°†workbook转为二进制xlsx文件并下载
      XLSX.writeFile(wb, '分析数据.xlsx')
    },
    getAbnormalDataByClick(val) {
      this.abnormalData = val
      this.total = this.abnormalData.length
      // é»˜è®¤æ˜¾ç¤ºç¬¬ä¸€é¡µ
      this.handleCurrentChange(1)
    },
    // æ ¹æ®å¼‚常类型返回店铺名称和设备编号
    // æ¯”如油烟超标对应的所有店铺名称和设备编号(已去除重复的店铺名)
    getShopNames() {
      axiosInstanceInstance
        .get('/fume/shopname', {
          params: {
            exceptionType: '0',
            beginTime: this.beginTime,
            endTime: this.endTime,
          },
        })
        .then((result) => {
          this.exception0 = result.data.data
        })
      axiosInstanceInstance
        .get('/fume/shopname', {
          params: {
            exceptionType: '1',
            beginTime: this.beginTime,
            endTime: this.endTime,
          },
        })
        .then((result) => {
          this.exception1 = result.data.data
        })
      axiosInstanceInstance
        .get('/fume/shopname', {
          params: {
            exceptionType: '2',
            beginTime: this.beginTime,
            endTime: this.endTime,
          },
        })
        .then((result) => {
          this.exception2 = result.data.data
        })
      /* å¼‚常数量 */
      axiosInstanceInstance
        .get('/fume/exceptionnum', {
          params: {
            exceptionType: '0',
            beginTime: this.beginTime,
            endTime: this.endTime,
          },
        })
        .then((result) => {
          this.exception0Num = result.data.data
        })
      axiosInstanceInstance
        .get('/fume/exceptionnum', {
          params: {
            exceptionType: '1',
            beginTime: this.beginTime,
            endTime: this.endTime,
          },
        })
        .then((result) => {
          this.exception1Num = result.data.data
        })
      axiosInstanceInstance
        .get('/fume/exceptionnum', {
          params: {
            exceptionType: '2',
            beginTime: this.beginTime,
            endTime: this.endTime,
          },
        })
        .then((result) => {
          this.exception2Num = result.data.data
        })
    },
    // é¡µé¢åŠ è½½æ—¶é»˜è®¤å±•ç¤º7天异常表数据
    getRecentSevenDays() {
      // ç»™çº§è”选择器设置默认的选择项
      this.devId = ['付小姐在成都', 'qinshi_31010320210010']
      let params = {}
      params['beginTime'] = this.beginTime
      params['endTime'] = this.endTime
      axiosInstanceInstance.get('/fume/abnormalthree', { params: params }).then((response) => {
        if (response.data.data.length == 0) {
          ElMessage('该时段无数据')
          return
        }
        // ä¿å­˜è¿”回的
        this.abnormalData = response.data.data
        // åˆ†é¡µ
        this.total = this.abnormalData.length
        // é»˜è®¤æ˜¾ç¤ºç¬¬ä¸€é¡µ
        this.handleCurrentChange(1)
        this.loading = false
      })
    },
  },
}
</script>
<style scoped>
.header-container {
  display: flex;
  margin-left: 20px;
  /* flex-wrap: wrap;
    align-items: center; */
}
.ellipsis {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.iconExcel {
  font-size: 25px;
  margin-left: 20px;
  bottom: -6px;
}
/* å¯é¼ æ ‡ç®­å¤´å˜ä¸ºå¯ç‚¹å‡»çŠ¶æ€ */
.clickable {
  cursor: pointer;
}
.card-header {
  margin: 0;
}
body {
  margin: 0;
}
.exception-divider-rowline {
  margin: 10px 0px;
}
/* å¼‚常分析数据与按钮 */
.exception-container {
  display: flex;
}
.example-showcase .el-loading-mask {
  z-index: 9;
}
.scrollbar-demo-item {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 20px;
  margin: 10px;
  text-align: center;
  border-radius: 4px;
  background: var(--el-color-primary-light-9);
  color: var(--el-color-primary);
}
.collapse-header {
  margin-left: 5px;
  font-size: 18px;
}
.collapse-header-text {
  margin-top: 5px;
  font-size: 14px;
  color: gray;
}
.box-card-label {
  font-size: 14px;
  white-space: nowrap;
}
.right-text {
  /* float :right; */
  /* text-align: right; */
  margin-left: 80px;
}
:deep().el-card {
  border-radius: 9px;
}
/* â€˜æŸ¥çœ‹è¯¦æƒ…’ çš„弹出框高度调整 */
:deep().el-dialog {
  height: 98%;
  /* ä¸å‡ºçŽ°æ»šåŠ¨æ¡ */
  overflow-y: hidden;
  border-radius: 9px;
}
.table-page {
  margin-left: 20px;
}
.table-text {
  font-size: 18px;
  margin: 5px 0px 10px 20px;
}
.text-blank {
  margin-right: 10px;
  color: #000000;
}
/* åº—铺名选择文本 */
.describe-info {
  margin-top: 5px;
  font-weight: bold;
  white-space: nowrap;
}
/* æ—¶é—´é€‰æ‹©æ–‡æœ¬ */
.describe-time-text {
  margin-left: 30px;
  margin-top: 5px;
  font-weight: bold;
}
/* å¼‚常表格下标签中的数组 */
.table-line-num {
  font-weight: bold;
  color: black;
}
.button_info.el-button_inner {
  text-align: left;
}
.el-collapse {
  margin-left: 20px;
}
:deep().el-collapse .el-collapse-item__content {
  padding-bottom: 0px;
}
.box-card {
  height: 190px;
}
.sub-box-card {
  height: 100px;
  border: 0px;
}
.mx-1 {
  margin-bottom: 0px;
}
.dialog-button-position {
  display: flex;
  justify-content: right;
  margin-bottom: 10px;
}
:deep().el-table__row .exceeding-row {
  background-color: #f53f3f;
}
:deep().el-table__row .abnormal-power-supply {
  background-color: #fdf4bf;
}
:deep().el-table__row .disconnect {
  background-color: #f7ba1e;
}
.el-table {
  color: #000000;
}
/* è¡¨æ ¼ä¸­çš„æŒ‰é’®å®½åº¦é“ºæ»¡ */
.table-button {
  width: 100%;
}
</style>
src/views/monitor/DataHistory.vue
@@ -1,6 +1,7 @@
<!-- åŽ†å²æ•°æ® -->
<script>
import { defineAsyncComponent } from 'vue'
import { ElMessage } from 'element-plus'
import * as XLSX from 'xlsx/xlsx.mjs'
import dayjs from 'dayjs'
import axiosInstanceInstance from '@/utils/request.js'
@@ -440,9 +441,8 @@
<template>
  <!-- solid #000000 -->
  <div style="margin: 20px; padding: 10px; border: 1px; height: 620px">
    <h1 style="margin-bottom: 20px; display: flex; white-space: nowrap">油烟历史数据</h1>
    <div style="display: flex">
      <span class="palce-text"><span class="palce-text-area">区域:</span>静安区 </span>
      <!-- <span class="palce-text"><span class="palce-text-area">区域:</span>静安区 </span> -->
      <!-- åº—铺名  çº§è” -->
      <!-- <el-cascader v-model="devId" :options="optionsShop" :props="{ expandTrigger: 'hover' }" placeholder="请选择店铺名"
@@ -550,51 +550,15 @@
        :data="displayData"
        style="width: 100%; margin-top: 25px"
        height="500px"
        table-layout="auto"
        table-layout="fixed"
        :show-overflow-tooltip="true"
      >
        <el-table-column fixed prop="mvStatCode" label="设备编号">
          <template #default="{ row }">
            <el-tooltip effect="dark" :content="row.mvStatCode">
              <div class="cell ellipsis">{{ row.mvStatCode }}</div>
            </el-tooltip>
          </template>
        </el-table-column>
        <el-table-column prop="diName" label="店铺名称">
          <template #default="{ row }">
            <el-tooltip effect="dark" :content="row.diName">
              <div class="cell ellipsis">{{ row.diName }}</div>
            </el-tooltip>
          </template>
        </el-table-column>
        <el-table-column prop="mvDataTime" label="采集时间">
          <template #default="{ row }">
            <el-tooltip effect="dark" :content="row.mvDataTime">
              <div class="cell ellipsis">{{ row.mvDataTime }}</div>
            </el-tooltip>
          </template>
        </el-table-column>
        <el-table-column prop="mvFanElectricity" label="风机电流(A)">
          <template #default="{ row }">
            <el-tooltip effect="dark" :content="row.mvFanElectricity">
              <div class="cell ellipsis">{{ row.mvFanElectricity }}</div>
            </el-tooltip>
          </template>
        </el-table-column>
        <el-table-column prop="mvPurifierElectricity" label="净化器电流(A)">
          <template #default="{ row }">
            <el-tooltip effect="dark" :content="row.mvPurifierElectricity">
              <div class="cell ellipsis">{{ row.mvPurifierElectricity }}</div>
            </el-tooltip>
          </template>
        </el-table-column>
        <el-table-column prop="mvFumeConcentration2" label="油烟浓度(mg/m³)">
          <template #default="{ row }">
            <el-tooltip effect="dark" :content="row.mvFumeConcentration2">
              <div class="cell ellipsis">{{ row.mvFumeConcentration2 }}</div>
            </el-tooltip>
          </template>
        </el-table-column>
        <el-table-column fixed prop="mvStatCode" label="设备编号"> </el-table-column>
        <el-table-column prop="diName" label="店铺名称"> </el-table-column>
        <el-table-column prop="mvDataTime" label="采集时间"> </el-table-column>
        <el-table-column prop="mvFanElectricity" label="风机电流(A)"> </el-table-column>
        <el-table-column prop="mvPurifierElectricity" label="净化器电流(A)"> </el-table-column>
        <el-table-column prop="mvFumeConcentration2" label="油烟浓度(mg/m³)"> </el-table-column>
      </el-table>
      <el-pagination
        background