餐饮油烟智能监测与监管一体化平台
riku
2026-03-18 8e8d00477b1f30183d0d09cd7ec744067595dc46
2026.3.18
已修改7个文件
已添加3个文件
494 ■■■■ 文件已修改
components.d.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/exceed.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/exception.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/offline.png 补丁 | 查看 | 原始文档 | blame | 历史
src/components/core/AppAside.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/constants/menu.js 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/debug/debugdata.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/map/marks.js 275 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/DataDashboard.vue 104 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/SystemManage.vue 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
components.d.ts
@@ -39,7 +39,6 @@
    ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
    ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
    ElDialog: typeof import('element-plus/es')['ElDialog']
    ElDivider: typeof import('element-plus/es')['ElDivider']
    ElDrawer: typeof import('element-plus/es')['ElDrawer']
    ElEmpty: typeof import('element-plus/es')['ElEmpty']
    ElForm: typeof import('element-plus/es')['ElForm']
@@ -47,9 +46,8 @@
    ElHeader: typeof import('element-plus/es')['ElHeader']
    ElIcon: typeof import('element-plus/es')['ElIcon']
    ElImage: typeof import('element-plus/es')['ElImage']
    ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
    ElInput: typeof import('element-plus/es')['ElInput']
    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
    ElLink: typeof import('element-plus/es')['ElLink']
    ElMain: typeof import('element-plus/es')['ElMain']
    ElMenu: typeof import('element-plus/es')['ElMenu']
    ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
@@ -57,12 +55,12 @@
    ElOption: typeof import('element-plus/es')['ElOption']
    ElPagination: typeof import('element-plus/es')['ElPagination']
    ElPopover: typeof import('element-plus/es')['ElPopover']
    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']
    ElStep: typeof import('element-plus/es')['ElStep']
    ElSteps: typeof import('element-plus/es')['ElSteps']
    ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
    ElSwitch: typeof import('element-plus/es')['ElSwitch']
    ElTable: typeof import('element-plus/es')['ElTable']
@@ -72,8 +70,6 @@
    ElTag: typeof import('element-plus/es')['ElTag']
    ElText: typeof import('element-plus/es')['ElText']
    ElTooltip: typeof import('element-plus/es')['ElTooltip']
    ElTree: typeof import('element-plus/es')['ElTree']
    ElUpload: typeof import('element-plus/es')['ElUpload']
    FormCol: typeof import('./src/components/layout/FormCol.vue')['default']
    FYBgTaskCard: typeof import('./src/components/bg-task/FYBgTaskCard.vue')['default']
    FYBgTaskDialog: typeof import('./src/components/bg-task/FYBgTaskDialog.vue')['default']
@@ -100,6 +96,8 @@
    FYReconfrimButton: typeof import('./src/components/button/FYReconfrimButton.vue')['default']
    FYSearchBar: typeof import('./src/components/search-option/FYSearchBar.vue')['default']
    FYTable: typeof import('./src/components/table/FYTable.vue')['default']
    IEpDownload: typeof import('~icons/ep/download')['default']
    IEpInfoFilled: typeof import('~icons/ep/info-filled')['default']
    ItemDevice: typeof import('./src/components/list-item/ItemDevice.vue')['default']
    ItemMonitorObj: typeof import('./src/components/list-item/ItemMonitorObj.vue')['default']
    ItemScene: typeof import('./src/components/list-item/ItemScene.vue')['default']
@@ -149,7 +147,6 @@
  const ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
  const ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
  const ElDialog: typeof import('element-plus/es')['ElDialog']
  const ElDivider: typeof import('element-plus/es')['ElDivider']
  const ElDrawer: typeof import('element-plus/es')['ElDrawer']
  const ElEmpty: typeof import('element-plus/es')['ElEmpty']
  const ElForm: typeof import('element-plus/es')['ElForm']
@@ -157,9 +154,8 @@
  const ElHeader: typeof import('element-plus/es')['ElHeader']
  const ElIcon: typeof import('element-plus/es')['ElIcon']
  const ElImage: typeof import('element-plus/es')['ElImage']
  const ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
  const ElInput: typeof import('element-plus/es')['ElInput']
  const ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
  const ElLink: typeof import('element-plus/es')['ElLink']
  const ElMain: typeof import('element-plus/es')['ElMain']
  const ElMenu: typeof import('element-plus/es')['ElMenu']
  const ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
@@ -167,12 +163,12 @@
  const ElOption: typeof import('element-plus/es')['ElOption']
  const ElPagination: typeof import('element-plus/es')['ElPagination']
  const ElPopover: typeof import('element-plus/es')['ElPopover']
  const 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 ElStep: typeof import('element-plus/es')['ElStep']
  const ElSteps: typeof import('element-plus/es')['ElSteps']
  const ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
  const ElSwitch: typeof import('element-plus/es')['ElSwitch']
  const ElTable: typeof import('element-plus/es')['ElTable']
@@ -182,8 +178,6 @@
  const ElTag: typeof import('element-plus/es')['ElTag']
  const ElText: typeof import('element-plus/es')['ElText']
  const ElTooltip: typeof import('element-plus/es')['ElTooltip']
  const ElTree: typeof import('element-plus/es')['ElTree']
  const ElUpload: typeof import('element-plus/es')['ElUpload']
  const FormCol: typeof import('./src/components/layout/FormCol.vue')['default']
  const FYBgTaskCard: typeof import('./src/components/bg-task/FYBgTaskCard.vue')['default']
  const FYBgTaskDialog: typeof import('./src/components/bg-task/FYBgTaskDialog.vue')['default']
@@ -210,6 +204,8 @@
  const FYReconfrimButton: typeof import('./src/components/button/FYReconfrimButton.vue')['default']
  const FYSearchBar: typeof import('./src/components/search-option/FYSearchBar.vue')['default']
  const FYTable: typeof import('./src/components/table/FYTable.vue')['default']
  const IEpDownload: typeof import('~icons/ep/download')['default']
  const IEpInfoFilled: typeof import('~icons/ep/info-filled')['default']
  const ItemDevice: typeof import('./src/components/list-item/ItemDevice.vue')['default']
  const ItemMonitorObj: typeof import('./src/components/list-item/ItemMonitorObj.vue')['default']
  const ItemScene: typeof import('./src/components/list-item/ItemScene.vue')['default']
src/assets/exceed.png
src/assets/exception.png
src/assets/offline.png
src/components/core/AppAside.vue
@@ -33,7 +33,7 @@
      <!-- </el-scrollbar> -->
      <!-- 商标 -->
      <el-row ref="subTitleRef" class="sub-title" justify="center">
        <el-space>{{ collapse ? '' : subTitle }}</el-space>
        <!-- <el-space>{{ collapse ? '' : subTitle }}</el-space> -->
      </el-row>
    </el-scrollbar>
  </el-aside>
@@ -55,7 +55,7 @@
    return {
      // collapse: false,
      menuHeight: '80vh',
      title: '油烟智能监测与监管',
      title: '餐饮油烟智能监测监管',
      subTitle: '©上海飞羽环保科技有限公司',
      appIcon: AppIcon,
    }
src/constants/menu.js
@@ -96,6 +96,11 @@
    name: '环信码',
    children: [
      {
        path: '/index/inspection/scene-info',
        icon: 'solar:shop-2-line-duotone',
        name: '店铺管理',
      },
      {
        path: '/index/analysis/auto-evalution',
        icon: 'solar:pie-chart-3-line-duotone',
        name: '自动评估',
@@ -103,18 +108,14 @@
      {
        path: '/index/analysis/huanxincode-manage',
        icon: 'solar:archive-down-minimlistic-line-duotone',
        name: '环信码',
        name: '评估排名',
      },
      {
        path: '/index/inspection/report-manage',
        icon: 'solar:folder-favourite-bookmark-line-duotone',
        name: '评估报告',
      },
      {
        path: '/index/inspection/scene-info',
        icon: 'solar:shop-2-line-duotone',
        name: '店铺管理',
      },
      // {
      //   path: '/index/analysis/data-product',
      //   icon: 'solar:document-add-line-duotone',
src/debug/debugdata.js
@@ -68,6 +68,12 @@
  // 先添加指定的店铺
  specifiedShops.forEach((name, index) => {
    // 随机生成在线状态(80%概率在线)
    const isOnline = Math.random() < 0.8
    // 随机生成异常状态(只有在线时才可能有异常)
    // 0: 油烟浓度超标, 1: 供电异常, 2: 设备或网络异常, 3: 无异常
    const exceptionStatus = isOnline ? Math.floor(Math.random() * 4) : 2 // 离线时默认为设备或网络异常
    shops.push({
      shop: {
        name: name,
@@ -76,6 +82,8 @@
        longitude: 121.45 + Math.random() * 0.1,
        ringCodeLevel: ringCodeLevels[Math.floor(Math.random() * ringCodeLevels.length)],
        ringCodePublishTime: '2023-03-16 10:00:00',
        isOnline: isOnline,
        exceptionStatus: exceptionStatus,
      },
      recentData: generateRecentData(),
    })
@@ -88,6 +96,12 @@
    const suffix = nameSuffixes[Math.floor(Math.random() * nameSuffixes.length)]
    const randomName = `${prefix}${cuisine}${suffix}${i}`
    // 随机生成在线状态(80%概率在线)
    const isOnline = Math.random() < 0.8
    // 随机生成异常状态(只有在线时才可能有异常)
    // 0: 油烟浓度超标, 1: 供电异常, 2: 设备或网络异常, 3: 无异常
    const exceptionStatus = isOnline ? Math.floor(Math.random() * 4) : 2 // 离线时默认为设备或网络异常
    shops.push({
      shop: {
        name: randomName,
@@ -96,6 +110,8 @@
        longitude: 121.41 + Math.random() * 0.1,
        ringCodeLevel: ringCodeLevels[Math.floor(Math.random() * ringCodeLevels.length)],
        ringCodePublishTime: '2023-03-16 10:00:00',
        isOnline: isOnline,
        exceptionStatus: exceptionStatus,
      },
      recentData: generateRecentData(),
    })
src/utils/map/marks.js
@@ -6,17 +6,22 @@
import { useToolboxStore } from '@/stores/toolbox'
import util from './util'
import * as echarts from 'echarts'
import exceedIcon from '@/assets/exceed.png'
import exceptionIcon from '@/assets/exception.png'
import offlineIcon from '@/assets/offline.png'
const toolboxStore = useToolboxStore()
var _massMarks = undefined
// 环信码等级和对应颜色
const ringCodeLevelColors = [
  '#52c41a', // 绿色
  '#faad14', // 黄色
  '#f5222d', // 红色
]
// 状态图标配置
const statusIcons = {
  exceed: exceedIcon, // 油烟浓度超标
  exception: exceptionIcon, // 供电异常
  offline: offlineIcon, // 设备或网络异常
  online: createCustomMarkerOnline(), // 在线状态
  offlineStatus: createCustomMarkerOffline(), // 离线状态
}
/**
 * 绘制海量点标记
@@ -28,6 +33,8 @@
 * @param {number} shops[].shop.longitude 经度
 * @param {string} shops[].shop.ringCodeLevel 最新环信码等级
 * @param {string} shops[].shop.ringCodePublishTime 最新环信码发布时间
 * @param {boolean} shops[].shop.isOnline 在线状态
 * @param {number} shops[].shop.exceptionStatus 异常状态(0: 油烟浓度超标, 1: 供电异常, 2: 设备或网络异常, 3: 无异常)
 * @param {Array} shops[].recentData 近1小时的监测数据
 * @param {string} shops[].recentData[].sampleTime 数据采样时间
 * @param {number} shops[].recentData[].oilSmokeConcentration 油烟浓度
@@ -36,21 +43,65 @@
 */
function drawMassMarks(shops) {
  // 配置样式
  const massMarksStyle = ringCodeLevelColors.map((color, index) => ({
    url: createCustomMarker(color),
  const massMarksStyle = [
    {
      url: statusIcons.exceed,
    size: new AMap.Size(20, 20),
    anchor: new AMap.Pixel(10, 10),
  }))
    },
    {
      url: statusIcons.exception,
      size: new AMap.Size(20, 20),
      anchor: new AMap.Pixel(10, 10),
    },
    {
      url: statusIcons.offline,
      size: new AMap.Size(20, 20),
      anchor: new AMap.Pixel(10, 10),
    },
    {
      url: statusIcons.online,
      size: new AMap.Size(32, 32),
      anchor: new AMap.Pixel(10, 10),
    },
    {
      url: statusIcons.offlineStatus,
      size: new AMap.Size(32, 32),
      anchor: new AMap.Pixel(10, 10),
    },
  ]
  // 准备海量点数据
  const massMarksData = shops.map((shop, index) => {
    // 根据环信码等级获取颜色
    const color = getColorByRingCodeLevel(shop.shop.ringCodeLevel)
    // 根据异常状态和在线状态获取样式索引
    let styleIndex = 3 // 默认在线状态
    if (shop.shop.exceptionStatus !== undefined) {
      switch (shop.shop.exceptionStatus) {
        case 0: // 油烟浓度超标
          styleIndex = 0
          break
        case 1: // 供电异常
          styleIndex = 1
          break
        case 2: // 设备或网络异常
          styleIndex = 2
          break
        case 3: // 无异常,根据在线状态决定
          styleIndex = shop.shop.isOnline ? 3 : 4
          break
        default:
          styleIndex = shop.shop.isOnline ? 3 : 4
      }
    } else {
      // 没有异常状态时,根据在线状态决定
      styleIndex = shop.shop.isOnline ? 3 : 4
    }
    return {
      id: index,
      name: shop.shop.name,
      lnglat: [shop.shop.longitude, shop.shop.latitude],
      style: shop.shop.ringCodeLevel, // 样式索引,对应 massMarksStyle
      style: styleIndex, // 样式索引,对应 massMarksStyle
      extData: shop, // 存储完整的店铺信息
    }
  })
@@ -134,6 +185,46 @@
}
/**
 * 根据异常状态获取中文
 * @param {number} status 异常状态
 * @returns {string} 中文表示
 */
function getExceptionStatusText(status) {
  switch (status + '') {
    case '0':
      return '油烟浓度超标'
    case '1':
      return '供电异常'
    case '2':
      return '设备或网络异常'
    case '3':
      return '无异常'
    default:
      return '未知状态'
  }
}
/**
 * 根据异常状态获取颜色
 * @param {number} status 异常状态
 * @returns {string} 颜色值
 */
function getColorByExceptionStatus(status) {
  switch (status + '') {
    case '0':
      return '#f5222d' // 油烟浓度超标 - 红色
    case '1':
      return '#faad14' // 供电异常 - 黄色
    case '2':
      return '#8c8c8c' // 设备或网络异常 - 灰色
    case '3':
      return '#52c41a' // 无异常 - 绿色
    default:
      return '#8c8c8c' // 灰色
  }
}
/**
 * 创建自定义标记
 * @param {string} color 标记颜色
 * @returns {string} 标记的SVG URL
@@ -149,41 +240,151 @@
}
/**
 * 创建在线状态的SVG标记(油烟监测设备形象)
 * @returns {string} 标记的SVG URL
 */
function createCustomMarkerOnline() {
  const svg = `
    <svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
      <!-- 设备主体 - 圆角矩形 -->
      <rect x="5" y="8" width="22" height="16" rx="3" fill="#52c41a" stroke="white" stroke-width="2"/>
      <!-- 设备顶部 - 弧形 -->
      <path d="M5 8 Q16 3 27 8" stroke="white" stroke-width="2" fill="#389e0d"/>
      <!-- 设备底部 - 弧形 -->
      <path d="M5 24 Q16 29 27 24" stroke="white" stroke-width="2" fill="#389e0d"/>
      <!-- 设备显示屏 -->
      <rect x="8" y="11" width="16" height="10" rx="2" fill="white"/>
      <!-- 显示屏数据 -->
      <path d="M11 14 L21 14" stroke="#52c41a" stroke-width="1.5"/>
      <path d="M11 17 L18 17" stroke="#52c41a" stroke-width="1.5"/>
      <path d="M11 20 L15 20" stroke="#52c41a" stroke-width="1.5"/>
      <!-- 设备天线 -->
      <line x1="16" y1="8" x2="16" y2="3" stroke="white" stroke-width="1.5"/>
      <circle cx="16" cy="3" r="1.5" fill="white"/>
      <!-- 设备指示灯 -->
      <circle cx="27" cy="16" r="3" fill="#ffffff"/>
      <circle cx="27" cy="16" r="1.5" fill="#52c41a"/>
      <!-- 装饰线条 -->
      <line x1="5" y1="13" x2="6" y2="13" stroke="white" stroke-width="1.5"/>
      <line x1="5" y1="19" x2="6" y2="19" stroke="white" stroke-width="1.5"/>
    </svg>
  `
  return 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg)))
}
/**
 * 创建离线状态的SVG标记(油烟监测设备形象)
 * @returns {string} 标记的SVG URL
 */
function createCustomMarkerOffline() {
  const svg = `
    <svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
      <!-- 设备主体 - 圆角矩形 -->
      <rect x="5" y="8" width="22" height="16" rx="3" fill="#8c8c8c" stroke="white" stroke-width="2"/>
      <!-- 设备顶部 - 弧形 -->
      <path d="M5 8 Q16 3 27 8" stroke="white" stroke-width="2" fill="#666666"/>
      <!-- 设备底部 - 弧形 -->
      <path d="M5 24 Q16 29 27 24" stroke="white" stroke-width="2" fill="#666666"/>
      <!-- 设备显示屏 -->
      <rect x="8" y="11" width="16" height="10" rx="2" fill="white"/>
      <!-- 显示屏无数据 - 交叉线 -->
      <line x1="11" y1="12" x2="21" y2="22" stroke="#8c8c8c" stroke-width="2"/>
      <line x1="11" y1="22" x2="21" y2="12" stroke="#8c8c8c" stroke-width="2"/>
      <!-- 设备天线 -->
      <line x1="16" y1="8" x2="16" y2="3" stroke="white" stroke-width="1.5"/>
      <circle cx="16" cy="3" r="1.5" fill="white"/>
      <!-- 设备指示灯 -->
      <circle cx="27" cy="16" r="3" fill="#ffffff"/>
      <circle cx="27" cy="16" r="1.5" fill="#8c8c8c"/>
      <!-- 装饰线条 -->
      <line x1="5" y1="13" x2="6" y2="13" stroke="white" stroke-width="1.5"/>
      <line x1="5" y1="19" x2="6" y2="19" stroke="white" stroke-width="1.5"/>
    </svg>
  `
  return 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg)))
}
/**
 * 显示店铺信息窗口
 * @param {Object} shop 店铺对象
 */
function showShopInfoWindow(shop) {
  // 准备信息窗口内容
  // const content = `
  //   <div style="padding: 10px; max-width: 300px;">
  //     <h3 style="margin: 0 0 10px 0; color: #333;">${shop.shop.name}</h3>
  //     <div style="font-size: 14px; line-height: 1.5;">
  //       <p><strong>地址:</strong>${shop.shop.address}</p>
  //       <p><strong>环信码等级:</strong><span style="color: ${getColorByRingCodeLevel(shop.shop.ringCodeLevel)}">${shop.shop.ringCodeLevel}</span></p>
  //       <p><strong>环信码发布时间:</strong>${shop.shop.ringCodePublishTime}</p>
  //       <h4 style="margin: 10px 0 5px 0; color: #666;">近1小时监测数据</h4>
  //       <div style="max-height: 150px; overflow-y: auto;">
  //         ${shop.recentData
  //           .map(
  //             (item) => `
  //           <div style="padding: 5px; border-bottom: 1px solid #f0f0f0;">
  //             <p><strong>采样时间:</strong>${item.sampleTime}</p>
  //             <p><strong>油烟浓度:</strong>${item.oilSmokeConcentration} mg/m³</p>
  //             <p><strong>净化器电流:</strong>${item.purifierCurrent} A</p>
  //             <p><strong>风机电流:</strong>${item.fanCurrent} A</p>
  //           </div>
  //         `,
  //           )
  //           .join('')}
  //       </div>
  //     </div>
  //   </div>
  // `
  // 获取在线状态文本
  const onlineStatusText = shop.shop.isOnline ? '在线' : '离线'
  // 获取异常状态文本
  const exceptionStatusText = getExceptionStatusText(shop.shop.exceptionStatus)
  // 获取异常状态颜色
  const exceptionStatusColor = getColorByExceptionStatus(shop.shop.exceptionStatus)
  // 根据状态获取对应的图标
  let statusIcon = statusIcons.online // 默认在线图标
  if (shop.shop.exceptionStatus !== undefined) {
    switch (shop.shop.exceptionStatus) {
      case 0:
        statusIcon = statusIcons.exceed
        break
      case 1:
        statusIcon = statusIcons.exception
        break
      case 2:
        statusIcon = statusIcons.offline
        break
      case 3:
        statusIcon = shop.shop.isOnline ? statusIcons.online : statusIcons.offlineStatus
        break
      default:
        statusIcon = shop.shop.isOnline ? statusIcons.online : statusIcons.offlineStatus
    }
  } else {
    statusIcon = shop.shop.isOnline ? statusIcons.online : statusIcons.offlineStatus
  }
  // 根据在线状态获取图标
  const onlineIcon = shop.shop.isOnline ? statusIcons.online : statusIcons.offlineStatus
  // 根据异常状态获取图标
  let exceptionIcon = statusIcons.online // 默认在线图标
  if (shop.shop.exceptionStatus !== undefined) {
    switch (shop.shop.exceptionStatus) {
      case 0:
        exceptionIcon = statusIcons.exceed
        break
      case 1:
        exceptionIcon = statusIcons.exception
        break
      case 2:
        exceptionIcon = statusIcons.offline
        break
      case 3:
        exceptionIcon = statusIcons.online
        break
      default:
        exceptionIcon = statusIcons.online
    }
  }
  const content = `
    <div style="padding: 10px; width: 400px;">
      <h3 style="margin: 0 0 10px 0; color: #333;">${shop.shop.name}</h3>
      <div style="font-size: 14px; line-height: 1.5;">
        <p><strong>地址:</strong>${shop.shop.address}</p>
        <p><strong>在线状态:</strong><span style="color: ${shop.shop.isOnline ? '#52c41a' : '#8c8c8c'}">${onlineStatusText}</span> </p>
        <p><strong>异常状态:</strong><span style="color: ${exceptionStatusColor}">${exceptionStatusText}</span></p>
        <p><strong>环信码等级:</strong><span style="color: ${getColorByRingCodeLevel(shop.shop.ringCodeLevel)}">${getRingCodeLevelText(shop.shop.ringCodeLevel)}</span></p>
        <p><strong>环信码发布时间:</strong>${shop.shop.ringCodePublishTime}</p>
        <h4 style="margin: 10px 0 5px 0; color: #666;">近1小时监测数据</h4>
src/views/monitor/DataDashboard.vue
@@ -186,10 +186,10 @@
          </div>
        </div>
        <!-- 任务完成率 -->
        <!-- 巡查点次 -->
        <div class="metric-card">
          <div class="card-header">
            <div class="card-title">任务完成率</div>
            <div class="card-title">巡查点次</div>
            <div class="card-icon task-icon">
              <svg
                width="20"
@@ -215,18 +215,18 @@
              </svg>
            </div>
          </div>
          <div class="card-value">{{ metrics.taskCompletionRate }}%</div>
          <div class="card-value">{{ metrics.inspectionPoints }}</div>
          <div class="card-trend">
            <span
              class="trend-arrow"
              :class="{
                up: metrics.taskCompletionRateTrend > 0,
                down: metrics.taskCompletionRateTrend < 0,
                up: metrics.inspectionPointsTrend > 0,
                down: metrics.inspectionPointsTrend < 0,
              }"
            >
              {{ metrics.taskCompletionRateTrend > 0 ? '↑' : '↓' }}
              {{ metrics.inspectionPointsTrend > 0 ? '↑' : '↓' }}
            </span>
            <span class="trend-text">{{ Math.abs(metrics.taskCompletionRateTrend) }}%</span>
            <span class="trend-text">{{ Math.abs(metrics.inspectionPointsTrend) }}</span>
            <span class="trend-label">{{ getCompareLabel() }}</span>
          </div>
        </div>
@@ -306,6 +306,43 @@
      <!-- 设备状态饼图 -->
      <div class="device-status-chart">
        <canvas id="deviceStatusChart"></canvas>
      </div>
    </div>
    <!-- 地图图例 -->
    <div class="map-legend">
      <div class="legend-header">
        <h4>图例</h4>
      </div>
      <div class="legend-items">
        <div class="legend-item">
          <img src="@/assets/exceed.png" alt="油烟浓度超标" class="legend-icon" />
          <span class="legend-text">油烟浓度超标</span>
        </div>
        <div class="legend-item">
          <img src="@/assets/exception.png" alt="供电异常" class="legend-icon" />
          <span class="legend-text">供电异常</span>
        </div>
        <div class="legend-item">
          <img src="@/assets/offline.png" alt="设备或网络异常" class="legend-icon" />
          <span class="legend-text">设备或网络异常</span>
        </div>
        <div class="legend-item">
          <img
            src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSI1IiB5PSI4IiB3aWR0aD0iMjIiIGhlaWdodD0iMTYiIHJ4PSIzIiBmaWxsPSIjNTJjNDFhIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiLz48cGF0aCBkPSJNNSA4IFEgMTYgMyAyNyA4IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9IiMzODllMGQiLz48cGF0aCBkPSJNNSAyNCBRIDE2IDI5IDI3IDI0IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9IiM2NjY2NjYiLz48cmVjdCB4PSI4IiB5PSIxMSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjEwIiByeD0iMiIgZmlsbD0id2hpdGUiLz48cGF0aCBkPSJNMTIgMTQgTCAyMSAxNCIgc3Ryb2tlPSIjNTJjNDFhIiBzdHJva2Utd2lkdGg9IjEuNSIvPjxwYXRoIGQ9Ik0xMiAxNyBMIDE4IDE3IiBzdHJva2U9IiM1MmM0MWEiIHN0cm9rZS13aWR0aD0iMS41Ii8+PHBhdGggZD0iTTEyIDIwIEwgMTUgMjAiIHN0cm9rZT0iIzUyYzQxYSIgc3Ryb2tlLXdpZHRoPSIxLjUiLz48bGluZSB4MT0iMTYiIHkxPSI4IiB4Mj0iMTYiIHkyPSIzIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEuNSIvPjxjaXJjbGUgY3g9IjE2IiBjeT0iMyIgcj0iMS41IiBmaWxsPSJ3aGl0ZSIvPjxjaXJjbGUgY3g9IjI3IiBjeT0iMTYiIHI9IjMiIGZpbGw9IiNmZmZmZmYiLz48Y2lyY2xlIGN4PSIyNyIgY3k9IjE2IiByPSIxLjUiIGZpbGw9IiM1MmM0MWEiLz48bGluZSB4MT0iNSIgeTE9IjEzIiB4Mj0iNiIgeTI9IjEzIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEuNSIvPjxsaW5lIHgxPSI1IiB5MT0iMTkiIHgyPSI2IiB5Mj0iMTkiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS41Ii8+PC9zdmc+"
            alt="在线状态"
            class="legend-icon"
          />
          <span class="legend-text">在线状态</span>
        </div>
        <div class="legend-item">
          <img
            src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSI1IiB5PSI4IiB3aWR0aD0iMjIiIGhlaWdodD0iMTYiIHJ4PSIzIiBmaWxsPSIjOGM4YzhjIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiLz48cGF0aCBkPSJNNSA4IFEgMTYgMyAyNyA4IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9IiM2NjY2NjYiLz48cGF0aCBkPSJNNSAyNCBRIDE2IDI5IDI3IDI0IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9IiM2NjY2NjYiLz48cmVjdCB4PSI4IiB5PSIxMSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjEwIiByeD0iMiIgZmlsbD0id2hpdGUiLz48bGluZSB4MT0iMTEiIHkxPSIxMiIgeDI9IjIxIiB5Mj0iMjIiIHN0cm9rZT0iIzhjOGM4YyIgc3Ryb2tlLXdpZHRoPSIyIi8+PGxpbmUgeDE9IjExIiB5MT0iMjIiIHgyPSIyMSIgeTI9IjEyIiBzdHJva2U9IiM4YzhjOGMiIHN0cm9rZS13aWR0aD0iMiIvPjxsaW5lIHgxPSIxNiIgeTE9IjgiIHgyPSIxNiIgeTI9IjMiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS41Ii8+PGNpcmNsZSBjeD0iMTYiIGN5PSIzIiByPSIxLjUiIGZpbGw9IndoaXRlIi8+PGNpcmNsZSBjeD0iMjciIGN5PSIxNiIgcj0iMyIgZmlsbD0iI2ZmZmZmZiIvPjxjaXJjbGUgY3g9IjI3IiBjeT0iMTYiIHI9IjEuNSIgZmlsbD0iIzhjOGM4YyIvPjxsaW5lIHgxPSI1IiB5MT0iMTMiIHgyPSI2IiB5Mj0iMTMiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS41Ii8+PGxpbmUgeDE9IjUiIHkxPSIxOSIgeDI9IjYiIHkyPSIxOSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiLz48L3N2Zz4="
            alt="离线状态"
            class="legend-icon"
          />
          <span class="legend-text">离线状态</span>
        </div>
      </div>
    </div>
    <!-- 巡查情况统计卡片 -->
@@ -469,8 +506,8 @@
        onlineRateTrend: 2,
        purifierEfficiency: 85,
        purifierEfficiencyTrend: -3,
        taskCompletionRate: 78,
        taskCompletionRateTrend: 10,
        inspectionPoints: 350,
        inspectionPointsTrend: 50,
      },
      overview: {
        totalShops: 245,
@@ -601,8 +638,8 @@
          onlineRateTrend: Math.floor(Math.random() * 10) - 5,
          purifierEfficiency: Math.floor(Math.random() * 30) + 70,
          purifierEfficiencyTrend: Math.floor(Math.random() * 10) - 5,
          taskCompletionRate: Math.floor(Math.random() * 40) + 60,
          taskCompletionRateTrend: Math.floor(Math.random() * 15) - 7,
          inspectionPoints: Math.floor(Math.random() * 100) + 300,
          inspectionPointsTrend: Math.floor(Math.random() * 100) - 50,
        }
        // 更新巡查统计数据
@@ -1263,6 +1300,51 @@
  animation: blink 1s infinite;
}
/* 地图图例样式 */
.map-legend {
  position: absolute;
  bottom: 4px;
  right: 4px;
  width: 200px;
  background-color: rgba(255, 255, 255, 0.9);
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  padding: 16px;
  z-index: 10;
}
.legend-header {
  margin-bottom: 12px;
}
.legend-header h4 {
  font-size: 14px;
  font-weight: 600;
  color: #262626;
  margin: 0;
  text-align: center;
}
.legend-items {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.legend-item {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: #4e5969;
}
.legend-icon {
  width: 24px;
  height: 24px;
  object-fit: contain;
}
/* 响应式设计 */
/* @media (max-width: 1200px) {
  .top-cards {
src/views/system/SystemManage.vue
@@ -202,12 +202,60 @@
// 用户管理相关
const users = ref([
  { id: 1, username: 'admin', name: '管理员', role: 'admin', status: 'active' },
  { id: 2, username: 'user1', name: '用户1', role: 'user', status: 'active' },
  {
    id: 3,
    username: 'restaurant1',
    name: '店铺管理员1',
    id: 1,
    username: 'fuxiaojie',
    name: '付小姐在成都',
    role: 'restaurant_admin',
    status: 'active',
  },
  { id: 2, username: 'jike', name: '吉刻联盟', role: 'restaurant_admin', status: 'active' },
  { id: 3, username: 'jiazaitala', name: '家在塔啦', role: 'restaurant_admin', status: 'active' },
  { id: 4, username: 'langlailiao', name: '狼来了', role: 'restaurant_admin', status: 'active' },
  { id: 5, username: 'lekaisai', name: '乐凯撒星游店', role: 'restaurant_admin', status: 'active' },
  {
    id: 6,
    username: 'xinyuan',
    name: '馨远美食小镇(哈尼美食广场)',
    role: 'restaurant_admin',
    status: 'active',
  },
  { id: 7, username: 'bangyuehan', name: '棒约翰', role: 'restaurant_admin', status: 'active' },
  { id: 8, username: 'nangtang', name: '弄堂咪道', role: 'restaurant_admin', status: 'active' },
  {
    id: 9,
    username: 'yangji',
    name: '杨记齐齐哈尔烤肉',
    role: 'restaurant_admin',
    status: 'active',
  },
  {
    id: 10,
    username: 'rensheng',
    name: '上海稔传餐饮管理有限公司(人生一串)',
    role: 'restaurant_admin',
    status: 'active',
  },
  { id: 11, username: 'yuanjia', name: '缘家', role: 'restaurant_admin', status: 'active' },
  {
    id: 12,
    username: 'quansheng',
    name: '泉盛餐饮(上海)有限公司(食其家)',
    role: 'restaurant_admin',
    status: 'active',
  },
  { id: 13, username: 'fengmao', name: '丰茂烤串', role: 'restaurant_admin', status: 'active' },
  {
    id: 14,
    username: 'taihuang',
    name: '上海泰煌餐饮管理有限公司(泰煌鸡)',
    role: 'restaurant_admin',
    status: 'active',
  },
  {
    id: 15,
    username: 'chenxi',
    name: '徐汇区辰熙餐馆(小铁君串烧居酒屋)',
    role: 'restaurant_admin',
    status: 'active',
  },