餐饮油烟智能监测与监管一体化平台
riku
2026-03-17 b1a0d701cf898c8b7812e66a808a1c91f2bae6cc
2026.3.17
已修改13个文件
已删除1个文件
已添加7个文件
3266 ■■■■ 文件已修改
components.d.ts 42 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/徐汇区餐饮监管简报(天钥桥)-2023年8月(1).doc 补丁 | 查看 | 原始文档 | blame | 历史
public/徐汇区餐饮监管简报(天钥桥)-2023年8月(1).docx 补丁 | 查看 | 原始文档 | blame | 历史
src/api/fytz/creditApi.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/loginPageBg.png 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SearchBar.vue 68 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bg-task/FYBgTaskCard.vue 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bg-task/FYBgTaskDialog.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bg-task/FYBgTaskItem.vue 218 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/core/AppHeader.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/search-option/CompQuickSet.vue 120 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/constants/menu.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/enum/scene copy.js 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/stores/bgtaskStore.js 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/LoginPage.vue 158 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/evalution/components/precheck/CompPreCheck.vue 44 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/analysis/huanxincode/HuanxinCodeManage.vue 466 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/MonitorControl.vue 1418 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/report/ReportManage.vue 180 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/SystemManage.vue 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components.d.ts
@@ -21,18 +21,14 @@
    CompQuickSet: typeof import('./src/components/search-option/CompQuickSet.vue')['default']
    DeviceStatus: typeof import('./src/components/monitor/DeviceStatus.vue')['default']
    DistrictRanking: typeof import('./src/components/monitor/DistrictRanking.vue')['default']
    ElAffix: typeof import('element-plus/es')['ElAffix']
    ElAside: typeof import('element-plus/es')['ElAside']
    ElBadge: typeof import('element-plus/es')['ElBadge']
    ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
    ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
    ElButton: typeof import('element-plus/es')['ElButton']
    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']
@@ -45,9 +41,6 @@
    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']
@@ -57,23 +50,17 @@
    ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
    ElInput: typeof import('element-plus/es')['ElInput']
    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
    ElLink: typeof import('element-plus/es')['ElLink']
    ElMain: typeof import('element-plus/es')['ElMain']
    ElMenu: typeof import('element-plus/es')['ElMenu']
    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']
    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
    ElRow: typeof import('element-plus/es')['ElRow']
    ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
    ElSelect: typeof import('element-plus/es')['ElSelect']
    ElSpace: typeof import('element-plus/es')['ElSpace']
    ElStatistic: typeof import('element-plus/es')['ElStatistic']
    ElStep: typeof import('element-plus/es')['ElStep']
    ElSteps: typeof import('element-plus/es')['ElSteps']
    ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
@@ -88,6 +75,9 @@
    ElTree: typeof import('element-plus/es')['ElTree']
    ElUpload: typeof import('element-plus/es')['ElUpload']
    FormCol: typeof import('./src/components/layout/FormCol.vue')['default']
    FYBgTaskCard: typeof import('./src/components/bg-task/FYBgTaskCard.vue')['default']
    FYBgTaskDialog: typeof import('./src/components/bg-task/FYBgTaskDialog.vue')['default']
    FYBgTaskItem: typeof import('./src/components/bg-task/FYBgTaskItem.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']
@@ -110,11 +100,6 @@
    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']
@@ -146,18 +131,14 @@
  const CompQuickSet: typeof import('./src/components/search-option/CompQuickSet.vue')['default']
  const DeviceStatus: typeof import('./src/components/monitor/DeviceStatus.vue')['default']
  const DistrictRanking: typeof import('./src/components/monitor/DistrictRanking.vue')['default']
  const ElAffix: typeof import('element-plus/es')['ElAffix']
  const ElAside: typeof import('element-plus/es')['ElAside']
  const ElBadge: typeof import('element-plus/es')['ElBadge']
  const ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
  const ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
  const ElButton: typeof import('element-plus/es')['ElButton']
  const 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']
@@ -170,9 +151,6 @@
  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']
@@ -182,23 +160,17 @@
  const ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
  const ElInput: typeof import('element-plus/es')['ElInput']
  const ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
  const ElLink: typeof import('element-plus/es')['ElLink']
  const ElMain: typeof import('element-plus/es')['ElMain']
  const ElMenu: typeof import('element-plus/es')['ElMenu']
  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']
  const ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
  const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
  const ElRow: typeof import('element-plus/es')['ElRow']
  const ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
  const ElSelect: typeof import('element-plus/es')['ElSelect']
  const ElSpace: typeof import('element-plus/es')['ElSpace']
  const ElStatistic: typeof import('element-plus/es')['ElStatistic']
  const ElStep: typeof import('element-plus/es')['ElStep']
  const ElSteps: typeof import('element-plus/es')['ElSteps']
  const ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
@@ -213,6 +185,9 @@
  const ElTree: typeof import('element-plus/es')['ElTree']
  const ElUpload: typeof import('element-plus/es')['ElUpload']
  const FormCol: typeof import('./src/components/layout/FormCol.vue')['default']
  const FYBgTaskCard: typeof import('./src/components/bg-task/FYBgTaskCard.vue')['default']
  const FYBgTaskDialog: typeof import('./src/components/bg-task/FYBgTaskDialog.vue')['default']
  const FYBgTaskItem: typeof import('./src/components/bg-task/FYBgTaskItem.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']
@@ -235,11 +210,6 @@
  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']
public/Ðì»ãÇø²ÍÒû¼à¹Ü¼ò±¨£¨ÌìÔ¿ÇÅ£©-2023Äê8ÔÂ(1).doc
Binary files differ
public/Ðì»ãÇø²ÍÒû¼à¹Ü¼ò±¨£¨ÌìÔ¿ÇÅ£©-2023Äê8ÔÂ(1).docx
Binary files differ
src/api/fytz/creditApi.js
@@ -5,18 +5,24 @@
 * ä¿¡ç”¨è¯„ä¼°API接口
 */
export default {
  /**
   * ä¸‹è½½ç”¨æˆ·çŽ¯ä¿¡ç 
   * @param {*} userId
   * @param {*} userName
   */
  downloadCode(userId, userName) {
  fetchCodeUrl(userId, userName) {
    return $fytz
      .get(`credit/ecCode/download?userId=${userId}`, { responseType: 'blob' })
      .then((res) => {
        const name = res.headers.get('fileName') || userName
        const fileName = Base64.decode(name)
        const url = window.URL.createObjectURL(res.data)
        return { fileName, url }
      })
  },
  /**
   * ä¸‹è½½ç”¨æˆ·çŽ¯ä¿¡ç 
   * @param {*} userId
   * @param {*} userName
   */
  downloadCode(userId, userName) {
    return this.fetchCodeUrl(userId, userName).then((res) => {
      const { fileName, url } = res
        const link = document.createElement('a')
        link.href = url
        link.setAttribute('download', fileName)
src/assets/loginPageBg.png
src/components/SearchBar.vue
@@ -4,21 +4,13 @@
      <el-form :inline="true" :model="formSearch">
        <el-form-item label="总任务">
          <!-- <el-input v-model="formSearch.topTaskId" placeholder="总任务" /> -->
          <el-select
            v-model="formSearch.topTaskId"
            placeholder="总任务"
            style="width: 260px"
          >
            <el-option
              v-for="s in topTasks"
              :key="s.value"
              :label="s.label"
              :value="s.value"
            />
          <el-select v-model="formSearch.topTaskId" placeholder="总任务" style="width: 260px">
            <el-option v-for="s in topTasks" :key="s.value" :label="s.label" :value="s.value" />
          </el-select>
        </el-form-item>
        <FYOptionScene
          :allOption="false"
          :init-value="false"
          :type="2"
          v-model:value="formSearch.scenetype"
        ></FYOptionScene>
@@ -36,19 +28,20 @@
</template>
<script>
import taskApi from '@/api/fysp/taskApi';
import taskApi from '@/api/fysp/taskApi'
import dayjs from 'dayjs'
export default {
  emits: ['onSubmit'],
  props: {
    btnShow: {
      type: Boolean,
      default: true
      default: true,
    },
    init: {
      type: Boolean,
      default: true
    }
      default: true,
    },
  },
  data() {
@@ -56,48 +49,53 @@
      topTasks: [],
      formSearch: {
        topTaskId: '',
        scenetype: ''
        scenetype: {
          label: '餐饮',
          value: '5',
        },
      },
      }
    };
  },
  methods: {
    //获取查询条件
    getOptions() {
      taskApi.getTopTask().then((res) => {
        const list = res.map((r) => {
        const list = res
          .filter((e) => {
            return e.districtname == '徐汇区' && dayjs(e.starttime).isBefore(dayjs('2023-12-31'))
          })
          .map((r) => {
          return {
            value: r.tguid,
            label: r.name,
            data: r
          };
        });
        this.topTasks = list;
        this.formSearch.topTaskId = list[0].value;
        if (this.init) {
          this.onSubmit();
              data: r,
        }
      });
          })
        this.topTasks = list
        this.formSearch.topTaskId = list[0].value
        if (this.init) {
          this.onSubmit()
        }
      })
    },
    //查询子任务统计信息
    onSubmit() {
      const task = this.topTasks.find(
        (t) => t.data.tguid == this.formSearch.topTaskId
      );
      const task = this.topTasks.find((t) => t.data.tguid == this.formSearch.topTaskId)
      const param = {
        topTask: task ? task.data : {},
        sceneTypeId: this.formSearch.scenetype.value,
        sceneTypeName: this.formSearch.scenetype.label,
      };
      }
      // console.log(param);
      this.$emit('onSubmit', param);
    }
      this.$emit('onSubmit', param)
    },
  },
  mounted() {
    this.getOptions();
    this.getOptions()
  },
  expose: ['onSubmit']
};
  expose: ['onSubmit'],
}
</script>
<style scoped>
src/components/bg-task/FYBgTaskCard.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,175 @@
<template>
  <el-card shadow="never" :body-style="{ padding: 0 }">
    <template #header>
      <el-row justify="space-between">
        <div>
          <div><el-text tag="b" size="large">后台任务</el-text></div>
          <el-text size="small" type="info"
            >显示当前正在进行的后台耗时任务状态</el-text
          >
        </div>
        <el-button
          icon="Refresh"
          type="primary"
          size="default"
          :loading="loading"
          @click="fetchTask"
          >刷新任务</el-button
        >
      </el-row>
      <!-- <el-row>
        <el-button type="default" size="default" @click="newTestTask">新增测试任务</el-button>
        <el-button type="default" size="default" @click="startNewTestTask"
          >新建并运行一个测试任务</el-button
        >
        <el-button type="default" size="default" @click="shutDownTask"
          >强制关闭所有测试任务</el-button
        >
      </el-row> -->
    </template>
    <el-scrollbar height="70vh" class="scrollbar">
      <template v-for="(v, i) in taskList" :key="i">
        <FYBgTaskItem
          :model="v"
          :index="i"
          @start="startTask"
          @shutDown="shutDownTask"
          @remove="removeTask"
          @gotoResult="gotoResult"
        ></FYBgTaskItem>
      </template>
    </el-scrollbar>
  </el-card>
</template>
<script>
/**
 * è‡ªåŠ¨è¯„ä¼°ä»»åŠ¡ç®¡ç†
 */
import { useFetchData } from '@/composables/fetchData';
import bgtaskApi from '@/api/fysp/bgtaskApi';
import { enumBgTask, BG_TASK_TYPE, BG_TASK_STATUS } from '@/enum/bgTask';
import { useBgtaskStore } from '@/stores/bgtaskStore';
export default {
  setup() {
    const { loading, fetchData } = useFetchData();
    const { registerOnFetchTask } = useBgtaskStore();
    return { loading, fetchData, registerOnFetchTask };
  },
  props: {
    modelValue: Number
  },
  emits: ['update:modelValue'],
  data() {
    return {
      taskList: [],
      taskIndex: 0
    };
  },
  watch: {
    taskList: {
      handler(nV) {
        let count = 0;
        for (const e of nV) {
          if (e.status == BG_TASK_STATUS.RUNNING.name) {
            count++;
          }
        }
        this.$emit('update:modelValue', count);
      },
      deep: true
    }
  },
  methods: {
    addTask() {},
    newTestTask() {
      this.fetchData((page, pageSize) => {
        return bgtaskApi
          .newTestTask(`Test-Task-${++this.taskIndex}`)
          .then((res) => {
            this.taskList.push(res.data);
          });
      });
    },
    startNewTestTask() {
      this.fetchData((page, pageSize) => {
        return bgtaskApi
          .startNewTestTask(`Test-Task-${++this.taskIndex}`)
          .then((res) => {
            this.taskList.push(res.data);
          });
      });
    },
    _getParam(taskStatus) {
      return {
        type: taskStatus.type,
        id: taskStatus.id
      };
    },
    fetchTask() {
      this.fetchData((page, pageSize) => {
        return bgtaskApi
          .fetchTaskStatus({
            // type: BG_TASK_TYPE.AUTO_SCORE.name
          })
          .then((res) => {
            this.taskList = res.data;
          });
      });
    },
    startTask(index, callback) {
      this.fetchData((page, pageSize) => {
        const param = this._getParam(this.taskList[index]);
        return bgtaskApi.startTask(param).then((res) => {
          this.taskList[index] = res.data;
          callback(true);
        });
      });
    },
    shutDownTask(index, callback) {
      this.fetchData((page, pageSize) => {
        const param = this._getParam(this.taskList[index]);
        return bgtaskApi.shutDownTask(param).then((res) => {
          if (index && res.data && res.data.length == 1) {
            this.taskList[index] = res.data[0];
          } else {
            res.data.forEach((e) => {
              let v = this.taskList.find((value) => {
                return value.id == e.id;
              });
              const i = this.taskList.indexOf(v);
              this.taskList[i] = e;
            });
          }
          callback(true);
        });
      });
    },
    removeTask(index, callback) {
      this.fetchData((page, pageSize) => {
        const param = this._getParam(this.taskList[index]);
        return bgtaskApi.removeTask(param).then((res) => {
          if (res.data) {
            this.taskList.splice(index, 1);
            callback(true);
          }
        });
      });
    },
    gotoResult(index) {}
  },
  mounted() {
    this.fetchTask();
    this.registerOnFetchTask(this.fetchTask);
    // setInterval(() => {
    //   this.fetchTask();
    // }, 10000);
  }
};
</script>
<style scoped>
.scrollbar {
  padding: 8px;
}
</style>
src/components/bg-task/FYBgTaskDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
<template>
  <el-popover
    placement="bottom"
    :width="600"
    trigger="click"
    v-model:visible="bgtaskStore.dialogShow"
  >
    <template #reference>
      <el-badge :value="runningNum" :hidden="runningNum == 0" class="m-r-16">
        <el-button circle>
          <el-icon v-if="runningNum > 0" color="red" class="is-loading"
            ><Clock
          /></el-icon>
          <el-icon v-else><Clock /></el-icon>
        </el-button>
      </el-badge>
    </template>
    <!-- <el-button circle icon="Close" type="danger" @click=</el-button> -->
    <FYBgTaskCard v-model="runningNum"></FYBgTaskCard>
  </el-popover>
</template>
<script setup>
import { ref } from 'vue';
import { useBgtaskStore } from '@/stores/bgtaskStore';
const bgtaskStore = useBgtaskStore();
const runningNum = ref(0);
</script>
<style scoped></style>
src/components/bg-task/FYBgTaskItem.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,218 @@
<template>
  <el-card class="m-b-8" shadow="always" :body-style="{ padding: '8px' }">
    <el-row>
      <el-col :span="4">
        <div class="status-btn">
          <el-icon v-if="waiting" color="var(--el-color-info)" :size="50"><VideoPlay /></el-icon>
          <el-icon v-else-if="running" color="var(--el-color-primary)" :size="50" class="is-loading"
            ><Loading
          /></el-icon>
          <el-icon v-else-if="success" color="var(--el-color-success)" :size="50"
            ><CircleCheck
          /></el-icon>
          <el-icon v-else-if="fail" color="var(--el-color-error)" :size="50"
            ><CircleClose
          /></el-icon>
          <el-icon v-else color="var(--el-color-warning)" :size="50"><Warning /></el-icon>
          <el-text type="info" size="small" style="position: absolute; bottom: 0">{{
            nameToLabel(model.status)
          }}</el-text>
        </div>
      </el-col>
      <el-col :span="20" class="p-l-8">
        <el-row justify="space-between">
          <el-text class="m-l-4px w-300px" tag="b" size="large" truncated>{{ model.name }}</el-text>
          <el-tag>{{ nameToLabel(model.type) }}</el-tag>
        </el-row>
        <el-row class="p-v-8" align="bottom">
          <el-col :span="12">
            <span class="timer">{{ time }}</span>
            <el-text type="info" size="small" tag="div">运行时长</el-text>
          </el-col>
          <el-col :span="12">
            <el-text type="default" size="default" tag="div"
              >开始:{{ $fm.formatYMDHMS(model.startTime) }}</el-text
            >
            <el-text type="default" size="default" tag="div"
              >结束:{{ $fm.formatYMDHMS(model.endTime) }}</el-text
            >
          </el-col>
        </el-row>
        <el-row justify="end" align="bottom">
          <!-- <span class="f-s color-i">ID:{{ model.id }}</span> -->
          <el-row>
            <FYReconfrimButton v-if="waiting" @confirm="startTask" v-model="startConfirm">
              <el-button
                plain
                icon="VideoPlay"
                type="primary"
                size="small"
                :loading="false"
                @click="startConfirm = true"
                >开始任务</el-button
              >
            </FYReconfrimButton>
            <FYReconfrimButton v-if="running" @confirm="stopTask" v-model="stopConfirm">
              <el-button
                icon="VideoPause"
                plain
                type="danger"
                size="small"
                :loading="false"
                @click="stopConfirm = true"
                >强制结束</el-button
              >
            </FYReconfrimButton>
            <FYReconfrimButton v-if="!running" @confirm="removeTask" v-model="removeConfirm">
              <el-button
                icon="Delete"
                plain
                type="danger"
                size="small"
                :loading="false"
                @click="removeConfirm = true"
                >移除任务</el-button
              >
            </FYReconfrimButton>
            <template v-if="success">
              <el-button
                v-if="btnType"
                plain
                type="success"
                size="small"
                :loading="false"
                @click="download"
                >下载文件<el-icon class="m-l-4"><Right /></el-icon
              ></el-button>
              <el-button
                v-else
                plain
                type="success"
                size="small"
                :loading="false"
                @click="gotoResult"
                >查看结果<el-icon class="m-l-4"><Right /></el-icon
              ></el-button>
            </template>
          </el-row>
        </el-row>
      </el-col>
    </el-row>
  </el-card>
</template>
<script>
import { nTlBgTask, BG_TASK_STATUS, BG_TASK_TYPE } from '@/enum/bgTask';
import { useTimer } from '@/composables/timer';
import downloadApi from '@/api/fysp/downloadApi';
// import { useTimer } from '@/composables/timer2';
export default {
  setup() {
    const { time, startTimer, pauseTimer, stopTimer, count } = useTimer();
    return { time, startTimer, pauseTimer, stopTimer, count };
  },
  props: {
    model: Object,
    index: Number
  },
  emits: ['start', 'shutDown', 'remove', 'gotoResult'],
  data() {
    return {
      startConfirm: false,
      stopConfirm: false,
      removeConfirm: false
    };
  },
  watch: {
    'model.status': {
      handler(nV) {
        switch (nV) {
          case BG_TASK_STATUS.WAITING.name:
            this.stopTimer();
            break;
          case BG_TASK_STATUS.RUNNING.name:
            this.startTimer();
            break;
          case BG_TASK_STATUS.SUCCESS.name:
          case BG_TASK_STATUS.FAIL.name:
          case BG_TASK_STATUS.SHUTDOWN.name:
            this.pauseTimer();
            break;
          default:
            this.stopTimer();
            break;
        }
        this.count = this.model.runTime;
      },
      immediate: true
    }
  },
  computed: {
    btnType() {
      return this.model.type == BG_TASK_TYPE.DOCUMENT.name;
    },
    waiting() {
      return this.model.status == BG_TASK_STATUS.WAITING.name;
    },
    running() {
      return this.model.status == BG_TASK_STATUS.RUNNING.name;
    },
    success() {
      return this.model.status == BG_TASK_STATUS.SUCCESS.name;
    },
    fail() {
      return this.model.status == BG_TASK_STATUS.FAIL.name;
    },
    shutdown() {
      return this.model.status == BG_TASK_STATUS.SHUTDOWN.name;
    }
  },
  methods: {
    nameToLabel(name) {
      const t = nTlBgTask(name);
      return t.label;
    },
    startTask() {
      this.$emit('start', this.index, (res) => {
        if (res) {
          this.startTimer();
        }
      });
    },
    stopTask() {
      this.$emit('shutDown', this.index, (res) => {
        if (res) {
          this.stopTimer();
        }
      });
    },
    removeTask() {
      this.$emit('remove', this.index, (res) => {
        if (res) {
          // this.stopTimer();
        }
      });
    },
    gotoResult() {
      this.$emit('gotoResult', this.index);
    },
    download() {
      downloadApi.downloadFile(this.model.extra);
    }
  }
};
</script>
<style scoped>
.status-btn {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  border: var(--el-border);
  border-radius: var(--el-border-radius-base);
}
.timer {
  font-size: 30px;
}
</style>
src/components/core/AppHeader.vue
@@ -9,6 +9,7 @@
      </el-space>
    </el-col>
    <el-col :span="12" class="logout">
      <FYBgTaskDialog></FYBgTaskDialog>
      <el-button icon="SwitchButton">退出登录</el-button>
    </el-col>
  </el-row>
src/components/search-option/CompQuickSet.vue
@@ -19,19 +19,19 @@
      type: Array,
      default: () => {
        return [
          {
            name: '静安工地',
            locations: {
              pCode: '31',
              pName: '上海市',
              cCode: '3100',
              cName: '上海市',
              dCode: '310106',
              dName: '静安区'
            },
            scenetype: { label: '工地', value: '1' },
            sourceType: 2
          },
          // {
          //   name: '静安工地',
          //   locations: {
          //     pCode: '31',
          //     pName: '上海市',
          //     cCode: '3100',
          //     cName: '上海市',
          //     dCode: '310106',
          //     dName: '静安区'
          //   },
          //   scenetype: { label: '工地', value: '1' },
          //   sourceType: 2
          // },
          {
            name: '徐汇餐饮',
            locations: {
@@ -40,50 +40,50 @@
              cCode: '3100',
              cName: '上海市',
              dCode: '310104',
              dName: '徐汇区'
              dName: '徐汇区',
            },
            scenetype: { label: '餐饮', value: '5' },
            sourceType: 2
            sourceType: 2,
          },
          {
            name: '金山工地',
            locations: {
              pCode: '31',
              pName: '上海市',
              cCode: '3100',
              cName: '上海市',
              dCode: '310116',
              dName: '金山区'
            },
            scenetype: { label: '工地', value: '1' },
            sourceType: 2
          },
          {
            name: '金山码头',
            locations: {
              pCode: '31',
              pName: '上海市',
              cCode: '3100',
              cName: '上海市',
              dCode: '310116',
              dName: '金山区'
            },
            scenetype: { label: '码头', value: '2' },
            sourceType: 2
          },
          {
            name: '金山搅拌站',
            locations: {
              pCode: '31',
              pName: '上海市',
              cCode: '3100',
              cName: '上海市',
              dCode: '310116',
              dName: '金山区'
            },
            scenetype: { label: '搅拌站', value: '3' },
            sourceType: 2
          }
          // {
          //   name: '金山工地',
          //   locations: {
          //     pCode: '31',
          //     pName: '上海市',
          //     cCode: '3100',
          //     cName: '上海市',
          //     dCode: '310116',
          //     dName: '金山区'
          //   },
          //   scenetype: { label: '工地', value: '1' },
          //   sourceType: 2
          // },
          // {
          //   name: '金山码头',
          //   locations: {
          //     pCode: '31',
          //     pName: '上海市',
          //     cCode: '3100',
          //     cName: '上海市',
          //     dCode: '310116',
          //     dName: '金山区'
          //   },
          //   scenetype: { label: '码头', value: '2' },
          //   sourceType: 2
          // },
          // {
          //   name: '金山搅拌站',
          //   locations: {
          //     pCode: '31',
          //     pName: '上海市',
          //     cCode: '3100',
          //     cName: '上海市',
          //     dCode: '310116',
          //     dName: '金山区'
          //   },
          //   scenetype: { label: '搅拌站', value: '3' },
          //   sourceType: 2
          // }
          // {
          //   name: '徐汇汽修',
          //   locations: {
@@ -97,9 +97,9 @@
          //   scenetype: { label: '汽修', value: '7' },
          //   sourceType: 1,
          // }
        ];
      }
    }
        ]
      },
    },
  },
  emits: ['quickSet'],
  methods: {
@@ -111,10 +111,10 @@
      // this.formSearch.locations = set.locations
      // this.formSearch.scenetype = set.scenetype
      this.$emit('quickSet', set);
      this.$emit('quickSet', set)
    },
  },
    }
  }
};
</script>
<style scoped>
.row {
src/constants/menu.js
@@ -103,7 +103,7 @@
      {
        path: '/index/analysis/huanxincode-manage',
        icon: 'solar:archive-down-minimlistic-line-duotone',
        name: '环信码管理',
        name: '环信码',
      },
      {
        path: '/index/inspection/report-manage',
src/enum/scene copy.js
ÎļþÒÑɾ³ý
src/router/index.js
@@ -119,7 +119,7 @@
            {
              name: 'auto-evalution',
              path: 'auto-evalution',
              component: () => import('@/views/analysis/evalution/EvalutationRecord.vue'),
              component: () => import('@/views/analysis/evalution/EvalutationTask.vue'),
            },
            {
              name: 'huanxincode-manage',
src/stores/bgtaskStore.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useBgtaskStore = defineStore('bgtask', () => {
  // å¼¹å‡ºæ¡†æ˜¾ç¤º
  const dialogShow = ref(false);
  const events = [];
  function toggleShow(show) {
    if (typeof show === 'boolean') {
      dialogShow.value = show;
    } else {
      dialogShow.value = !dialogShow.value;
    }
  }
  function registerOnFetchTask(func) {
    events.push(func);
  }
  function fetchTask() {
    events.forEach((e) => {
      if (typeof e === 'function') {
        e();
      }
    });
  }
  return {
    dialogShow,
    toggleShow,
    registerOnFetchTask,
    fetchTask
  };
});
src/views/LoginPage.vue
@@ -1,14 +1,162 @@
<template>
  LoginPage
  <el-button type="primary" class="login-btn" @click="login">登录</el-button>
  <div class="login-container">
    <div class="login-wrapper">
      <div class="login-header">
        <h1 class="login-title">餐饮油烟智能监测与监管一体化平台</h1>
        <p class="login-subtitle">欢迎登录</p>
      </div>
      <div class="login-form">
        <el-form :model="loginForm" :rules="rules" ref="loginFormRef" label-position="top">
          <el-form-item label="账号" prop="username">
            <el-input
              v-model="loginForm.username"
              placeholder="请输入账号"
              prefix-icon="User"
              size="large"
            />
          </el-form-item>
          <el-form-item label="密码" prop="password">
            <el-input
              v-model="loginForm.password"
              type="password"
              placeholder="请输入密码"
              prefix-icon="Lock"
              size="large"
              show-password
            />
          </el-form-item>
          <el-form-item>
            <el-button
              type="primary"
              class="login-btn"
              :loading="loading"
              @click="login"
              size="large"
              style="width: 100%"
            >
              ç™»å½•
            </el-button>
          </el-form-item>
        </el-form>
      </div>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
const router = useRouter()
function login() {
const loginFormRef = ref<FormInstance>()
const loading = ref(false)
const loginForm = reactive({
  username: 'admin',
  password: '123456',
})
const rules = reactive<FormRules>({
  username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
  password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
})
async function login() {
  if (!loginFormRef.value) return
  try {
    await loginFormRef.value.validate()
    loading.value = true
    // æ¨¡æ‹Ÿç™»å½•验证
    setTimeout(() => {
      if (loginForm.username === 'admin' && loginForm.password === '123456') {
  router.push('/index/monitor/data-dashboard')
  // router.push('/index')
      } else {
        ElMessage.error('账号或密码错误')
      }
      loading.value = false
    }, 500)
  } catch (error) {
    console.log('验证失败:', error)
  }
}
</script>
<style>
/* å…¨å±€æ ·å¼é‡ç½® */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
html,
body {
  width: 100%;
  height: 100%;
  overflow: hidden;
}
</style>
<style scoped>
.login-container {
  width: 100vw;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  background-image: url('@/assets/loginPageBg.png');
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  padding-right: 10%;
}
.login-wrapper {
  width: 400px;
  background: rgba(255, 255, 255, 0.95);
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  padding: 32px;
  animation: fadeIn 0.5s ease-in-out;
}
.login-header {
  text-align: center;
  margin-bottom: 32px;
}
.login-title {
  font-size: 20px;
  font-weight: 600;
  color: #1a1a1a;
  margin-bottom: 8px;
}
.login-subtitle {
  font-size: 14px;
  color: #666;
}
.login-form {
  width: 100%;
}
.login-btn {
  margin-top: 16px;
}
@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(-20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
</style>
src/views/analysis/evalution/components/precheck/CompPreCheck.vue
@@ -2,7 +2,7 @@
  <el-steps :active="stepIndex" finish-status="success" style="" align-center>
    <el-step title="评估范围" />
    <el-step title="数据源检查" />
    <el-step title="条目豁免" />
    <!-- <el-step title="条目豁免" /> -->
    <el-step title="自动评估" />
  </el-steps>
  <CompCheckArea v-show="stepIndex == 0" v-model="stepIndex" @change="onAreaChange"></CompCheckArea>
@@ -12,13 +12,13 @@
    ref="refSource"
    @change="onDataSourceChange"
  ></CompCheckSource>
  <CompCheckExemption
  <!-- <CompCheckExemption
    v-show="stepIndex == 2"
    v-model="stepIndex"
    @change="onExemptionChange"
  ></CompCheckExemption>
  ></CompCheckExemption> -->
  <CompCheckConfirm
    v-show="stepIndex == 3"
    v-show="stepIndex == 2"
    v-model="stepIndex"
    :area-info="area"
    :data-source="dataSource"
@@ -28,11 +28,11 @@
</template>
<script>
import dayjs from 'dayjs';
import CompCheckArea from './components/CompCheckArea.vue';
import CompCheckSource from './components/CompCheckSource.vue';
import CompCheckExemption from './components/CompCheckExemption.vue';
import CompCheckConfirm from './components/CompCheckConfirm.vue';
import dayjs from 'dayjs'
import CompCheckArea from './components/CompCheckArea.vue'
import CompCheckSource from './components/CompCheckSource.vue'
import CompCheckExemption from './components/CompCheckExemption.vue'
import CompCheckConfirm from './components/CompCheckConfirm.vue'
/**
 * è‡ªåŠ¨è¯„ä¼°æ¡ä»¶åˆè§„æ€§æ£€æŸ¥
@@ -48,20 +48,20 @@
      stepIndex: 0,
      area: {
        _locations: {},
        _scenetype: {}
        _scenetype: {},
      },
      dataSource: {},
      // è±å…æ¡ç›®
      exemptionItems: {}
    };
      exemptionItems: {},
    }
  },
  methods: {
    /**
     * ç›‘听评估范围变更
     */
    onAreaChange(val) {
      const v = val.value;
      this.area = v;
      const v = val.value
      this.area = v
      const a = {
        provincecode: v._locations.pCode,
        provincename: v._locations.pName,
@@ -75,23 +75,23 @@
        endtime: this.$fm.formatYMDHMS(v.time),
        scensetypeid: v._scenetype.value,
        online: true,
        sourceType: v.sourceType
      };
      this.$refs.refSource.startCheck(a);
        sourceType: v.sourceType,
      }
      this.$refs.refSource.startCheck(a)
    },
    onDataSourceChange(val) {
      this.dataSource = val;
      this.dataSource = val
    },
    onExemptionChange(val) {
      this.exemptionItems = val;
      this.exemptionItems = val
    },
    /**
     * è‡ªåŠ¨è¯„ä¼°å‰ç½®åˆè§„æ€§æ£€æŸ¥
     * æ£€æŸ¥æ‰€é€‰èŒƒå›´å†…各项评估数据源是否完整
     */
    onNewTask() {
      this.$emit('startTask');
      this.$emit('startTask')
    },
  },
    }
  }
};
</script>
src/views/analysis/huanxincode/HuanxinCodeManage.vue
@@ -1,5 +1,31 @@
<template>
  <div class="huanxin-code-manage">
    <FYSearchBar @search="onSearch">
      <template #options>
        <!-- åŒºåŽ¿ -->
        <FYOptionLocation
          :initValue="false"
          :allOption="false"
          :level="3"
          :checkStrictly="false"
          v-model:value="formSearch.locations"
        ></FYOptionLocation>
        <!-- åœºæ™¯ç±»åž‹ -->
        <FYOptionScene
          :initValue="false"
          :allOption="false"
          :type="1"
          v-model:value="formSearch.scenetype"
        ></FYOptionScene>
        <!-- æ—¶é—´ -->
        <FYOptionTime
          :initValue="false"
          type="month"
          v-model:value="formSearch.time"
        ></FYOptionTime>
      </template>
      <template #buttons v-if="$slots.buttons"> </template>
    </FYSearchBar>
    <!-- é¡¶éƒ¨å®è§‚看板区 -->
    <el-row :gutter="20" class="dashboard">
      <el-col :span="8">
@@ -39,11 +65,9 @@
      </el-button>
    </div> -->
    <!-- ä¸­éƒ¨è§†å›¾åˆ‡æ¢åŒº -->
    <el-tabs v-model="activeView" class="view-tabs">
      <!-- åˆ—表视图 -->
      <el-tab-pane label="列表视图" name="list">
        <el-table :data="filteredShopList" style="width: 100%">
    <!-- åº—铺列表 -->
    <div class="shop-list">
      <el-table :data="pagedShopList" style="width: 100%">
          <el-table-column prop="shopName" label="店铺名称" />
          <el-table-column prop="district" label="所在区县" width="120" />
          <el-table-column prop="town" label="所在街镇" width="150" />
@@ -67,7 +91,7 @@
            </template>
          </el-table-column>
          <el-table-column prop="lastUpdate" label="上次更新时间" width="180" />
          <el-table-column label="操作" width="200" fixed="right">
        <el-table-column label="操作" width="100" fixed="right">
            <template #default="scope">
              <el-button size="small" @click="viewDetails(scope.row)">查看详情</el-button>
              <!-- <el-button size="small" type="warning" @click="viewRiskWarnings(scope.row)"
@@ -76,44 +100,44 @@
            </template>
          </el-table-column>
        </el-table>
      </el-tab-pane>
      <!-- åœ°å›¾è§†å›¾ -->
      <el-tab-pane label="地图视图" name="map">
        <div class="map-container">
          <div class="map-placeholder">
            <el-empty description="地图加载中..." />
            <!-- è¿™é‡Œåº”该集成真实的地图组件 -->
          </div>
          <div class="map-legend">
            <div class="legend-item">
              <div class="legend-dot green"></div>
              <span>绿码</span>
            </div>
            <div class="legend-item">
              <div class="legend-dot yellow"></div>
              <span>黄码</span>
            </div>
            <div class="legend-item">
              <div class="legend-dot red"></div>
              <span>红码</span>
      <!-- åˆ†é¡µç»„ä»¶ -->
      <div class="pagination">
        <el-pagination
          v-model:current-page="currentPage"
          v-model:page-size="pageSize"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          :total="filteredShopList.length"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        />
            </div>
          </div>
        </div>
      </el-tab-pane>
    </el-tabs>
    <!-- è¯¦æƒ…抽屉 -->
    <el-drawer v-model="drawerVisible" title="店铺详情" direction="rtl" size="70%">
    <el-drawer
      v-model="drawerVisible"
      :title="selectedShop?.shopName || '店铺详情'"
      direction="rtl"
      size="60%"
    >
      <div v-if="selectedShop" class="shop-details">
        <el-row justify="space-between" style="flex-wrap: nowrap">
        <!-- çŽ¯ä¿¡ç å¤§å›¾æ ‡åŠå½“å‰è¯„åˆ† -->
        <div class="code-header">
          <div class="code-icon" :class="selectedShop.code">
            {{ getCodeText(selectedShop.code) }}
          </div>
          <div class="score-info">
            <div class="score-label">当前评分</div>
            <div class="score-value">{{ selectedShop.score }}</div>
            </div>
            <div class="code-icon">
              <el-image
                class="image"
                :src="codeImageUrl"
                :preview-src-list="[codeImageUrl]"
                :initial-index="0"
                fit="cover"
                lazy
              />
          </div>
        </div>
@@ -121,17 +145,16 @@
        <div class="chart-section">
          <h3>评分维度分析</h3>
          <div class="radar-chart">
            <!-- è¿™é‡Œåº”该集成真实的雷达图组件 -->
            <el-empty description="雷达图加载中..." />
              <canvas ref="radarChart" width="500" height="400"></canvas>
          </div>
        </div>
        </el-row>
        <!-- è¯„分历史趋势图 -->
        <div class="chart-section">
          <h3>评分历史趋势</h3>
          <div class="trend-chart">
            <!-- è¿™é‡Œåº”该集成真实的趋势图组件 -->
            <el-empty description="趋势图加载中..." />
            <canvas ref="trendChart" width="800" height="350"></canvas>
          </div>
        </div>
@@ -171,16 +194,46 @@
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import dayjs from 'dayjs'
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { Setting, ArrowUp, ArrowDown } from '@element-plus/icons-vue'
import * as echarts from 'echarts'
import userApi from '@/api/fytz/userApi'
import creditApi from '@/api/fytz/creditApi'
// æœç´¢è¡¨å•
const formSearch = ref({
  locations: {
    aCode: null,
    aName: null,
    cCode: '3100',
    cName: '上海市',
    dCode: '310104',
    dName: '徐汇区',
    mCode: null,
    mName: null,
    pCode: '31',
    pName: '上海市',
    tCode: null,
    tName: null,
  },
  scenetype: {
    label: '餐饮',
    value: '1',
  },
  time: dayjs('2023-08-01').date(1).toDate(),
})
// çŠ¶æ€
const activeView = ref('list')
const drawerVisible = ref(false)
const modelConfigVisible = ref(false)
const selectedShop = ref(null)
const isAdmin = ref(true) // æ¨¡æ‹Ÿç®¡ç†å‘˜æƒé™
const filterCode = ref('all')
// åˆ†é¡µç›¸å…³
const currentPage = ref(1)
const pageSize = ref(10)
// çŽ¯ä¿¡ç å›¾ç‰‡URL
const codeImageUrl = ref('')
// ç»Ÿè®¡æ•°æ®
const statistics = reactive({
@@ -228,6 +281,61 @@
  '华泾镇',
]
function onSearch() {
  const f = formSearch.value
  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.scenetype.value == null ? (area.sceneTypes = []) : (area.sceneTypes = [f.scenetype.value])
  // ä¸Šä¸‹çº¿çŠ¶æ€
  area.online = true
  // å…³é”®å­—
  area.searchText = ''
  userApi.fetchUser(currentPage.value, pageSize.value, area).then((res) => {
    if (res) {
      res.data
      res.head.totalCount
      shopList.value = res.data.map((item, index) => {
        const { score, code } = generateRandomScore()
        return {
          id: index + 1,
          guid: item.biGuid,
          shopName: item.biName,
          district: item.biDistrictName,
          town: item.biTownName,
          code: code,
          score: score,
          trend: generateRandomTrend(),
          lastUpdate: generateRandomDate(),
          warnings: [
            {
              time: generateRandomDate(),
              content: '净化器运行时长不足',
              score: 90,
              handled: true,
            },
          ],
        }
      })
    }
  })
}
// ç”Ÿæˆ2023å¹´8月内的随机时间
function generateRandomDate() {
  const year = 2023
@@ -245,6 +353,23 @@
  return array[Math.floor(Math.random() * array.length)]
}
// ç”Ÿæˆéšæœºè¯„分和对应环信码等级
function generateRandomScore() {
  const score = Math.floor(Math.random() * 101) // 0-100
  let code
  if (score >= 90) {
    code = 'green'
  } else if (score >= 60) {
    code = 'yellow'
  } else {
    code = 'red'
  }
  return {
    score,
    code,
  }
}
// ç”Ÿæˆéšæœºè¯„分趋势
function generateRandomTrend() {
  return Math.floor(Math.random() * 11) - 5 // -5 åˆ° 5
@@ -258,7 +383,7 @@
    district: '徐汇区',
    town: getRandomElement(xuhuiTowns),
    code: 'green',
    score: 95,
    score: 90,
    trend: generateRandomTrend(),
    lastUpdate: generateRandomDate(),
    warnings: [
@@ -427,10 +552,18 @@
  return shopList.value.filter((shop) => shop.code === filterCode.value)
})
// åˆ†é¡µåŽçš„店铺列表
const pagedShopList = computed(() => {
  const start = (currentPage.value - 1) * pageSize.value
  const end = start + pageSize.value
  return filteredShopList.value.slice(start, end)
})
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  // è¿™é‡Œå¯ä»¥ä»ŽAPI获取数据
  console.log('环信码管理页面加载')
  onSearch()
})
// æ–¹æ³•
@@ -441,7 +574,17 @@
function filterByCode(code) {
  filterCode.value = code === filterCode.value ? 'all' : code
  activeView.value = 'list' // åˆ‡æ¢åˆ°åˆ—表视图
  currentPage.value = 1 // é‡ç½®åˆ°ç¬¬ä¸€é¡µ
}
// åˆ†é¡µæ–¹æ³•
function handleSizeChange(size) {
  pageSize.value = size
  currentPage.value = 1
}
function handleCurrentChange(current) {
  currentPage.value = current
}
function getCodeType(code) {
@@ -470,9 +613,183 @@
  }
}
// é›·è¾¾å›¾å’Œè¶‹åŠ¿å›¾å¼•ç”¨
const radarChart = ref(null)
const trendChart = ref(null)
let radarChartInstance = null
let trendChartInstance = null
function viewDetails(shop) {
  selectedShop.value = shop
  drawerVisible.value = true
  // èŽ·å–çŽ¯ä¿¡ç å›¾ç‰‡
  if (shop.guid && shop.shopName) {
    creditApi.fetchCodeUrl(shop.guid, shop.shopName).then((res) => {
      if (res && res.url) {
        codeImageUrl.value = res.url
      }
    })
  }
  // å»¶è¿Ÿç»˜åˆ¶å›¾è¡¨ï¼Œç¡®ä¿DOM已更新
  setTimeout(() => {
    drawRadarChart()
    drawTrendChart()
  }, 100)
}
// ç»˜åˆ¶é›·è¾¾å›¾
function drawRadarChart() {
  if (!radarChart.value) return
  // é”€æ¯æ—§å®žä¾‹
  if (radarChartInstance) {
    radarChartInstance.dispose()
  }
  // åˆå§‹åŒ–echarts实例
  radarChartInstance = echarts.init(radarChart.value)
  // é›·è¾¾å›¾æ•°æ®
  const labels = [
    '在线监测设备',
    '净化设施设备',
    '在线监测设备维护',
    '净化设施设备维护',
    '在线监测数据量级',
    '空调和风机噪声',
    '台站管理',
    '信用承诺自评',
  ]
  // ç”Ÿæˆéšæœºè¯„分数据(实际项目中应从API获取)
  const data = labels.map(() => Math.floor(Math.random() * 40) + 60) // 60-100分
  // é…ç½®é¡¹
  const option = {
    radar: {
      indicator: labels.map((label) => ({
        name: label,
        max: 100,
      })),
      radius: '70%',
    },
    series: [
      {
        type: 'radar',
        data: [
          {
            value: data,
            name: '评分维度',
            areaStyle: {
              color: 'rgba(103, 194, 58, 0.2)',
            },
            lineStyle: {
              color: '#67c23a',
            },
            itemStyle: {
              color: '#67c23a',
            },
          },
        ],
      },
    ],
  }
  // æ¸²æŸ“图表
  radarChartInstance.setOption(option)
  // ç›‘听窗口大小变化
  window.addEventListener('resize', () => {
    radarChartInstance.resize()
  })
}
// ç»˜åˆ¶è¶‹åŠ¿å›¾
function drawTrendChart() {
  if (!trendChart.value) return
  // é”€æ¯æ—§å®žä¾‹
  if (trendChartInstance) {
    trendChartInstance.dispose()
  }
  // åˆå§‹åŒ–echarts实例
  trendChartInstance = echarts.init(trendChart.value)
  // ç”Ÿæˆè¿‡åŽ»12个月的标签
  const labels = []
  const data = []
  const now = dayjs()
  for (let i = 11; i >= 0; i--) {
    const date = now.subtract(i, 'month')
    labels.push(date.format('YYYY-MM'))
    // ç”Ÿæˆéšæœºè¯„分数据(实际项目中应从API获取)
    data.push(Math.floor(Math.random() * 30) + 70) // 70-100分
  }
  // é…ç½®é¡¹
  const option = {
    tooltip: {
      trigger: 'axis',
    },
    grid: {
      left: '3%',
      right: '4%',
      bottom: '3%',
      containLabel: true,
    },
    xAxis: {
      type: 'category',
      boundaryGap: false,
      data: labels,
      axisLabel: {
        rotate: 45,
      },
    },
    yAxis: {
      type: 'value',
      min: 60,
      max: 100,
      interval: 8,
    },
    series: [
      {
        name: '评分',
        type: 'line',
        data: data,
        smooth: true,
        lineStyle: {
          color: '#409eff',
        },
        itemStyle: {
          color: '#409eff',
        },
        areaStyle: {
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            {
              offset: 0,
              color: 'rgba(64, 158, 255, 0.3)',
            },
            {
              offset: 1,
              color: 'rgba(64, 158, 255, 0.1)',
            },
          ]),
        },
      },
    ],
  }
  // æ¸²æŸ“图表
  trendChartInstance.setOption(option)
  // ç›‘听窗口大小变化
  window.addEventListener('resize', () => {
    trendChartInstance.resize()
  })
}
function viewRiskWarnings(shop) {
@@ -548,8 +865,13 @@
  margin-bottom: 20px;
}
.view-tabs {
.shop-list {
  margin-bottom: 20px;
}
.pagination {
  margin-top: 20px;
  text-align: right;
}
.trend {
@@ -633,33 +955,13 @@
.code-header {
  display: flex;
  align-items: center;
  margin-bottom: 30px;
  align-items: flex-start;
  flex-direction: column;
  margin-bottom: 20px;
}
.code-icon {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  font-weight: bold;
  color: white;
  margin-right: 30px;
}
.code-icon.green {
  background-color: #67c23a;
}
.code-icon.yellow {
  background-color: #e6a23c;
}
.code-icon.red {
  background-color: #f56c6c;
}
.score-info {
@@ -677,17 +979,26 @@
}
.chart-section {
  margin-bottom: 30px;
  margin-bottom: 20px;
}
.chart-section h3 {
  margin-bottom: 15px;
  font-size: 18px;
  margin-bottom: 10px;
  font-size: 16px;
}
.radar-chart,
.radar-chart {
  width: 500px;
  height: 500px;
  border: 1px solid #e4e7ed;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.trend-chart {
  height: 300px;
  height: 350px;
  border: 1px solid #e4e7ed;
  border-radius: 4px;
  display: flex;
@@ -696,11 +1007,18 @@
}
.warning-section {
  margin-top: 30px;
  margin-top: 20px;
}
.warning-section h3 {
  margin-bottom: 15px;
  font-size: 18px;
  margin-bottom: 10px;
  font-size: 16px;
}
.image {
  width: 300px;
  /* height: 250px; */
  border-radius: 4px;
  margin-bottom: 6px;
}
</style>
src/views/inspection/MonitorControl.vue
@@ -1 +1,1417 @@
<template>s</template>
<template>
  <div class="monitor-control">
    <!-- æ€»è§ˆçŽ°åœºå·¡æŸ¥å¡ç‰‡ -->
    <el-card class="mb-4">
      <template #header>
        <div class="card-header">
          <span>现场巡查总览</span>
          <div class="filter-group">
            <FYOptionTime
              :initValue="false"
              type="daterange"
              v-model:value="params.timeRange"
              style="width: 300px; margin-bottom: 0px"
              :shortcuts="shortcuts"
            ></FYOptionTime>
            <!-- åŒºåŽ¿ -->
            <FYOptionLocation
              class="m-l-8"
              :allOption="false"
              :level="3"
              :checkStrictly="false"
              :initValue="false"
              v-model:value="params.locations"
              style="width: 300px; margin-bottom: 0px"
            ></FYOptionLocation>
          </div>
        </div>
      </template>
      <!-- ç»Ÿè®¡æ•°æ®åŒºåŸŸ -->
      <div class="stats-sections">
        <!-- å·¦ä¾§ï¼šå·²å·¡æŸ¥åº—铺率、巡查点次、复查点次 -->
        <div class="stats-section left-section">
          <h3>巡查概况</h3>
          <div class="chart-item">
            <div class="progress-container">
              <el-progress
                type="dashboard"
                :percentage="parseFloat(inspectionStats.inspectedRate)"
                :color="['#409EFF', '#67C23A']"
                :width="120"
              />
              <div class="progress-label">已巡查店铺率</div>
              <div class="progress-value">
                {{ `${inspectionStats.inspectedShops}/${inspectionStats.totalShops}` }}
              </div>
            </div>
          </div>
          <div class="stats-grid m-t-16">
            <el-statistic
              class="stat-item"
              :value="inspectionStats.inspectionPoints"
              title="巡查点次"
            />
            <el-statistic
              class="stat-item"
              :value="inspectionStats.reviewPoints"
              title="复查点次"
            />
          </div>
        </div>
        <!-- å³ä¾§ï¼šé—®é¢˜æ•°ã€é—®é¢˜æ•´æ”¹æ•°ã€é—®é¢˜æ•´æ”¹çŽ‡ç»Ÿè®¡å›¾ -->
        <div class="stats-section right-section">
          <h3>问题整改概况</h3>
          <div class="stats-grid">
            <el-statistic class="stat-item" :value="inspectionStats.problemCount" title="问题数" />
            <el-statistic
              class="stat-item"
              :value="inspectionStats.rectifiedProblems"
              title="问题整改数"
            />
          </div>
          <!-- <div class="chart-item"> -->
          <div ref="rectificationRateChart" class="chart"></div>
          <!-- </div> -->
        </div>
      </div>
      <!-- å…¶ä»–图表展示 -->
      <div class="chart-container">
        <div class="chart-item">
          <h3>巡查完成情况趋势</h3>
          <div ref="inspectionTrendChart" class="chart"></div>
        </div>
        <div class="chart-item">
          <h3>问题类型分布</h3>
          <div ref="problemTypeChart" class="chart"></div>
        </div>
      </div>
    </el-card>
    <!-- é¤é¥®åº—铺行政处罚卡片 -->
    <el-card class="mb-4">
      <template #header>
        <div class="card-header">
          <span>餐饮店铺行政处罚</span>
          <div class="filter-group">
            <FYOptionTime
              class="m-r-8"
              :initValue="false"
              type="daterange"
              v-model:value="punishmentDateRange"
              style="width: 300px; margin-bottom: 0px"
              :shortcuts="shortcuts"
            ></FYOptionTime>
            <el-button type="success" icon="Plus" @click="addPunishment">新增处罚</el-button>
            <el-button type="info" icon="Upload" @click="importPunishment">批量导入</el-button>
          </div>
        </div>
      </template>
      <!-- å›¾è¡¨å±•示 -->
      <div class="chart-container">
        <div class="chart-item">
          <div class="chart-header">
            <h3>处罚数趋势</h3>
            <div class="chart-summary">处罚总数: {{ punishmentStats.totalCount }}</div>
          </div>
          <div ref="dailyPunishmentChart" class="chart"></div>
        </div>
        <div class="chart-item">
          <h3>店铺类型处罚分布</h3>
          <div ref="shopTypePunishmentChart" class="chart"></div>
        </div>
      </div>
      <!-- å¤„罚记录表格 -->
      <el-table
        :data="filteredPunishmentData"
        table-layout="fixed"
        :show-overflow-tooltip="true"
        height="400px"
        border
      >
        <el-table-column prop="shopName" label="处罚店铺" />
        <el-table-column prop="punishmentItem" label="处罚事项" width="180" />
        <el-table-column prop="punishmentTime" label="处罚时间" width="180" />
        <el-table-column prop="punishmentReason" label="处罚理由" width="200" />
        <el-table-column prop="punishmentResult" label="处罚结果" width="150" />
        <el-table-column prop="punishmentDepartment" label="处罚部门" width="150" />
        <el-table-column width="250">
          <template #header>
            <el-input v-model="punishmentKeyword" placeholder="关键字搜索" style="width: 120px" />
          </template>
          <template #default="scope">
            <el-button size="small" type="primary" @click="editPunishment(scope.row)"
              >编辑</el-button
            >
            <el-button size="small" type="danger" @click="deletePunishment(scope.row.id)"
              >删除</el-button
            >
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <div class="pagination-container">
        <el-pagination
          v-model:current-page="punishmentPagination.currentPage"
          v-model:page-size="punishmentPagination.pageSize"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          :total="punishmentPagination.total"
          @size-change="handlePunishmentSizeChange"
          @current-change="handlePunishmentCurrentChange"
        />
      </div>
    </el-card>
    <!-- é¤é¥®åº—铺信访投诉卡片 -->
    <el-card class="mb-4">
      <template #header>
        <div class="card-header">
          <span>餐饮店铺信访投诉</span>
          <div class="filter-group">
            <FYOptionTime
              class="m-r-8"
              :initValue="false"
              type="daterange"
              v-model:value="complaintDateRange"
              style="width: 300px; margin-bottom: 0px"
              :shortcuts="shortcuts"
            ></FYOptionTime>
            <el-button type="success" icon="Plus" @click="addComplaint">新增投诉</el-button>
            <el-button type="info" icon="Upload" @click="importComplaint">批量导入</el-button>
          </div>
        </div>
      </template>
      <!-- å›¾è¡¨å±•示 -->
      <div class="chart-container">
        <div class="chart-item">
          <div class="chart-header">
            <h3>投诉数趋势</h3>
            <div class="chart-summary">投诉总数: {{ complaintStats.totalCount }}</div>
          </div>
          <div ref="dailyComplaintChart" class="chart"></div>
        </div>
        <div class="chart-item">
          <h3>投诉来源分布</h3>
          <div ref="sourceComplaintChart" class="chart"></div>
        </div>
      </div>
      <!-- æŠ•诉记录表格 -->
      <el-table
        :data="filteredComplaintData"
        table-layout="fixed"
        :show-overflow-tooltip="true"
        height="400px"
        border
      >
        <el-table-column prop="shopName" label="投诉店铺" />
        <el-table-column prop="complaintReason" label="投诉原因" width="180" />
        <el-table-column prop="complaintRequest" label="投诉诉求" width="180" />
        <el-table-column prop="complaintTime" label="投诉时间" width="180" />
        <el-table-column prop="complaintSource" label="投诉来源" width="150" />
        <el-table-column prop="handlingDepartment" label="处理部门" width="150" />
        <el-table-column prop="complaintResult" label="投诉结果" width="150" />
        <el-table-column width="250">
          <template #header>
            <el-input v-model="complaintKeyword" placeholder="关键字搜索" style="width: 120px" />
          </template>
          <template #default="scope">
            <el-button size="small" type="primary" @click="editComplaint(scope.row)"
              >编辑</el-button
            >
            <el-button size="small" type="danger" @click="deleteComplaint(scope.row.id)"
              >删除</el-button
            >
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <div class="pagination-container">
        <el-pagination
          v-model:current-page="complaintPagination.currentPage"
          v-model:page-size="complaintPagination.pageSize"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          :total="complaintPagination.total"
          @size-change="handleComplaintSizeChange"
          @current-change="handleComplaintCurrentChange"
        />
      </div>
    </el-card>
    <!-- æ–°å¢ž/编辑处罚对话框 -->
    <el-dialog v-model="punishmentDialogVisible" title="处罚信息" width="600px">
      <el-form :model="punishmentForm" label-width="100px">
        <el-form-item label="处罚店铺">
          <el-input v-model="punishmentForm.shopName" />
        </el-form-item>
        <el-form-item label="处罚事项">
          <el-input v-model="punishmentForm.punishmentItem" />
        </el-form-item>
        <el-form-item label="处罚时间">
          <el-date-picker v-model="punishmentForm.punishmentTime" type="datetime" />
        </el-form-item>
        <el-form-item label="处罚理由">
          <el-input v-model="punishmentForm.punishmentReason" type="textarea" />
        </el-form-item>
        <el-form-item label="处罚结果">
          <el-input v-model="punishmentForm.punishmentResult" />
        </el-form-item>
        <el-form-item label="处罚部门">
          <el-input v-model="punishmentForm.punishmentDepartment" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="punishmentDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="savePunishment">保存</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- æ–°å¢ž/编辑投诉对话框 -->
    <el-dialog v-model="complaintDialogVisible" title="投诉信息" width="600px">
      <el-form :model="complaintForm" label-width="100px">
        <el-form-item label="投诉店铺">
          <el-input v-model="complaintForm.shopName" />
        </el-form-item>
        <el-form-item label="投诉原因">
          <el-input v-model="complaintForm.complaintReason" />
        </el-form-item>
        <el-form-item label="投诉诉求">
          <el-input v-model="complaintForm.complaintRequest" type="textarea" />
        </el-form-item>
        <el-form-item label="投诉时间">
          <el-date-picker v-model="complaintForm.complaintTime" type="datetime" />
        </el-form-item>
        <el-form-item label="投诉来源">
          <el-input v-model="complaintForm.complaintSource" />
        </el-form-item>
        <el-form-item label="处理部门">
          <el-input v-model="complaintForm.handlingDepartment" />
        </el-form-item>
        <el-form-item label="投诉结果">
          <el-input v-model="complaintForm.complaintResult" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="complaintDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="saveComplaint">保存</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- å¤„罚批量导入对话框 -->
    <el-dialog v-model="punishmentImportDialogVisible" title="处罚批量导入" width="600px">
      <div class="import-container">
        <p class="import-tip">请选择要导入的Excel文件</p>
        <el-upload
          class="upload-demo"
          action="#"
          :auto-upload="false"
          :on-change="handlePunishmentFileChange"
          :file-list="punishmentImportFileList"
          accept=".xlsx,.xls"
          :limit="1"
          :on-exceed="handleExceed"
        >
          <el-button type="primary">选择文件</el-button>
          <template #tip>
            <div class="el-upload__tip">只能上传Excel文件,且不超过5MB</div>
          </template>
        </el-upload>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="punishmentImportDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="confirmPunishmentImport">导入</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- æŠ•诉批量导入对话框 -->
    <el-dialog v-model="complaintImportDialogVisible" title="投诉批量导入" width="600px">
      <div class="import-container">
        <p class="import-tip">请选择要导入的Excel文件</p>
        <el-upload
          class="upload-demo"
          action="#"
          :auto-upload="false"
          :on-change="handleComplaintFileChange"
          :file-list="complaintImportFileList"
          accept=".xlsx,.xls"
          :limit="1"
          :on-exceed="handleExceed"
        >
          <el-button type="primary">选择文件</el-button>
          <template #tip>
            <div class="el-upload__tip">只能上传Excel文件,且不超过5MB</div>
          </template>
        </el-upload>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="complaintImportDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="confirmComplaintImport">导入</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
import * as echarts from 'echarts'
import { Icon } from '@iconify/vue'
import dayjs from 'dayjs'
import { ElMessage } from 'element-plus'
// æ€»è§ˆçŽ°åœºå·¡æŸ¥æ•°æ®
const dayStart = dayjs('2023-08-01').startOf('date')
const dayEnd = dayStart.endOf('date')
const shortcuts = [
  {
    text: '今天',
    value: [dayStart.toDate(), dayEnd.toDate()],
  },
  {
    text: '本周',
    value: [dayStart.startOf('week').toDate(), dayEnd.endOf('week').toDate()],
  },
  {
    text: '上周',
    value: [dayStart.day(-7).toDate(), dayEnd.day(-1).toDate()],
  },
  {
    text: '本月',
    value: [dayStart.startOf('month').toDate(), dayEnd.endOf('month').toDate()],
  },
  {
    text: '上月',
    value: [
      dayStart.subtract(1, 'month').startOf('month').toDate(),
      dayEnd.subtract(1, 'month').endOf('month').toDate(),
    ],
  },
  {
    text: '本季度',
    value: [dayStart.startOf('quarter').toDate(), dayEnd.endOf('quarter').toDate()],
  },
  {
    text: '上季度',
    value: [
      dayStart.subtract(1, 'quarter').startOf('quarter').toDate(),
      dayEnd.subtract(1, 'quarter').endOf('quarter').toDate(),
    ],
  },
  {
    text: '去年',
    value: [
      dayStart.subtract(1, 'year').startOf('year').toDate(),
      dayEnd.subtract(1, 'year').endOf('year').toDate(),
    ],
  },
  {
    text: '今年',
    value: [dayStart.startOf('year').toDate(), dayEnd.endOf('year').toDate()],
  },
]
const params = ref({
  prodBaseTypes: [],
  prodCheck: '',
  scenetype: '',
  topTask: '',
  locations: {
    aCode: null,
    aName: null,
    cCode: '3100',
    cName: '上海市',
    dCode: '310104',
    dName: '徐汇区',
    mCode: null,
    mName: null,
    pCode: '31',
    pName: '上海市',
    tCode: null,
    tName: null,
  },
  timeRange: [dayStart.startOf('month').toDate(), dayEnd.endOf('month').toDate()],
})
const inspectionStats = ref({
  // totalShops: 1250,
  // inspectedShops: 980,
  // inspectedRate: '78.4%',
  // inspectionPoints: 2350,
  // reviewPoints: 450,
  // problemCount: 320,
  // rectifiedProblems: 280,
  // sameDayRectificationRate: '65.2%',
  // effectiveRectificationRate: '78.5%',
  // comprehensiveRectificationRate: '82.3%',
  // auditPassRate: '87.5%',
})
// å›¾è¡¨å¼•用
const inspectionTrendChart = ref(null)
const problemTypeChart = ref(null)
const rectificationRateChart = ref(null)
const dailyPunishmentChart = ref(null)
const shopTypePunishmentChart = ref(null)
const dailyComplaintChart = ref(null)
const sourceComplaintChart = ref(null)
// è¡Œæ”¿å¤„罚数据
const punishmentDateRange = ref([
  dayStart.startOf('month').toDate(),
  dayEnd.endOf('month').toDate(),
])
const punishmentKeyword = ref('')
const punishmentStats = ref({
  totalCount: 120,
})
const punishmentTableData = ref([
  // {
  //   id: 1,
  //   shopName: '味美餐厅',
  //   punishmentItem: '油烟超标排放',
  //   punishmentTime: '2026-03-10 14:30',
  //   punishmentReason: '未安装油烟净化设备',
  //   punishmentResult: '罚款5000元',
  //   punishmentDepartment: '徐汇区环保局',
  // },
  // {
  //   id: 2,
  //   shopName: '香辣小龙虾',
  //   punishmentItem: '油烟超标排放',
  //   punishmentTime: '2026-03-08 10:15',
  //   punishmentReason: '油烟净化设备未正常运行',
  //   punishmentResult: '罚款3000元',
  //   punishmentDepartment: '徐汇区环保局',
  // },
])
const punishmentPagination = ref({
  currentPage: 1,
  pageSize: 10,
  total: 2,
})
// è¿‡æ»¤åŽçš„处罚数据
const filteredPunishmentData = computed(() => {
  if (!punishmentKeyword.value) {
    return punishmentTableData.value
  }
  const keyword = punishmentKeyword.value.toLowerCase()
  return punishmentTableData.value.filter((item) => {
    return Object.values(item).some((value) => {
      return String(value).toLowerCase().includes(keyword)
    })
  })
})
// ä¿¡è®¿æŠ•诉数据
const complaintDateRange = ref([dayStart.startOf('month').toDate(), dayEnd.endOf('month').toDate()])
const complaintKeyword = ref('')
const complaintStats = ref({
  totalCount: 85,
})
const complaintTableData = ref([
  // {
  //   id: 1,
  //   shopName: '鲜味馆',
  //   complaintReason: '油烟扰民',
  //   complaintRequest: '要求安装油烟净化设备',
  //   complaintTime: '2026-03-12 09:20',
  //   complaintSource: '12345热线',
  //   handlingDepartment: '徐汇区环保局',
  //   complaintResult: '已处理',
  // },
  // {
  //   id: 2,
  //   shopName: '烧烤达人',
  //   complaintReason: '夜间油烟污染',
  //   complaintRequest: '要求整改',
  //   complaintTime: '2026-03-10 22:30',
  //   complaintSource: '居民投诉',
  //   handlingDepartment: '徐汇区环保局',
  //   complaintResult: '处理中',
  // },
])
const complaintPagination = ref({
  currentPage: 1,
  pageSize: 10,
  total: 2,
})
// è¿‡æ»¤åŽçš„æŠ•诉数据
const filteredComplaintData = computed(() => {
  if (!complaintKeyword.value) {
    return complaintTableData.value
  }
  const keyword = complaintKeyword.value.toLowerCase()
  return complaintTableData.value.filter((item) => {
    return Object.values(item).some((value) => {
      return String(value).toLowerCase().includes(keyword)
    })
  })
})
// å¯¹è¯æ¡†çŠ¶æ€
const punishmentDialogVisible = ref(false)
const complaintDialogVisible = ref(false)
const punishmentImportDialogVisible = ref(false)
const complaintImportDialogVisible = ref(false)
// å¯¼å…¥æ–‡ä»¶åˆ—表
const punishmentImportFileList = ref([])
const complaintImportFileList = ref([])
// è¡¨å•数据
const punishmentForm = ref({
  id: '',
  shopName: '',
  punishmentItem: '',
  punishmentTime: '',
  punishmentReason: '',
  punishmentResult: '',
  punishmentDepartment: '',
})
const complaintForm = ref({
  id: '',
  shopName: '',
  complaintReason: '',
  complaintRequest: '',
  complaintTime: '',
  complaintSource: '',
  handlingDepartment: '',
  complaintResult: '',
})
const refreshInspectionData = () => {
  // æ¨¡æ‹Ÿåˆ·æ–°æ•°æ®
  console.log('刷新巡查数据', params.value)
  // ç”Ÿæˆæ–°çš„æ¨¡æ‹Ÿæ•°æ®
  const totalShops = 90 + Math.floor(Math.random() * 10)
  const inspectedShops = 70 + Math.floor(Math.random() * 10)
  const inspectedRate = ((inspectedShops / totalShops) * 100).toFixed(1) + '%'
  // è®¡ç®—时间范围
  const startTime = params.value.timeRange[0]
  const endTime = params.value.timeRange[1]
  const startDate = dayjs(startTime)
  const endDate = dayjs(endTime)
  const daysDiff = endDate.diff(startDate, 'day')
  const months = daysDiff / 30
  // æ ¹æ®æ—¶é—´èŒƒå›´è°ƒæ•´æ•°æ®é‡çº§
  const inspectionPoints = Math.floor(100 * months) + Math.floor(Math.random() * 20)
  const problemCount = Math.floor(200 * months) + Math.floor(Math.random() * 30)
  const reviewPoints = Math.floor(inspectionPoints * 0.2) + Math.floor(Math.random() * 10)
  const rectifiedProblems = Math.floor(problemCount * 0.8) + Math.floor(Math.random() * 20)
  inspectionStats.value = {
    totalShops,
    inspectedShops,
    inspectedRate,
    inspectionPoints,
    reviewPoints,
    problemCount,
    rectifiedProblems,
    sameDayRectificationRate: (60 + Math.random() * 20).toFixed(1) + '%',
    effectiveRectificationRate: (70 + Math.random() * 20).toFixed(1) + '%',
    comprehensiveRectificationRate: (75 + Math.random() * 15).toFixed(1) + '%',
    auditPassRate: (80 + Math.random() * 15).toFixed(1) + '%',
  }
  // é‡æ–°åˆå§‹åŒ–图表以更新数据
  initCharts()
}
const searchPunishment = () => {
  // æ¨¡æ‹Ÿæœç´¢å¤„罚数据
  console.log('搜索处罚数据', {
    dateRange: punishmentDateRange.value,
    keyword: punishmentKeyword.value,
  })
  // è®¡ç®—时间范围
  const startTime = punishmentDateRange.value[0]
  const endTime = punishmentDateRange.value[1]
  const startDate = dayjs(startTime)
  const endDate = dayjs(endTime)
  const daysDiff = endDate.diff(startDate, 'day')
  const months = daysDiff / 30
  // ç”Ÿæˆæ–°çš„æ¨¡æ‹Ÿæ•°æ®
  const totalCount = Math.floor(10 * months) + Math.floor(Math.random() * 3)
  punishmentStats.value = {
    totalCount,
  }
  // ç”Ÿæˆæ–°çš„处罚记录
  const newData = []
  const shopNames = [
    '味美餐厅',
    '香辣小龙虾',
    '鲜味馆',
    '烧烤达人',
    '川菜馆',
    '西餐厅',
    '日料店',
    '火锅店',
  ]
  const punishmentItems = ['油烟超标排放', '未安装油烟净化设备', '设备未正常运行', '噪声污染']
  const departments = ['徐汇区环保局', '长宁区环保局', '静安区环保局', '普陀区环保局']
  for (let i = 0; i < totalCount; i++) {
    // ç”Ÿæˆåœ¨æ—¶é—´èŒƒå›´å†…的随机时间
    const randomDays = Math.floor(Math.random() * (daysDiff + 1))
    const randomTime = startDate
      .add(randomDays, 'day')
      .add(Math.floor(Math.random() * 24), 'hour')
      .add(Math.floor(Math.random() * 60), 'minute')
    newData.push({
      id: i + 1,
      shopName: shopNames[Math.floor(Math.random() * shopNames.length)],
      punishmentItem: punishmentItems[Math.floor(Math.random() * punishmentItems.length)],
      punishmentTime: randomTime.format('YYYY-MM-DD HH:mm'),
      punishmentReason:
        punishmentItems[Math.floor(Math.random() * punishmentItems.length)] + '的违规行为',
      punishmentResult: '罚款' + (Math.floor(Math.random() * 5) + 1) * 1000 + '元',
      punishmentDepartment: departments[Math.floor(Math.random() * departments.length)],
    })
  }
  punishmentTableData.value = newData
  punishmentPagination.value.total = newData.length
  // é‡æ–°åˆå§‹åŒ–图表以更新数据
  initCharts()
}
const searchComplaint = () => {
  // æ¨¡æ‹Ÿæœç´¢æŠ•诉数据
  console.log('搜索投诉数据', {
    dateRange: complaintDateRange.value,
    keyword: complaintKeyword.value,
  })
  // è®¡ç®—时间范围
  const startTime = complaintDateRange.value[0]
  const endTime = complaintDateRange.value[1]
  const startDate = dayjs(startTime)
  const endDate = dayjs(endTime)
  const daysDiff = endDate.diff(startDate, 'day')
  const months = daysDiff / 30
  // ç”Ÿæˆæ–°çš„æ¨¡æ‹Ÿæ•°æ®
  const totalCount = Math.floor(20 * months) + Math.floor(Math.random() * 5)
  complaintStats.value = {
    totalCount,
  }
  // ç”Ÿæˆæ–°çš„æŠ•诉记录
  const newData = []
  const shopNames = [
    '鲜味馆',
    '烧烤达人',
    '味美餐厅',
    '香辣小龙虾',
    '川菜馆',
    '西餐厅',
    '日料店',
    '火锅店',
  ]
  const complaintReasons = ['油烟扰民', '夜间噪声', '异味污染', '卫生问题']
  const sources = ['12345热线', '居民投诉', '网络平台', '其他']
  const departments = ['徐汇区环保局', '长宁区环保局', '静安区环保局', '普陀区环保局']
  const results = ['已处理', '处理中', '未处理']
  for (let i = 0; i < totalCount; i++) {
    // ç”Ÿæˆåœ¨æ—¶é—´èŒƒå›´å†…的随机时间
    const randomDays = Math.floor(Math.random() * (daysDiff + 1))
    const randomTime = startDate
      .add(randomDays, 'day')
      .add(Math.floor(Math.random() * 24), 'hour')
      .add(Math.floor(Math.random() * 60), 'minute')
    newData.push({
      id: i + 1,
      shopName: shopNames[Math.floor(Math.random() * shopNames.length)],
      complaintReason: complaintReasons[Math.floor(Math.random() * complaintReasons.length)],
      complaintRequest:
        '要求整改' + complaintReasons[Math.floor(Math.random() * complaintReasons.length)],
      complaintTime: randomTime.format('YYYY-MM-DD HH:mm'),
      complaintSource: sources[Math.floor(Math.random() * sources.length)],
      handlingDepartment: departments[Math.floor(Math.random() * departments.length)],
      complaintResult: results[Math.floor(Math.random() * results.length)],
    })
  }
  complaintTableData.value = newData
  complaintPagination.value.total = newData.length
  // é‡æ–°åˆå§‹åŒ–图表以更新数据
  initCharts()
}
// ç›‘听处罚日期范围变化
watch(
  () => punishmentDateRange.value,
  () => {
    searchPunishment()
  },
  { deep: true },
)
// ç›‘听投诉日期范围变化
watch(
  () => complaintDateRange.value,
  () => {
    searchComplaint()
  },
  { deep: true },
)
const addPunishment = () => {
  // é‡ç½®è¡¨å•
  punishmentForm.value = {
    id: '',
    shopName: '',
    punishmentItem: '',
    punishmentTime: '',
    punishmentReason: '',
    punishmentResult: '',
    punishmentDepartment: '',
  }
  punishmentDialogVisible.value = true
}
const editPunishment = (row) => {
  // å¡«å……表单
  punishmentForm.value = { ...row }
  punishmentDialogVisible.value = true
}
const savePunishment = () => {
  // æ¨¡æ‹Ÿä¿å­˜æ•°æ®
  console.log('保存处罚数据', punishmentForm.value)
  punishmentDialogVisible.value = false
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„æ•°æ®ä¿å­˜é€»è¾‘
}
const deletePunishment = (id) => {
  // æ¨¡æ‹Ÿåˆ é™¤æ•°æ®
  console.log('删除处罚数据', id)
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„æ•°æ®åˆ é™¤é€»è¾‘
}
const importPunishment = () => {
  // æ‰“开导入对话框
  punishmentImportFileList.value = []
  punishmentImportDialogVisible.value = true
}
const handlePunishmentFileChange = (file, fileList) => {
  punishmentImportFileList.value = fileList
  console.log('选择的处罚文件:', file)
}
const confirmPunishmentImport = () => {
  if (punishmentImportFileList.value.length === 0) {
    ElMessage.warning('请选择要导入的文件')
    return
  }
  const file = punishmentImportFileList.value[0]
  console.log('开始导入处罚数据:', file.name)
  // é¢„留导入逻辑
  // è¿™é‡Œå°†å®žçŽ°å®žé™…çš„æ–‡ä»¶è§£æžå’Œæ•°æ®å¯¼å…¥
  punishmentImportDialogVisible.value = false
  ElMessage.success('导入操作已触发,预留导入逻辑')
}
const addComplaint = () => {
  // é‡ç½®è¡¨å•
  complaintForm.value = {
    id: '',
    shopName: '',
    complaintReason: '',
    complaintRequest: '',
    complaintTime: '',
    complaintSource: '',
    handlingDepartment: '',
    complaintResult: '',
  }
  complaintDialogVisible.value = true
}
const editComplaint = (row) => {
  // å¡«å……表单
  complaintForm.value = { ...row }
  complaintDialogVisible.value = true
}
const saveComplaint = () => {
  // æ¨¡æ‹Ÿä¿å­˜æ•°æ®
  console.log('保存投诉数据', complaintForm.value)
  complaintDialogVisible.value = false
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„æ•°æ®ä¿å­˜é€»è¾‘
}
const deleteComplaint = (id) => {
  // æ¨¡æ‹Ÿåˆ é™¤æ•°æ®
  console.log('删除投诉数据', id)
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„æ•°æ®åˆ é™¤é€»è¾‘
}
const importComplaint = () => {
  // æ‰“开导入对话框
  complaintImportFileList.value = []
  complaintImportDialogVisible.value = true
}
const handleComplaintFileChange = (file, fileList) => {
  complaintImportFileList.value = fileList
  console.log('选择的投诉文件:', file)
}
const confirmComplaintImport = () => {
  if (complaintImportFileList.value.length === 0) {
    ElMessage.warning('请选择要导入的文件')
    return
  }
  const file = complaintImportFileList.value[0]
  console.log('开始导入投诉数据:', file.name)
  // é¢„留导入逻辑
  // è¿™é‡Œå°†å®žçŽ°å®žé™…çš„æ–‡ä»¶è§£æžå’Œæ•°æ®å¯¼å…¥
  complaintImportDialogVisible.value = false
  ElMessage.success('导入操作已触发,预留导入逻辑')
}
const handleExceed = (files, fileList) => {
  ElMessage.warning('只能上传一个文件')
}
const handlePunishmentSizeChange = (size) => {
  punishmentPagination.value.pageSize = size
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„åˆ†é¡µé€»è¾‘
}
const handlePunishmentCurrentChange = (current) => {
  punishmentPagination.value.currentPage = current
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„åˆ†é¡µé€»è¾‘
}
const handleComplaintSizeChange = (size) => {
  complaintPagination.value.pageSize = size
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„åˆ†é¡µé€»è¾‘
}
const handleComplaintCurrentChange = (current) => {
  complaintPagination.value.currentPage = current
  // è¿™é‡Œå¯ä»¥æ·»åŠ å®žé™…çš„åˆ†é¡µé€»è¾‘
}
// åˆå§‹åŒ–图表
const initCharts = () => {
  // å·¡æŸ¥å®Œæˆæƒ…况趋势图
  if (inspectionTrendChart.value) {
    const chart = echarts.init(inspectionTrendChart.value)
    // è®¡ç®—时间范围并生成x轴数据
    const startTime = params.value.timeRange[0]
    const endTime = params.value.timeRange[1]
    const startDate = dayjs(startTime)
    const endDate = dayjs(endTime)
    const daysDiff = endDate.diff(startDate, 'day')
    let xAxisData = []
    let seriesData = []
    if (daysDiff <= 30) {
      // åŒä¸€ä¸ªæœˆå†…,按日显示
      for (let i = 0; i <= daysDiff; i++) {
        const date = startDate.add(i, 'day')
        xAxisData.push(date.format('MM/DD'))
        seriesData.push(60 + Math.floor(Math.random() * 30)) // ç”Ÿæˆéšæœºæ•°æ®
      }
    } else {
      // è¶…过一个月,按月显示
      const startMonth = startDate.startOf('month')
      const endMonth = endDate.endOf('month')
      const monthsDiff = endMonth.diff(startMonth, 'month')
      for (let i = 0; i <= monthsDiff; i++) {
        const date = startMonth.add(i, 'month')
        xAxisData.push(date.format('YYYY/MM'))
        seriesData.push(60 + Math.floor(Math.random() * 30)) // ç”Ÿæˆéšæœºæ•°æ®
      }
    }
    chart.setOption({
      xAxis: {
        type: 'category',
        data: xAxisData,
      },
      yAxis: {
        type: 'value',
        axisLabel: {
          formatter: '{value}%',
        },
      },
      series: [
        {
          data: seriesData,
          type: 'bar',
        },
      ],
    })
  }
  // é—®é¢˜ç±»åž‹åˆ†å¸ƒå›¾
  if (problemTypeChart.value) {
    const chart = echarts.init(problemTypeChart.value)
    chart.setOption({
      series: [
        {
          type: 'pie',
          data: [
            { value: 30, name: '油烟在线监测设备' },
            { value: 25, name: '油烟净化设施设备' },
            { value: 20, name: '油烟在线监测设备维护' },
            { value: 15, name: '油烟净化设施设备维护' },
            { value: 10, name: '油烟在线监测数据量级' },
            { value: 20, name: '空调和风机噪声' },
            { value: 18, name: '台账管理' },
            { value: 22, name: '信用承诺自评' },
          ],
          label: {
            show: true,
            formatter: '{b}: {d}%',
          },
        },
      ],
    })
  }
  // æ¯æ—¥å¤„罚数量图
  if (dailyPunishmentChart.value && punishmentDateRange.value.length > 0) {
    const chart = echarts.init(dailyPunishmentChart.value)
    // è®¡ç®—时间范围并生成x轴数据
    const startTime = punishmentDateRange.value[0]
    const endTime = punishmentDateRange.value[1]
    const startDate = dayjs(startTime)
    const endDate = dayjs(endTime)
    const daysDiff = endDate.diff(startDate, 'day')
    let xAxisData = []
    let seriesData = []
    // å¤„理处罚数据
    const punishmentData = punishmentTableData.value
    const dateFormat = daysDiff <= 30 ? 'MM/DD' : 'YYYY/MM'
    // ç”Ÿæˆæ—¥æœŸèŒƒå›´
    if (daysDiff <= 30) {
      // åŒä¸€ä¸ªæœˆå†…,按日显示
      for (let i = 0; i <= daysDiff; i++) {
        const date = startDate.add(i, 'day')
        xAxisData.push(date.format(dateFormat))
        // è®¡ç®—该日期的处罚数量
        const count = punishmentData.filter((item) => {
          const itemDate = dayjs(item.punishmentTime)
          return itemDate.format(dateFormat) === date.format(dateFormat)
        }).length
        seriesData.push(count)
      }
    } else {
      // è¶…过一个月,按月显示
      const startMonth = startDate.startOf('month')
      const endMonth = endDate.endOf('month')
      const monthsDiff = endMonth.diff(startMonth, 'month')
      for (let i = 0; i <= monthsDiff; i++) {
        const date = startMonth.add(i, 'month')
        xAxisData.push(date.format(dateFormat))
        // è®¡ç®—该月份的处罚数量
        const count = punishmentData.filter((item) => {
          const itemDate = dayjs(item.punishmentTime)
          return itemDate.format(dateFormat) === date.format(dateFormat)
        }).length
        seriesData.push(count)
      }
    }
    chart.setOption({
      xAxis: {
        type: 'category',
        data: xAxisData,
      },
      yAxis: {
        type: 'value',
      },
      series: [
        {
          data: seriesData,
          type: 'bar',
        },
      ],
    })
  }
  // åº—铺类型处罚分布图
  if (shopTypePunishmentChart.value) {
    const chart = echarts.init(shopTypePunishmentChart.value)
    chart.setOption({
      series: [
        {
          type: 'pie',
          data: [
            { value: 50, name: '中餐' },
            { value: 30, name: '烧烤' },
            { value: 20, name: '西餐' },
            { value: 15, name: '其他' },
          ],
          label: {
            show: true,
            formatter: '{b}: {d}%',
          },
        },
      ],
    })
  }
  // æ¯æ—¥æŠ•诉数量图
  if (dailyComplaintChart.value && complaintDateRange.value.length > 0) {
    const chart = echarts.init(dailyComplaintChart.value)
    // è®¡ç®—时间范围并生成x轴数据
    const startTime = complaintDateRange.value[0]
    const endTime = complaintDateRange.value[1]
    const startDate = dayjs(startTime)
    const endDate = dayjs(endTime)
    const daysDiff = endDate.diff(startDate, 'day')
    let xAxisData = []
    let seriesData = []
    // å¤„理投诉数据
    const complaintData = complaintTableData.value
    const dateFormat = daysDiff <= 30 ? 'MM/DD' : 'YYYY/MM'
    // ç”Ÿæˆæ—¥æœŸèŒƒå›´
    if (daysDiff <= 30) {
      // åŒä¸€ä¸ªæœˆå†…,按日显示
      for (let i = 0; i <= daysDiff; i++) {
        const date = startDate.add(i, 'day')
        xAxisData.push(date.format(dateFormat))
        // è®¡ç®—该日期的投诉数量
        const count = complaintData.filter((item) => {
          const itemDate = dayjs(item.complaintTime)
          return itemDate.format(dateFormat) === date.format(dateFormat)
        }).length
        seriesData.push(count)
      }
    } else {
      // è¶…过一个月,按月显示
      const startMonth = startDate.startOf('month')
      const endMonth = endDate.endOf('month')
      const monthsDiff = endMonth.diff(startMonth, 'month')
      for (let i = 0; i <= monthsDiff; i++) {
        const date = startMonth.add(i, 'month')
        xAxisData.push(date.format(dateFormat))
        // è®¡ç®—该月份的投诉数量
        const count = complaintData.filter((item) => {
          const itemDate = dayjs(item.complaintTime)
          return itemDate.format(dateFormat) === date.format(dateFormat)
        }).length
        seriesData.push(count)
      }
    }
    chart.setOption({
      xAxis: {
        type: 'category',
        data: xAxisData,
      },
      yAxis: {
        type: 'value',
      },
      series: [
        {
          data: seriesData,
          type: 'bar',
        },
      ],
    })
  }
  // æŠ•诉来源分布图
  if (sourceComplaintChart.value) {
    const chart = echarts.init(sourceComplaintChart.value)
    chart.setOption({
      series: [
        {
          type: 'pie',
          data: [
            { value: 40, name: '12345热线' },
            { value: 25, name: '居民投诉' },
            { value: 15, name: '网络平台' },
            { value: 5, name: '其他' },
          ],
          label: {
            show: true,
            formatter: '{b}: {d}%',
          },
        },
      ],
    })
  }
  // é—®é¢˜æ•´æ”¹çŽ‡ç»Ÿè®¡å›¾
  if (rectificationRateChart.value) {
    const chart = echarts.init(rectificationRateChart.value)
    chart.setOption({
      xAxis: {
        type: 'category',
        data: ['当日整改率', '48小时内整改率', '综合整改率', '审核通过率'],
      },
      yAxis: {
        type: 'value',
        axisLabel: {
          formatter: '{value}%',
        },
      },
      series: [
        {
          data: [
            parseFloat(inspectionStats.value.sameDayRectificationRate),
            parseFloat(inspectionStats.value.effectiveRectificationRate),
            parseFloat(inspectionStats.value.comprehensiveRectificationRate),
            parseFloat(inspectionStats.value.auditPassRate),
          ],
          type: 'bar',
          itemStyle: {
            color: function (params) {
              const colors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C']
              return colors[params.dataIndex]
            },
          },
        },
      ],
    })
  }
}
// ç›‘听窗口大小变化
const handleResize = () => {
  // é‡æ–°è°ƒæ•´å›¾è¡¨å¤§å°
  if (inspectionTrendChart.value) {
    echarts.init(inspectionTrendChart.value).resize()
  }
  if (problemTypeChart.value) {
    echarts.init(problemTypeChart.value).resize()
  }
  if (rectificationRateChart.value) {
    echarts.init(rectificationRateChart.value).resize()
  }
  if (dailyPunishmentChart.value) {
    echarts.init(dailyPunishmentChart.value).resize()
  }
  if (shopTypePunishmentChart.value) {
    echarts.init(shopTypePunishmentChart.value).resize()
  }
  if (dailyComplaintChart.value) {
    echarts.init(dailyComplaintChart.value).resize()
  }
  if (sourceComplaintChart.value) {
    echarts.init(sourceComplaintChart.value).resize()
  }
}
// ç›‘听params变化
watch(
  () => params.value,
  () => {
    refreshInspectionData()
  },
  { deep: true },
)
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  refreshInspectionData()
  searchPunishment()
  searchComplaint()
  initCharts()
  window.addEventListener('resize', handleResize)
})
// ç»„件卸载时清理事件监听
onUnmounted(() => {
  cleanup()
})
// æ¸…理
const cleanup = () => {
  window.removeEventListener('resize', handleResize)
}
</script>
<style scoped>
.monitor-control {
  padding: 20px;
  background-color: #f5f7fa;
  min-height: 100vh;
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.filter-group {
  display: flex;
  align-items: center;
}
.mr-2 {
  margin-right: 10px;
}
.stats-sections {
  display: flex;
  gap: 20px;
  margin-bottom: 30px;
}
.stats-section {
  flex: 1;
  background-color: #fff;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.stats-section h3 {
  margin-bottom: 15px;
  font-size: 16px;
  font-weight: 600;
  color: #303133;
}
.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
  margin-bottom: 20px;
}
.stat-item {
  background-color: #fff;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.progress-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
}
.progress-label {
  margin-top: 10px;
  font-size: 14px;
  font-weight: 500;
  color: #606266;
}
.progress-value {
  margin-top: 5px;
  font-size: 18px;
  font-weight: 600;
  color: #409eff;
}
.chart-container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
  gap: 20px;
  margin-bottom: 30px;
}
.chart-item {
  background-color: #fff;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.chart-item h3 {
  margin-bottom: 15px;
  font-size: 16px;
  font-weight: 600;
}
.chart-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
}
.chart-summary {
  font-size: 14px;
  color: #606266;
  font-weight: 500;
}
.chart {
  width: 100%;
  height: 300px;
}
.pagination-container {
  margin-top: 20px;
  display: flex;
  justify-content: flex-end;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
}
.import-container {
  padding: 20px 0;
}
.import-tip {
  margin-bottom: 20px;
  font-size: 14px;
  color: #606266;
}
.upload-demo {
  margin-bottom: 20px;
}
</style>
src/views/inspection/report/ReportManage.vue
@@ -1 +1,179 @@
<template>巡查评估报告</template>
<template>
  <div class="report-manage-container">
    <h2>巡查评估报告</h2>
    <!-- æŸ¥è¯¢æ¡ä»¶åŒºåŸŸ -->
    <div class="search-section">
      <el-form :inline="true" :model="searchForm" class="search-form">
        <el-form-item label="区县">
          <el-select v-model="searchForm.district" placeholder="请选择区县" style="width: 120px">
            <el-option
              v-for="district in districts"
              :key="district.value"
              :label="district.label"
              :value="district.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="searchReports">查询</el-button>
        </el-form-item>
      </el-form>
    </div>
    <!-- æŠ¥å‘Šè¡¨æ ¼åŒºåŸŸ -->
    <div class="table-section">
      <el-table :data="reportList" style="width: 100%">
        <el-table-column prop="index" label="索引编号" width="80" />
        <el-table-column prop="name" label="报告名称" min-width="200" />
        <el-table-column prop="district" label="区县" width="120" />
        <el-table-column prop="reportMonth" label="报告时间" width="120" />
        <el-table-column prop="generateTime" label="生成时间" width="180" />
        <el-table-column prop="auditStatus" label="审核状态" width="100">
          <template #default="scope">
            <el-tag :type="scope.row.auditStatus === '已审核' ? 'success' : 'warning'">
              {{ scope.row.auditStatus }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="120" fixed="right">
          <template #default="scope">
            <el-button size="small" type="primary" @click="viewReport(scope.row)">
              æŸ¥çœ‹æŠ¥å‘Š
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- æŠ¥å‘Šé¢„览抽屉 -->
    <el-drawer v-model="drawerVisible" title="报告预览" direction="rtl" size="80%">
      <!-- <div class="report-preview">
        <iframe :src="reportUrl" width="100%" height="800px" frameborder="0"></iframe>
      </div> -->
      <div ref="refWord" class="report-preview">
        <div :id="`word-preview`"></div>
      </div>
    </el-drawer>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import {
  exportDocx,
  prepareDocxBlob,
  preparePdf,
  previewDocx,
  downloadDocx,
  print,
} from '@/utils/doc'
const refWord = ref(null)
// ä¸Šæµ·å¸‚区县列表
const districts = [
  { value: 'xuhui', label: '徐汇区' },
  { value: 'changning', label: '长宁区' },
  { value: 'jingan', label: '静安区' },
  { value: 'putuo', label: '普陀区' },
  { value: 'hongkou', label: '虹口区' },
  { value: 'yangpu', label: '杨浦区' },
  { value: 'minhang', label: '闵行区' },
  { value: 'baoshan', label: '宝山区' },
  { value: 'pudong', label: '浦东新区' },
  { value: 'jiading', label: '嘉定区' },
  { value: 'jinshan', label: '金山区' },
  { value: 'songjiang', label: '松江区' },
  { value: 'qingpu', label: '青浦区' },
  { value: 'fengxian', label: '奉贤区' },
  { value: 'chongming', label: '崇明区' },
]
// æŸ¥è¯¢è¡¨å•
const searchForm = ref({
  district: 'xuhui', // é»˜è®¤é€‰ä¸­å¾æ±‡åŒº
})
// æŠ¥å‘Šåˆ—表数据
const reportList = ref([])
// æŠ½å±‰çŠ¶æ€
const drawerVisible = ref(false)
// æŠ¥å‘Šé¢„览URL
const reportUrl = ref('')
// æ¨¡æ‹Ÿæ•°æ®ç”Ÿæˆ
const generateMockData = () => {
  const data = []
  const baseName = '餐饮监管简报'
  const areas = ['天钥桥', '徐家汇', '衡山路', '龙华']
  for (let i = 1; i <= 10; i++) {
    const area = areas[Math.floor(Math.random() * areas.length)]
    const month = Math.floor(Math.random() * 12) + 1
    const date = new Date()
    date.setMonth(month - 1)
    data.push({
      index: i,
      name: `${searchForm.value.district === 'xuhui' ? '徐汇区' : '其他区'}${baseName}(${area})-2023å¹´${month}月`,
      district: searchForm.value.district === 'xuhui' ? '徐汇区' : '其他区',
      reportMonth: `2023å¹´${month}月`,
      generateTime: new Date().toLocaleString('zh-CN'),
      auditStatus: Math.random() > 0.5 ? '已审核' : '未审核',
    })
  }
  return data
}
// æŸ¥è¯¢æŠ¥å‘Š
const searchReports = () => {
  reportList.value = generateMockData()
}
// æŸ¥çœ‹æŠ¥å‘Š
const viewReport = (row) => {
  // ä½¿ç”¨æ¨¡æ‹Ÿçš„æŠ¥å‘Šæ–‡ä»¶
  reportUrl.value = '/徐汇区餐饮监管简报(天钥桥)-2023å¹´8月(1).docx'
  drawerVisible.value = true
  prepareDocxBlob(reportUrl.value).then((blob) => {
    previewDocx(blob, document.getElementById(`word-preview`))
  })
}
// é¡µé¢åŠ è½½æ—¶ç”Ÿæˆåˆå§‹æ•°æ®
onMounted(() => {
  searchReports()
})
</script>
<style scoped>
.report-manage-container {
  padding: 20px;
}
.search-section {
  margin-bottom: 20px;
  padding: 20px;
  background-color: #f5f7fa;
  border-radius: 4px;
}
.search-form {
  display: flex;
  align-items: center;
}
.table-section {
  background-color: #fff;
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.report-preview {
  padding: 20px;
}
</style>
src/views/system/SystemManage.vue
@@ -60,108 +60,6 @@
            />
          </div>
        </el-tab-pane>
        <!-- é¤é¥®åº—铺管理 -->
        <el-tab-pane label="餐饮店铺管理" name="restaurants">
          <div class="tab-content">
            <!-- æœç´¢å’Œæ·»åŠ æŒ‰é’® -->
            <div class="search-add-bar">
              <el-input
                v-model="restaurantSearchQuery"
                placeholder="搜索店铺"
                style="width: 200px"
                prefix-icon="el-icon-search"
              />
              <el-button type="primary" @click="openRestaurantDialog">
                <el-icon><Plus /></el-icon> æ·»åŠ åº—é“º
              </el-button>
            </div>
            <!-- åº—铺表格 -->
            <el-table :data="filteredRestaurants" style="width: 100%">
              <el-table-column prop="id" label="ID" width="80" />
              <el-table-column prop="name" label="店铺名称" />
              <el-table-column prop="address" label="地址" />
              <el-table-column prop="contact" label="联系人" />
              <el-table-column prop="phone" label="联系电话" />
              <el-table-column label="操作" width="250">
                <template #default="scope">
                  <el-button size="small" @click="editRestaurant(scope.row)"> ç¼–辑 </el-button>
                  <el-button size="small" type="danger" @click="deleteRestaurant(scope.row.id)">
                    åˆ é™¤
                  </el-button>
                  <el-button size="small" @click="manageDevices(scope.row)"> è®¾å¤‡ç®¡ç† </el-button>
                </template>
              </el-table-column>
            </el-table>
            <!-- åˆ†é¡µ -->
            <el-pagination
              v-model:current-page="restaurantCurrentPage"
              v-model:page-size="restaurantPageSize"
              :page-sizes="[10, 20, 50]"
              layout="total, sizes, prev, pager, next, jumper"
              :total="restaurants.length"
              style="margin-top: 20px"
            />
          </div>
        </el-tab-pane>
        <!-- è®¾å¤‡ç®¡ç† -->
        <el-tab-pane label="设备管理" name="devices">
          <div class="tab-content" v-if="selectedRestaurant">
            <h3>{{ selectedRestaurant.name }} - è®¾å¤‡åˆ—表</h3>
            <!-- æœç´¢å’Œæ·»åŠ æŒ‰é’® -->
            <div class="search-add-bar">
              <el-input
                v-model="deviceSearchQuery"
                placeholder="搜索设备"
                style="width: 200px"
                prefix-icon="el-icon-search"
              />
              <el-button type="primary" @click="openDeviceDialog">
                <el-icon><Plus /></el-icon> æ·»åŠ è®¾å¤‡
              </el-button>
            </div>
            <!-- è®¾å¤‡è¡¨æ ¼ -->
            <el-table :data="filteredDevices" style="width: 100%">
              <el-table-column prop="id" label="ID" width="80" />
              <el-table-column prop="deviceId" label="设备编号" />
              <el-table-column prop="type" label="设备类型" />
              <el-table-column prop="status" label="状态">
                <template #default="scope">
                  <el-tag :type="scope.row.status === 'online' ? 'success' : 'danger'">
                    {{ scope.row.status === 'online' ? '在线' : '离线' }}
                  </el-tag>
                </template>
              </el-table-column>
              <el-table-column prop="installDate" label="安装日期" />
              <el-table-column label="操作" width="200">
                <template #default="scope">
                  <el-button size="small" @click="editDevice(scope.row)"> ç¼–辑 </el-button>
                  <el-button size="small" type="danger" @click="deleteDevice(scope.row.id)">
                    åˆ é™¤
                  </el-button>
                </template>
              </el-table-column>
            </el-table>
            <!-- åˆ†é¡µ -->
            <el-pagination
              v-model:current-page="deviceCurrentPage"
              v-model:page-size="devicePageSize"
              :page-sizes="[10, 20, 50]"
              layout="total, sizes, prev, pager, next, jumper"
              :total="devices.length"
              style="margin-top: 20px"
            />
          </div>
          <div class="tab-content" v-else>
            <el-empty description="请先选择一个餐饮店铺" />
          </div>
        </el-tab-pane>
      </el-tabs>
    </el-card>