餐饮油烟智能监测与监管一体化平台
riku
2026-03-20 59af55dc3e72f8f2655ae06af9d1b6f766bac423
src/utils/map/marks.js
@@ -2,82 +2,605 @@
 * 高德地图点标记绘制相关
 */
import { map, AMap } from './index';
import { useToolboxStore } from '@/stores/toolbox';
import { map, AMap } from './index'
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();
const toolboxStore = useToolboxStore()
var _massMarks = undefined;
var _massMarks = undefined
// 状态图标配置
const statusIcons = {
  exceed: exceedIcon, // 油烟浓度超标
  exception: exceptionIcon, // 供电异常
  offline: offlineIcon, // 设备或网络异常
  online: createCustomMarkerOnline(), // 在线状态
  offlineStatus: createCustomMarkerOffline(), // 离线状态
}
/**
 * 绘制海量点标记
 * @param {Array} shops 店铺对象数组
 * @param {Object} shops[].shop 店铺基本信息
 * @param {string} shops[].shop.name 店铺名称
 * @param {string} shops[].shop.address 店铺地址
 * @param {number} shops[].shop.latitude 纬度
 * @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 油烟浓度
 * @param {number} shops[].recentData[].purifierCurrent 净化器电流
 * @param {number} shops[].recentData[].fanCurrent 风机电流
 */
function drawMassMarks(shops) {
  // 配置样式
  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) => {
    // 根据异常状态和在线状态获取样式索引
    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: styleIndex, // 样式索引,对应 massMarksStyle
      extData: shop, // 存储完整的店铺信息
    }
  })
  // 清除现有的海量点标记
  if (window.massMarks) {
    window.massMarks.clear()
  }
  // 创建新的海量点标记
  window.massMarks = new AMap.MassMarks(massMarksData, {
    zIndex: 111,
    cursor: 'pointer',
    style: massMarksStyle,
  })
  // 添加点击事件
  window.massMarks.on('click', function (e) {
    const shop = e.data.extData
    showShopInfoWindow(shop)
  })
  var marker = new AMap.Marker({
    content: ' ',
    map: map,
    offset: new AMap.Pixel(13, 12),
  })
  var timeout
  window.massMarks.on('mouseover', (e) => {
    if (timeout) {
      clearTimeout(timeout)
    }
    marker.setPosition(e.data.lnglat)
    marker.setLabel({ content: e.data.name })
    map.add(marker)
    timeout = setTimeout(() => {
      map.remove(marker)
    }, 2000)
  })
  // 添加到地图
  window.massMarks.setMap(map)
  util.setBound(massMarksData.map((item) => item.lnglat))
}
/**
 * 根据环信码等级获取颜色
 * @param {string} level 环信码等级
 * @returns {string} 颜色值
 */
function getColorByRingCodeLevel(level) {
  switch (level + '') {
    case '0':
      return '#52c41a' // 绿色
    case '1':
      return '#faad14' // 黄色
    case '2':
      return '#f5222d' // 红色
    default:
      return '#8c8c8c' // 灰色
  }
}
/**
 * 根据环信码等级获取中文
 * @param {string} level 环信码等级
 * @returns {string} 中文表示
 */
function getRingCodeLevelText(level) {
  switch (level + '') {
    case '0':
      return '绿色'
    case '1':
      return '黄色'
    case '2':
      return '红色'
    default:
      return '未知'
  }
}
/**
 * 根据异常状态获取中文
 * @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
 */
function createCustomMarker(color) {
  const svg = `
    <svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
      <circle cx="10" cy="10" r="8" fill="${color}" stroke="white" stroke-width="2"/>
      <circle cx="10" cy="10" r="3" fill="white"/>
    </svg>
  `
  return 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg)))
}
/**
 * 创建在线状态的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 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>
        <div style="display: flex; flex-direction: row;">
          <span style="flex:1"><strong>在线状态:</strong><span style="color: ${shop.shop.isOnline ? '#52c41a' : '#8c8c8c'}">${onlineStatusText}</span> </span>
          <span style="flex:1"><strong>异常状态:</strong><span style="color: ${exceptionStatusColor}">${exceptionStatusText}</span></span>
        </div>
        <div style="display: flex; flex-direction: row;">
          <span style="flex:1"><strong>环信码等级:</strong><span style="color: ${getColorByRingCodeLevel(shop.shop.ringCodeLevel)}">${getRingCodeLevelText(shop.shop.ringCodeLevel)}</span></span>
          <span style="flex:1"><strong>发布时间:</strong>${shop.shop.ringCodePublishTime}</span>
        </div>
        <h4 style="margin: 10px 0 5px 0; color: #666;">近1小时监测数据</h4>
        <div id="infowindowChartContainer" style="width: 100%; height: 250px;"></div>
      </div>
    </div>
  `
  // 清除现有的信息窗口
  if (window.infoWindow) {
    window.infoWindow.close()
  }
  // 创建新的信息窗口
  window.infoWindow = new AMap.InfoWindow({
    content: content,
    size: new AMap.Size(400, 400),
    offset: new AMap.Pixel(-24, -80),
  })
  // 打开信息窗口
  window.infoWindow.open(map, [shop.shop.longitude, shop.shop.latitude])
  // 等待信息窗口加载完成后初始化图表
  setTimeout(() => {
    const chartdom = document.getElementById('infowindowChartContainer')
    if (chartdom) {
      initChart(chartdom, shop.recentData)
    }
  }, 100)
}
/**
 * 初始化监测数据图表
 * @param {Array} data 监测数据
 */
function initChart(chartdom, data) {
  // 准备图表数据
  const times = data.map((item) => item.sampleTime)
  const oilSmokeData = data.map((item) => item.oilSmokeConcentration)
  const purifierData = data.map((item) => item.purifierCurrent)
  const fanData = data.map((item) => item.fanCurrent)
  // 初始化图表
  const chart = echarts.init(chartdom)
  // 图表配置
  const option = {
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'cross',
        label: {
          backgroundColor: '#6a7985',
        },
      },
    },
    legend: {
      data: ['油烟浓度', '净化器电流', '风机电流'],
      top: 0,
    },
    grid: {
      left: '3%',
      right: '4%',
      bottom: '3%',
      containLabel: true,
    },
    xAxis: [
      {
        type: 'category',
        boundaryGap: false,
        data: times.map((time) => time.split(' ')[1]),
        axisLabel: {
          rotate: 0,
          fontSize: 10,
        },
      },
    ],
    yAxis: [
      {
        type: 'value',
        name: '油烟浓度 (mg/m³)',
        position: 'left',
        left: '30%',
      },
      {
        type: 'value',
        name: '电流 (A)',
        position: 'right',
      },
    ],
    series: [
      {
        name: '油烟浓度',
        type: 'line',
        data: oilSmokeData,
        yAxisIndex: 0,
        smooth: true,
      },
      {
        name: '净化器电流',
        type: 'line',
        data: purifierData,
        yAxisIndex: 1,
        smooth: true,
      },
      {
        name: '风机电流',
        type: 'line',
        data: fanData,
        yAxisIndex: 1,
        smooth: true,
      },
    ],
  }
  // 应用配置
  chart.setOption(option)
  // 响应式处理
  window.addEventListener('resize', function () {
    chart.resize()
  })
}
/**
 * 清除海量点标记
 */
function clearMassMarks() {
  if (window.massMarks) {
    window.massMarks.clear()
    window.massMarks.setMap(null)
    window.massMarks = null
  }
  if (window.infoWindow) {
    window.infoWindow.close()
    window.infoWindow = null
  }
}
export default {
  /**
   * 绘制海量点标记
   * @param fDatas 完整监测数据
   * @param _factor 当前展示的监测因子对象
   */
  drawMassMarks(fDatas, _factor, onClick) {
    if (!toolboxStore.dataMarkerStatus) {
      return;
    }
    this.clearMassMarks();
    const lnglats = fDatas.lnglats_GD;
    var data = [];
    for (let i = 0; i < lnglats.length; i++) {
      data.push({
        lnglat: lnglats[i], //点标记位置
        name: `${fDatas.times[i]}<br/>${_factor.factorName}: ${_factor.datas[i].factorData} μg/m³`,
        id: i
      });
    }
  drawMassMarks,
  clearMassMarks,
  // /**
  //  * 绘制海量点标记
  //  * @param fDatas 完整监测数据
  //  * @param _factor 当前展示的监测因子对象
  //  */
  // drawMassMarks(fDatas, _factor, onClick) {
  //   if (!toolboxStore.dataMarkerStatus) {
  //     return;
  //   }
  //   this.clearMassMarks();
  //   const lnglats = fDatas.lnglats_GD;
  //   var data = [];
  //   for (let i = 0; i < lnglats.length; i++) {
  //     data.push({
  //       lnglat: lnglats[i], //点标记位置
  //       name: `${fDatas.times[i]}<br/>${_factor.factorName}: ${_factor.datas[i].factorData} μg/m³`,
  //       id: i
  //     });
  //   }
    // 创建样式对象
    var styleObject = {
      url: 'https://a.amap.com/jsapi_demos/static/images/mass1.png',
      // url: './asset/mipmap/ic_up_white.png', // 图标地址
  //   // 创建样式对象
  //   var styleObject = {
  //     url: 'https://a.amap.com/jsapi_demos/static/images/mass1.png',
  //     // url: './asset/mipmap/ic_up_white.png', // 图标地址
      size: new AMap.Size(11, 11), // 图标大小
  //     size: new AMap.Size(11, 11), // 图标大小
      anchor: new AMap.Pixel(5, 5) // 图标显示位置偏移量,基准点为图标左上角
    };
  //     anchor: new AMap.Pixel(5, 5) // 图标显示位置偏移量,基准点为图标左上角
  //   };
    var massMarks = new AMap.MassMarks(data, {
      zIndex: 5, // 海量点图层叠加的顺序
      zooms: [15, 18], // 在指定地图缩放级别范围内展示海量点图层
      style: styleObject // 设置样式对象
    });
    massMarks.on('click', (event) => {
      const i = event.data.id;
      // 3. 自定义点击事件
      onClick(i);
    });
  //   var massMarks = new AMap.MassMarks(data, {
  //     zIndex: 5, // 海量点图层叠加的顺序
  //     zooms: [15, 18], // 在指定地图缩放级别范围内展示海量点图层
  //     style: styleObject // 设置样式对象
  //   });
  //   massMarks.on('click', (event) => {
  //     const i = event.data.id;
  //     // 3. 自定义点击事件
  //     onClick(i);
  //   });
    var marker = new AMap.Marker({
      content: ' ',
      map: map,
  //   var marker = new AMap.Marker({
  //     content: ' ',
  //     map: map,
      offset: new AMap.Pixel(13, 12)
    });
    var timeout;
    massMarks.on('mouseover', (e) => {
      if (timeout) {
        clearTimeout(timeout);
      }
      marker.setPosition(e.data.lnglat);
      marker.setLabel({ content: e.data.name });
      map.add(marker);
      timeout = setTimeout(() => {
        map.remove(marker);
      }, 2000);
    });
    _massMarks = massMarks;
    map.add(massMarks);
  },
  clearMassMarks() {
    if (_massMarks) {
      map.remove(_massMarks);
      _massMarks = undefined;
    }
  },
  //     offset: new AMap.Pixel(13, 12)
  //   });
  //   var timeout;
  //   massMarks.on('mouseover', (e) => {
  //     if (timeout) {
  //       clearTimeout(timeout);
  //     }
  //     marker.setPosition(e.data.lnglat);
  //     marker.setLabel({ content: e.data.name });
  //     map.add(marker);
  //     timeout = setTimeout(() => {
  //       map.remove(marker);
  //     }, 2000);
  //   });
  //   _massMarks = massMarks;
  //   map.add(massMarks);
  // },
  // clearMassMarks() {
  //   if (_massMarks) {
  //     map.remove(_massMarks)
  //     _massMarks = undefined
  //   }
  // },
  /**
   * 创建标记点
@@ -93,14 +616,14 @@
      // 开启标注避让,默认为开启,v1.4.15 新增属性
      collision: collision,
      // 开启标注淡入动画,默认为开启,v1.4.15 新增属性
      animation: true
    });
      animation: true,
    })
    map.add(layer);
    map.add(layer)
    // var markers = [];
    for (var i = 0; i < dataList.length; i++) {
      const data = dataList[i];
      const data = dataList[i]
      var curData = {
        name: data.name,
        position: [data.longitude, data.latitude],
@@ -115,7 +638,7 @@
          size: [30, 30],
          anchor: 'bottom-center',
          angel: 0,
          retina: true
          retina: true,
        },
        text: {
          content: showTxt ? data.name : '',
@@ -127,22 +650,22 @@
            fillColor: '#fff',
            strokeColor: '#333',
            strokeWidth: 0,
            backgroundColor: '#122b54a9'
          }
        }
      };
            backgroundColor: '#122b54a9',
          },
        },
      }
      curData.extData = {
        index: i
      };
        index: i,
      }
      var labelMarker = new AMap.LabelMarker(curData);
      var labelMarker = new AMap.LabelMarker(curData)
      // markers.push(labelMarker);
      layer.add(labelMarker);
      layer.add(labelMarker)
    }
    return layer;
    return layer
  },
  createMarker({ position, img, title, content, label = '', extData }) {
@@ -151,8 +674,8 @@
      size: new AMap.Size(30, 30), //图标尺寸
      image: img, //Icon 的图像
      // imageOffset: new AMap.Pixel(-9, -3), //图像相对展示区域的偏移量,适于雪碧图等
      imageSize: new AMap.Size(30, 30) //根据所设置的大小拉伸或压缩图片
    });
      imageSize: new AMap.Size(30, 30), //根据所设置的大小拉伸或压缩图片
    })
    const marker = new AMap.Marker({
      position: position,
      // offset: new AMap.Pixel(-13, -30),
@@ -161,11 +684,11 @@
      title: title,
      label: {
        content: label,
        direction: 'bottom'
        direction: 'bottom',
      },
      extData
    });
      extData,
    })
    // map.add(marker);
    return marker;
  }
};
    return marker
  },
}