餐饮油烟智能监测与监管一体化平台
riku
2026-03-16 0488cc32d225a28289ba6c70a2a297f347cacdad
2026.3.16
已修改12个文件
已添加10个文件
2039 ■■■■ 文件已修改
public/单体模版-v1.0.docx 补丁 | 查看 | 原始文档 | blame | 历史
public/单体模版(静安).docx 补丁 | 查看 | 原始文档 | blame | 历史
public/工地巡查单据模板-简版.docx 补丁 | 查看 | 原始文档 | blame | 历史
public/工地巡查单据模板-详版.docx 补丁 | 查看 | 原始文档 | blame | 历史
public/扬尘污染监管简报模板.docx 补丁 | 查看 | 原始文档 | blame | 历史
public/扬尘监测数据月度统计模板.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
public/现场监管场景信息导入模板.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
public/秋冬季空气质量攻坚工作提示模板.docx 补丁 | 查看 | 原始文档 | blame | 历史
public/餐饮巡查单据模板.docx 补丁 | 查看 | 原始文档 | blame | 历史
src/components/map/BaseMap.vue 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/constants/menu.js 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/debug/debugdata.js 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/map/districtsearch.js 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/map/index.js 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/map/marks.js 494 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/map/util.js 93 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/scene/SceneInspectFile.vue 235 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/task/TaskManage.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/task/components/CompMonitorObjEdit.vue 355 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspection/task/components/CompSubTaskList.vue 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/DataDashboard.vue 591 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/µ¥ÌåÄ£°æ-v1.0.docx
Binary files differ
public/µ¥ÌåÄ£°æ£¨¾²°²£©.docx
Binary files differ
public/¹¤µØÑ²²éµ¥¾ÝÄ£°å-¼ò°æ.docx
Binary files differ
public/¹¤µØÑ²²éµ¥¾ÝÄ£°å-Ïê°æ.docx
Binary files differ
public/Ñï³¾ÎÛȾ¼à¹Ü¼ò±¨Ä£°å.docx
Binary files differ
public/Ñï³¾¼à²âÊý¾ÝÔ¶Èͳ¼ÆÄ£°å.xlsx
Binary files differ
public/ÏÖ³¡¼à¹Ü³¡¾°ÐÅÏ¢µ¼ÈëÄ£°å.xlsx
Binary files differ
public/Çﶬ¼¾¿ÕÆøÖÊÁ¿¹¥¼á¹¤×÷Ìáʾģ°å.docx
Binary files differ
public/²ÍÒûѲ²éµ¥¾ÝÄ£°å.docx
Binary files differ
src/components/map/BaseMap.vue
@@ -3,17 +3,30 @@
</template>
<script setup>
import { onMounted, onUnmounted } from 'vue';
import { createMap, map, destroyMap } from '@/utils/map/index';
import { onMounted, onUnmounted } from 'vue'
import { onMapMounted, createMap, satellite, map, destroyMap } from '@/utils/map/index'
const props = defineProps({
  showSatellite: {
    type: Boolean,
    default: false,
  },
})
onMounted(() => {
  // é«˜å¾·åœ°å›¾åˆå§‹åŒ–
  setTimeout(() => {
    createMap('container');
  }, 1000);
});
    createMap('container')
    onMapMounted(() => {
      if (props.showSatellite) {
        satellite.show()
      } else {
        satellite.hide()
      }
    })
  }, 1000)
})
onUnmounted(() => {
  destroyMap();
});
  destroyMap()
})
</script>
<style>
#container {
src/constants/menu.js
@@ -14,10 +14,22 @@
        name: '监测预警',
      },
      {
        path: '/index/monitor/data-analysis-all',
        icon: 'solar:presentation-graph-line-duotone',
        name: '数据分析',
        icon: 'solar:structure-line-duotone',
        name: '数据管理',
        children: [
          {
            path: '/index/monitor/data-analysis-all',
            icon: 'solar:presentation-graph-line-duotone',
            name: '数据分析',
          },
          {
            path: '/index/monitor/data-history',
            icon: 'solar:graph-new-line-duotone',
            name: '历史数据',
          },
        ],
      },
      // {
      //   icon: 'solar:presentation-graph-line-duotone',
      //   name: '数据分析',
@@ -55,11 +67,6 @@
      //     },
      //   ],
      // },
      {
        path: '/index/monitor/data-history',
        icon: 'solar:graph-new-line-duotone',
        name: '历史数据',
      },
    ],
  },
  {
src/debug/debugdata.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,125 @@
function generateTestShops() {
  // æŒ‡å®šçš„店铺名称
  const specifiedShops = [
    '付小姐在成都',
    '吉刻联盟',
    '家在塔啦',
    '狼来了',
    '乐凯撒星游店',
    '馨远美食小镇(哈尼美食广场)',
    '棒约翰',
    '弄堂咪道',
    '杨记齐齐哈尔烤肉',
    '上海稔传餐饮管理有限公司(人生一串)',
    '缘家',
    '泉盛餐饮(上海)有限公司(食其家)',
    '丰茂烤串',
    '上海泰煌餐饮管理有限公司(泰煌鸡)',
    '徐汇区辰熙餐馆(小铁君串烧居酒屋)',
  ]
  // éšæœºåº—铺名称前缀和后缀
  const namePrefixes = [
    '风味',
    '特色',
    '正宗',
    '传统',
    '经典',
    '创意',
    '时尚',
    '休闲',
    '精致',
    '地道',
  ]
  const nameSuffixes = [
    '餐厅',
    '饭店',
    '酒楼',
    '菜馆',
    '小吃',
    '食堂',
    '美食',
    '饮食',
    '餐饮',
    '饭庄',
  ]
  const cuisines = [
    '川菜',
    '粤菜',
    '鲁菜',
    '淮扬菜',
    '闽菜',
    '湘菜',
    '徽菜',
    '浙菜',
    '东北菜',
    '西北菜',
    '西餐',
    '日料',
    '韩料',
    '东南亚菜',
  ]
  // çŽ¯ä¿¡ç ç­‰çº§
  const ringCodeLevels = [0, 1, 2]
  // ç”Ÿæˆæµ‹è¯•数据
  const shops = []
  // å…ˆæ·»åŠ æŒ‡å®šçš„åº—é“º
  specifiedShops.forEach((name, index) => {
    shops.push({
      shop: {
        name: name,
        address: `上海市徐汇区${index + 1}号`,
        latitude: 31.17 + Math.random() * 0.1,
        longitude: 121.45 + Math.random() * 0.1,
        ringCodeLevel: ringCodeLevels[Math.floor(Math.random() * ringCodeLevels.length)],
        ringCodePublishTime: '2023-03-16 10:00:00',
      },
      recentData: generateRecentData(),
    })
  })
  // ç”Ÿæˆå‰©ä½™çš„随机店铺
  for (let i = specifiedShops.length; i < 100; i++) {
    const prefix = namePrefixes[Math.floor(Math.random() * namePrefixes.length)]
    const cuisine = cuisines[Math.floor(Math.random() * cuisines.length)]
    const suffix = nameSuffixes[Math.floor(Math.random() * nameSuffixes.length)]
    const randomName = `${prefix}${cuisine}${suffix}${i}`
    shops.push({
      shop: {
        name: randomName,
        address: `上海市徐汇区${i + 1}号`,
        latitude: 31.19 + Math.random() * 0.1,
        longitude: 121.41 + Math.random() * 0.1,
        ringCodeLevel: ringCodeLevels[Math.floor(Math.random() * ringCodeLevels.length)],
        ringCodePublishTime: '2023-03-16 10:00:00',
      },
      recentData: generateRecentData(),
    })
  }
  return shops
}
function generateRecentData() {
  // ç”Ÿæˆè¿‘1小时的监测数据,每10分钟一条
  const data = []
  const now = new Date()
  now.setFullYear(2023)
  for (let i = 5; i >= 0; i--) {
    const time = new Date(now.getTime() - i * 10 * 60 * 1000)
    data.push({
      sampleTime: time.toISOString().slice(0, 19).replace('T', ' '),
      oilSmokeConcentration: (Math.random() * 5).toFixed(2),
      purifierCurrent: (Math.random() * 10).toFixed(2),
      fanCurrent: (Math.random() * 15).toFixed(2),
    })
  }
  return data
}
export { generateTestShops }
src/router/index.js
@@ -75,6 +75,16 @@
              component: () => import('@/views/inspection/task/TaskManage.vue'),
            },
            {
              name: 'monitorObjEdit',
              path: 'monitorObjEdit',
              component: () => import('@/views/inspection/task/MonitorObjEdit.vue'),
            },
            {
              name: 'monitorPlanEdit',
              path: 'monitorPlanEdit',
              component: () => import('@/views/inspection/task/MonitorPlanEdit.vue'),
            },
            {
              name: 'scene-info',
              path: 'scene-info',
              component: () => import('@/views/inspection/scenenew/UserInfo.vue'),
src/utils/map/districtsearch.js
@@ -6,7 +6,7 @@
var activeDistrict = undefined
export default {
  // ç»˜åˆ¶åŒºåŽ¿è¾¹ç•Œ
  drawDistrict(districtName, isNew) {
  drawDistrictMask(districtName, isNew) {
    if (!districtName) return
    onMapMounted(() => {
      if (!isNew && districtPolygonMap.has(districtName)) {
@@ -49,6 +49,43 @@
      }
    })
  },
  // ç»˜åˆ¶åŒºåŽ¿è¾¹ç•Œ
  drawDistrict(districtName, isNew) {
    if (!districtName) return
    onMapMounted(() => {
      if (!isNew && districtPolygonMap.has(districtName)) {
        const districtPolygon = districtPolygonMap.get(districtName)
        map.add(districtPolygon)
        map.setFitView(districtPolygon)
        activeDistrict = districtPolygon
      } else {
        var district = new AMap.DistrictSearch({
          extensions: 'all', //返回行政区边界坐标等具体信息
          level: 'district', //设置查询行政区级别为区
        })
        district.search(districtName, function (status, result) {
          var bounds = result.districtList[0].boundaries //获取边界信息
          if (bounds) {
            for (var i = 0; i < bounds.length; i++) {
              //生成行政区划 polygon
              const districtPolygon = new AMap.Polygon({
                map: map, //显示该覆盖物的地图对象
                strokeWeight: 1, //轮廓线宽度
                path: bounds[i], //多边形轮廓线的节点坐标数组
                fillOpacity: 0.4, //多边形填充透明度
                fillColor: '#0077ff',
                strokeColor: '#CC66CC', //线条颜色
              })
              districtPolygonMap.set(districtName, districtPolygon)
              activeDistrict = districtPolygon
              map.setFitView(districtPolygon, true)
            }
          }
        })
      }
    })
  },
  removeDistrict() {
    onMapMounted(() => {
      if (activeDistrict) {
src/utils/map/index.js
@@ -67,20 +67,20 @@
  map = new AMap.Map(elementId, {
    // mapStyle: 'amap://styles/e1e78509de64ddcd2efb4cb34c6fae2a',
    features: ['bg', 'road'],
    pitch: 30, // åœ°å›¾ä¿¯ä»°è§’度,有效范围 0 åº¦- 83 åº¦
    pitch: 0, // åœ°å›¾ä¿¯ä»°è§’度,有效范围 0 åº¦- 83 åº¦
    viewMode: '3D', // åœ°å›¾æ¨¡å¼
    resizeEnable: true,
    center: [121.6039283, 31.25295567],
    zooms: [2, 26],
    zoom: 14,
    zoom: 11,
  })
  // map = new AMap.Map(elementId);
  // æ·»åŠ å«æ˜Ÿåœ°å›¾
  satellite = new AMap.TileLayer.Satellite()
  const roadNet = new AMap.TileLayer.RoadNet()
  // satellite.hide()
  map.add([satellite, roadNet])
  // const roadNet = new AMap.TileLayer.RoadNet()
  satellite.hide()
  map.add([satellite])
  // _initMouseTool();
  // _init3DLayer();
@@ -112,4 +112,14 @@
  })
}
export { createMap, destroyMap, onMapMounted, map, AMap, mouseTool, object3Dlayer, isDragging }
export {
  createMap,
  destroyMap,
  onMapMounted,
  map,
  AMap,
  satellite,
  mouseTool,
  object3Dlayer,
  isDragging,
}
src/utils/map/marks.js
@@ -2,82 +2,400 @@
 * é«˜å¾·åœ°å›¾ç‚¹æ ‡è®°ç»˜åˆ¶ç›¸å…³
 */
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'
const toolboxStore = useToolboxStore();
const toolboxStore = useToolboxStore()
var _massMarks = undefined;
var _massMarks = undefined
// çŽ¯ä¿¡ç ç­‰çº§å’Œå¯¹åº”é¢œè‰²
const ringCodeLevelColors = [
  '#52c41a', // ç»¿è‰²
  '#faad14', // é»„色
  '#f5222d', // çº¢è‰²
]
/**
 * ç»˜åˆ¶æµ·é‡ç‚¹æ ‡è®°
 * @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 {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 = ringCodeLevelColors.map((color, index) => ({
    url: createCustomMarker(color),
    size: new AMap.Size(20, 20),
    anchor: new AMap.Pixel(10, 10),
  }))
  // å‡†å¤‡æµ·é‡ç‚¹æ•°æ®
  const massMarksData = shops.map((shop, index) => {
    // æ ¹æ®çŽ¯ä¿¡ç ç­‰çº§èŽ·å–é¢œè‰²
    const color = getColorByRingCodeLevel(shop.shop.ringCodeLevel)
    return {
      id: index,
      name: shop.shop.name,
      lnglat: [shop.shop.longitude, shop.shop.latitude],
      style: shop.shop.ringCodeLevel, // æ ·å¼ç´¢å¼•,对应 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 {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)))
}
/**
 * æ˜¾ç¤ºåº—铺信息窗口
 * @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 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: ${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>
        <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 +411,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 +433,7 @@
          size: [30, 30],
          anchor: 'bottom-center',
          angel: 0,
          retina: true
          retina: true,
        },
        text: {
          content: showTxt ? data.name : '',
@@ -127,22 +445,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 +469,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 +479,11 @@
      title: title,
      label: {
        content: label,
        direction: 'bottom'
        direction: 'bottom',
      },
      extData
    });
      extData,
    })
    // map.add(marker);
    return marker;
  }
};
    return marker
  },
}
src/utils/map/util.js
@@ -1,5 +1,5 @@
import { map, AMap, isDragging } from '@/utils/map/index';
import marks from '@/utils/map/marks';
import { map, AMap, isDragging } from '@/utils/map/index'
import marks from '@/utils/map/marks'
/**
 * åæ ‡é›†åˆçš„æœ€è¥¿å—角和最东北角
@@ -7,35 +7,35 @@
 *  list æ˜¯æŽ¥å£èŽ·å–çš„ç‚¹ çš„æ•°ç»„
 */
const getBound = (list) => {
  const offset = 0.005;
  let south = null;
  let west = null;
  let north = null;
  let east = null;
  const offset = 0.05
  let south = null
  let west = null
  let north = null
  let east = null
  for (let item of list) {
    // æŽ’除无效经纬度
    if (item[0] == 0 && item[1] == 0) {
      continue;
      continue
    }
    if ((west && item[0] < west) || !west) {
      west = item[0] - offset;
      west = item[0] - offset
    }
    if ((south && item[1] < south) || !south) {
      south = item[1] - offset;
      south = item[1] - offset
    }
    if ((east && item[0] > east) || !east) {
      east = item[0] + offset;
      east = item[0] + offset
    }
    if ((north && item[1] > north) || !north) {
      north = item[1] + offset;
      north = item[1] + offset
    }
  }
  if (!south || !west || !north || !east) {
    return { sw: null, ne: null };
    return { sw: null, ne: null }
  } else {
    return { sw: [west, south], ne: [east, north] };
    return { sw: [west, south], ne: [east, north] }
  }
};
}
/**
 * æ ¹æ®ä¸­å¿ƒç‚¹å‡ºå‘的半径,得到合适的地图缩放系数
@@ -45,64 +45,61 @@
 */
const distanceToZoom = (d) => {
  let baseDis = 250,
    z = 0;
    z = 0
  while (baseDis < d) {
    baseDis *= 2;
    z++;
    baseDis *= 2
    z++
  }
  // å¤šä½™çš„地图缩放系数
  const x = (baseDis - d) / (baseDis / 2);
  z -= x;
  z = z < 0 ? 0 : z;
  const x = (baseDis - d) / (baseDis / 2)
  z -= x
  z = z < 0 ? 0 : z
  z = 18 - z;
  z = z < 3 ? 3 : z;
  return z;
};
  z = 18 - z
  z = z < 3 ? 3 : z
  return z
}
export default {
  setCenter(lnglat, ignore = false) {
    if (!ignore && isDragging) {
      return;
      return
    }
    var now = new Date();
    if (
      this.lasttime == undefined ||
      now.getTime() - this.lasttime.getTime() >= 200
    ) {
      map.setCenter(lnglat);
      this.lasttime = now;
    var now = new Date()
    if (this.lasttime == undefined || now.getTime() - this.lasttime.getTime() >= 200) {
      map.setCenter(lnglat)
      this.lasttime = now
    }
  },
  addViews(view) {
    map.add(view);
    map.add(view)
  },
  removeViews(view) {
    map.remove(view);
    map.remove(view)
  },
  clearMap() {
    marks.clearMassMarks();
    map.clearMap();
    marks.clearMassMarks()
    map.clearMap()
  },
  setFitView(views) {
    if (views) {
      map.setFitView(views);
      map.setFitView(views)
    } else {
      map.setFitView();
      map.setFitView()
    }
  },
  setFitSector({ p, r }) {
    this.setCenter(p);
    const z = distanceToZoom(r);
    map.setZoom(z);
    this.setCenter(p)
    const z = distanceToZoom(r)
    map.setZoom(z)
  },
  setBound(lnglats_GD) {
    const { sw, ne } = getBound(lnglats_GD);
    const { sw, ne } = getBound(lnglats_GD)
    if (!sw || !ne) {
      return;
      return
    }
    var mybounds = new AMap.Bounds(sw, ne); // sw, ne > [xxx,xxx], [xxx,xxx]
    map.setBounds(mybounds);
  }
};
    var mybounds = new AMap.Bounds(sw, ne) // sw, ne > [xxx,xxx], [xxx,xxx]
    map.setBounds(mybounds)
  },
}
src/views/inspection/scene/SceneInspectFile.vue
@@ -6,19 +6,9 @@
    class="dialog-wrapper"
    v-loading="loading"
  >
    <el-scrollbar
      ref="scrollbarRef"
      height="50vh"
      v-loading="loading"
      :always="true"
    >
    <el-scrollbar ref="scrollbarRef" height="50vh" v-loading="loading" :always="true">
      <el-checkbox-group v-model="checkList">
        <el-space
          direction="vertical"
          alignment="flex-start"
          fill
          style="width: 90%"
        >
        <el-space direction="vertical" alignment="flex-start" fill style="width: 90%">
          <el-checkbox
            v-for="(item, index) in sceneInfoList"
            :key="item.scense.guid"
@@ -27,23 +17,15 @@
            :class="(item.invalid ? 'checkbox-invalid' : '') + ' checkbox'"
          >
            <div>
              <el-text size="large" truncated style="width: 600px">{{
                item.scense.name
              }}</el-text>
              <el-text size="large" truncated style="width: 600px">{{ item.scense.name }}</el-text>
            </div>
            <div class="m-t-4">
              <el-text size="small">{{
                '地址:' + item.scense.location
              }}</el-text>
              <el-text size="small">{{ '地址:' + item.scense.location }}</el-text>
            </div>
            <el-row justify="space-between">
              <el-space class="m-t-4">
                <el-tag>
                  {{
                    item.scense.cityname +
                    item.scense.districtname +
                    item.scense.townname
                  }}
                  {{ item.scense.cityname + item.scense.districtname + item.scense.townname }}
                </el-tag>
                <el-tag>{{ item.scense.type }}</el-tag>
              </el-space>
@@ -58,10 +40,7 @@
                    :loading="item._loading"
                    :disabled="item._isFirstInspect"
                    inline-prompt
                    style="
                      --el-switch-on-color: #13ce66;
                      --el-switch-off-color: #c75000;
                    "
                    style="--el-switch-on-color: #13ce66; --el-switch-off-color: #c75000"
                    active-text="详版"
                    inactive-text="简版"
                  />
@@ -73,10 +52,12 @@
                  type="default"
                  size="small"
                  class="m-t-4"
                  icon="IconPrinter"
                  :disabled="!item._valid"
                  @click="handlePreview(item)"
                >
                  <el-icon>
                    <Icon icon="solar:printer-minimalistic-line-duotone" />
                  </el-icon>
                </el-button>
              </el-space>
            </el-row>
@@ -86,9 +67,7 @@
    </el-scrollbar>
    <template #footer>
      <div class="dialog-footer">
        <el-button type="danger" @click="cancel" icon="CloseBold"
          >取消</el-button
        >
        <el-button type="danger" @click="cancel" icon="CloseBold">取消</el-button>
        <el-button
          type="primary"
          :loading="docLoading"
@@ -103,8 +82,10 @@
          :loading="docLoading"
          :disabled="checkList.length == 0"
          @click="handlePreview()"
          icon="IconPrinter"
        >
          <el-icon class="el-icon--left">
            <Icon icon="solar:printer-minimalistic-line-duotone" />
          </el-icon>
          æ‰“印所选
        </el-button>
      </div>
@@ -113,24 +94,14 @@
  <el-dialog v-model="previewVisible" :show-close="false" fullscreen>
    <template #header="{ close, titleId, titleClass }">
      <el-row justify="end" style="background-color: white">
        <el-button type="danger" @click="close" icon="CircleCloseFilled">
          å…³é—­
        </el-button>
        <el-button
          type="primary"
          @click="handelPrint(refWord)"
          icon="IconPrinter"
        >
        <el-button type="danger" @click="close" icon="CircleCloseFilled"> å…³é—­ </el-button>
        <el-button type="primary" @click="handelPrint(refWord)" icon="IconPrinter">
          æ‰“印
        </el-button>
      </el-row>
    </template>
    <div ref="refWord">
      <div
        :id="`word-preview-${i}`"
        v-for="(item, i) in previewList"
        :key="item"
      ></div>
      <div :id="`word-preview-${i}`" v-for="(item, i) in previewList" :key="item"></div>
    </div>
    <!-- <iframe ref="pdfPreview" width="100%" height="100vh" style="height: calc(100vh - 60px);"></iframe> -->
  </el-dialog>
@@ -139,108 +110,109 @@
/**
 * åœºæ™¯å·¡æŸ¥å•据自动下载
 */
import { ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { Icon } from '@iconify/vue'
import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import {
  exportDocx,
  prepareDocxBlob,
  preparePdf,
  previewDocx,
  downloadDocx,
  print
} from '@/utils/doc';
import sceneApi from '@/api/fysp/sceneApi';
import subtaskApi from '@/api/fysp/subtaskApi';
  print,
} from '@/utils/doc'
import sceneApi from '@/api/fysp/sceneApi'
import subtaskApi from '@/api/fysp/subtaskApi'
// 2025.12.22:有巡查单据模板的场景类型,[1:工地, 5:餐饮]
const validSceneType = [1, 5];
const validSceneType = [1, 5]
const props = defineProps({
  // å¯¹è¯æ¡†å¼€å…³
  modelValue: Boolean,
  // åœºæ™¯åŸºç¡€ä¿¡æ¯æ•°ç»„
  value: Array,
  previewElement: String
});
  previewElement: String,
})
const emits = defineEmits(['update:modelValue']);
const emits = defineEmits(['update:modelValue'])
const router = useRouter();
const router = useRouter()
const refWord = ref(null);
const pdfPreview = ref(null);
const refWord = ref(null)
const pdfPreview = ref(null)
const loading = ref(false);
const scrollbarRef = ref();
const sceneInfoList = ref([]);
const checkList = ref([]);
const docLoading = ref(false);
const loading = ref(false)
const scrollbarRef = ref()
const sceneInfoList = ref([])
const checkList = ref([])
const docLoading = ref(false)
// é¢„览对话框开关
const previewVisible = ref(false);
const previewVisible = ref(false)
// é¢„览的文档
const previewList = ref([]);
const previewList = ref([])
watch(
  () => [props.modelValue, props.value],
  (nV, oV) => {
    if (nV[0] && nV[1] && nV[1] != oV[1]) {
      fetchSceneInfo(nV[1]);
      fetchSceneInfo(nV[1])
    }
  }
);
  },
)
function fetchSceneInfo(sceneIdList) {
  loading.value = true;
  sceneInfoList.value = [];
  checkList.value = [];
  loading.value = true
  sceneInfoList.value = []
  checkList.value = []
  sceneIdList.forEach((sid) => {
    sceneApi
      .getSceneDetail(sid)
      .then((res) => {
        sceneInfoList.value.push(res.data);
        sceneInfoList.value.push(res.data)
        if (validSceneType.indexOf(res.data?.scense?.typeid) != -1) {
          checkList.value.push(sceneInfoList.value.length - 1);
          checkList.value.push(sceneInfoList.value.length - 1)
          // todo _valid çš„逻辑有错误
          const lastScene = sceneInfoList.value[sceneInfoList.value.length - 1];
          lastScene._valid = true;
          lastScene._loading = true;
          const lastScene = sceneInfoList.value[sceneInfoList.value.length - 1]
          lastScene._valid = true
          lastScene._loading = true
          subtaskApi
            .findByDate(sid)
            .then((res) => {
              if (res.length == 0) {
                lastScene._isFirstInspect = true;
                lastScene._isDetail = true;
                lastScene._isFirstInspect = true
                lastScene._isDetail = true
              }
            })
            .finally(() => {
              lastScene._loading = false;
            });
              lastScene._loading = false
            })
        }
      })
      .finally(() => {
        loading.value = false;
        scrollbarRef.value.setScrollTop(0);
      });
  });
        loading.value = false
        scrollbarRef.value.setScrollTop(0)
      })
  })
}
function handleDialogChange(value) {
  emits('update:modelValue', value);
  emits('update:modelValue', value)
}
function setParam(value, length) {
  const _value = value ? value : '';
  const offset = length - _value.length;
  const _value = value ? value : ''
  const offset = length - _value.length
  if (offset > 0) {
    let str = _value;
    let str = _value
    for (let i = 0; i < offset; i++) {
      str += ' ';
      str += ' '
    }
    return str;
    return str
  } else {
    return _value;
    return _value
  }
}
@@ -249,8 +221,8 @@
  const selected = item
    ? [item]
    : sceneInfoList.value.filter((v, i) => {
        return checkList.value.indexOf(i) != -1;
      });
        return checkList.value.indexOf(i) != -1
      })
  const param = selected.map((v) => {
    switch (v.scense.typeid) {
      // å·¥åœ°
@@ -262,22 +234,19 @@
            district: v.scense?.districtname ?? '',
            name: setParam(v.scense?.name ?? '', 0),
            employerUnit: setParam(v.subScene?.csEmployerUnit ?? '', 0),
            constructionUnit: setParam(
              v.subScene ? v.subScene.csConstructionUnit : '',
              0
            ),
            constructionUnit: setParam(v.subScene ? v.subScene.csConstructionUnit : '', 0),
            timeRange: setParam(
              v.subScene && v.subScene.csStartTime
                ? `${v.subScene.csStartTime}至${v.subScene.csEndTime}`
                : '',
              0
              0,
            ),
            stage: setParam(v.subScene ? v.subScene.siExtension1 : '', 0),
            contacts: setParam(v.scense?.contacts ?? '', 0),
            contactsTel: setParam(v.scense?.contactst ?? '', 0),
            location: setParam(v.scense?.location ?? '', 0)
          }
        };
            location: setParam(v.scense?.location ?? '', 0),
          },
        }
      // é¤é¥®
      case 5:
        return {
@@ -287,56 +256,54 @@
            location: setParam(v.scense.location, 63),
            name: setParam(v.scense.name, 64),
            contacts: setParam(v.scense.contacts, 67),
            contactsTel: setParam(v.scense.contactst, 62)
          }
        };
            contactsTel: setParam(v.scense.contactst, 62),
          },
        }
      // default:
      //   v.invalid = true;
      //   return undefined;
    }
  });
  })
  return param;
  return param
}
// æ ¹æ®åœºæ™¯ç±»åž‹ï¼Œç”Ÿæˆå¯¹åº”çš„word文档
function generateDoc(param, callback) {
  param.map((p, index) => {
    let template, _param;
    let template, _param
    switch (p.type) {
      // å·¥åœ°
      case 1:
        template = p._isDetail
          ? '/工地巡查单据模板-详版.docx'
          : '/工地巡查单据模板-简版.docx';
        _param = p.params;
        break;
        template = p._isDetail ? '/工地巡查单据模板-详版.docx' : '/工地巡查单据模板-简版.docx'
        _param = p.params
        break
      // é¤é¥®
      case 5:
        template = '/餐饮巡查单据模板.docx';
        _param = p.params;
        break;
        template = '/餐饮巡查单据模板.docx'
        _param = p.params
        break
      default:
        break;
        break
    }
    prepareDocxBlob(template, _param).then((blob) => {
      callback(blob, `${_param.name}巡查单据.docx`, index);
    });
  });
      callback(blob, `${_param.name}巡查单据.docx`, index)
    })
  })
}
function filePrepare(callback) {
  const param = parseParam();
  const param = parseParam()
  if (param) {
    return generateDoc(param, callback);
    return generateDoc(param, callback)
  }
}
// ç‚¹å‡»ä¸‹è½½æŒ‰é’®æ“ä½œ, ä¸‹è½½word文档
function handelDownload() {
  filePrepare((blob, name) => {
    downloadDocx(blob, name);
  });
    downloadDocx(blob, name)
  })
}
// ç‚¹å‡»æ‰“印按钮操作
@@ -346,8 +313,8 @@
      ref,
      // æ ¹æ®ç›®å‰ä½¿ç”¨çš„docx-preview组件,设置打印样式,主要去除多余的margin和padding,以及阴影效果
      style: `
        @page{size:A4;margin: 0 !important;padding:0 !important;}
        body {margin: 0 !important;padding:0 !important;}
        @page{size:A4;margin: 0 !important;padding:0 !important;}
        body {margin: 0 !important;padding:0 !important;}
        header {color: rgb(182, 182, 182);}
        footer {color: rgb(182, 182, 182);}
        .docx-wrapper {padding: 0 !important;}
@@ -357,29 +324,29 @@
          justify-content: space-between;
          align-items: flex-end;
        }
      `
    });
      `,
    })
  }
}
function handlePreview(item) {
  // é¢„览的文档,区分单独打印和打印全部
  previewList.value = item ? ['0'] : checkList.value;
  const param = item ? parseParam(item) : parseParam();
  previewList.value = item ? ['0'] : checkList.value
  const param = item ? parseParam(item) : parseParam()
  if (param) {
    generateDoc(param, (blob, name, index) => {
      previewVisible.value = true;
      previewVisible.value = true
      setTimeout(() => {
        previewDocx(blob, document.getElementById(`word-preview-${index}`));
      }, 200);
    });
        previewDocx(blob, document.getElementById(`word-preview-${index}`))
      }, 200)
    })
  }
}
// å–消操作
function cancel() {
  // å…³é—­å¯¹è¯æ¡†
  handleDialogChange(false);
  handleDialogChange(false)
}
</script>
<style scoped>
src/views/inspection/task/TaskManage.vue
@@ -296,7 +296,11 @@
            data: r,
          }
        })
        this.tasks = list
        this.tasks = list.filter((e) => {
          return (
            e.data.districtname == '徐汇区' && dayjs(e.data.starttime).isBefore(dayjs('2023-12-31'))
          )
        })
        if (list.length == 0) {
          this.sideLoading = false
          this.mainLoading = false
@@ -331,7 +335,9 @@
      taskApi
        .fetchMonitorObjectVersion(task.data.tguid)
        .then((res) => {
          this.curMonitorObjList = res
          this.curMonitorObjList = res.filter((item) => {
            return item.scene.type == '餐饮'
          })
        })
        .finally(() => {
          this.mainLoading = false
src/views/inspection/task/components/CompMonitorObjEdit.vue
@@ -1,9 +1,6 @@
<template>
  <el-row gutter="20">
    <el-col
      :span="16"
      style="border-right: 1px solid var(--el-color-info-light-7)"
    >
    <el-col :span="16" style="border-right: 1px solid var(--el-color-info-light-7)">
      <div>
        <el-text>已选场景</el-text>
        <el-text type="info" size="small">{{ statisticText }}</el-text>
@@ -22,48 +19,38 @@
    </el-col>
    <el-col :span="8">
      <!-- <el-affix :offset="140"> -->
        <div>
          <el-text>可选场景</el-text>
        </div>
        <el-divider />
        <el-scrollbar class="scrollbar-flex-content" always >
          <!-- <el-segmented v-model="curSceneType" :options="sceneTypeOptions" /> -->
          <el-tabs v-model="curSceneType">
            <el-tab-pane
              v-for="item in sceneTypeOptions"
              :key="item"
              :label="item"
              :name="item"
            ></el-tab-pane>
          </el-tabs>
        </el-scrollbar>
        <FYInfoSearch
          placeholder="请输入场景名称关键字"
          :data="showSceneList"
          :on-search="searchScene"
          :total="total"
          scroll-height="60vh"
          :page-show="false"
        >
          <template #default="{ row }">
            <ItemScene :item="row">
              <el-button-group>
                <el-button
                  size="small"
                  type="primary"
                  @click="openInsertDialog(row)"
                  >插入</el-button
                >
                <el-button
                  size="small"
                  type="primary"
                  @click="openAddDialog(row)"
                  >新增</el-button
                >
              </el-button-group>
            </ItemScene>
          </template>
        </FYInfoSearch>
      <div>
        <el-text>可选场景</el-text>
      </div>
      <el-divider />
      <el-scrollbar class="scrollbar-flex-content" always>
        <!-- <el-segmented v-model="curSceneType" :options="sceneTypeOptions" /> -->
        <el-tabs v-model="curSceneType">
          <el-tab-pane
            v-for="item in sceneTypeOptions"
            :key="item"
            :label="item"
            :name="item"
          ></el-tab-pane>
        </el-tabs>
      </el-scrollbar>
      <FYInfoSearch
        placeholder="请输入场景名称关键字"
        :data="showSceneList"
        :on-search="searchScene"
        :total="total"
        scroll-height="60vh"
        :page-show="false"
      >
        <template #default="{ row }">
          <ItemScene :item="row">
            <el-button-group>
              <el-button size="small" type="primary" @click="openInsertDialog(row)">插入</el-button>
              <el-button size="small" type="primary" @click="openAddDialog(row)">新增</el-button>
            </el-button-group>
          </ItemScene>
        </template>
      </FYInfoSearch>
      <!-- </el-affix> -->
    </el-col>
  </el-row>
@@ -72,19 +59,12 @@
    <div v-if="valibleIndex.length > 0">以下为可选的空余编号</div>
    <div v-else>无可选的空余编号</div>
    <el-radio-group v-model="selectedIndex" size="default">
      <el-radio-button
        v-for="item in valibleIndex"
        :key="item"
        :label="item"
        :value="item"
      />
      <el-radio-button v-for="item in valibleIndex" :key="item" :label="item" :value="item" />
    </el-radio-group>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="insertDialog = false">取消</el-button>
        <el-button :disabled="!selectedIndex" type="primary" @click="insertMov">
          ç¡®è®¤
        </el-button>
        <el-button :disabled="!selectedIndex" type="primary" @click="insertMov"> ç¡®è®¤ </el-button>
      </div>
    </template>
  </el-dialog>
@@ -100,14 +80,14 @@
</template>
<script>
import { useCloned } from '@vueuse/core';
import CompMonitorObj from './CompMonitorObj.vue';
import taskApi from '@/api/fysp/taskApi';
import sceneApi from '@/api/fysp/sceneApi';
import { ElMessage, ElNotification, ElMessageBox } from 'element-plus';
import { useCloned } from '@vueuse/core'
import CompMonitorObj from './CompMonitorObj.vue'
import taskApi from '@/api/fysp/taskApi'
import sceneApi from '@/api/fysp/sceneApi'
import { ElMessage, ElNotification, ElMessageBox } from 'element-plus'
const MODE_CREATE = 'create';
const MODE_UPDATE = 'update';
const MODE_CREATE = 'create'
const MODE_UPDATE = 'update'
export default {
  components: { CompMonitorObj },
@@ -115,18 +95,18 @@
    // ç¼–辑模式,新增create或更新update
    mode: {
      type: String,
      default: MODE_CREATE
      default: MODE_CREATE,
    },
    create: Boolean,
    // å·¡æŸ¥æ€»ä»»åŠ¡
    task: {
      type: Object,
      default: () => {
        return {};
      }
        return {}
      },
    },
    // ç›‘管场景集合
    objList: Array
    objList: Array,
  },
  data() {
    return {
@@ -157,19 +137,19 @@
      // åˆ é™¤çš„监管场景
      deleteObj: [],
      // æ›´æ–°çš„场景基本信息(更新场景的编号)
      updateScene: []
    };
      updateScene: [],
    }
  },
  emits: ['uploadSuccess', 'uploadFail'],
  watch: {
    objList: {
      handler(nV, oV) {
        if (nV != oV) {
          this.curMonitorObjList = useCloned(nV).cloned.value;
          this.curMonitorObjList = useCloned(nV).cloned.value
        }
      },
      immediate: true
    }
      immediate: true,
    },
    // task: {
    //   handler(nV, oV) {
    //     if (nV != oV) {
@@ -191,158 +171,149 @@
        districtname: this.task.districtname,
        towncode: this.task.towncode,
        townname: this.task.townname,
        online: true
      };
        online: true,
        scensetypeid: '5',
      }
    },
    // å½“前场景类型下的展示场景
    showSceneList() {
      return this.sceneList.filter((v) => {
        const index = this.curMonitorObjList.findIndex((o) => {
          return o.sguid == v.guid;
        });
        return index == -1 && v.type == this.curSceneType;
      });
          return o.sguid == v.guid
        })
        return index == -1 && v.type == this.curSceneType
      })
    },
    sceneTypeOptions() {
      const list = [];
      const list = []
      this.sceneList.forEach((d) => {
        if (list.indexOf(d.type) == -1) list.push(d.type);
      });
      return list;
        if (list.indexOf(d.type) == -1) list.push(d.type)
      })
      return list
    },
    // å½“前场景类型下的可插入编号
    valibleIndex() {
      // åŽŸåˆ—è¡¨å·²ç»æŒ‰ç…§ç¼–å·é¡ºåºæŽ’åˆ—
      let index = 1;
      const indexList = [];
      let index = 1
      const indexList = []
      this.showMonitorObjList.forEach((l) => {
        while (l.displayid > index) {
          indexList.push(index);
          index++;
          indexList.push(index)
          index++
        }
        index++;
      });
        index++
      })
      if (this.showMonitorObjList.length == 0 && indexList.length == 0) {
        indexList.push(1);
        indexList.push(1)
      }
      return indexList;
      return indexList
    },
    lastIndex() {
      const len = this.showMonitorObjList.length;
      const len = this.showMonitorObjList.length
      if (len > 0) {
        return this.showMonitorObjList[len - 1].displayid + 1;
        return this.showMonitorObjList[len - 1].displayid + 1
      } else {
        return 1;
        return 1
      }
    },
    isEdit() {
      // æ–°å»ºç›‘管总任务模式
      if (this.create) {
        return this.curMonitorObjList.length > 0;
        return this.curMonitorObjList.length > 0
      }
      // æ›´æ–°ç›‘管总任务模式
      else {
        return (
          this.insertObj.length > 0 ||
          this.deleteObj.length > 0 ||
          this.updateObj.length > 0
        );
        return this.insertObj.length > 0 || this.deleteObj.length > 0 || this.updateObj.length > 0
      }
    },
    statisticText() {
      const total = this.curMonitorObjList.length;
      const map = new Map();
      const total = this.curMonitorObjList.length
      const map = new Map()
      this.curMonitorObjList.forEach((e) => {
        const d = e.scene;
        const d = e.scene
        if (!map.has(d.type)) {
          map.set(d.type, { num: 0 });
          map.set(d.type, { num: 0 })
        }
        map.get(d.type).num++;
      });
        map.get(d.type).num++
      })
      let res = `(总计${total}个`;
      let res = `(总计${total}个`
      for (const [key, value] of map) {
        res += `,${key}${value.num}个`;
        res += `,${key}${value.num}个`
      }
      res += ')';
      return res;
    }
      res += ')'
      return res
    },
  },
  methods: {
    // æŸ¥è¯¢
    searchScene({ text, page, pageSize }) {
      this.area.sceneName = text;
      this.area.sceneName = text
      return sceneApi.searchScene(this.area, 1, 10000).then((res) => {
        if (res.success) {
          // æŸ¥è¯¢ç»“æžœ
          this.sceneList = res.data;
          this.sceneList = res.data
          // æ€»æ•°æ®é‡
          this.total = res.head.totalCount;
          this.total = res.head.totalCount
        }
      });
      })
    },
    deleteMov(item) {
      if (
        !(
          item.extension1 == null ||
          item.extension1 == undefined ||
          item.extension1 == '0'
        )
      ) {
      if (!(item.extension1 == null || item.extension1 == undefined || item.extension1 == '0')) {
        ElMessage({
          message: '已监管场景无法移除',
          type: 'error'
        });
        return;
          type: 'error',
        })
        return
      }
      const i = this.curMonitorObjList.indexOf(item);
      this.curMonitorObjList.splice(i, 1);
      const i1 = this.insertObj.indexOf(item);
      this.insertObj.splice(i1, 1);
      const i2 = this.updateObj.indexOf(item);
      this.updateObj.splice(i2, 1);
      const i = this.curMonitorObjList.indexOf(item)
      this.curMonitorObjList.splice(i, 1)
      const i1 = this.insertObj.indexOf(item)
      this.insertObj.splice(i1, 1)
      const i2 = this.updateObj.indexOf(item)
      this.updateObj.splice(i2, 1)
      const i3 = this.updateScene.findIndex((s) => {
        return s.guid == item.sguid;
      });
      this.updateScene.splice(i3, 1);
        return s.guid == item.sguid
      })
      this.updateScene.splice(i3, 1)
      this.deleteObj.push(item);
      this.deleteObj.push(item)
    },
    openInsertDialog(item) {
      this.insertDialog = true;
      this.selectedScene = item;
      this.monitorTimes = 1;
      this.insertDialog = true
      this.selectedScene = item
      this.monitorTimes = 1
    },
    openAddDialog(item) {
      this.addDialog = true;
      this.selectedScene = item;
      this.monitorTimes = 1;
      this.addDialog = true
      this.selectedScene = item
      this.monitorTimes = 1
    },
    insertMov() {
      // 1. åˆ›å»ºæ–°åœºæ™¯
      let mov = this.createMov(this.selectedIndex, this.selectedScene);
      let mov = this.createMov(this.selectedIndex, this.selectedScene)
      // 2. æŸ¥æ‰¾ç¬¬ä¸€ä¸ªç¼–号大于插入编号的值,将新监管对象插入其之前
      const insertAtIndex = this.curMonitorObjList.findIndex((v) => {
        return v.displayid > this.selectedIndex;
      });
      this.curMonitorObjList.splice(insertAtIndex, 0, mov);
      this.selectedIndex = undefined;
      this.insertDialog = false;
        return v.displayid > this.selectedIndex
      })
      this.curMonitorObjList.splice(insertAtIndex, 0, mov)
      this.selectedIndex = undefined
      this.insertDialog = false
    },
    addMov() {
      // 1. åˆ›å»ºæ–°åœºæ™¯
      let mov = this.createMov(this.lastIndex, this.selectedScene);
      let mov = this.createMov(this.lastIndex, this.selectedScene)
      // 2. æ·»åŠ è‡³æœ«å°¾
      this.curMonitorObjList.push(mov);
      this.addDialog = false;
      this.curMonitorObjList.push(mov)
      this.addDialog = false
    },
    // åˆ›å»ºä¸€ä¸ªæ–°çš„监管对象
    createMov(displayid, scene) {
      // 1. æŸ¥æ‰¾è¯¥åœºæ™¯æ˜¯å¦ä¹‹å‰å·²è¢«åˆ é™¤
      const index = this.deleteObj.findIndex((v) => {
        return v.sguid == scene.guid;
      });
      let mov;
        return v.sguid == scene.guid
      })
      let mov
      // 2. è‹¥æ˜¯å…¨æ–°çš„场景,则新生成一个监管对象,否则只更新编号
      if (index == -1) {
        mov = {
@@ -354,27 +325,27 @@
          monitornum: this.monitorTimes,
          displayid: displayid,
          sceneTypeId: scene.typeid,
          sceneType: scene.type
        };
        this.insertObj.push(mov);
          sceneType: scene.type,
        }
        this.insertObj.push(mov)
      } else {
        mov = this.deleteObj[index];
        mov.displayid = displayid;
        this.updateObj.push(mov);
        this.deleteObj.splice(index, 1);
        mov = this.deleteObj[index]
        mov.displayid = displayid
        this.updateObj.push(mov)
        this.deleteObj.splice(index, 1)
      }
      // 3. åŒæ­¥æ›´æ–°åœºæ™¯åŸºæœ¬ä¿¡æ¯ä¸­çš„编号
      scene._index = displayid;
      this.updateScene.push(scene);
      scene._index = displayid
      this.updateScene.push(scene)
      return mov;
      return mov
    },
    // ä¿å­˜ä¿®æ”¹
    saveEdit() {
      if (this.create) {
        this.createTask();
        this.createTask()
      } else {
        this.updateTask();
        this.updateTask()
      }
    },
    createTask() {
@@ -386,13 +357,13 @@
              title: `巡查总任务创建完成`,
              message: `新增场景${res}个`,
              type: 'success',
              position: 'bottom-left'
            });
            this.$emit('uploadSuccess');
              position: 'bottom-left',
            })
            this.$emit('uploadSuccess')
          })
          .catch((err) => this.$emit('uploadFail', err));
          .catch((err) => this.$emit('uploadFail', err))
      }
      this.updateSceneList();
      this.updateSceneList()
    },
    updateTask() {
      // new Promise((resolve, reject)=>{
@@ -405,10 +376,10 @@
            title: `巡查任务新增完成`,
            message: `新增场景${res}个`,
            type: 'success',
            position: 'bottom-left'
          });
          this.insertObj = [];
        });
            position: 'bottom-left',
          })
          this.insertObj = []
        })
      }
      if (this.updateObj.length > 0) {
        const p2 = taskApi.updateMonitorObject(this.updateObj).then((res) => {
@@ -416,10 +387,10 @@
            title: `巡查任务更新完成`,
            message: `更新场景${res}个`,
            type: 'success',
            position: 'bottom-left'
          });
          this.updateObj = [];
        });
            position: 'bottom-left',
          })
          this.updateObj = []
        })
      }
      if (this.deleteObj.length > 0) {
        const p3 = taskApi.deleteMonitorObject(this.deleteObj).then((res) => {
@@ -427,12 +398,12 @@
            title: `巡查任务删除完成`,
            message: `删除场景${res}个`,
            type: 'success',
            position: 'bottom-left'
          });
          this.deleteObj = [];
        });
            position: 'bottom-left',
          })
          this.deleteObj = []
        })
      }
      this.updateSceneList();
      this.updateSceneList()
      // return Promise.all([p1, p2, p3]).finally(() => {
      //   this.saveLoading = false;
      // });
@@ -440,26 +411,26 @@
    updateSceneList() {
      if (this.updateScene.length > 0) {
        this.updateScene.forEach((s) => {
          s.index = s._index;
        });
          s.index = s._index
        })
        sceneApi.updateSceneList(this.updateScene).then((res) => {
          ElNotification({
            title: `场景编号更新完成`,
            message: `更新场景${res}个`,
            type: 'success',
            position: 'bottom-left'
          });
          this.updateScene = [];
        });
            position: 'bottom-left',
          })
          this.updateScene = []
        })
      }
    }
    },
  },
  mounted() {
    setTimeout(() => {
      this.searchScene({ text: '' });
    }, 1000);
  }
};
      this.searchScene({ text: '' })
    }, 1000)
  },
}
</script>
<style scoped>
src/views/inspection/task/components/CompSubTaskList.vue
@@ -11,14 +11,12 @@
      <el-text>{{ dateStr }}计划</el-text>
    </el-space>
    <div v-show="create && data && data.length > 0">
      <el-button
        icon="IconPrinter"
        type="success"
        size="small"
        plain
        @click="handleInspectFileDownload"
        >单据打印</el-button
      >
      <el-button type="success" size="small" plain @click="handleInspectFileDownload">
        <el-icon class="el-icon--left">
          <Icon icon="solar:printer-minimalistic-line-duotone" />
        </el-icon>
        å•据打印
      </el-button>
      <el-button type="success" size="small" @click="add" icon="Switch">任务调整</el-button>
      <el-button type="primary" size="small" @click="openMap">
        ä»»åŠ¡åœ°å›¾<el-icon class="el-icon--right"><Right /></el-icon>
@@ -97,6 +95,7 @@
import CompSubTaskEdit from './CompSubTaskEdit.vue'
import SceneInspectFile from '@/views/inspection/scene/SceneInspectFile.vue'
import subtaskApi from '@/api/fysp/subtaskApi'
import { Icon } from '@iconify/vue'
const props = defineProps({
  modelValue: Array,
src/views/monitor/DataDashboard.vue
@@ -2,10 +2,10 @@
  <div class="data-dashboard">
    <!-- é¡¶éƒ¨æŒ‡æ ‡å¡ç‰‡åŒº -->
    <div class="top-cards">
      <div class="cards-container">
        <!-- æ—¶é—´å‘¨æœŸé€‰é¡¹å¡ç‰‡ -->
        <div class="time-period-card">
          <div class="card-title">时间周期</div>
      <!-- æ—¶é—´å‘¨æœŸé€‰é¡¹å¡ç‰‡ -->
      <div class="time-period-card">
        <div class="card-title">时间选择</div>
        <div class="time-controls">
          <div class="time-tab-container">
            <div
              v-for="tab in timeTabs"
@@ -17,8 +17,46 @@
              {{ tab.label }}
            </div>
          </div>
          <div class="time-navigator">
            <button class="nav-btn" @click="navigateTime(-1)" title="上一个周期">
              <svg
                width="16"
                height="16"
                viewBox="0 0 24 24"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path
                  d="M15 18L9 12L15 6"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
              </svg>
            </button>
            <div class="current-time">{{ currentTimeDisplay }}</div>
            <button class="nav-btn" @click="navigateTime(1)" title="下一个周期">
              <svg
                width="16"
                height="16"
                viewBox="0 0 24 24"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path
                  d="M9 18L15 12L9 6"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
              </svg>
            </button>
          </div>
        </div>
      </div>
      <div class="cards-container">
        <!-- è¶…标数 -->
        <div class="metric-card">
          <div class="card-header">
@@ -200,7 +238,7 @@
      <!-- ä¸­éƒ¨GIS地图区 -->
      <div class="map-section">
        <div id="map" class="map-container">
          <BaseMap></BaseMap>
          <BaseMap :showSatellite="true"></BaseMap>
        </div>
        <!-- åœ°å›¾ç‚¹ä½å¼¹çª— -->
@@ -240,50 +278,175 @@
          </template>
        </el-dialog>
      </div>
    </div>
    <!-- å³ä¾§å®žæ—¶ç›‘测总览区 -->
    <div class="overview-section">
      <div class="section-header">
        <h3>设备监控</h3>
        <!-- <span class="view-more">查看更多</span> -->
      </div>
      <!-- å³ä¾§å®žæ—¶ç›‘测总览区 -->
      <div class="overview-section">
        <div class="section-header">
          <h3>实时监测总览</h3>
          <span class="view-more">查看更多</span>
      <div class="overview-items-container">
        <div class="overview-item">
          <div class="overview-label">餐饮店铺总数</div>
          <div class="overview-value">{{ overview.totalShops }}</div>
        </div>
        <div class="overview-items-container">
          <div class="overview-item">
            <div class="overview-label">餐饮店铺总数</div>
            <div class="overview-value">{{ overview.totalShops }}</div>
          </div>
          <div class="overview-item">
            <div class="overview-label">在线设备数</div>
            <div class="overview-value">{{ overview.onlineDevices }}</div>
          </div>
          <div class="overview-item">
            <div class="overview-label">离线设备数</div>
            <div class="overview-value">{{ overview.offlineDevices }}</div>
          </div>
        <div class="overview-item">
          <div class="overview-label">在线设备数</div>
          <div class="overview-value">{{ overview.onlineDevices }}</div>
        </div>
        <!-- è®¾å¤‡çŠ¶æ€é¥¼å›¾ -->
        <div class="device-status-chart">
          <canvas id="deviceStatusChart"></canvas>
        <div class="overview-item">
          <div class="overview-label">离线设备数</div>
          <div class="overview-value">{{ overview.offlineDevices }}</div>
        </div>
      </div>
      <!-- è®¾å¤‡çŠ¶æ€é¥¼å›¾ -->
      <div class="device-status-chart">
        <canvas id="deviceStatusChart"></canvas>
      </div>
    </div>
    <!-- å·¡æŸ¥æƒ…况统计卡片 -->
    <el-scrollbar class="inspection-section">
      <div class="section-header">
        <h3>巡查汇总</h3>
      </div>
      <!-- å·¡æŸ¥é‡ç»Ÿè®¡ -->
      <div class="inspection-metrics">
        <div class="inspection-metric-item">
          <div class="inspection-metric-label">店铺总计</div>
          <div class="inspection-metric-value">{{ inspectionStats.totalShops }}</div>
        </div>
        <div class="inspection-metric-item">
          <div class="inspection-metric-label">巡查店铺</div>
          <div class="inspection-metric-value">{{ inspectionStats.inspectedShops }}</div>
        </div>
        <div class="inspection-metric-item">
          <div class="inspection-metric-label">巡查点次</div>
          <div class="inspection-metric-value">{{ inspectionStats.inspectionPoints }}</div>
        </div>
        <div class="inspection-metric-item">
          <div class="inspection-metric-label">复查点次</div>
          <div class="inspection-metric-value">{{ inspectionStats.reviewPoints }}</div>
        </div>
      </div>
      <!-- é—®é¢˜æ•´æ”¹æƒ…况 -->
      <div class="inspection-chart-container">
        <div class="section-header"><h3>整改汇总</h3></div>
        <canvas id="rectificationChart"></canvas>
      </div>
      <!-- é—®é¢˜å®¡æ ¸æƒ…况 -->
      <div class="inspection-table-container">
        <div class="section-header"><h3>审核汇总</h3></div>
        <div class="inspection-metric-label">问题审核</div>
        <div class="inspection-table">
          <div class="inspection-metric-item">
            <div class="inspection-metric-label">无问题</div>
            <div class="inspection-metric-value">{{ inspectionStats.noProblemShops }}</div>
          </div>
          <div class="inspection-metric-item">
            <div class="inspection-metric-label">未审核</div>
            <div class="inspection-metric-value">
              {{ inspectionStats.unreviewedProblemShops }}
            </div>
          </div>
          <div class="inspection-metric-item">
            <div class="inspection-metric-label">部分审核</div>
            <div class="inspection-metric-value">
              {{ inspectionStats.partiallyReviewedProblemShops }}
            </div>
          </div>
          <div class="inspection-metric-item">
            <div class="inspection-metric-label">全部审核</div>
            <div class="inspection-metric-value">
              {{ inspectionStats.fullyReviewedProblemShops }}
            </div>
          </div>
        </div>
        <div class="inspection-metric-label">整改审核</div>
        <div class="inspection-table">
          <div class="inspection-metric-item">
            <div class="inspection-metric-label">未整改</div>
            <div class="inspection-metric-value">{{ inspectionStats.unrectifiedShops }}</div>
          </div>
          <div class="inspection-metric-item">
            <div class="inspection-metric-label">未审核</div>
            <div class="inspection-metric-value">
              {{ inspectionStats.unreviewedRectifiedShops }}
            </div>
          </div>
          <div class="inspection-metric-item">
            <div class="inspection-metric-label">部分审核</div>
            <div class="inspection-metric-value">
              {{ inspectionStats.partiallyReviewedRectifiedShops }}
            </div>
          </div>
          <div class="inspection-metric-item">
            <div class="inspection-metric-label">全部审核</div>
            <div class="inspection-metric-value">
              {{ inspectionStats.fullyReviewedRectifiedShops }}
            </div>
          </div>
          <!-- <div class="table-row">
            <div class="table-cell">无问题店铺数量</div>
            <div class="table-cell value">{{ inspectionStats.noProblemShops }}</div>
          </div>
          <div class="table-row">
            <div class="table-cell">问题未审核店铺数量</div>
            <div class="table-cell value">{{ inspectionStats.unreviewedProblemShops }}</div>
          </div>
          <div class="table-row">
            <div class="table-cell">问题部分审核店铺数量</div>
            <div class="table-cell value">
              {{ inspectionStats.partiallyReviewedProblemShops }}
            </div>
          </div>
          <div class="table-row">
            <div class="table-cell">问题全部审核店铺数量</div>
            <div class="table-cell value">{{ inspectionStats.fullyReviewedProblemShops }}</div>
          </div>
          <div class="table-row">
            <div class="table-cell">未整改店铺数</div>
            <div class="table-cell value">{{ inspectionStats.unrectifiedShops }}</div>
          </div>
          <div class="table-row">
            <div class="table-cell">整改未审核店铺数</div>
            <div class="table-cell value">{{ inspectionStats.unreviewedRectifiedShops }}</div>
          </div>
          <div class="table-row">
            <div class="table-cell">整改部分审核店铺数</div>
            <div class="table-cell value">
              {{ inspectionStats.partiallyReviewedRectifiedShops }}
            </div>
          </div>
          <div class="table-row">
            <div class="table-cell">整改全部审核店铺数</div>
            <div class="table-cell value">{{ inspectionStats.fullyReviewedRectifiedShops }}</div>
          </div> -->
        </div>
      </div>
    </el-scrollbar>
  </div>
</template>
<script>
import * as echarts from 'echarts'
import { onMapMounted, map, AMap } from '@/utils/map/index'
import { onMapMounted, satellite } from '@/utils/map/index'
import districtSearch from '@/utils/map/districtsearch.js'
import marks from '@/utils/map/marks.js'
import { generateTestShops } from '@/debug/debugdata'
export default {
  name: 'DataDashboard',
  data() {
    return {
      activeTime: 'day',
      currentDate: new Date(),
      timeTabs: [
        { label: '日', value: 'day' },
        { label: '周', value: 'week' },
@@ -314,13 +477,54 @@
        onlineDevices: 220,
        offlineDevices: 25,
      },
      inspectionStats: {
        // å·¡æŸ¥é‡
        totalShops: 245,
        inspectedShops: 180,
        inspectionPoints: 350,
        reviewPoints: 80,
        // é—®é¢˜æ•´æ”¹æƒ…况
        problemCount: 45,
        rectifiedCount: 38,
        rectificationRate: 84.4,
        // é—®é¢˜å®¡æ ¸æƒ…况
        noProblemShops: 135,
        unreviewedProblemShops: 8,
        partiallyReviewedProblemShops: 5,
        fullyReviewedProblemShops: 32,
        unreviewedRectifiedShops: 3,
        partiallyReviewedRectifiedShops: 2,
        fullyReviewedRectifiedShops: 33,
        unrectifiedShops: 7,
      },
      map: null,
      refreshTimer: null,
    }
  },
  computed: {
    currentTimeDisplay() {
      const date = this.currentDate
      switch (this.activeTime) {
        case 'day':
          return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
        case 'week':
          // ç®€å•计算周显示,实际项目中可能需要更复杂的周计算逻辑
          let weekStart = new Date(date)
          weekStart.setDate(date.getDate() - date.getDay() + 1)
          let weekEnd = new Date(date)
          weekEnd.setDate(date.getDate() + (7 - date.getDay()))
          return `${weekStart.getFullYear()}-${String(weekStart.getMonth() + 1).padStart(2, '0')}-${String(weekStart.getDate()).padStart(2, '0')} ~ ${weekEnd.getFullYear()}-${String(weekEnd.getMonth() + 1).padStart(2, '0')}-${String(weekEnd.getDate()).padStart(2, '0')}`
        case 'month':
          return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`
        default:
          return ''
      }
    },
  },
  mounted() {
    this.initMap()
    this.initDeviceStatusChart()
    this.initRectificationChart()
    this.startAutoRefresh()
  },
  beforeUnmount() {
@@ -334,16 +538,44 @@
      // æ¨¡æ‹Ÿåˆ‡æ¢æ—¶é—´å‘¨æœŸåŽçš„æ•°æ®æ›´æ–°
      this.updateMetrics()
    },
    getPeriodLabel() {
    navigateTime(direction) {
      const newDate = new Date(this.currentDate)
      switch (this.activeTime) {
        case 'day':
          return '今日'
          newDate.setDate(newDate.getDate() + direction)
          break
        case 'week':
          return '本周'
          newDate.setDate(newDate.getDate() + direction * 7)
          break
        case 'month':
          return '本月'
          newDate.setMonth(newDate.getMonth() + direction)
          break
      }
      this.currentDate = newDate
      // æ¨¡æ‹Ÿåˆ‡æ¢æ—¶é—´åŽçš„æ•°æ®æ›´æ–°
      this.updateMetrics()
    },
    getPeriodLabel() {
      const today = new Date()
      const isToday =
        this.activeTime === 'day' &&
        this.currentDate.getDate() === today.getDate() &&
        this.currentDate.getMonth() === today.getMonth() &&
        this.currentDate.getFullYear() === today.getFullYear()
      if (isToday) {
        return '今日'
      }
      switch (this.activeTime) {
        case 'day':
          return '当日'
        case 'week':
          return '当周'
        case 'month':
          return '当月'
        default:
          return '今日'
          return '当日'
      }
    },
    getCompareLabel() {
@@ -372,14 +604,46 @@
          taskCompletionRate: Math.floor(Math.random() * 40) + 60,
          taskCompletionRateTrend: Math.floor(Math.random() * 15) - 7,
        }
        // æ›´æ–°å·¡æŸ¥ç»Ÿè®¡æ•°æ®
        this.inspectionStats = {
          totalShops: 245,
          inspectedShops: Math.floor(Math.random() * 50) + 150,
          inspectionPoints: Math.floor(Math.random() * 100) + 300,
          reviewPoints: Math.floor(Math.random() * 50) + 50,
          problemCount: Math.floor(Math.random() * 30) + 20,
          rectifiedCount: Math.floor(Math.random() * 25) + 15,
          rectificationRate: Math.round((Math.random() * 30 + 70) * 10) / 10,
          noProblemShops: Math.floor(Math.random() * 50) + 100,
          unreviewedProblemShops: Math.floor(Math.random() * 10),
          partiallyReviewedProblemShops: Math.floor(Math.random() * 8),
          fullyReviewedProblemShops: Math.floor(Math.random() * 20) + 15,
          unreviewedRectifiedShops: Math.floor(Math.random() * 5),
          partiallyReviewedRectifiedShops: Math.floor(Math.random() * 5),
          fullyReviewedRectifiedShops: Math.floor(Math.random() * 20) + 15,
          unrectifiedShops: Math.floor(Math.random() * 10),
        }
        // æ›´æ–°å›¾è¡¨
        this.initRectificationChart()
      }, 300)
    },
    initMap() {
      // setTimeout(() => {
      districtSearch.removeDistrict()
      districtSearch.drawDistrict('上海市')
      districtSearch.drawDistrictMask('上海市')
      // districtSearch.districtLayer('310106')
      // }, 2000)
      onMapMounted(() => {
        setTimeout(() => {
          marks.clearMassMarks()
          const shops = generateTestShops()
          console.log(shops)
          marks.drawMassMarks(shops)
        }, 2000)
      })
    },
    initDeviceStatusChart() {
      const chartDom = document.getElementById('deviceStatusChart')
@@ -452,9 +716,83 @@
        chart.setOption(option)
        // å“åº”式调整
        window.addEventListener('resize', () => {
          chart.resize()
        })
        // window.addEventListener('resize', () => {
        //   chart.resize()
        // })
      }
    },
    initRectificationChart() {
      const chartDom = document.getElementById('rectificationChart')
      if (chartDom) {
        const chart = echarts.init(chartDom)
        const option = {
          tooltip: {
            trigger: 'axis',
            axisPointer: {
              type: 'shadow',
            },
            backgroundColor: 'rgba(255, 255, 255, 0.95)',
            borderColor: '#e8e8e8',
            borderWidth: 1,
            textStyle: {
              color: '#333',
            },
          },
          grid: {
            left: '3%',
            right: '4%',
            bottom: '3%',
            containLabel: true,
          },
          xAxis: {
            type: 'category',
            data: ['问题数', '整改数'],
            axisLabel: {
              color: '#86909c',
              fontSize: 12,
            },
          },
          yAxis: {
            type: 'value',
            axisLabel: {
              color: '#86909c',
              fontSize: 12,
            },
          },
          series: [
            {
              name: '数量',
              type: 'bar',
              data: [
                {
                  value: this.inspectionStats.problemCount,
                  itemStyle: {
                    color: '#fa8c16',
                  },
                },
                {
                  value: this.inspectionStats.rectifiedCount,
                  itemStyle: {
                    color: '#52c41a',
                  },
                },
              ],
              barWidth: '60%',
              label: {
                show: true,
                position: 'top',
                color: '#262626',
                fontSize: 12,
              },
            },
          ],
        }
        chart.setOption(option)
        // å“åº”式调整
        // window.addEventListener('resize', () => {
        //   chart.resize()
        // })
      }
    },
    startAutoRefresh() {
@@ -476,7 +814,7 @@
/* å…¨å±€æ ·å¼ */
.data-dashboard {
  width: 100%;
  height: calc(100vh - 60px);
  height: calc(100vh - 70px);
  background-color: #f5f7fa;
  color: #333;
  box-sizing: border-box;
@@ -488,32 +826,34 @@
/* é¡¶éƒ¨æŒ‡æ ‡å¡ç‰‡åŒº */
.top-cards {
  position: absolute;
  top: 24px;
  left: 24px;
  top: 4px;
  left: 4px;
  z-index: 10;
  margin-bottom: 24px;
}
.cards-container {
  display: grid;
  grid-template-columns: 280px;
  grid-template-rows: auto repeat(4, auto);
  gap: 16px;
  background-color: rgba(255, 255, 255, 0.9);
  padding: 16px;
  grid-template-columns: repeat(2, 180px);
  grid-template-rows: auto repeat(2, auto);
  gap: 8px;
  /* background-color: rgba(255, 255, 255, 0.9); */
  /* padding: 16px; */
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  /* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); */
}
/* æ—¶é—´å‘¨æœŸå¡ç‰‡ */
.time-period-card {
  background-color: #ffffff;
  border-radius: 8px;
  padding: 20px;
  padding: 4px 16px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
  display: flex;
  flex-direction: column;
  justify-content: center;
  margin-bottom: 8px;
  min-width: 300px;
}
.time-period-card .card-title {
@@ -522,6 +862,12 @@
  font-weight: 500;
  margin-bottom: 16px;
  text-align: center;
}
.time-controls {
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.time-tab-container {
@@ -533,7 +879,7 @@
}
.time-tab {
  padding: 2px 4px;
  padding: 4px 8px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
@@ -543,6 +889,7 @@
  text-align: center;
  border: 1px solid #e8e8e8;
  background-color: #fafafa;
  flex: 1;
}
.time-tab.active {
@@ -556,6 +903,43 @@
  color: #1890ff;
  border-color: #e6f7ff;
  background-color: #e6f7ff;
}
.time-navigator {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 8px 0;
}
.nav-btn {
  width: 32px;
  height: 32px;
  border: 1px solid #e8e8e8;
  background-color: #fafafa;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.3s ease;
  color: #4e5969;
}
.nav-btn:hover {
  border-color: #1890ff;
  color: #1890ff;
  background-color: #e6f7ff;
}
.current-time {
  font-size: 14px;
  font-weight: 500;
  color: #262626;
  min-width: 180px;
  text-align: center;
  padding: 0 12px;
}
/* æŒ‡æ ‡å¡ç‰‡ */
@@ -714,8 +1098,8 @@
/* å³ä¾§å®žæ—¶ç›‘测总览区 */
.overview-section {
  position: absolute;
  top: 200px;
  right: 24px;
  bottom: 4px;
  left: 4px;
  width: 320px;
  background-color: rgba(255, 255, 255, 0.9);
  border-radius: 8px;
@@ -731,7 +1115,6 @@
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 24px;
  padding-bottom: 16px;
  border-bottom: 1px solid #f0f0f0;
}
@@ -759,11 +1142,101 @@
.device-status-chart {
  flex: 1;
  margin-top: 16px;
  min-height: 200px;
  min-height: 100px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 16px;
}
/* å·¡æŸ¥æƒ…况统计 */
.inspection-section {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 320px;
  background-color: rgba(255, 255, 255, 0.9);
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  padding: 20px;
  display: flex;
  flex-direction: column;
  z-index: 10;
  max-height: calc(70vh);
  border-top: 1px solid #f0f0f0;
}
.inspection-metrics {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
  margin-bottom: 20px;
}
.inspection-metric-item {
  background-color: #fafafa;
  border-radius: 6px;
  padding: 12px;
  text-align: center;
}
.inspection-metric-label {
  font-size: 12px;
  color: #86909c;
  margin-bottom: 4px;
}
.inspection-metric-value {
  font-size: 18px;
  font-weight: bold;
  color: #262626;
}
.inspection-chart-container {
  margin-bottom: 20px;
}
.chart-title {
  font-size: 14px;
  font-weight: 500;
  color: #262626;
  margin-bottom: 12px;
}
.inspection-table-container {
  /* max-height: 200px; */
  /* overflow-y: auto; */
}
.inspection-table {
  /* width: 100%;
  border-collapse: collapse; */
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 2px;
  margin-bottom: 20px;
}
.table-row {
  display: flex;
  border-bottom: 1px solid #f0f0f0;
  padding: 8px 0;
}
.table-row:last-child {
  border-bottom: none;
}
.table-cell {
  flex: 1;
  font-size: 12px;
  color: #4e5969;
}
.table-cell.value {
  font-weight: 500;
  color: #262626;
  text-align: right;
}
/* å¼¹çª—样式 */
@@ -791,7 +1264,7 @@
}
/* å“åº”式设计 */
@media (max-width: 1200px) {
/* @media (max-width: 1200px) {
  .top-cards {
    position: relative;
    margin-bottom: 24px;
@@ -817,9 +1290,9 @@
    margin-top: 16px;
    background-color: #ffffff;
  }
}
} */
@media (max-width: 768px) {
/* @media (max-width: 768px) {
  .data-dashboard {
    padding: 16px;
  }
@@ -861,5 +1334,5 @@
    width: 100%;
    text-align: left;
  }
}
} */
</style>