Explorar el Código

绘图对接最新

wangfumin hace 1 mes
padre
commit
fbf0a0db60

+ 50 - 0
package-lock.json

@@ -17,6 +17,7 @@
         "axios": "^1.4.0",
         "echarts": "^5.4.3",
         "element-plus": "^2.3.8",
+        "html2canvas": "^1.4.1",
         "js-base64": "^3.7.5",
         "mime": "^3.0.0",
         "mitt": "^3.0.1",
@@ -1137,6 +1138,15 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/base64-arraybuffer": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+      "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
     "node_modules/brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -1283,6 +1293,15 @@
         "node": ">=4.8"
       }
     },
+    "node_modules/css-line-break": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz",
+      "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+      "license": "MIT",
+      "dependencies": {
+        "utrie": "^1.0.2"
+      }
+    },
     "node_modules/csstype": {
       "version": "3.1.3",
       "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
@@ -2039,6 +2058,19 @@
       "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
       "dev": true
     },
+    "node_modules/html2canvas": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz",
+      "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+      "license": "MIT",
+      "dependencies": {
+        "css-line-break": "^2.1.0",
+        "text-segmentation": "^1.0.3"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
     "node_modules/immutable": {
       "version": "4.3.7",
       "resolved": "https://registry.npmmirror.com/immutable/-/immutable-4.3.7.tgz",
@@ -3384,6 +3416,15 @@
         "node": ">= 4.7.0"
       }
     },
+    "node_modules/text-segmentation": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz",
+      "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+      "license": "MIT",
+      "dependencies": {
+        "utrie": "^1.0.2"
+      }
+    },
     "node_modules/three": {
       "version": "0.158.0",
       "resolved": "https://registry.npmmirror.com/three/-/three-0.158.0.tgz",
@@ -3573,6 +3614,15 @@
         "node": ">=16.14.0"
       }
     },
+    "node_modules/utrie": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz",
+      "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+      "license": "MIT",
+      "dependencies": {
+        "base64-arraybuffer": "^1.0.2"
+      }
+    },
     "node_modules/validate-npm-package-license": {
       "version": "3.0.4",
       "resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",

+ 1 - 0
package.json

@@ -27,6 +27,7 @@
     "axios": "^1.4.0",
     "echarts": "^5.4.3",
     "element-plus": "^2.3.8",
+    "html2canvas": "^1.4.1",
     "js-base64": "^3.7.5",
     "mime": "^3.0.0",
     "mitt": "^3.0.1",

+ 3 - 1
src/request/config.ts

@@ -24,7 +24,8 @@ import {
   userLogin,
   userReg,
   getCaseHasDownloadProcess,
-  ffmpegMergeImage
+  ffmpegMergeImage,
+  addOrUpdateCaseTabulation
 } from "./urls";
 
 // 不需要登录就能请求的接口
@@ -64,6 +65,7 @@ export const PostUrls = [
   getMessageList,
   getSceneList,
   getModelSceneList,
+  addOrUpdateCaseTabulation,
 ];
 
 // 未认证code

+ 14 - 1
src/request/urls.ts

@@ -265,4 +265,17 @@ export const editMedia = `/fusion/dictFile/addOrUpdate/media-library`;
 
 export const deleteMedia = `/fusion/dictFile/del/media-library`;
 export const downFile = `/fusion/dictFile/downFile`;
-export const downhash = `/fusion/dictFile/downHash`
+export const downhash = `/fusion/dictFile/downHash`;
+
+// 案件制表
+export const addOrUpdateCaseTabulation = `/fusion/caseTabulation/addOrUpdate`;
+// 方位图列表
+export const getcaseTabulationList = `/fusion/caseTabulation/getByCaseId`;
+// 获取平面图列表
+export const getcaseOverviewList = `/fusion/caseOverview/getByCaseId`;
+// 更新方位图和平面图
+export const updateCaseTabulation = `/fusion/caseTabulation/addOrUpdate`;
+export const updateCaseOverview = `/fusion/caseOverview/addOrUpdate`;
+// 删除方位图和平面图
+export const delCaseTabulation = `/fusion/caseTabulation/del`;
+export const delCaseOverview = `/fusion/caseOverview/del`;

+ 22 - 2
src/store/case.ts

@@ -21,7 +21,13 @@ import {
   copyExample,
   saveCaseImgTag,
   getCaseImgTag,
-  ffmpegMergeImage
+  ffmpegMergeImage,
+  getcaseTabulationList,
+  getcaseOverviewList,
+  updateCaseTabulation as updateCaseTabulationUrl,
+  updateCaseOverview as updateCaseOverviewUrl,
+  delCaseTabulation as delCaseTabulationUrl,
+  delCaseOverview as delCaseOverviewUrl
 } from "@/request";
 import { ModelScene, QuoteScene, Scene, SceneType } from "./scene";
 import { CaseFile } from "./caseFile";
@@ -159,4 +165,18 @@ export const getCaseImgTagData = (caseId: number) =>
   axios.get(getCaseImgTag, { params: { caseId } });
 
 
-export const submitMergePhotos = (data) => axios.post(ffmpegMergeImage, { ...data })
+export const submitMergePhotos = (data) => axios.post(ffmpegMergeImage, { ...data })
+
+// 绘图接口
+export const getCaseTabulationList = (caseId: number) =>
+  axios.get(getcaseTabulationList, { params: { caseId } });
+
+export const getCaseOverviewList = (caseId: number) =>
+  axios.get(getcaseOverviewList, { params: { caseId } });
+
+export const updateCaseTabulation = (data: any) => axios.post(updateCaseTabulationUrl, { ...data });
+export const updateCaseOverview = (data: any) => axios.post(updateCaseOverviewUrl, { ...data });
+
+// 删除接口
+export const delCaseTabulation = (data: { id: number }) => axios.post(delCaseTabulationUrl, data);
+export const delCaseOverview = (data: { id: number }) => axios.post(delCaseOverviewUrl, data);

+ 17 - 0
src/store/caseFile.ts

@@ -34,6 +34,23 @@ export interface CaseFile {
   updateTime: string;
   imgType?: BoardType;
   content: BoardData;
+  // 扩展属性
+  type?: 'old' | 'tabulation' | 'overview';
+  listCover?: string;
+  title?: string;
+  id?: number; // tabulation 类型需要的 id
+  // tabulation 类型的属性
+  store?: any;
+  viewport?: any;
+  cover?: string;
+  paperKey?: string;
+  overviewId?: number;
+  isAutoGen?: boolean;
+  mapUrl?: string;
+  high?: number;
+  width?: number;
+  // overview 类型的属性
+  kankanCover?: string;
 }
 
 export const getCaseFileTypes = async () =>

+ 76 - 0
src/util/upload-utils.ts

@@ -0,0 +1,76 @@
+import { axios } from '@/request'
+import { uploadFile } from '@/request/urls'
+import { ElMessage } from 'element-plus'
+
+/**
+ * 上传文件的公共方法
+ * @param file - 要上传的文件
+ * @param onProgress - 上传进度回调函数
+ * @returns Promise<{url: string, fileName: string}> 返回上传成功后的文件信息
+ */
+export const uploadFileToServer = async (
+  file: File,
+  onProgress?: (progressEvent: any) => void
+): Promise<{url: string, fileName: string}> => {
+  try {
+    const formData = new FormData()
+    formData.append('file', file)
+
+    const response = await axios({
+      url: uploadFile,
+      method: 'POST',
+      data: { file },
+      onUploadProgress: onProgress
+    })
+
+    if (response.code === 0 || response.code === '000000' || response.code === 200) {
+      return {
+        url: response.data?.url || response.data,
+        fileName: file.name
+      }
+    } else {
+      throw new Error(response.msg || '上传失败')
+    }
+  } catch (error) {
+    console.error('文件上传失败:', error)
+    ElMessage.error('文件上传失败')
+    throw error
+  }
+}
+
+/**
+ * 将Canvas转换为Blob对象
+ * @param canvas - Canvas元素
+ * @param quality - 图片质量(0-1)
+ * @param format - 图片格式
+ * @returns Promise<Blob>
+ */
+export const canvasToBlob = (
+  canvas: HTMLCanvasElement,
+  quality: number = 0.8,
+  format: string = 'image/jpeg'
+): Promise<Blob> => {
+  return new Promise((resolve, reject) => {
+    canvas.toBlob(
+      (blob) => {
+        if (blob) {
+          resolve(blob)
+        } else {
+          reject(new Error('Canvas转换为Blob失败'))
+        }
+      },
+      format,
+      quality
+    )
+  })
+}
+
+/**
+ * Blob转换为File对象
+ * @param blob - Blob对象
+ * @param fileName - 文件名
+ * @returns File对象
+ */
+export const blobToFile = (blob: Blob, fileName: string): File => {
+  return new File([blob], fileName, { type: blob.type })
+} 

+ 415 - 28
src/view/case/drawMap/creatMap.vue

@@ -2,7 +2,7 @@
   <el-dialog
     v-model="visible"
     title="选择地图位置"
-    width="80%"
+    width="1200px"
     :before-close="handleClose"
     destroy-on-close
     class="map-fire-dialog"
@@ -11,21 +11,27 @@
       <!-- 搜索框 -->
       <div id="panel" class="scrollbar1">
             <div id="searchBar">
-                <input id="searchInput" @input="handleSearch" placeholder="输入关键字搜素POI" />
+                <el-select class="search-select" v-model="selectedSearchAdress" placeholder="地址">
+                  <el-option label="地址" value="1"></el-option>
+                  <el-option label="经纬度" value="2"></el-option>
+                </el-select>
+                <input class="search-input-latlng" v-if="selectedSearchAdress === '2'" @input="handleSearch" autocomplete="off" @keydown="handleKeyDown" placeholder="输入经纬度" />
+                <input class="search-input-address" v-else id="searchInput" @input="handleSearch" autocomplete="off" placeholder="输入地址" />
+                <!-- <input id="searchInput" @input="handleSearch" autocomplete="off" @keydown="handleKeyDown" :placeholder="selectedSearchAdress === '1' ? '输入地址' : '输入经纬度'" /> -->
             </div>
             <div id="searchResults">暂无数据</div>
         </div>
 
       <!-- 地图容器 -->
       <div class="map-container">
-        <div id="container" class="map" style="width: 100%; height: 100%" tabindex="0"></div>
+        <div id="container" class="map" style="width: 800px; height: 600px" tabindex="0"></div>
       </div>
     </div>
 
     <template #footer>
       <div class="dialog-footer">
         <el-button @click="handleClose">取消</el-button>
-        <el-button type="primary" @click="handleConfirm" :disabled="!selectedLocation">
+        <el-button type="primary" @click="handleConfirm">
           确定
         </el-button>
       </div>
@@ -38,20 +44,32 @@ import { ref, watch, nextTick, onUnmounted, onMounted } from 'vue'
 import { ElDialog, ElInput, ElButton, ElDescriptions, ElDescriptionsItem, ElIcon, ElMessage } from 'element-plus'
 import { Search } from '@element-plus/icons-vue'
 import AMapLoader from '@amap/amap-jsapi-loader'
+import html2canvas from 'html2canvas'
+import { uploadFileToServer, canvasToBlob, blobToFile } from '@/util/upload-utils'
+import { axios } from '@/request'
+import { addOrUpdateCaseTabulation } from '@/request/urls'
+import { user } from "@/store/user";
+
+// 添加高德地图类型声明
+declare const AMap: any
+declare const AMapUI: any
 
 // 定义组件props和emits
 interface Props {
   modelValue: boolean
+  caseId?: string | number
 }
 
 interface LocationInfo {
-  name: string
-  address: string
-  lat: number
-  lng: number
+  name?: string
+  address?: string
+  lat?: number
+  lng?: number
   adcode?: string
   citycode?: string
   district?: string
+  screenshotUrl?: string
+  screenshotFileName?: string
 }
 
 const props = defineProps<Props>()
@@ -76,7 +94,11 @@ let poiPicker: any = null
 
 // 高德地图配置
 const AMAP_KEY = '2ae5a7713612a8d5a65cfd54c989c969'
+const selectedSearchAdress = ref('1')
+const searchInputValue = ref('')
 
+// 防抖定时器
+let searchDebounceTimer: number | null = null
 // 监听visible变化
 watch(() => props.modelValue, (newVal) => {
   visible.value = newVal
@@ -90,12 +112,49 @@ watch(() => props.modelValue, (newVal) => {
 watch(visible, (newVal) => {
   emit('update:modelValue', newVal)
 })
+
+// 监听搜索类型变化,切换时清空输入框和重置搜索
+watch(selectedSearchAdress, (newVal, oldVal) => {
+  if (newVal !== oldVal) {
+    // 清空输入框
+    searchInputValue.value = ''
+    const searchInput = document.getElementById('searchInput') as HTMLInputElement
+    if (searchInput) {
+      searchInput.value = ''
+    }
+    
+    // 重置搜索结果
+    const searchResults = document.getElementById('searchResults')
+    if (searchResults) {
+      searchResults.innerHTML = '暂无数据'
+    }
+    
+    // 清空地图标记
+    if (map) {
+      map.clearMap()
+    }
+    
+    // 根据搜索类型调整POI选择器的显示
+    if (poiPicker) {
+      if (newVal === '2') {
+        // 经纬度搜索模式,隐藏选择列表
+        poiPicker.hideSearchResults()
+      } else {
+        // 地址搜索模式,可以显示选择列表
+        poiPicker.searchByKeyword('')
+      }
+    }
+  }
+})
 // 初始化地图
 const initMap = async () => {
     map = new AMap.Map('container', {
-        zoom: 10,
+        zoom: 11,
         key: AMAP_KEY, // 替换为你的API密钥
         center: [116.35, 39.86],
+        WebGLParams: {
+            preserveDrawingBuffer: true
+        }
     });
 
     AMapUI.loadUI(['misc/PoiPicker'], function(PoiPicker) {
@@ -118,7 +177,8 @@ const initMap = async () => {
             poiPicker.searchByKeyword(poi.name);
 
         } else {
-            addMarker(poi.location.lng, poi.location.lat)
+          // 舒琪说不需要marker
+            // addMarker(poi.location.lng, poi.location.lat)
             // console.log(poi);
         }
     });
@@ -129,15 +189,169 @@ const initMap = async () => {
 });
 
 }
-const handleSearch = (e) => {
-    // console.log('handleSearch', e.target.value)
-    let mapData = e.target.value
-    poiPicker.searchByKeyword( e.target.value)
-    // setTimeout(() => {
-    //     map.clearMap();
-    // }, 100)
+const handleKeyDown = (e: KeyboardEvent) => {
+  if (e.key === 'Enter') {
+    if(selectedSearchAdress.value === '2'){
+      console.log(11111)
+      e.preventDefault()
+      e.stopPropagation()
+      return
+    }
+  }
+}
+// 验证经纬度格式的函数
+const validateCoordinates = (input: string): { isValid: boolean; lat?: number; lng?: number } => {
+  // 移除空格
+  const trimmed = input.trim()
+  
+  // 检查是否包含逗号
+  if (!trimmed.includes(',')) {
+    return { isValid: false }
+  }
+  
+  // 分割经纬度
+  const parts = trimmed.split(',')
+  if (parts.length !== 2) {
+    return { isValid: false }
+  }
+  
+  // 转换为数字并验证
+  const lat = parseFloat(parts[0].trim())
+  const lng = parseFloat(parts[1].trim())
+  
+  // 验证是否为有效数字
+  if (isNaN(lat) || isNaN(lng)) {
+    return { isValid: false }
+  }
+  
+  // 验证纬度范围 (-90 到 90)
+  if (lat < -90 || lat > 90) {
+    return { isValid: false }
+  }
+  
+  // 验证经度范围 (-180 到 180)
+  if (lng < -180 || lng > 180) {
+    return { isValid: false }
+  }
+  
+  return { isValid: true, lat, lng }
 }
 
+// 根据经纬度定位地图
+const locateByCoordinates = (lat: number, lng: number) => {
+  if (!map) {
+    ElMessage.error('地图未初始化')
+    return
+  }
+  
+  try {
+    // 创建经纬度点
+    const lngLat = new AMap.LngLat(lng, lat)
+    
+    // 设置地图中心点并调整缩放级别
+    map.setZoomAndCenter(15, lngLat)
+    
+    // 清除之前的标记
+    map.clearMap()
+    
+    // 添加标记
+    const marker = new AMap.Marker({
+      position: lngLat,
+      content: '<div class="amap_lib_placeSearch_poi"></div>',
+      offset: new AMap.Pixel(-10, -31)
+    })
+    map.add(marker)
+    
+    // 清空搜索结果显示
+    const searchResults = document.getElementById('searchResults')
+    if (searchResults) {
+      searchResults.innerHTML = `
+        <div style="padding: 16px; text-align: center; color: #606266;">
+          <div style="font-weight: 500; margin-bottom: 8px;">经纬度定位成功</div>
+          <div style="font-size: 12px;">
+            纬度: ${lat}<br/>
+            经度: ${lng}
+          </div>
+        </div>
+      `
+    }
+    
+    ElMessage.success('经纬度定位成功')
+  } catch (error) {
+    console.error('经纬度定位失败:', error)
+    ElMessage.error('经纬度定位失败')
+  }
+}
+
+// 清除防抖定时器
+const clearSearchDebounce = () => {
+  if (searchDebounceTimer) {
+    clearTimeout(searchDebounceTimer)
+    searchDebounceTimer = null
+  }
+}
+
+// 防抖搜索函数
+const debouncedSearch = (inputValue: string, searchType: string) => {
+  clearSearchDebounce()
+  
+  searchDebounceTimer = setTimeout(() => {
+    if (searchType === '2') {
+      // 经纬度搜索
+      if (!inputValue.trim()) {
+        // 输入为空时,清空搜索结果
+        const searchResults = document.getElementById('searchResults')
+        if (searchResults) {
+          searchResults.innerHTML = '暂无数据'
+        }
+        return
+      }
+      
+      const validation = validateCoordinates(inputValue)
+      
+      if (validation.isValid && validation.lat !== undefined && validation.lng !== undefined) {
+        // 经纬度格式正确,进行定位
+        locateByCoordinates(validation.lat, validation.lng)
+      } else {
+        // 经纬度格式错误,只在搜索结果区域显示提示,不弹窗
+        const searchResults = document.getElementById('searchResults')
+        if (searchResults) {
+          searchResults.innerHTML = `
+            <div style="padding: 16px; text-align: center; color: #f56c6c;">
+              <div style="font-weight: 500; margin-bottom: 8px;">经纬度格式错误</div>
+              <div style="font-size: 12px; line-height: 1.5;">
+                请输入正确的经纬度格式:<br/>
+                纬度,经度(例如:23.11766,113.28122)<br/>
+                纬度范围:-90 到 90<br/>
+                经度范围:-180 到 180
+              </div>
+            </div>
+          `
+        }
+      }
+    } else {
+      console.log(22222)
+      // 地址搜索,保持原有逻辑
+      if (poiPicker) {
+        poiPicker.searchByKeyword(inputValue)
+      }
+    }
+  }, 500) // 500ms防抖延迟
+}
+
+const handleSearch = (e) => {
+    console.log('handleSearch', e.target.value)
+    const inputValue = e.target.value
+    searchInputValue.value = inputValue
+    
+    // 对于经纬度搜索,强制隐藏POI选择列表
+    if (selectedSearchAdress.value === '2' && poiPicker) {
+      poiPicker.hideSearchResults()
+    }
+    
+    // 使用防抖搜索
+    debouncedSearch(inputValue, selectedSearchAdress.value)
+}
 
 // 添加标记
 const addMarker = (lng: number, lat: number) => {
@@ -165,11 +379,171 @@ const handleClose = () => {
   }
 }
 
+const getMapSize = () => {
+  if (!map) {
+    ElMessage.error('地图未初始化')
+    return
+  }
+
+  try {
+    // 获取当前地图的可视区域边界
+    const bounds = map.getBounds()
+    const southwest = bounds.getSouthWest() // 西南角
+    const northeast = bounds.getNorthEast() // 东北角
+    const northwest = new (window as any).AMap.LngLat(southwest.lng, northeast.lat) // 西北角
+    const southeast = new (window as any).AMap.LngLat(northeast.lng, southwest.lat) // 东南角
+    
+    // 使用高德地图的几何工具计算距离(单位:米)
+    const AMap = (window as any).AMap
+    
+    // 计算宽度(东西方向距离)
+    const width = AMap.GeometryUtil.distance(southwest, southeast)
+    
+    // 计算高度(南北方向距离)
+    const height = AMap.GeometryUtil.distance(southwest, northwest)
+    
+    // 计算面积(平方米)
+    const area = width * height
+    
+    // 获取当前缩放级别
+    const zoom = map.getZoom()
+    
+    // 获取地图中心点
+    const center = map.getCenter()
+    
+    const viewportInfo = {
+      width: width, // 宽度(米)
+      height: height, // 高度(米)
+      area: Math.round(area), // 面积(平方米)
+      zoom: zoom, // 缩放级别
+      center: {
+        lat: center.lat,
+        lng: center.lng
+      },
+      bounds: {
+        southwest: { lat: southwest.lat, lng: southwest.lng },
+        northeast: { lat: northeast.lat, lng: northeast.lng }
+      }
+    }
+    
+    console.log('当前可视区域信息:', viewportInfo)
+    
+    // ElMessage.success(`
+    //   可视区域信息:
+    //   宽度:${viewportInfo.width.toLocaleString()} 米
+    //   高度:${viewportInfo.height.toLocaleString()} 米
+    //   面积:${viewportInfo.area.toLocaleString()} 平方米
+    //   缩放级别:${viewportInfo.zoom.toFixed(2)}
+    // `)
+    return {
+      width: viewportInfo.width,
+      height: viewportInfo.height,
+    }
+  } catch (error) {
+    console.error('计算可视区域失败:', error)
+    ElMessage.error('计算可视区域失败')
+  }
+}
+
+const getCanvasImage = async (): Promise<{url: string, fileName: string} | null> => {
+  try {
+    // 使用html2canvas截图高德地图可视化区域
+    const mapElement = document.getElementById('container')
+    if (!mapElement) {
+      ElMessage.error('地图容器未找到')
+      return null
+    }
+
+    ElMessage.info('正在生成地图截图...')
+    
+    // 配置html2canvas选项
+    const canvas = await html2canvas(mapElement, {
+      useCORS: true,
+      allowTaint: true,
+      scale: 1,
+      logging: false,
+      imageTimeout: 15000,
+      // 忽略某些元素(比如控件)
+      ignoreElements: (element) => {
+        return element.classList.contains('amap-logo') || 
+               element.classList.contains('amap-copyright') ||
+               element.classList.contains('amap-controls')
+      }
+    })
+
+    // 将canvas转换为Blob
+    const blob = await canvasToBlob(canvas, 0.8, 'image/jpeg')
+    
+    // 生成文件名
+    const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
+    const fileName = `map-screenshot-${timestamp}.jpg`
+    
+    // 转换为File对象
+    const file = blobToFile(blob, fileName)
+    
+    // 上传文件
+    const uploadResult = await uploadFileToServer(file, (progressEvent) => {
+      // 可选:显示上传进度
+      if (progressEvent.lengthComputable) {
+        const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
+        console.log(`上传进度: ${percentCompleted}%`)
+      }
+    })
+
+    console.log('上传成功:', uploadResult)
+    return uploadResult
+  } catch (error) {
+    console.error('截图或上传失败:', error)
+    ElMessage.error('截图或上传失败,请重试')
+    return null
+  }
+}
 // 处理确认选择
-const handleConfirm = () => {
-  if (selectedLocation.value) {
-    emit('confirm', selectedLocation.value)
+const handleConfirm = async () => {
+  try {
+    if (!props.caseId) {
+      ElMessage.error('缺少案件ID参数')
+      return
+    }
+
+    // 获取地图尺寸信息
+    const mapSizeInfo = getMapSize()
+    if (!mapSizeInfo) {
+      ElMessage.error('获取地图尺寸失败')
+      return
+    }
+
+    // 获取截图并上传
+    const uploadResult = await getCanvasImage()
+    if (!uploadResult) {
+      ElMessage.error('截图上传失败')
+      return
+    }
+
+    ElMessage.info('正在保存方位图信息...')
+
+    // 调用案件制表接口
+    const response = await axios({
+      url: addOrUpdateCaseTabulation,
+      method: 'POST',
+      data: {
+        caseId: props.caseId,
+        width: mapSizeInfo.width,
+        high: mapSizeInfo.height,
+        listCover: uploadResult.url, // 封面图
+        mapUrl: uploadResult.url,    // 地图URL
+        title: '方位图'
+      }
+    })
+
+    ElMessage.success('方位图保存成功!')
+    console.log('方位图保存成功:', response)
+    console.log(`http://test-mix3d.4dkankan.com/draw/index.html#/tabulation?caseId=${props.caseId}&tabulationId=${response.data.id}&token=${user.value.token}`)
+    window.open(`http://test-mix3d.4dkankan.com/draw/index.html#/tabulation?caseId=${props.caseId}&tabulationId=${response.data.id}&token=${user.value.token}`, '_blank')
     handleClose()
+  } catch (error) {
+    console.error('保存方位图失败:', error)
+    ElMessage.error('保存方位图失败,请重试')
   }
 }
 
@@ -178,6 +552,8 @@ onUnmounted(() => {
   if (map) {
     map.destroy()
   }
+  // 清除防抖定时器
+  clearSearchDebounce()
 })
 </script>
 
@@ -188,9 +564,9 @@ onUnmounted(() => {
         margin-bottom: 16px;
     }
     // https://a.amap.com/jsapi/static/image/plugin/marker_red.png
-    // .amap-marker{
-    //     display: none!important;
-    // }
+    .amap-marker{
+        display: none!important;
+    }
 }
 </style>
 <style scoped lang="scss">
@@ -213,15 +589,25 @@ onUnmounted(() => {
         display: none;
     }
     #searchBar{
-        width: 300px;
+        display: flex;
+        width: 380px;
         text-align: left;
         margin-bottom: 16px;
     }
-    :deep(#searchInput) {
-        width: 378px;
+    .search-select{
+        width: 100px;
         height: 40px;
-        border-radius: 4px;
+        
+        :deep(.el-select__wrapper){
+          border-radius: 4px 0 0 4px;
+        }
+    }
+    :deep(#searchInput) {
+        width: 260px;
+        height: 38px;
+        border-radius: 0 4px 4px 0;
         border: 1px solid #dcdfe6;
+        border-left: 0;
         padding: 0 10px;
         font-size: 14px;
         color: #303133;
@@ -281,7 +667,8 @@ onUnmounted(() => {
 }
 
 .map-container {
-  flex: 1;
+  width: 800px;
+  height: 600px;
   position: relative;
   border: 1px solid #dcdfe6;
   border-radius: 4px;

+ 213 - 42
src/view/case/newCaseFile.vue

@@ -27,7 +27,10 @@
             <el-button type="primary" @click="openMapDialog">
               创建{{ BoardTypeDesc[BoardType.map] }}
             </el-button>
-            <el-button type="primary" @click="gotoDraw(BoardType.scene, -1)">
+            <!-- <el-button type="primary" @click="gotoDraw(BoardType.scene, -1)">
+              创建{{ BoardTypeDesc[BoardType.scene] }}
+            </el-button> -->
+            <el-button type="primary" @click="openOverView()">
               创建{{ BoardTypeDesc[BoardType.scene] }}
             </el-button>
           </template>
@@ -102,7 +105,7 @@
           <div class="card-image-container">
             <el-image
               ref="imageRef"
-              :src="file.filesUrl"
+              :src="file.type === 'old' ? file.filesUrl : file.listCover"
               :preview-src-list="srcList"
               :initial-index="index"
               fit="cover"
@@ -118,7 +121,7 @@
                 </div>
               </template>
             </el-image>
-            
+             
             <!-- 悬浮操作按钮 -->
             <div class="card-overlay">
               <div class="card-actions">
@@ -131,16 +134,28 @@
                 >
                   <el-icon :size="20"><View /></el-icon>
                 </el-button>
+                <!-- 编辑按钮:old 类型不显示,其他类型显示 -->
                 <el-button 
                   class="card-overlay-btn"
                   size="default" 
                   circle
-                  @click.stop="gotoDraw(file.imgType!, file.filesId)"
-                  v-if="file.imgType !== null"
+                  @click.stop="handleEdit(file)"
+                  v-if="file.type !== 'old'"
                   title="编辑"
                 >
                   <el-icon :size="20"><Edit /></el-icon>
                 </el-button>
+                <!-- 对于 old 类型,保留原来的编辑逻辑 -->
+                <!-- <el-button 
+                  class="card-overlay-btn"
+                  size="default" 
+                  circle
+                  @click.stop="gotoDraw(file.imgType!, file.filesId)"
+                  v-if="file.type === 'old' && file.imgType !== null"
+                  title="编辑"
+                >
+                  <el-icon :size="20"><Edit /></el-icon>
+                </el-button> -->
                 <el-button 
                   class="card-overlay-btn"
                   size="default" 
@@ -158,13 +173,14 @@
           <div class="card-footer">
             <div class="file-title">
               <span v-if="!inputCaseTitles.includes(file)" class="title-text">
-                {{ file.filesTitle }}
+                {{ file.type === 'old' ? file.filesTitle : file.title }}
                 <el-icon class="edit-title" @click="inputCaseTitles.push(file)">
                   <EditPen />
                 </el-icon>
               </span>
               <template v-else>
                 <ElInput
+                  v-if="file.type === 'old'"
                   v-model="file.filesTitle"
                   placeholder="请输入文件名"
                   focus
@@ -178,14 +194,28 @@
                     </el-button>
                   </template>
                 </ElInput>
+                <ElInput
+                  v-else
+                  v-model="file.title"
+                  placeholder="请输入文件名"
+                  focus
+                  :maxlength="50"
+                  size="default"
+                  class="edit-input"
+                >
+                  <template #append>
+                    <el-button type="primary" plain @click="updateFileTitle(file)">
+                      确定
+                    </el-button>
+                  </template>
+                </ElInput>
               </template>
             </div>
           </div>
           <el-image-viewer
             v-if="showPreview"
             :url-list="srcList"
-            show-progress
-            :initial-index="0"
+            :initial-index="currentPreviewIndex"
             @close="showPreview = false"
           />
         </div>
@@ -194,6 +224,7 @@
       <!-- 地图选择弹窗 -->
       <CreatMap 
         v-model="showMapDialog" 
+        :caseId="caseId"
         @confirm="handleMapConfirm"
       />
     </template>
@@ -216,14 +247,15 @@ import {
   delCaseFile,
   BoardType,
 } from "@/store/caseFile";
-import { getCaseInfo, updateCaseInfo } from "@/store/case";
+import { getCaseInfo, updateCaseInfo, getCaseTabulationList, getCaseOverviewList, updateCaseTabulation, updateCaseOverview, delCaseTabulation, delCaseOverview } from "@/store/case";
 import { appConstant } from "@/app";
-import { ElIcon, ElInput, ElMessage, ElImage, ElButton, ImageInstance } from "element-plus";
+import { ElIcon, ElInput, ElMessage, ElImage, ElButton } from "element-plus";
 import { EditPen, Document, View, Edit, Delete } from "@element-plus/icons-vue";
 import Photos from "./photos/index.vue";
 import Records from "./records/index.vue";
 import Manifest from "./records/manifest.vue";
 import CreatMap from "./drawMap/creatMap.vue";
+import { user } from "@/store/user";
 
 const props = defineProps<{
   caseId?: number;
@@ -243,14 +275,71 @@ const caseInfoData = ref<any>();
 
 const inputCaseTitles = ref<CaseFile[]>([]);
 
+// 处理标题输入事件
+const handleTitleInput = (file: CaseFile, value: string) => {
+  if (file.type === 'old') {
+    file.filesTitle = value;
+  } else {
+    file.title = value;
+  }
+};
+
 const updateFileTitle = async (caseFile: CaseFile) => {
-  if (!caseFile.filesTitle.trim()) {
-    return ElMessage.error("卷宗标题不能为空!");
+  // 根据文件类型检查不同的标题字段
+  const title = caseFile.type === 'old' ? caseFile.filesTitle : caseFile.title;
+  if (!title || !title.trim()) {
+    ElMessage.error("标题不能为空!");
+    return;
+  }
+  
+  try {
+    // 根据文件类型调用不同的更新接口
+    if (caseFile.type === 'old') {
+      await updateCaseInfo(caseFile);
+    } else if (caseFile.type === 'tabulation') {
+      // updateCaseTabulation 参数:id, caseId, store, viewport, cover, paperKey, overviewId, isAutoGen, listCover, mapUrl, high, width
+      await updateCaseTabulation({
+        id: caseFile.id,
+        caseId: caseFile.caseId,
+        store: caseFile.store,
+        viewport: caseFile.viewport,
+        cover: caseFile.cover,
+        paperKey: caseFile.paperKey,
+        overviewId: caseFile.overviewId,
+        isAutoGen: caseFile.isAutoGen,
+        listCover: caseFile.listCover,
+        mapUrl: caseFile.mapUrl,
+        high: caseFile.high,
+        width: caseFile.width,
+        title: title // 使用最新输入的 title
+      });
+    } else if (caseFile.type === 'overview') {
+      // updateCaseOverview 参数:id, caseId, store, title, cover, mapUrl, listCover, high, width, kankanCover
+      await updateCaseOverview({
+        id: caseFile.id,
+        caseId: caseFile.caseId,
+        store: caseFile.store,
+        title: title, // 使用最新输入的 title
+        cover: caseFile.cover,
+        mapUrl: caseFile.mapUrl,
+        listCover: caseFile.listCover,
+        high: caseFile.high,
+        width: caseFile.width,
+        kankanCover: caseFile.kankanCover
+      });
+    }
+    
+    inputCaseTitles.value = inputCaseTitles.value.filter(
+      (item) => item !== caseFile
+    );
+    
+    // 更新成功后刷新列表
+    refresh();
+    ElMessage.success("更新成功!");
+  } catch (error) {
+    console.error('更新失败:', error);
+    ElMessage.error("更新失败!");
   }
-  await updateCaseInfo(caseFile);
-  inputCaseTitles.value = inputCaseTitles.value.filter(
-    (item) => item !== caseFile
-  );
 };
 
 const currentTypeId = ref<number>();
@@ -282,43 +371,93 @@ const files = ref<CaseFile[]>([]);
 
 // 计算预览图片列表
 const srcList = computed(() => {
-  return files.value.map(file => file.filesUrl);
+  return files.value.map(file => {
+    // 根据文件类型返回对应的图片URL
+    return file.type === 'old' ? file.filesUrl : (file.listCover || file.filesUrl);
+  });
 });
 
 // 预览图片方法
-const imageRef = ref<ImageInstance>()
+const imageRef = ref()
 const showPreview = ref(false)
+const currentPreviewIndex = ref(0)
 const previewImage = (index: number) => {
+  console.log(index)
   const file = files.value[index];
-  const ext = file.filesUrl
-    .substring(file.filesUrl.lastIndexOf("."))
-    .toLocaleLowerCase();
   
-  // 如果是图片文件,让 el-image 的预览功能自动处理
-  const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
-  console.log(imageRef.value, 999)
-  if (imageExts.includes(ext)) {
-    showPreview.value = true
-    return
-  }
+  // 设置当前预览图片的索引
+  currentPreviewIndex.value = index;
   
-  // 如果是其他文件类型,使用原来的查看逻辑
-  const appId = import.meta.env.VITE_APP_APP || 'fire';
-  if ([".raw", ".dcm"].includes(ext)) {
-    window.open(
-      `/${appId}/xfile-viewer/index.html?file=${file.filesUrl}&name=${file.filesTitle}&time=` +
-        Date.now()
-    );
+  // 根据文件类型处理查看逻辑
+  if (file.type === 'old') {
+    // old 类型使用原来的查看逻辑
+    const ext = file.filesUrl
+      .substring(file.filesUrl.lastIndexOf("."))
+      .toLocaleLowerCase();
+    
+    // 如果是图片文件,让 el-image 的预览功能自动处理
+    const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
+    if (imageExts.includes(ext)) {
+      showPreview.value = true
+      return
+    }
+    
+    // 如果是其他文件类型,使用原来的查看逻辑
+    const appId = import.meta.env.VITE_APP_APP || 'fire';
+    if ([".raw", ".dcm"].includes(ext)) {
+      window.open(
+        `/${appId}/xfile-viewer/index.html?file=${file.filesUrl}&name=${file.filesTitle}&time=` +
+          Date.now()
+      );
+    } else {
+      window.open(file.filesUrl + "?time=" + Date.now());
+    }
   } else {
-    window.open(file.filesUrl + "?time=" + Date.now());
+    // tabulation 和 overview 类型的查看逻辑
+    if (file.listCover) {
+      // 如果有封面图,显示预览
+      showPreview.value = true
+    } else {
+      // 否则直接打开文件
+      window.open(file.filesUrl + "?time=" + Date.now());
+    }
   }
 };
 
 const refresh = async () => {
-  files.value = await getCaseFiles({
-    caseId: caseId.value!,
-    filesTypeId: currentTypeId.value,
-  });
+  try {
+    // 并行调用三个接口
+    const [tabulationRes, overviewRes, caseFilesRes] = await Promise.all([
+      getCaseTabulationList(caseId.value!),
+      getCaseOverviewList(caseId.value!),
+      getCaseFiles({
+        caseId: caseId.value!,
+        filesTypeId: currentTypeId.value,
+      })
+    ]);
+    
+    // 提取数据并为每种类型添加标记
+    const tabulationList = (tabulationRes?.data || []).map(item => ({
+      ...item,
+      title: item.title || '方位图',
+      type: 'tabulation' as const
+    }));
+    const overviewList = (overviewRes?.data || []).map(item => ({
+      ...item,
+      title: item.title || '平面图',
+      type: 'overview' as const
+    }));
+    const caseFiles = (caseFilesRes || []).map(item => ({
+      ...item,
+      type: 'old' as const
+    }));
+    
+    files.value = [...tabulationList, ...overviewList, ...caseFiles];
+    console.log(files.value)
+  } catch (error) {
+    console.error('获取文件列表失败:', error);
+    files.value = [];
+  }
 };
 watchEffect(() => caseId.value && currentTypeId.value && refresh());
 
@@ -338,8 +477,25 @@ const query = (file: CaseFile) => {
 };
 const del = async (file: CaseFile) => {
   if (await confirm("确定要删除此数据?")) {
-    await delCaseFile({ caseId: caseId.value!, filesId: file.filesId });
-    refresh();
+    try {
+      // 根据文件类型调用不同的删除接口
+      if (file.type === 'old') {
+        // old 类型的删除逻辑不变
+        await delCaseFile({ caseId: caseId.value!, filesId: file.filesId });
+      } else if (file.type === 'tabulation') {
+        // tabulation 类型调用 /fusion/caseTabulation/del
+        await delCaseTabulation({ id: file.id! });
+      } else if (file.type === 'overview') {
+        // overview 类型调用 /fusion/caseOverview/del
+        await delCaseOverview({ id: file.id! });
+      }
+      
+      refresh();
+      ElMessage.success("删除成功!");
+    } catch (error) {
+      console.error('删除失败:', error);
+      ElMessage.error("删除失败!");
+    }
   }
 };
 
@@ -354,6 +510,17 @@ const gotoDraw = (type: BoardType, id: number) => {
     params: { caseId: caseId.value!, type, id },
   });
 };
+
+// 处理不同类型的编辑逻辑
+const handleEdit = (file: CaseFile) => {
+  if (file.type === 'tabulation') {
+    // tabulation 类型的编辑链接
+    window.open(`http://test-mix3d.4dkankan.com/draw/index.html#/tabulation?caseId=${caseId.value}&tabulationId=${file.id}&token=${user.value.token}`, '_blank');
+  } else if (file.type === 'overview') {
+    // overview 类型的编辑链接
+    window.open(`http://test-mix3d.4dkankan.com/draw/index.html#/overview?caseId=${caseId.value!}&overviewId=${file.id}&token=${user.value.token}`, '_blank');
+  }
+};
 // 地图弹窗相关
 const showMapDialog = ref(false)
 
@@ -361,6 +528,10 @@ const showMapDialog = ref(false)
 const openMapDialog = () => {
   showMapDialog.value = true
 }
+// 创建现场图
+const openOverView = () => {
+  window.open(`http://test-mix3d.4dkankan.com/draw/index.html#/overview?caseId=${caseId.value!}&token=${user.value.token}`, '_blank')
+}
 
 // 处理地图选择确认
 const handleMapConfirm = async (location: any) => {

+ 34 - 110
yarn.lock

@@ -43,111 +43,6 @@
   resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz"
   integrity sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==
 
-"@esbuild/android-arm@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz"
-  integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==
-
-"@esbuild/android-arm64@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz"
-  integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==
-
-"@esbuild/android-x64@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz"
-  integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==
-
-"@esbuild/darwin-arm64@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz"
-  integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==
-
-"@esbuild/darwin-x64@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz"
-  integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==
-
-"@esbuild/freebsd-arm64@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz"
-  integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==
-
-"@esbuild/freebsd-x64@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz"
-  integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==
-
-"@esbuild/linux-arm@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz"
-  integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==
-
-"@esbuild/linux-arm64@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz"
-  integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==
-
-"@esbuild/linux-ia32@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz"
-  integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==
-
-"@esbuild/linux-loong64@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz"
-  integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==
-
-"@esbuild/linux-mips64el@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz"
-  integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==
-
-"@esbuild/linux-ppc64@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz"
-  integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==
-
-"@esbuild/linux-riscv64@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz"
-  integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==
-
-"@esbuild/linux-s390x@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz"
-  integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==
-
-"@esbuild/linux-x64@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz"
-  integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==
-
-"@esbuild/netbsd-x64@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz"
-  integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==
-
-"@esbuild/openbsd-x64@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz"
-  integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==
-
-"@esbuild/sunos-x64@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz"
-  integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==
-
-"@esbuild/win32-arm64@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz"
-  integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==
-
-"@esbuild/win32-ia32@0.18.20":
-  version "0.18.20"
-  resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz"
-  integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==
-
 "@esbuild/win32-x64@0.18.20":
   version "0.18.20"
   resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz"
@@ -521,6 +416,11 @@ balanced-match@^1.0.0:
   resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
+base64-arraybuffer@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz"
+  integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
+
 brace-expansion@^1.1.7:
   version "1.1.11"
   resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz"
@@ -623,6 +523,13 @@ cross-spawn@^6.0.5:
     shebang-command "^1.2.0"
     which "^1.2.9"
 
+css-line-break@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz"
+  integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==
+  dependencies:
+    utrie "^1.0.2"
+
 csstype@^3.1.3:
   version "3.1.3"
   resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz"
@@ -900,11 +807,6 @@ form-data@^4.0.0:
     combined-stream "^1.0.8"
     mime-types "^2.1.12"
 
-fsevents@~2.3.2:
-  version "2.3.3"
-  resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz"
-  integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
-
 function-bind@^1.1.2:
   version "1.1.2"
   resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz"
@@ -1031,6 +933,14 @@ hosted-git-info@^2.1.4:
   resolved "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz"
   integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
 
+html2canvas@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz"
+  integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
+  dependencies:
+    css-line-break "^2.1.0"
+    text-segmentation "^1.0.3"
+
 immutable@^4.0.0:
   version "4.3.7"
   resolved "https://registry.npmmirror.com/immutable/-/immutable-4.3.7.tgz"
@@ -1830,6 +1740,13 @@ swiper@^11.1.4:
   resolved "https://registry.npmmirror.com/swiper/-/swiper-11.1.14.tgz"
   integrity sha512-VbQLQXC04io6AoAjIUWuZwW4MSYozkcP9KjLdrsG/00Q/yiwvhz9RQyt0nHXV10hi9NVnDNy1/wv7Dzq1lkOCQ==
 
+text-segmentation@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz"
+  integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==
+  dependencies:
+    utrie "^1.0.2"
+
 three@^0.158.0:
   version "0.158.0"
   resolved "https://registry.npmmirror.com/three/-/three-0.158.0.tgz"
@@ -1942,6 +1859,13 @@ unplugin@^1.14.1, unplugin@^1.3.2:
     acorn "^8.12.1"
     webpack-virtual-modules "^0.6.2"
 
+utrie@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz"
+  integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==
+  dependencies:
+    base64-arraybuffer "^1.0.2"
+
 validate-npm-package-license@^3.0.1:
   version "3.0.4"
   resolved "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz"