tangning 7 godzin temu
rodzic
commit
d01f6bbfab

+ 7 - 0
package.json

@@ -13,17 +13,24 @@
   "dependencies": {
     "@amap/amap-jsapi-loader": "^1.0.1",
     "@element-plus/icons-vue": "^2.1.0",
+    "@pdf-lib/fontkit": "^1.1.1",
     "@types/qs": "^6.9.7",
     "axios": "^1.4.0",
     "echarts": "^5.4.3",
     "element-plus": "^2.3.8",
+    "file-saver": "^2.0.5",
+    "fontkit": "^2.0.4",
     "html2canvas": "^1.4.1",
+    "html2pdf.js": "^0.14.0",
     "js-base64": "^3.7.5",
+    "jspdf": "^4.2.1",
+    "jszip": "^3.10.1",
     "leaflet": "^1.9.4",
     "leaflet.chinatmsproviders": "^3.0.6",
     "mime": "^3.0.0",
     "mitt": "^3.0.1",
     "ollama": "^0.5.14",
+    "pdf-lib": "^1.17.1",
     "province-city-china": "^8.5.8",
     "qs": "^6.11.2",
     "sass": "^1.64.2",

BIN
public/SourceHanSansSC-Regular.otf


+ 233 - 3
src/assets/font/demo_index.html

@@ -55,6 +55,66 @@
           <ul class="icon_lists dib-box">
           
             <li class="dib">
+              <span class="icon iconfont">&#xe7f5;</span>
+                <div class="name">backout</div>
+                <div class="code-name">&amp;#xe7f5;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7f6;</span>
+                <div class="name">redo</div>
+                <div class="code-name">&amp;#xe7f6;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7f7;</span>
+                <div class="name">screen_f</div>
+                <div class="code-name">&amp;#xe7f7;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7f8;</span>
+                <div class="name">del</div>
+                <div class="code-name">&amp;#xe7f8;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7f9;</span>
+                <div class="name">download</div>
+                <div class="code-name">&amp;#xe7f9;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7fa;</span>
+                <div class="name">indexes</div>
+                <div class="code-name">&amp;#xe7fa;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7f4;</span>
+                <div class="name">layout</div>
+                <div class="code-name">&amp;#xe7f4;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7fb;</span>
+                <div class="name">layout_two</div>
+                <div class="code-name">&amp;#xe7fb;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7fc;</span>
+                <div class="name">layout_one</div>
+                <div class="code-name">&amp;#xe7fc;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe7fd;</span>
+                <div class="name">layout_h</div>
+                <div class="code-name">&amp;#xe7fd;</div>
+              </li>
+          
+            <li class="dib">
               <span class="icon iconfont">&#xe7c8;</span>
                 <div class="name">Upload</div>
                 <div class="code-name">&amp;#xe7c8;</div>
@@ -258,9 +318,9 @@
 <pre><code class="language-css"
 >@font-face {
   font-family: 'iconfont';
-  src: url('iconfont.woff2?t=1756902711273') format('woff2'),
-       url('iconfont.woff?t=1756902711273') format('woff'),
-       url('iconfont.ttf?t=1756902711273') format('truetype');
+  src: url('iconfont.woff2?t=1774585738290') format('woff2'),
+       url('iconfont.woff?t=1774585738290') format('woff'),
+       url('iconfont.ttf?t=1774585738290') format('truetype');
 }
 </code></pre>
           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -287,6 +347,96 @@
         <ul class="icon_lists dib-box">
           
           <li class="dib">
+            <span class="icon iconfont icon-backout"></span>
+            <div class="name">
+              backout
+            </div>
+            <div class="code-name">.icon-backout
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-redo"></span>
+            <div class="name">
+              redo
+            </div>
+            <div class="code-name">.icon-redo
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-screen_f"></span>
+            <div class="name">
+              screen_f
+            </div>
+            <div class="code-name">.icon-screen_f
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-del"></span>
+            <div class="name">
+              del
+            </div>
+            <div class="code-name">.icon-del
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-download"></span>
+            <div class="name">
+              download
+            </div>
+            <div class="code-name">.icon-download
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-indexes"></span>
+            <div class="name">
+              indexes
+            </div>
+            <div class="code-name">.icon-indexes
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-layout"></span>
+            <div class="name">
+              layout
+            </div>
+            <div class="code-name">.icon-layout
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-layout_two"></span>
+            <div class="name">
+              layout_two
+            </div>
+            <div class="code-name">.icon-layout_two
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-layout_one"></span>
+            <div class="name">
+              layout_one
+            </div>
+            <div class="code-name">.icon-layout_one
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-layout_h"></span>
+            <div class="name">
+              layout_h
+            </div>
+            <div class="code-name">.icon-layout_h
+            </div>
+          </li>
+          
+          <li class="dib">
             <span class="icon iconfont icon-Upload"></span>
             <div class="name">
               Upload
@@ -594,6 +744,86 @@
           
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-backout"></use>
+                </svg>
+                <div class="name">backout</div>
+                <div class="code-name">#icon-backout</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-redo"></use>
+                </svg>
+                <div class="name">redo</div>
+                <div class="code-name">#icon-redo</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-screen_f"></use>
+                </svg>
+                <div class="name">screen_f</div>
+                <div class="code-name">#icon-screen_f</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-del"></use>
+                </svg>
+                <div class="name">del</div>
+                <div class="code-name">#icon-del</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-download"></use>
+                </svg>
+                <div class="name">download</div>
+                <div class="code-name">#icon-download</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-indexes"></use>
+                </svg>
+                <div class="name">indexes</div>
+                <div class="code-name">#icon-indexes</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-layout"></use>
+                </svg>
+                <div class="name">layout</div>
+                <div class="code-name">#icon-layout</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-layout_two"></use>
+                </svg>
+                <div class="name">layout_two</div>
+                <div class="code-name">#icon-layout_two</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-layout_one"></use>
+                </svg>
+                <div class="name">layout_one</div>
+                <div class="code-name">#icon-layout_one</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-layout_h"></use>
+                </svg>
+                <div class="name">layout_h</div>
+                <div class="code-name">#icon-layout_h</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
                   <use xlink:href="#icon-Upload"></use>
                 </svg>
                 <div class="name">Upload</div>

+ 43 - 3
src/assets/font/iconfont.css

@@ -1,8 +1,8 @@
 @font-face {
   font-family: "iconfont"; /* Project id 4789215 */
-  src: url('iconfont.woff2?t=1756902711273') format('woff2'),
-       url('iconfont.woff?t=1756902711273') format('woff'),
-       url('iconfont.ttf?t=1756902711273') format('truetype');
+  src: url('iconfont.woff2?t=1774585738290') format('woff2'),
+       url('iconfont.woff?t=1774585738290') format('woff'),
+       url('iconfont.ttf?t=1774585738290') format('truetype');
 }
 
 .iconfont {
@@ -13,6 +13,46 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-backout:before {
+  content: "\e7f5";
+}
+
+.icon-redo:before {
+  content: "\e7f6";
+}
+
+.icon-screen_f:before {
+  content: "\e7f7";
+}
+
+.icon-del:before {
+  content: "\e7f8";
+}
+
+.icon-download:before {
+  content: "\e7f9";
+}
+
+.icon-indexes:before {
+  content: "\e7fa";
+}
+
+.icon-layout:before {
+  content: "\e7f4";
+}
+
+.icon-layout_two:before {
+  content: "\e7fb";
+}
+
+.icon-layout_one:before {
+  content: "\e7fc";
+}
+
+.icon-layout_h:before {
+  content: "\e7fd";
+}
+
 .icon-Upload:before {
   content: "\e7c8";
 }

Plik diff jest za duży
+ 1 - 1
src/assets/font/iconfont.js


+ 70 - 0
src/assets/font/iconfont.json

@@ -6,6 +6,76 @@
   "description": "",
   "glyphs": [
     {
+      "icon_id": "47190607",
+      "name": "backout",
+      "font_class": "backout",
+      "unicode": "e7f5",
+      "unicode_decimal": 59381
+    },
+    {
+      "icon_id": "47190606",
+      "name": "redo",
+      "font_class": "redo",
+      "unicode": "e7f6",
+      "unicode_decimal": 59382
+    },
+    {
+      "icon_id": "47190605",
+      "name": "screen_f",
+      "font_class": "screen_f",
+      "unicode": "e7f7",
+      "unicode_decimal": 59383
+    },
+    {
+      "icon_id": "47190604",
+      "name": "del",
+      "font_class": "del",
+      "unicode": "e7f8",
+      "unicode_decimal": 59384
+    },
+    {
+      "icon_id": "47190603",
+      "name": "download",
+      "font_class": "download",
+      "unicode": "e7f9",
+      "unicode_decimal": 59385
+    },
+    {
+      "icon_id": "47190602",
+      "name": "indexes",
+      "font_class": "indexes",
+      "unicode": "e7fa",
+      "unicode_decimal": 59386
+    },
+    {
+      "icon_id": "47190582",
+      "name": "layout",
+      "font_class": "layout",
+      "unicode": "e7f4",
+      "unicode_decimal": 59380
+    },
+    {
+      "icon_id": "47190581",
+      "name": "layout_two",
+      "font_class": "layout_two",
+      "unicode": "e7fb",
+      "unicode_decimal": 59387
+    },
+    {
+      "icon_id": "47190580",
+      "name": "layout_one",
+      "font_class": "layout_one",
+      "unicode": "e7fc",
+      "unicode_decimal": 59388
+    },
+    {
+      "icon_id": "47190579",
+      "name": "layout_h",
+      "font_class": "layout_h",
+      "unicode": "e7fd",
+      "unicode_decimal": 59389
+    },
+    {
       "icon_id": "45434743",
       "name": "Upload",
       "font_class": "Upload",

BIN
src/assets/font/iconfont.ttf


BIN
src/assets/font/iconfont.woff


BIN
src/assets/font/iconfont.woff2


+ 3 - 4
src/assets/style/public.scss

@@ -326,10 +326,9 @@ body {
   display: none;
 }
 
-// .el-message-box__message {
-//   min-height: 50px;
-//   padding: 0 30px;
-// }
+.el-message-box__message {
+  white-space: pre-line;
+}
 
 // .el-message-box__status {
 //   top: -13px !important;

+ 10 - 1
src/request/urls.ts

@@ -185,6 +185,9 @@ export const deleteCaseFile = "/fusion/caseFiles/delete";
 export const updateCaseFile = "/fusion/caseFiles/updateTitle";
 export const newFileupload = "/service/manage/common/upload/fileNew";
 export const newupload = "/service/manage/common/upload/files";
+export const uploadSceneCheck = '/service/manage/scene/uploadSceneCheck'
+export const uploadSceneOrig = '/service/manage/scene/uploadSceneOrig'
+
 //勘验笔录信息
 export const caseInquestInfoOld = "/fusion/caseInquestCriminal/info";
 export const caseInquestOpt = "/fusion/caseInquestCriminal/saveOrUpdate";
@@ -273,7 +276,13 @@ export const caseaddOrUpdate = '/service/manage/case/addOrUpdate';
 export const addFusionIds = '/fusion/case/addFusionIds';
 export const getByImage = '/fusion/ai/getByImage';
 export const getFloor = "/fusion/ai/getFloor/";
-//地图相关
+
+// 照片制卷
+export const casePhotoRollList = "/fusion/casePhotoRoll/getByCaseId";
+export const getAllPhoto = "/fusion/casePhotoRoll/getAllPhoto";
+export const casePhotoRollUpdate = "/fusion/casePhotoRoll/addOrUpdate";
+export const casePhotoRolldel = "/fusion/casePhotoRoll/del";
+
 const VITE_VIBE = import.meta.env.VITE_VIBE;
 let getTipss = "/s/api/gettips";
 let getTipsNames = "/s/api/gettips_name";

+ 33 - 0
src/store/case.ts

@@ -11,6 +11,8 @@ import {
   repCaseScenes,
   setCasePsw,
   addFusionIds,
+  uploadSceneCheck,
+  uploadSceneOrig,
   syncInfo,
   updateCaseFile,
   caseInquestInfo,
@@ -52,6 +54,10 @@ import {
   caseTabulationList,
   caseOverviewList,
   caseOverviewExport,
+  casePhotoRollList,
+  getAllPhoto,
+  casePhotoRollUpdate,
+  casePhotoRolldel,
 } from "@/request";
 import { router } from "@/router";
 import { ModelScene, QuoteScene, Scene, SceneType } from "./scene";
@@ -212,6 +218,20 @@ export const getcaseLists = async (caseId: number): Promise<Scene[]> => {
   return (await axios.get(isdyrh, { params: { caseId } })).data;
 };
 
+export const getCasePhotoRollList = async (caseId = router.currentRoute.value?.params?.caseId): Promise<Scene[]> => {
+  return (await axios.get(casePhotoRollList, { params: { caseId } })).data;
+};
+export const getAllPhotoList = async (filesTypeId): Promise<Scene[]> => {
+  return (await axios.get(getAllPhoto, { params: { caseId: router.currentRoute.value?.params?.caseId, filesTypeId } })).data;
+};
+export const casePhotoUpdate = (params) =>
+  axios.post(casePhotoRollUpdate, params);
+export const casePhotodel = (params) => {
+  console.log(params)
+  return  axios.post(casePhotoRolldel, params);
+}
+  
+
 export const updateByTreeFileLists = async (caseId = router.currentRoute.value?.params?.caseId): Promise<Scene[]> => {
   let list = (await axios.get(getByTree, { params: { caseId:caseId } })).data
   // function getTreeList(lists: any[]) {
@@ -326,6 +346,19 @@ export const uploadFiles = (data) => axios<undefined>({
   data: data,
 });
 
+export const SceneCheck = (data) => axios<undefined>({
+  method: "POST",
+  url: uploadSceneCheck,
+  data: data,
+});
+
+export const SceneOrig = (data) => axios<undefined>({
+  method: "POST",
+  url: uploadSceneOrig,
+  data: data,
+});
+
+
 export type CaseImg = {
   id: number;
   caseId: number;

+ 26 - 1
src/util/index.ts

@@ -525,4 +525,29 @@ export const recursiveSearch = (nodes) => {
     }
     
     return false;
-  }
+  }
+  /**
+ * 从 URL 中提取文件名
+ * @param {string} url - 完整的 URL 地址
+ * @returns {string} 提取到的文件名,无文件名时返回空字符串
+ */
+  export function getFileNameFromUrl(url) {
+  try {
+    // 1. 解析 URL,分离出路径部分(去除参数和锚点)
+    const parsedUrl = new URL(url);
+    let path = parsedUrl.pathname;
+
+    // 2. 去除路径末尾的斜杠(如果有)
+    path = path.replace(/\/$/, '');
+
+    // 3. 提取最后一个斜杠后的部分(即文件名)
+    const fileName = path.split('/').pop();
+
+    // 4. 如果提取到的是目录名(无扩展名),返回空字符串
+    return fileName.includes('.') ? fileName : '';
+  } catch (error) {
+    // 处理无效 URL 的情况
+    console.error('URL 格式错误:', error);
+    return '';
+  }
+}

+ 43 - 14
src/view/abstract/index.vue

@@ -210,6 +210,22 @@
               </el-form-item>
             </el-col>
 
+            <el-col :span="8">
+              <el-form-item label="负责人">
+                <el-input
+                  v-model="bindExample.commander"
+                  placeholder="请输入"
+                  clearable
+                  @blur="submit"
+                  maxlength="100"
+                  disabled
+                >
+                  <template #append>
+                    <el-button :icon="Search" @click="searchAMapAddress" />
+                  </template>
+                </el-input>
+              </el-form-item>
+            </el-col>
             <el-col :span="4">
               <el-form-item label="是否命案" prop="region">
                 <el-select
@@ -276,6 +292,26 @@
                 /> </el-form-item
             ></el-col>
             <el-col :span="8"
+              ><el-form-item label="指派/报告单位">
+                <el-input
+                  v-model="ruleForm.assignDept"
+                  @blur="submit"
+                  placeholder="请输入"
+                  show-word-limit
+                  maxlength="100"
+                /> </el-form-item
+            ></el-col>
+            <el-col :span="8"
+              ><el-form-item label="指派方式">
+                <el-input
+                  v-model="ruleForm.assignType"
+                  @blur="submit"
+                  placeholder="请输入"
+                  show-word-limit
+                  maxlength="100"
+                /> </el-form-item
+            ></el-col>
+            <el-col :span="8"
               ><el-form-item label="接警时间">
                 <el-date-picker
                   v-model="ruleForm.alarmTime"
@@ -298,9 +334,9 @@
                 /> </el-form-item
             ></el-col>
             <el-col :span="8"
-              ><el-form-item label="现场勘验单位">
+              ><el-form-item label="报警信息">
                 <el-input
-                  v-model="ruleForm.inquestDept"
+                  v-model="ruleForm.alarmMsg"
                   @blur="submit"
                   placeholder="请输入"
                   show-word-limit
@@ -308,19 +344,9 @@
                 /> </el-form-item
             ></el-col>
             <el-col :span="8"
-              ><el-form-item label="指派/报告单位">
-                <el-input
-                  v-model="ruleForm.assignDept"
-                  @blur="submit"
-                  placeholder="请输入"
-                  show-word-limit
-                  maxlength="100"
-                /> </el-form-item
-            ></el-col>
-            <el-col :span="8"
-              ><el-form-item label="指派方式">
+              ><el-form-item label="现场勘验单位">
                 <el-input
-                  v-model="ruleForm.assignType"
+                  v-model="ruleForm.inquestDept"
                   @blur="submit"
                   placeholder="请输入"
                   show-word-limit
@@ -413,6 +439,7 @@ const ruleForm = ref({
   times: [],
   resource: "",
   desc: "",
+  alarmMsg: ''
 });
 const bindExample = ref({
   caseTitle: "",
@@ -428,6 +455,8 @@ const bindExample = ref({
   latAndLong: "",
   latAndLongs: "",
   criminalType: "",
+  commander:'',
+  alarmMsg:'',
 });
 const criminalType = [
   "杀人",

Plik diff jest za duży
+ 1460 - 0
src/view/case/photos/canvas-photo-editor.js


Plik diff jest za duży
+ 441 - 506
src/view/case/photos/index.vue


+ 259 - 0
src/view/case/photos/style.scss

@@ -0,0 +1,259 @@
+
+.phoneContent{
+  .header{
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 32px 48px 24px 48px;
+    background-color: #fff;
+    border: 1px solid #E6E6E6;
+    .header-left{
+      font-weight: 400;
+      font-size: 32px;
+      color: rgba(0,0,0,0.85);
+      line-height: 32px;
+    }
+    .header-main{
+      font-size: 24px;
+      .border-icon{
+        border-left: 1px solid #e5e7eb;
+        border-right: 1px solid #e5e7eb;
+        display: inline-block;
+        line-height: 16px;
+        height: 20px;
+        margin: 0 24px;
+        padding: 0 24px;
+      }
+      i{
+        cursor: pointer;
+      }
+      .avtive{
+        background: rgba(16,155,224,0.3);
+      }
+      .el-dropdown{
+        width: auto !important;
+      }
+      .disable{
+            cursor: not-allowed;
+      }
+    }
+    .header-right{
+      .view {
+        height: 40px;
+        width: 88px;
+        text-align: center;
+        line-height: 40px;
+        box-shadow: 0px 2px 0px 0px rgba(0, 0, 0, 0.04);
+        border-radius: 2px;
+        border: 1px solid #26559b;
+        font-size: 14px;
+        color: #26559b;
+        cursor: pointer;
+      }
+    }
+  }
+}
+.photo-editor {
+  display: flex;
+  height: calc(100vh - 98px);
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+  overflow: hidden;
+}
+
+/* 左侧面板 */
+.left-panel {
+  width: 510px;
+  min-width: 510px;
+  background: #fff;
+  border-right: 1px solid #e5e7eb;
+  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  box-sizing: border-box;
+  position: relative;
+  &-title{
+    font-weight: bold;
+    padding: 30px 0 12px 0;
+    font-size: 16px;
+    color: rgba(0,0,0,0.85);
+    line-height: 22px;
+  }
+  &-top{
+    padding: 0 48px;
+  }
+}
+
+.photo-list {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 8px;
+}
+
+.photo-item {
+  position: relative;
+  border: 2px solid transparent;
+  border-radius: 4px;
+  cursor: pointer;
+  overflow: hidden;
+}
+
+.photo-item.selected {
+  border-color: #3b82f6;
+}
+
+.photo-item img {
+  width: 128px;
+  height: 128px;
+  display: block;
+  object-fit: cover;
+}
+
+.used-tag {
+  position: absolute;
+  top: 8px;
+  left: 8px;
+  width: 24px;
+  height: 24px;
+  background: #ffffff;
+  font-size: 18px;
+  text-align: center;
+  line-height: 28px;
+  border-radius: 50%;
+}
+
+.layout-setting {
+  padding: 16px;
+  background: #ffffff;
+  border-radius: 6px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.radio-label {
+  display: block;
+  margin: 8px 0;
+  cursor: pointer;
+}
+
+.auto-layout-bar button {
+  width: 100%;
+  padding: 10px;
+  background: #3b82f6;
+  color: white;
+  border: none;
+  border-radius: 6px;
+  cursor: pointer;
+  transition: background 0.2s;
+}
+
+.auto-layout-bar button:hover {
+  background: #2563eb;
+}
+.auto-layout-bar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 25px 48px;
+  position: absolute;
+  border-top: 1px solid #E6E6E6;
+  bottom: 0;
+width: 100%;
+        .view {
+        height: 40px;
+        width: 88px;
+        text-align: center;
+        line-height: 40px;
+        box-shadow: 0px 2px 0px 0px rgba(0, 0, 0, 0.04);
+        border-radius: 2px;
+        border: 1px solid #26559b;
+        font-size: 14px;
+        color: #26559b;
+        cursor: pointer;
+      }
+}
+/* 右侧面板 */
+.right-panel {
+  flex: 1;
+  display: flex;
+  max-width: calc(100vw - 510px);
+  flex-direction: column;
+  padding: 20px;
+  background-color: #f3f4f6;
+}
+
+.page-control-bar {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+  margin-bottom: 16px;
+  padding: 12px;
+  background: #fff;
+  border-radius: 6px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.page-control-bar 
+button {
+  padding: 8px 16px;
+  background: #3b82f6;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  transition: background 0.2s;
+}
+
+.page-control-bar button:disabled {
+  background: #9ca3af;
+  cursor: not-allowed;
+}
+
+.page-control-bar button:hover:not(:disabled) {
+  background: #2563eb;
+}
+
+/* Canvas容器 */
+.canvas-container {
+  flex: 1;
+  border: 1px solid #e5e7eb;
+  background-color: #efefef;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+  cursor: grab;
+}
+
+.canvas-content {
+  display: block;
+  transition: transform 0.05s ease;
+  cursor: grab;
+}
+
+.canvas-content:active {
+  cursor: grabbing;
+}
+
+.control-bar {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  gap: 20px;
+  margin-top: 16px;
+  padding: 12px;
+  background: #fff;
+  border-radius: 6px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.control-bar button {
+  padding: 8px 16px;
+  border: 1px solid #d1d5db;
+  border-radius: 4px;
+  background: white;
+  cursor: pointer;
+}
+
+.control-bar button:hover {
+  background-color: #f3f4f6;
+}

+ 150 - 0
src/view/case/photos/usePDF.js

@@ -0,0 +1,150 @@
+// composables/usePDF.js
+import { ref } from 'vue'
+import html2pdf from 'html2pdf.js'
+import { PDFDocument, rgb } from 'pdf-lib'
+import fontkit from '@pdf-lib/fontkit' // 关键:导入适配版fontkit
+
+export function usePDF() {
+  const loading = ref(false)
+  const error = ref(null)
+
+  // 基础HTML转PDF(单页)
+  const generateSinglePDF = async (element, options = {}) => {
+    const defaultOptions = {
+      margin: [10, 10, 10, 10],
+      image: { type: 'jpeg', quality: 0.98 },
+      html2canvas: {
+        scale: 2,
+        letterRendering: true,
+        useCORS: true
+      },
+      jsPDF: {
+        unit: 'mm',
+        format: 'a4',
+        orientation: 'portrait'
+      },
+      filename: 'temp.pdf',
+      save: false
+    }
+
+    const mergedOptions = { ...defaultOptions, ...options }
+    const pdfBlob = await html2pdf().set(mergedOptions).from(element).output('blob')
+    return pdfBlob
+  }
+
+  // 加载中文字体(修复fontkit注册问题)
+  const loadChineseFont = async (pdfDoc) => {
+    // 1. 注册fontkit到PDFDocument(核心修复)
+    pdfDoc.registerFontkit(fontkit)
+
+    // 2. 加载中文字体文件(思源黑体,确保路径正确)
+    const fontUrl = '/SourceHanSansSC-Regular.otf' 
+    // try {
+      const fontResponse = await fetch(fontUrl)
+      if (!fontResponse.ok) throw new Error('字体文件加载失败')
+      const fontArrayBuffer = await fontResponse.arrayBuffer()
+      
+      // 3. 嵌入中文字体
+      const font = await pdfDoc.embedFont(fontArrayBuffer)
+      return font
+    // } catch (err) {
+    //   console.error('字体加载失败:', err)
+    //   throw new Error('中文字体加载失败,请检查字体文件路径')
+    // }
+  }
+
+  // 物理分页生成PDF(最终完整版)
+  const generateWithPagination = async (pageElements, options = {}) => {
+    loading.value = true
+    error.value = null
+
+    try {
+      const { filename = '中文分页文档.pdf' } = options
+      
+      // 1. 创建PDF文档实例
+      const finalPdfDoc = await PDFDocument.create()
+
+      // 2. 加载中文字体
+      const chineseFont = await loadChineseFont(finalPdfDoc)
+
+      // 3. 遍历所有分页节点,逐个生成并合并页面
+      for (let i = 0; i < pageElements.length; i++) {
+        const element = pageElements[i]
+        if (!element) continue // 跳过空节点
+
+        // 生成当前页的PDF Blob
+        const pageBlob = await generateSinglePDF(element, options)
+        const pageArrayBuffer = await new Response(pageBlob).arrayBuffer()
+        
+        // 加载临时PDF并复制页面
+        const pagePdfDoc = await PDFDocument.load(pageArrayBuffer)
+        const [copiedPage] = await finalPdfDoc.copyPages(pagePdfDoc, [0])
+
+        // 绘制中文页码(支持中文渲染)
+        const { width, height } = copiedPage.getSize()
+        copiedPage.drawText(`第 ${i + 1} 页 / 共 ${pageElements.length} 页`, {
+          x: width / 2 - 60, // 微调位置适配中文字符
+          y: 12,             // 页码在页面底部
+          size: 12,          // 字体大小
+          font: chineseFont, // 使用嵌入的中文字体
+          color: rgb(0.3, 0.3, 0.3) // 灰色页码
+        })
+
+        // 添加到最终PDF(物理分页核心)
+        finalPdfDoc.addPage(copiedPage)
+      }
+
+      // 4. 生成并下载PDF
+      const finalPdfBytes = await finalPdfDoc.save()
+      const finalPdfBlob = new Blob([finalPdfBytes], { type: 'application/pdf' })
+      
+      // 原生下载(无需file-saver)
+      const downloadLink = document.createElement('a')
+      downloadLink.href = URL.createObjectURL(finalPdfBlob)
+      downloadLink.download = filename
+      downloadLink.click()
+      URL.revokeObjectURL(downloadLink.href)
+
+      return true
+    } catch (err) {
+      error.value = `PDF生成失败:${err.message}`
+      console.error('分页PDF生成错误:', err)
+      return false
+    } finally {
+      loading.value = false
+    }
+  }
+
+  // 原有HTML转PDF方法(保持兼容)
+  const generateFromHTML = async (element, options = {}) => {
+    loading.value = true
+    error.value = null
+
+    try {
+      const defaultOptions = {
+        margin: [10, 10, 10, 10],
+        filename: 'document.pdf',
+        image: { type: 'jpeg', quality: 0.98 },
+        html2canvas: { scale: 2, letterRendering: true, useCORS: true },
+        jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
+      }
+
+      const mergedOptions = { ...defaultOptions, ...options }
+      await html2pdf().set(mergedOptions).from(element).save()
+      return true
+    } catch (err) {
+      error.value = err.message
+      console.error('PDF生成失败:', err)
+      return false
+    } finally {
+      loading.value = false
+    }
+  }
+
+  return {
+    loading,
+    error,
+    generateFromHTML,
+    generateWithPagination // 物理分页核心方法
+  }
+}

+ 2 - 2
src/view/layout/index.vue

@@ -108,9 +108,9 @@ updateByTreeFileLists();
 const hiddenSlide = computed(
   () => !menuRouteNames.includes(router.currentRoute.value.name as string)
 );
-
+console.log(router.currentRoute.value, "hiddenSlide");
 const hiddenTop = computed(
-  () => router.currentRoute.value.name != 'records' && router.currentRoute.value.name != 'login' && router.currentRoute.value.name != 'register' && router.currentRoute.value.name != 'forgetpassword'
+  () => router.currentRoute.value.name != 'drawCasePhotos' && router.currentRoute.value.name != 'records' && router.currentRoute.value.name != 'login' && router.currentRoute.value.name != 'register' && router.currentRoute.value.name != 'forgetpassword'
 
 );
 const handleCommand = (command) => {

+ 1 - 12
src/view/layout/top/index.vue

@@ -191,15 +191,4 @@ const updatePwdHandler = async () => {
     cursor: pointer;
   }
 }
-</style>
-<style lang="scss">
-.promptClass {
-  .el-message-box__header {
-    padding-left: 0 !important;
-    padding-bottom: 0 !important;
-  }
-  .el-message-box__content {
-    padding-left: 32px !important;
-  }
-}
-</style>
+</style>

+ 218 - 0
src/view/material/exportPhotos.vue

@@ -0,0 +1,218 @@
+<template>
+  <el-form
+    ref="form"
+    :model="caseFile"
+    label-width="90px"
+    class="camera-from dispatch-file-from jm-file-upload"
+  >
+    <el-form-item label="分类:" class="mandatory">
+      <el-cascader
+        v-model="caseFile.filesTypeId"
+        :how-all-levels="false"
+        style="width: 300px;"
+        :options="fileOptions"
+        :props="{
+          checkStrictly: false,
+        }"
+        clearable
+      />
+    </el-form-item>
+    <el-form-item label="上传类别:" class="mandatory">
+      <el-select
+        style="width: 300px;"
+        v-model="caseFile.filesType"
+        placeholder="请选择上传类别"
+      >
+        <el-option label="本地上传" :value="1" />
+        <el-option label="媒体库上传" :value="0" />
+      </el-select>
+      <!-- <el-cascader
+        v-model="caseFile.filesTypeId"
+        :how-all-levels="false"
+        style="width: 300px;"
+        :options="fileOptions"
+        :props="{
+          checkStrictly: false,
+        }"
+        clearable
+      /> -->
+    </el-form-item>
+    <el-form-item label="文件:" v-if="caseFile.filesType == 1" class="mandatory uploadFile">
+      <el-upload
+        class="upload-demo"
+        :multiple="false"
+        drag
+        :limit="1"
+        :before-upload="upload"
+        :file-list="fileList"
+        :http-request="uploadNewFile"
+        :on-success="handleSuccess"
+        :on-preview="previewFile"
+        :on-exceed="handleExceed"
+        :accept="accept"
+        :before-remove="removeFile"
+      >
+        <div type="primary" :disabled="!!file">
+          <div>点击或拖拽文件上传</div>
+          <div class="">{{ size }}以内的{{ formatDesc }}</div>
+        </div>
+        <template v-slot:file="{ file }: { file: UploadFile }">
+          <div class="file" @click.stop="previewFile()">
+            <div>
+              <el-icon><Document /></el-icon>
+              <span class="name">{{ file.name }}</span>
+            </div>
+            <el-icon @click.stop="removeFile()"><Close /></el-icon>
+          </div>
+        </template>
+      </el-upload>
+    </el-form-item>
+    
+    <el-form-item label="文件:" v-else class="mandatory uploadFile">
+      <el-button  type="primary" class="mtk" @click="handleAdd"
+      >从媒体库上传</el-button>
+    </el-form-item>
+    <div style="padding-left: 90px" v-if="caseFile.filesType == 0">
+        <viewImg :list="mtkList" edit delete @handleItem="handleItem"/>
+      </div>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { uploadNewFile, addByMediaLiBrary } from "@/store/case";
+import viewImg from "@/components/viewImg/index.vue"
+import {
+  DrawFormatDesc,
+  DrawFormats,
+  FileDrawType,
+  OtherFormatDesc,
+  OtherFormats,
+} from "@/constant/caseFile";
+import { maxFileSize } from "@/constant/caseFile";
+import { useUpload } from "@/hook/upload";
+import { CaseFile, addCaseFile } from "@/store/caseFile";
+import { ElMessage, UploadFile } from "element-plus";
+import { computed, ref, watchEffect, onMounted } from "vue";
+import { addCaseScenes } from "./quisk";
+import { QuiskExpose } from "@/helper/mount";
+import { updateSelectByTreeFileLists } from "@/store/case";
+
+const props = defineProps<{
+  caseId: number;
+  fileType: number;
+  filesTypeName: [string];
+  fileInfo?: Object;
+}>();
+const fileOptions = ref([])
+const mtkList = ref([]);
+onMounted(async () => {
+  let newfileOptions = await updateSelectByTreeFileLists();
+  if(props.filesTypeName){
+    let newChildren = newfileOptions
+    props.filesTypeName.map(ele => {
+      newChildren = newChildren.find((item) => item.label == ele).children || []
+    })
+    fileOptions.value = newChildren
+    // fileOptions.value = newfileOptions.find((item) => item.label == props.filesTypeName).children || [];
+  } else {
+    fileOptions.value = newfileOptions;
+  }
+  console.log('fileOptions', fileOptions.value, newfileOptions);
+});
+
+const caseFile = ref({
+  caseId: props.caseId,
+  filesTypeId: props.fileType,
+  filesTitle: "",
+  dictId: '',
+  uploadId: '',
+  filesType: 1,
+});
+console.log('caseFile', props.fileInfo);
+const { size, fileList, upload, removeFile, previewFile, file, accept } = useUpload({
+  maxSize: props.fileInfo?.fileSize || 50 * 1024 * 1024,
+  formats: props.fileInfo?.formats || [".jpg", ".jpeg", ".png"],
+});
+
+const formatDesc = computed(() => {
+  return props.fileInfo?.DrawFormatDesc||'jpg、png、jpeg上传'
+});
+
+// 上传请求
+const handleSuccess = (option) => {
+  console.log('handleSuccess', option);
+}
+const handleExceed = (option) => {
+  ElMessage.error("只能上传1个文件!");
+}
+
+const handleAdd = async () => {
+  let fileId =  await addCaseScenes({formats: props.fileInfo?.formats || [".jpg", ".jpeg", ".png"]});
+  mtkList.value = fileId.map(ele => {
+    return {
+      filesUrl: ele.fileUrl,
+      ...ele,
+     }
+  }) || [];
+  console.log("handleItem", mtkList.value);
+};
+const handleItem = (type, item) => {
+  console.log("handleItem", type, item);
+  if(type == 'delete'){
+    mtkList.value = mtkList.value.filter(ele => ele.id != item.id)
+  }
+};
+watchEffect(() => {
+  console.log('file', file.value);
+  if (file.value?.name) {
+    caseFile.value.uploadId = file.value?.response?.data.id
+  }
+});
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    let filesTypeId = caseFile.value.filesTypeId && Array.isArray(caseFile.value.filesTypeId)?caseFile.value.filesTypeId.slice(-1):caseFile.value.filesTypeId
+    if (!file.value && caseFile.value.filesType == 1 || caseFile.value.filesType == 0 && mtkList.value.length == 0) {
+      ElMessage.error("请上传文件");
+      throw "请上传文件";
+    } else if (!filesTypeId) {
+      ElMessage.error("文件分类不能为空!");
+      throw "文件分类不能为空!";
+    }
+    let uploadId = file.value?.response?.data.id,
+    uploadIds = mtkList.value.map(ele => ele.uploadId) || [];
+    let params = { ...caseFile.value, file: file.value, filesTypeId: filesTypeId[0] };
+    if(caseFile.value.filesType == 1) params.uploadId = uploadId;
+    if(caseFile.value.filesType == 0) params.uploadIds = uploadIds;
+    await addByMediaLiBrary(params);
+    return caseFile.value;
+  },
+});
+</script>
+
+<style scoped lang="scss">
+.upload-demo {
+  overflow: hidden;
+}
+
+.file {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  > div {
+    display: flex;
+    align-items: center;
+  }
+
+  .name {
+    margin-left: 10px;
+  }
+}
+.jm-file-upload {
+  // .mtk {
+  //   position: absolute;
+  //   right: 0;
+  //   top: 0;
+  // }
+}
+</style>

+ 330 - 44
src/view/material/sceneImg.vue

@@ -1,5 +1,22 @@
 <template>
   <div class="scene abstract sceneImg">
+    
+    <el-dialog v-model="casePhotoItem.show" title="重命名" width="500">
+      <el-form :model="casePhotoItem">
+        <el-form-item :label-width="0">
+          <el-input v-model="casePhotoItem.name" maxlength="15" show-word-limit autocomplete="off" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer text-center" >
+          <el-button @click="casePhotoItem.show = false">取消</el-button>
+          <el-button type="primary" @click="handleConfirm">
+            确定
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+          <!-- <el-button class="w-full" @click="handledrawCasePhotos">照片卷</el-button> -->
     <div class="demo-image__preview">
       <el-image-viewer
         hide-on-click-modal
@@ -23,15 +40,16 @@
           ref="tabsRef"
           @tab-click="handleClick"
         >
+  <!-- //new  0 现场图 1 照片卷 2 现场照片 3 勘验笔录 -->
           <el-tab-pane
             v-for="(item, index) in list"
-            :key="index"
+            :key="item.filesTypeName"
             :label="showText?item.filesTypeName:''"
             :childrenList="item.childrenList"
             :class="`handleIsShow show${handleIsShow(item)}`"
-            :name="index"
+            :name="item.filesTypeName"
           >
-            <div class="leftCenter" v-if="active1 == 1">
+            <div class="leftCenter" v-if="active1 == '现场图'">
               <div v-for="(a, b) in item.childrenList" :key="b">
                 <div
                   class="listTitle"
@@ -91,6 +109,7 @@
                         ><Download
                       /></el-icon>
                       <el-icon title="编辑"
+                      v-if="items.oldData != 1"
                         @click="handleoverviewEdit(a.filesTypeName, items)"
                         ><Edit
                       /></el-icon>
@@ -102,7 +121,7 @@
                 </div>
               </div>
             </div>
-            <div class="leftCenter" v-if="active1 == 0">
+            <div class="leftCenter" v-if="active1 == '现场照片'">
               <div v-for="(a, b) in item.childrenList" :key="b" v-show="show?recursiveSearch(a.childrenList):true">
                 <div class="listTitle">{{ a.filesTypeName }}</div>
                 <div
@@ -116,7 +135,7 @@
                 </div>
               </div>
             </div>
-            <div class="leftCenter" v-if="active1 == 2">
+            <div class="leftCenter" v-if="active1 == '勘验笔录'">
               <div
                 class="addButton flex"
                 v-show="!show"
@@ -179,11 +198,88 @@
                 </div>
               </div>
             </div>
+            <div class="leftCenter" v-if="active1 == '照片卷'">
+              <div
+                class="addButton flex"
+                v-show="!show"
+                style="margin-bottom: 10px"
+              >
+                <el-button
+                  class="newbut"
+                  :icon="Edit"
+                  @click="handledrawCasePhotos([])"
+                  >填写</el-button
+                      
+                >
+                <el-dropdown trigger="click" @command="exportToPDF" @visible-change="handleOpen">
+                  <span class="el-dropdown-link">
+                      <el-button class="newbut" :icon="Download">下载</el-button>
+                  </span>
+                  <template #dropdown>
+                    <el-dropdown-menu>
+                      <el-dropdown-item command="four">4联卡纸</el-dropdown-item>
+                      <el-dropdown-item command="a4">A4纸</el-dropdown-item>
+                      <el-dropdown-item command="a3">A3纸</el-dropdown-item>
+                    </el-dropdown-menu>
+                  </template>
+                </el-dropdown>
+              </div>
+              <div
+                class="itemList"
+                v-for="(items, indexs) in casePhotoList"
+                @click="handlItem2(items)"
+                :class="{ active: childrenList.value == items.id }"
+                :key="indexs"
+              >
+                <div class="ItemTitle">
+                  <div class="text">
+                    {{ items.name }}
+                  </div>
+                  <div
+                    class="operation"
+                    v-if="childrenList.value == items.id"
+                  >
+                  
+                <el-dropdown trigger="click" @command="exportToPDF">
+                  <span class="el-dropdown-link">
+                    <el-icon title="下载"><Download/></el-icon>
+                  </span>
+                  <template #dropdown>
+                    <el-dropdown-menu>
+                      <el-dropdown-item command="four">4联卡纸</el-dropdown-item>
+                      <el-dropdown-item command="a4">A4纸</el-dropdown-item>
+                      <el-dropdown-item command="a3">A3纸</el-dropdown-item>
+                    </el-dropdown-menu>
+                  </template>
+                </el-dropdown>
+                  <i title="重命名" class="el-icon iconfont icon-rename" @click="handlemtk(items)"></i>
+                    <!-- <el-icon title="重命名"
+                      @click="
+                        downloadByUrl({
+                          url: items.filesUrl,
+                          fileName: items.filesTitle,
+                        })
+                      "
+                      ><Download
+                    /></el-icon> -->
+                    <el-icon title="编辑"
+                      v-if="!show && items.createType != 'upload'"
+                      @click="handledrawCasePhotos(items.id)"
+                      ><Edit
+                    /></el-icon>
+                    <el-icon v-if="!show" @click="del2(items)" title="删除"
+                      ><CircleClose
+                    /></el-icon>
+                  </div>
+                </div>
+              </div>
+            </div>
           </el-tab-pane>
         </el-tabs>
       </div>
     </div>
-    <div class="abstractCentenr" v-if="active1 == 0">
+    <!-- 现场照片 -->
+    <div class="abstractCentenr" v-if="active1 == '现场照片'">
       <div class="centerTop" v-show="!show">
         <div class="import" @click="handleYr">
           <el-icon size="26" color="#26559B">
@@ -229,7 +325,8 @@
         </div>
       </div>
     </div>
-    <div class="abstractCentenr" v-if="active1 == 1">
+    <!-- 现场图 -->
+    <div class="abstractCentenr" v-if="active1 == '现场图'">
       <div class="imgSrcimport noView" v-if="!childrenList.src">暂无数据</div>
       <div class="imgSrccentenr" v-else>
         <img
@@ -240,7 +337,8 @@
         />
       </div>
     </div>
-    <div class="abstractCentenr" v-if="active1 == 2">
+    <!-- 勘验笔录 -->
+    <div class="abstractCentenr" v-if="active1 == '勘验笔录'">
       <div
         class="kybl"
         v-if="childrenList.item && childrenList.item.createType == 'online'"
@@ -392,6 +490,29 @@
         <div v-else class="noViewTitle">暂无数据</div>
       </div>
     </div>
+    <div class="abstractCentenr" style="padding: 0" v-if="active1 == '照片卷'">
+      <showpages ref="showPagesRef" :photos="photos" v-if="childrenList.src" :content="childrenList.src"/>
+      <div v-else class="noView" >
+        <div
+          class="noViewTitle"
+          v-if="childrenList.item && childrenList.item.filesUrl"
+        >
+          <div class="zbzc">暂不支持预览</div>
+          <div
+            class="cursor-pointer down"
+            @click="
+              downloadByUrl({
+                fileName: childrenList.item && childrenList.item.filesTitle,
+                url: childrenList.item && childrenList.item.filesUrl,
+              })
+            "
+          >
+            下载
+          </div>
+        </div>
+        <div v-else class="noViewTitle">暂无数据</div>
+      </div>
+    </div>
   </div>
 </template>
 
@@ -399,9 +520,11 @@
 import viewImg from "@/components/viewImg/list.vue";
 import { computed, ref, reactive } from "vue";
 import { addCaseScenes } from "./quisk";
+import showpages from "./showpages.vue";
 import {
   updateByTreeFileLists,
   getByTreeFileLists,
+  getCasePhotoRollList,
   caseExportImg,
   overviewAdd,
   TabulationAdd,
@@ -413,6 +536,9 @@ import {
   addByMediaLiBrary,
   getSceneListHasAi,
   getCaseInquestInfoOld,
+  casePhotoUpdate,
+  casePhotodel,
+  getAllPhotoList,
   show,
 } from "@/store/case";
 import {
@@ -440,10 +566,13 @@ import { addCaseFile, setTypeFile, addSceneImg1, addSceneImg2 } from "./quisk";
 const caseId = computed(() => router.currentRoute.value?.params?.caseId);
 const filesTypeId = ref(0);
 const ImgsrcList = ref([]);
-const active1 = ref(0);
+const active1 = ref('现场图');
 const active = ref(true);
 const urlindex = ref(-1);
 
+const canvas = ref(null);
+const editor = ref(null);
+
 const tabsRef = ref(null);
 const showText = ref(show.value?false:true);
 const settype = ref(false);
@@ -456,6 +585,13 @@ const childrenList = ref({
   item: {},
   caseFilesList: [],
 });
+const casePhotoItem = ref({
+  id: '',
+  name: '',
+  content: '',
+  show: false,
+})
+const showPagesRef = ref(null);
 const data = reactive({
   title: "",
   inquestNum: "", //现场勘验号
@@ -508,6 +644,8 @@ const data = reactive({
   witnessInfo: [],
   remark: "",
 });
+const xczpfilesTypeId = ref(1);
+const photos = ref([]);
 const showModal = ref(false);
 const srcList = ref([]);
 const klblId = ref(0);
@@ -515,6 +653,12 @@ const fileLists = ref([]);
 const imgfileLists = ref([]);
 const list = ref([]);
 const fileList = ref([]);
+const casePhotoList = ref([{
+      id: 0,
+      name: '现场照片',
+      caseId: 336,
+      content:'现场照片',
+    }]);
 const activeNames = ref(["1"]);
 const { size, upload, removeFile, previewFile, file, accept } = useUpload({
   maxSize: 10 * 1024 * 1024,
@@ -535,6 +679,10 @@ const {
 const handleChange = (val: string[]) => {
   console.log(val);
 };
+const getImgList = async () => {
+  let imgList = await getAllPhotoList(filesTypeId.value)
+  photos.value = imgList.map(ele => ({...ele, url: ele.filesUrl,id: ele.filesId, name: ele.filesTitle})) || [];
+}
 getList();
 function handleActive(params) {
   console.log("handleActive", params);
@@ -557,7 +705,7 @@ function handleSuccess(item) {
   let uploadId = item?.data.id;
   addByMediaLiBrary({
     caseId: caseId.value,
-    filesTypeId: active1.value == 2 ? klblId.value : childrenList.value.value,
+    filesTypeId: active1.value == '勘验笔录' ? klblId.value : childrenList.value.value,
     uploadId,
   }).then((res) => {
     fileLists.value = [];
@@ -565,13 +713,25 @@ function handleSuccess(item) {
     getList(true);
   });
 }
+
 function getList(refresh = false) {
+  //new  0 现场图 1 照片卷 2 现场照片 3 勘验笔录
+  //old  0 现场照片 1 现场图 2 勘验笔录 
+  getCasePhotoRollList(caseId.value).then(res => {
+    casePhotoList.value = res || []
+    if(active1.value == '照片卷' && res && res.length){
+      handlItem2(res[0])
+    }
+  })
   updateByTreeFileLists(caseId.value).then((res) => {
     list.value =
       res.find((ele) => ele.filesTypeName == "三录材料")?.childrenList || [];
     klblId.value = list.value.find(
       (ele) => ele.filesTypeName == "勘验笔录"
     ).filesTypeId;
+    let zpj = list.value.find((ele) => ele.filesTypeName == "现场照片");
+    filesTypeId.value = zpj.filesTypeId
+    getImgList()
     if (show.value) {
       //展示也过滤现场照片空数据
       list.value[0].childrenList.map((ele) => {
@@ -588,19 +748,15 @@ function getList(refresh = false) {
     }
     if (!refresh) {
       handleClick({
-        index: active1.value,
+        paneName: active1.value,
         childrenListvalue: childrenList.value,
       });
       return;
     }
 
-    let item = active1.value
-      ? list.value[active1.value]
-      : list.value.find(
-          (item) => item.childrenList && item.childrenList.length
-        );
+    let item = list.value.find((item) => item.filesTypeName == active1.value);
     let arr2 = [];
-    if (active1.value == 2 || active1.value == 1) {
+    if (active1.value == '现场照片' || active1.value == '现场图') {//现场图和勘验笔录
       item.childrenList.map((ele) => {
         if (ele.caseFilesList && ele.caseFilesList.length) {
           ele.caseFilesList.map((element) => {
@@ -611,17 +767,18 @@ function getList(refresh = false) {
         }
       });
     }
-    console.log("activeItem", list.value, "arr2", arr2);
-    if (active1.value == 0 && childrenList.value.parentId) {
+    if (active1.value == '现场照片' && childrenList.value.parentId) {//现场照片
       item = item.childrenList.find(
         (ele) => ele.filesTypeId == childrenList.value.parentId
       );
     }
-    if (active1.value == 0 && !childrenList.value.parentId) {
+    if (active1.value == '现场图' && !childrenList.value.parentId) {
       item = item.childrenList[0];
     }
+    console.log("activeItem", list.value, "arr2", arr2, item);
     srcList.value =
-      active1.value == 2 || active1.value == 1 ? arr2 : item.childrenList;
+      active1.value == '现场照片' || active1.value == '现场图' ? arr2 : item.childrenList;
+    console.log("srcList", srcList.value, item, childrenList, active1.value);
     let activeIndex =
       srcList.value.findIndex(
         (item) => item.filesTypeId == childrenList.value.value
@@ -647,7 +804,7 @@ function getList(refresh = false) {
       activeItem.filesUrl ||
       (activeItem.caseFilesList && activeItem.caseFilesList[0]?.filesUrl);
     childrenList.value.caseFilesList =
-      active1.value == 2 || active1.value == 0
+      active1.value == '勘验笔录' || active1.value == '现场照片'
         ? activeItem.caseFilesList
         : activeItem.childrenList;
   });
@@ -656,43 +813,47 @@ async function handleAdd() {
   await addCaseFile({ caseId: caseId.value, filesTypeName: ["三录材料"] });
   getList(true);
 }
-const handleClick = ({ index, childrenListvalue }) => {
+const handleClick = (a) => {
+  let { paneName, childrenListvalue } = a;
   let newImgsrcList = [];
+  let index = list.value.findIndex((item) => item.filesTypeName == paneName);
+  console.log("handleClick", a ,paneName, list.value, index);
   let childrenLists = list.value[index].childrenList;
-  console.log("handleClick", index, childrenListvalue);
+  console.log("handleClickchildrenLists", list.value, index, childrenLists);
   if (childrenListvalue && show.value) {
     setTimeout(() => {
       let indexshow1 = recursiveSearch(list.value[0].childrenList);
-      let indexshow2 = recursiveSearch(list.value[1].childrenList);
-      let indexshow3 = recursiveSearch([list.value[2]]);
-      let tabsdom0 = document.getElementById("tab-0");
-      let tabsdom1 = document.getElementById("tab-1");
-      let tabsdom2 = document.getElementById("tab-2");
+      let indexshow2 = recursiveSearch(list.value[2].childrenList);
+      let indexshow3 = recursiveSearch([list.value[3]]);
+      let tabsdom0 = document.getElementById("tab-现场图");
+      let tabsdom1 = document.getElementById("tab-现场照片");
+      let tabsdom2 = document.getElementById("tab-勘验笔录");
       if (!indexshow1 && tabsdom0) tabsdom0.style.display = "none";
       if (!indexshow2 && tabsdom1) tabsdom1.style.display = "none";
       if (!indexshow3 && tabsdom2) tabsdom2.style.display = "none";
       if (indexshow1) {
       } else if (indexshow2) {
-        active1.value = 1;
-        handleClick({index: active1.value})
+        active1.value = '现场照片';
+        handleClick({paneName: active1.value})
       } else {
-        active1.value = 2;
-        handleClick({index: active1.value})
+        active1.value = '勘验笔录 ';
+        handleClick({paneName: active1.value})
       }
       showText.value = true
     }, 500);
   }
-  if (index == 1) {
+  if (paneName == '现场照片') {
     //现场照片
-    let newindex = childrenLists.findIndex(
+    let ArrList = [];
+    childrenLists.map(ele => {
+      ArrList.push(...ele.childrenList)
+    })
+    let newindex = ArrList.findIndex(
       (items) => items.caseFilesList && items.caseFilesList.length
     );
     newindex = newindex == -1?0:newindex
-    let activeItem =
-      (childrenLists[newindex]?.caseFilesList &&
-        childrenLists[newindex]?.caseFilesList[0]) ||
-      {};
-    console.log("handleClick", activeItem, !activeItem);
+    let activeItem =ArrList[newindex] || {};
+    console.log("handleClick", activeItem, childrenLists);
     if(!activeItem) return
     childrenList.value.value = activeItem.filesTypeId;
     childrenList.value.parentId = activeItem.parentId;
@@ -705,7 +866,7 @@ const handleClick = ({ index, childrenListvalue }) => {
         ele.caseFilesList && newSrc.push(...ele.caseFilesList);
       });
     newImgsrcList = newSrc.map((ele) => ele.filesUrl);
-  } else if (index == 2) {
+  } else if (paneName == '勘验笔录') {
     //笔录
     let activeItem =
       (list.value[index].caseFilesList && list.value[index].caseFilesList[0]) ||
@@ -722,6 +883,15 @@ const handleClick = ({ index, childrenListvalue }) => {
       [];
     console.log("handleClick", newSrc, childrenLists.caseFilesList);
     newImgsrcList = newSrc.filter((ele) => isImage(ele));
+  } else if (paneName == '照片卷') {
+    childrenList.value.src =''
+    //照片卷
+    let activeItem = casePhotoList.value[0]
+    console.log("handleClick", childrenLists,activeItem);
+    childrenList.value.src = activeItem.content;
+    childrenList.value.item = activeItem;
+    childrenList.value.filesId = activeItem.id;
+    childrenList.value.value = activeItem.id;
   } else {
     //现场图
     let ArrList = [];
@@ -760,6 +930,13 @@ function handlItem1(item) {
   // childrenList.value.caseFilesList = item.caseFilesList || [];
   console.log("handleClick", item, childrenList.value);
 }
+const handlItem2 = (item) => {
+  console.log("handlItem2", item, childrenList.value);
+  childrenList.value.value = item.id;
+  childrenList.value.filesId = item.id;
+  childrenList.value.item = item;
+  childrenList.value.src = item.content;
+}
 const handleClick2 = (item) => {
   console.log("handleClick2", item);
   childrenList.value.value = item.filesTypeId;
@@ -826,7 +1003,23 @@ const del = async (file) => {
     getList(true);
   });
 };
-
+const del2 = async (file) => {
+  console.log(file, "file");
+  ElMessageBox.confirm("确定删除?", "提示", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
+    type: "warning",
+  }).then(async () => {
+    await casePhotodel({ caseId: caseId.value, id: file.id });
+    ElMessage({
+      type: "success",
+      message: "删除成功",
+    });
+    childrenList.value.src = "";
+    childrenList.value.value = "";
+    getList(true);
+  });
+};
 const OverviewDel = async (file, filesTypeName) => {
   console.log(file, "file");
   ElMessageBox.confirm("确定删除?", "提示", {
@@ -895,15 +1088,29 @@ const handleEdit = async (lists) => {
     lists,
     lists.map((ele) => ele.filesId)
   );
+  let item = list.value.find(ele => ele.filesTypeName == active1.value);
   await setTypeFile({
     caseId: caseId.value,
     // filesId: lists.map(ele => ele.filesId),
     filesTypeId: childrenList.value.value,
-    fileOptions: list.value[active1.value].childrenList,
+    fileOptions: item.childrenList,
     filesIds: lists.map((ele) => ele.filesId),
   });
   getList(true);
 };
+async function handledrawCasePhotos(val) {
+  const id = Array.isArray(val) ? val.toString() : val;
+  router.push({
+    name: RouteName.drawCasePhotos,
+    params: { caseId: caseId.value! },
+    query: {
+      id: id,
+      filesTypeId: filesTypeId.value,
+    }
+
+  });
+  console.log("handleAdd");
+}
 const handleView = (src) => {
   if (src && !ImgsrcList.value.includes(src)) {
     ImgsrcList.value.push(src);
@@ -914,6 +1121,83 @@ const handleIsShow = (item) => {
   if (!show.value) return true;
   return !recursiveSearch(item && item.childrenList);
 };
+
+const handlemtk = (item) => {
+  casePhotoItem.value = {
+    show: true,
+    ...item,
+  };
+  // ElMessageBox.prompt("", "重命名", {
+  //   confirmButtonText: "确定",
+  //   customClass: "promptClass",
+  //   cancelButtonText: "取消",
+  //   showClose: true,
+  //   inputValue: item.name,
+  //   inputValidator: (value) => {
+  //     if (!value) {
+  //       return false;
+  //     }
+  //     return true;
+  //   },
+  //   inputErrorMessage: "请输入名称",
+  // })
+  //   .then(async ({ value }) => {
+  //     ElMessage({
+  //       type: "success",
+  //       message: `修改成功`,
+  //     });
+  //   })
+  //   .catch(() => {
+  //   });
+};
+const handleConfirm = async () => {
+      let value = casePhotoItem.value.name;
+      if(!value){
+        return  ElMessage.error("请输入修改名称");;
+      }
+      await casePhotoUpdate({
+        ...casePhotoItem.value,
+        name: value,
+      });
+      ElMessage({
+        type: "success",
+        message: `修改成功`,
+      });
+      casePhotoItem.value.show = false;
+      await getList()
+}
+const exportToPDF = (paperType) => {
+  showPagesRef.value.exportToPDF(paperType, childrenList.value?.item?.name);
+}
+const handleOpen = (val) => {
+  if(!val){
+    handleClick({paneName: '照片卷'})
+  }else{
+    console.log("handleOpen");
+    let Alllist = {
+      pages: [],
+      indexingLineList: [],
+    }
+    casePhotoList.value.map(ele => {
+      let content = ele.content && JSON.parse(ele.content);
+      let { pages , indexingLineList } = content
+      let xAdd = (600 + 4)*Alllist.pages.length
+      if(xAdd){
+        indexingLineList = indexingLineList.map(element => {
+          let points = element.points.map(item => ({...item,x:item.x+xAdd}))
+          return {...element,points}
+        })
+      }
+      Alllist.pages.push(...pages)
+      Alllist.indexingLineList.push(...indexingLineList)
+      console.log("handleOpen", content);
+    })
+    childrenList.value.src = Alllist;
+    childrenList.value.value = -1;
+    
+    console.log("handleOpen", Alllist);
+  }
+}
 </script>
 <style scoped lang="scss">
 .scene {
@@ -925,7 +1209,7 @@ const handleIsShow = (item) => {
     overflow-x: scroll;
     .list {
       .addButton {
-        padding: 0 34px;
+        padding: 20px 34px 0 34px;
         margin-left: 0px;
       }
       .listTitle {
@@ -1130,3 +1414,5 @@ const handleIsShow = (item) => {
   }
 }
 </style>
+
+

+ 95 - 0
src/view/material/showpages.vue

@@ -0,0 +1,95 @@
+<template>
+    <div class="canvas-container" ref="parentElement">
+      <canvas
+        ref="canvas"
+        class="canvas-content"
+        @click="handleCanvasClick"
+        @drop="handleDrop"
+        @dragover="handleDragOver"
+      ></canvas>
+    </div>
+</template>
+
+<script setup>
+import { ref, watch, onMounted } from 'vue';
+import { CanvasPhotoEditor } from "@/view/case/photos/canvas-photo-editor.js";
+
+const props = defineProps({
+  // SVG图片的URL
+  content: {
+    type: String,
+    required: true
+  },
+  photos: {
+    type: Array,
+    required: true
+  }
+}); 
+// --- 响应式数据 ---
+const canvas = ref(null);
+const parentElement = ref(null);
+const editor = ref(null);
+
+// 监听属性变化并重新加载/处理SVG
+watch(() => props.content, ()=>{
+  if(editor.value && props.content){
+        let content = typeof props.content === 'string'?JSON.parse(props.content):props.content
+        console.log("newcontent", content);
+        editor.value.pages = content.pages;
+        editor.value.indexingLineList = content.indexingLineList;
+        editor.value.drawAllPages(props.photos);
+        editor.value.resetPosition(); // 移除scrollToCenter,用resetPosition
+  }
+});
+// --- 初始化 ---
+onMounted(() => {
+  if (canvas.value) {
+    // 创建编辑器实例
+    editor.value = new CanvasPhotoEditor(canvas.value, {
+      pageWidth: 600,
+      pageHeight: 840,
+      canvasBgColor: "#efefef",
+      pageBgColor: "#ffffff",
+      photos: props.photos,
+      scale: 0.5,
+      show: true,
+      updata: (value) => {
+        // selectedPageItem.value = value.selectedPageItem;
+        // selectedPageIndex.value = value.selectedPageIndex;
+        // historylength.value = value.historylength;
+        // currentIndex.value = value.currentIndex;
+        console.log("updata", value);
+      },
+    });
+
+    // 绑定容器(用于居中计算)
+    editor.value.bindScrollWrapper(parentElement.value);
+    console.log("updata", parentElement.value);
+    if(props.content){
+        let content = typeof props.content === 'string'?JSON.parse(props.content):props.content
+        editor.value.pages = content.pages;
+        editor.value.indexingLineList = content.indexingLineList;
+        editor.value.drawAllPages(props.photos);  
+    }
+    // 初始化绘制
+    editor.value.resetPosition(); // 移除scrollToCenter,用resetPosition
+
+    // 同步初始数据
+    // scale.value = editor.value.scale;
+  }
+});
+const exportToPDF = (paperType, name) => {
+  editor.value.exportPagesToPDF(paperType, name);
+}
+defineExpose({
+  exportToPDF
+})
+</script>
+
+<style scoped>
+.canvas-container {
+  width: 100%;
+  height: 100%;
+}
+/* 可以根据需要添加更多样式 */
+</style>

+ 198 - 0
src/view/vrmodel/addCaseFile.vue

@@ -0,0 +1,198 @@
+<template>
+  <el-form
+    ref="form"
+    :model="caseFile"
+    label-width="90px"
+    class="camera-from dispatch-file-from jm-file-upload"
+  >
+    <el-form-item label="分类:" class="mandatory">
+      <el-radio-group v-model="caseFile.sourceType">
+        <el-radio value="orig">原始数据</el-radio>
+        <el-radio value="offline">离线包</el-radio>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item label="操作:" class="mandatory uploadFile">
+      <el-upload
+        v-model:file-list="caseFile.file"
+        class="upload-demo"
+        action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
+        multiple
+        :before-upload="upload"
+        :http-request="uploadFiles" 
+        :on-preview="handlePreview"
+        :on-remove="handleRemove"
+        accept=".zip"
+        :limit="1"
+        :on-exceed="handleExceed"
+      >
+        <el-button type="primary">上传</el-button>
+      </el-upload>
+    </el-form-item>
+    <div v-if="caseFile.file && caseFile.file.length" style="padding-left: 83px">
+      <span v-if="caseFile.sourceType == 'orig'"
+        >‌支持采集完成后,U 盘导出的场景原始数据上传计算。</span
+      >
+      <div style="width: 360px" v-else>
+        <div>重要提醒:</div>
+        <div
+          >数据上传规则‌:只接收‌其他单位/部门私有化部署的系统‌(比如XX分局自建的平台)下载的数据,本平台已有的场景‌‌不能重复上传‌;</div
+        >
+        <div
+          >‌版本号核对‌:请确保来源平台和本平台的版本号完全一致,如来源平台是
+          v2.2.0,本平台也必须是 v2.2.0。</div
+        >
+      </div>
+      </div>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { uploadFiles, addByMediaLiBrary, SceneCheck, SceneOrig } from "@/store/case";
+import viewImg from "@/components/viewImg/index.vue"
+import {
+  DrawFormatDesc,
+  DrawFormats,
+  FileDrawType,
+  OtherFormatDesc,
+  OtherFormats,
+} from "@/constant/caseFile";
+import { maxFileSize } from "@/constant/caseFile";
+import { useUpload } from "@/hook/upload";
+import { CaseFile, addCaseFile } from "@/store/caseFile";
+import { ElMessage, UploadFile, ElMessageBox } from "element-plus";
+import { computed, ref, watchEffect, onMounted } from "vue";
+import { QuiskExpose } from "@/helper/mount";
+import { updateSelectByTreeFileLists } from "@/store/case";
+
+const props = defineProps<{
+  caseId: number;
+  fileType: number;
+  filesTypeName: [string];
+  fileInfo?: Object;
+}>();
+const mtkList = ref([]);
+onMounted(async () => {
+});
+
+const caseFile = ref({
+  sourceType: "orig",
+  caseId: props.caseId,
+  file:[],
+  filesTypeId: props.fileType,
+  filesTitle: "",
+  dictId: '',
+  uploadId: '',
+  filesType: 1,
+});
+console.log('caseFile', props.fileInfo);
+// const { size, fileList, upload, removeFile, previewFile, file, accept } = useUpload({
+//   maxSize:  5 * 1024 * 1024 * 1024,
+//   formats:  [".zip"],
+// });
+
+  const upload = async (file: File, fileLists) => {
+    if(!file.name) return false;
+    const fileType = file.name && file.name.substring(file.name.lastIndexOf(".")).toUpperCase();
+    console.log('uploadts', file, fileType);
+    if (fileType !== '.ZIP') {
+      ElMessage.error(`请上传zip文件`);
+      return false;
+    } else if (file.size >  5 * 1024 * 1024 * 1024 ) {
+      ElMessage.error(`请上传5GB以内的文件`);
+      return false;
+    } else {
+      return true;
+    }
+  };
+
+const formatDesc = computed(() => {
+  return 'zip上传'
+});
+
+// 上传请求
+const handleSuccess = (option) => {
+  console.log('handleSuccess', option);
+}
+const handleExceed = (option) => {
+  ElMessage.error("只能上传1个文件!");
+}
+
+const handleItem = (type, item) => {
+  console.log("handleItem", type, item);
+  if(type == 'delete'){
+    mtkList.value = mtkList.value.filter(ele => ele.id != item.id)
+  }
+};
+const Submit = async (filePath) => {
+    const res = await SceneOrig({ filePath, sourceType: caseFile.value.sourceType });
+    // loading.value = false;
+    console.log('res', res, filePath);
+    ElMessage.success('上传成功。');
+    caseFile.value.file = [];
+};
+defineExpose<QuiskExpose>({
+  async submit() {
+    let filesTypeId = caseFile.value.filesTypeId && Array.isArray(caseFile.value.filesTypeId)?caseFile.value.filesTypeId.slice(-1):caseFile.value.filesTypeId
+    let file = caseFile.value.file[0];
+    let filePath = ''
+    if (!file) {
+      ElMessage.error("请上传文件");
+      throw "请上传文件";
+    }else{
+      filePath = file?.response?.data
+    }
+    console.log('caseFile', caseFile.value);
+    const resCheck = await SceneCheck({ filePath, sourceType: caseFile.value.sourceType });
+    if (
+     resCheck.code == 60042 && await ElMessageBox.confirm(`此场景原属于${resCheck.data},继续上传将重新计算并覆盖原场景:\n归属权转移至当前账号;\n清除原所有者的权限设置;\n清空场景内由用户手动添加的空间模型。\n确定继续吗?`, '提示')
+    ) {
+       return Submit(filePath);
+    } 
+    if (
+     resCheck.code == 60043 && await ElMessageBox.confirm(`此场景此前已上传过‌,继续上传将重新计算并覆盖原场景,场景内由用户手动添加的空间模型也会清空,确定继续吗?`, '提示')
+    ) {
+       return Submit(filePath);
+    } 
+    
+    if (
+     resCheck.code == 60051 && await ElMessageBox.confirm(resCheck.data, '提示')
+    ) {
+       return Submit(filePath);
+    } 
+        
+    if (
+     resCheck.code == 60051 && await ElMessageBox.confirm(resCheck.data, '提示')
+    ) {
+       return Submit(filePath);
+    } 
+    return Submit(filePath);
+  },
+});
+</script>
+
+<style scoped lang="scss">
+.upload-demo {
+  overflow: hidden;
+}
+
+.file {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  > div {
+    display: flex;
+    align-items: center;
+  }
+
+  .name {
+    margin-left: 10px;
+  }
+}
+.jm-file-upload {
+  // .mtk {
+  //   position: absolute;
+  //   right: 0;
+  //   top: 0;
+  // }
+}
+</style>

+ 8 - 4
src/view/vrmodel/list.vue

@@ -2,8 +2,9 @@
   <com-head :options="headOptions" v-model="params.pagging.state.query.isObj">
     <el-form label-width="84px" inline>
       <slot name="header" />
-      <el-form-item class="searh-btns" style="grid-area: 1 / 4 / 2 / 4">
+      <el-form-item class="searh-btns" style="grid-area: 1 / 6 / 2 / 4">
         <el-button style="visibility: hidden;" type="primary" @click="params.pagging.refresh">查询</el-button>
+        <el-button v-if="upload" type="primary" @click="addUploadSecen">上传场景</el-button>
         <el-button class="float-right" type="primary" plain @click="params.pagging.queryReset"
           >重置</el-button
         >
@@ -29,9 +30,12 @@ import comPagination from "@/components/pagination/index.vue";
 import { SceneType } from "@/store/scene";
 import { SceneTypeDesc } from "@/constant/scene";
 import { useScenePaggingParams } from "./pagging";
-
-defineProps<{ params: ReturnType<typeof useScenePaggingParams> }>();
-
+import { addModelScene } from "./quisk" 
+defineProps<{ params: ReturnType<typeof useScenePaggingParams>, upload: boolean }>();
+const addUploadSecen = async () => {
+  await addModelScene();
+  params.pagging.refresh();
+}
 const headOptions = [
   // { value: SceneType.SWKK, name: SceneTypeDesc[SceneType.SWKK] },
   // { value: SceneType.SWKJ, name: SceneTypeDesc[SceneType.SWKJ] },

+ 5 - 1
src/view/vrmodel/quisk.ts

@@ -4,7 +4,7 @@ import tableModel from "./tableModel.vue";
 import SceneDownload from "./sceneDownload.vue";
 import { quiskMountFactory } from "@/helper/mount";
 import { axios, checkHasDownload } from "@/request";
-
+import addCaseFile from "./addCaseFile.vue";
 export const editModelScene = quiskMountFactory(EditModel, {
   title: "编辑模型",
   width: 500,
@@ -13,6 +13,10 @@ export const tableModelScene = quiskMountFactory(tableModel, {
   title: "导入实景三维",
   width: 1000,
 });
+export const addModelScene = quiskMountFactory(addCaseFile, {
+  title: "上传",
+  width: 500,
+});
 export type SceneDpwnloadProps = { scene: QuoteScene };
 export const sceneDownload = async(props: SceneDpwnloadProps) => {
   const params = {

+ 1 - 1
src/view/vrmodel/tableModel.vue

@@ -1,5 +1,5 @@
 <template>
-  <List :params="params">
+  <List :params="params" upload>
     <template v-slot:header>
       <el-form-item label="场景名称:" style="width: 250px">
         <el-input v-model="params.keyword" placeholder="请输入"></el-input>

+ 348 - 1
yarn.lock

@@ -29,6 +29,11 @@
   dependencies:
     "@babel/types" "^7.26.3"
 
+"@babel/runtime@^7.12.5", "@babel/runtime@^7.28.6":
+  version "7.28.6"
+  resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz#d267a43cb1836dc4d182cce93ae75ba954ef6d2b"
+  integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==
+
 "@babel/types@^7.25.8", "@babel/types@^7.26.3":
   version "7.26.3"
   resolved "https://mirrors.cloud.tencent.com/npm/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0"
@@ -292,6 +297,27 @@
     "@parcel/watcher-win32-ia32" "2.5.0"
     "@parcel/watcher-win32-x64" "2.5.0"
 
+"@pdf-lib/fontkit@^1.1.1":
+  version "1.1.1"
+  resolved "https://registry.npmmirror.com/@pdf-lib/fontkit/-/fontkit-1.1.1.tgz#f18473892b65e3253eb73f4569785abd2c03b1e0"
+  integrity sha512-KjMd7grNapIWS/Dm0gvfHEilSyAmeLvrEGVcqLGi0VYebuqqzTbgF29efCx7tvx+IEbG3zQciRSWl3GkUSvjZg==
+  dependencies:
+    pako "^1.0.6"
+
+"@pdf-lib/standard-fonts@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz#8ba691c4421f71662ed07c9a0294b44528af2d7f"
+  integrity sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==
+  dependencies:
+    pako "^1.0.6"
+
+"@pdf-lib/upng@^1.0.1":
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/@pdf-lib/upng/-/upng-1.0.1.tgz#7dc9c636271aca007a9df4deaf2dd7e7960280cb"
+  integrity sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==
+  dependencies:
+    pako "^1.0.10"
+
 "@popperjs/core@npm:@sxzz/popperjs-es@^2.11.7":
   version "2.11.7"
   resolved "https://mirrors.cloud.tencent.com/npm/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz#a7f69e3665d3da9b115f9e71671dae1b97e13671"
@@ -311,6 +337,13 @@
     estree-walker "^2.0.2"
     picomatch "^4.0.2"
 
+"@swc/helpers@^0.5.12":
+  version "0.5.19"
+  resolved "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.19.tgz#9a8c8a0bdaecfdfb9b8ae5421c0c8e09246dfee9"
+  integrity sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==
+  dependencies:
+    tslib "^2.8.0"
+
 "@types/estree@^1.0.0":
   version "1.0.6"
   resolved "https://mirrors.cloud.tencent.com/npm/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
@@ -335,16 +368,31 @@
   dependencies:
     undici-types "~6.19.2"
 
+"@types/pako@^2.0.3":
+  version "2.0.4"
+  resolved "https://registry.npmmirror.com/@types/pako/-/pako-2.0.4.tgz#c3575ef8125e176c345fa0e7b301c1db41170c15"
+  integrity sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==
+
 "@types/qs@^6.9.7":
   version "6.9.17"
   resolved "https://mirrors.cloud.tencent.com/npm/@types/qs/-/qs-6.9.17.tgz#fc560f60946d0aeff2f914eb41679659d3310e1a"
   integrity sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==
 
+"@types/raf@^3.4.0":
+  version "3.4.3"
+  resolved "https://registry.npmmirror.com/@types/raf/-/raf-3.4.3.tgz#85f1d1d17569b28b8db45e16e996407a56b0ab04"
+  integrity sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==
+
 "@types/sortablejs@^1.15.8":
   version "1.15.8"
   resolved "https://mirrors.cloud.tencent.com/npm/@types/sortablejs/-/sortablejs-1.15.8.tgz#11ed555076046e00869a5ef85d1e7651e7a66ef6"
   integrity sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==
 
+"@types/trusted-types@^2.0.7":
+  version "2.0.7"
+  resolved "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
+  integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
+
 "@types/web-bluetooth@^0.0.16":
   version "0.0.16"
   resolved "https://mirrors.cloud.tencent.com/npm/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz#1d12873a8e49567371f2a75fe3e7f7edca6662d8"
@@ -583,6 +631,11 @@ base64-arraybuffer@^1.0.2:
   resolved "https://mirrors.cloud.tencent.com/npm/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
   integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
 
+base64-js@^1.1.2, base64-js@^1.3.0:
+  version "1.5.1"
+  resolved "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
 brace-expansion@^2.0.1:
   version "2.0.1"
   resolved "https://mirrors.cloud.tencent.com/npm/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
@@ -597,6 +650,13 @@ braces@^3.0.3:
   dependencies:
     fill-range "^7.1.1"
 
+brotli@^1.3.2:
+  version "1.3.3"
+  resolved "https://registry.npmmirror.com/brotli/-/brotli-1.3.3.tgz#7365d8cc00f12cf765d2b2c898716bcf4b604d48"
+  integrity sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==
+  dependencies:
+    base64-js "^1.1.2"
+
 call-bind-apply-helpers@^1.0.0:
   version "1.0.1"
   resolved "https://mirrors.cloud.tencent.com/npm/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840"
@@ -615,6 +675,20 @@ call-bind@^1.0.7:
     get-intrinsic "^1.2.4"
     set-function-length "^1.2.2"
 
+canvg@^3.0.11:
+  version "3.0.11"
+  resolved "https://registry.npmmirror.com/canvg/-/canvg-3.0.11.tgz#4b4290a6c7fa36871fac2b14e432eff33b33cf2b"
+  integrity sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@types/raf" "^3.4.0"
+    core-js "^3.8.3"
+    raf "^3.4.1"
+    regenerator-runtime "^0.13.7"
+    rgbcolor "^1.0.1"
+    stackblur-canvas "^2.0.0"
+    svg-pathdata "^6.0.3"
+
 chokidar@^4.0.0:
   version "4.0.1"
   resolved "https://mirrors.cloud.tencent.com/npm/chokidar/-/chokidar-4.0.1.tgz#4a6dff66798fb0f72a94f616abbd7e1a19f31d41"
@@ -622,6 +696,11 @@ chokidar@^4.0.0:
   dependencies:
     readdirp "^4.0.1"
 
+clone@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.npmmirror.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
+  integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
+
 combined-stream@^1.0.8:
   version "1.0.8"
   resolved "https://mirrors.cloud.tencent.com/npm/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@@ -639,6 +718,16 @@ confbox@^0.1.8:
   resolved "https://mirrors.cloud.tencent.com/npm/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06"
   integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==
 
+core-js@^3.6.0, core-js@^3.8.3:
+  version "3.48.0"
+  resolved "https://registry.npmmirror.com/core-js/-/core-js-3.48.0.tgz#1f813220a47bbf0e667e3885c36cd6f0593bf14d"
+  integrity sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==
+
+core-util-is@~1.0.0:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
+  integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
+
 css-line-break@^2.1.0:
   version "2.1.0"
   resolved "https://mirrors.cloud.tencent.com/npm/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0"
@@ -687,6 +776,18 @@ detect-libc@^1.0.3:
   resolved "https://mirrors.cloud.tencent.com/npm/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
   integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
 
+dfa@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/dfa/-/dfa-1.2.0.tgz#96ac3204e2d29c49ea5b57af8d92c2ae12790657"
+  integrity sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==
+
+dompurify@^3.3.1:
+  version "3.3.1"
+  resolved "https://registry.npmmirror.com/dompurify/-/dompurify-3.3.1.tgz#c7e1ddebfe3301eacd6c0c12a4af284936dbbb86"
+  integrity sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==
+  optionalDependencies:
+    "@types/trusted-types" "^2.0.7"
+
 dunder-proto@^1.0.0:
   version "1.0.0"
   resolved "https://mirrors.cloud.tencent.com/npm/dunder-proto/-/dunder-proto-1.0.0.tgz#c2fce098b3c8f8899554905f4377b6d85dabaa80"
@@ -783,6 +884,11 @@ estree-walker@^2.0.2:
   resolved "https://mirrors.cloud.tencent.com/npm/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
   integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
 
+fast-deep-equal@^3.1.3:
+  version "3.1.3"
+  resolved "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
 fast-glob@^3.3.2:
   version "3.3.2"
   resolved "https://mirrors.cloud.tencent.com/npm/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
@@ -794,6 +900,15 @@ fast-glob@^3.3.2:
     merge2 "^1.3.0"
     micromatch "^4.0.4"
 
+fast-png@^6.2.0:
+  version "6.4.0"
+  resolved "https://registry.npmmirror.com/fast-png/-/fast-png-6.4.0.tgz#807fc353ccab060d09151b7d082786e02d8e92d6"
+  integrity sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==
+  dependencies:
+    "@types/pako" "^2.0.3"
+    iobuffer "^5.3.2"
+    pako "^2.1.0"
+
 fastq@^1.6.0:
   version "1.17.1"
   resolved "https://mirrors.cloud.tencent.com/npm/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47"
@@ -801,6 +916,16 @@ fastq@^1.6.0:
   dependencies:
     reusify "^1.0.4"
 
+fflate@^0.8.1:
+  version "0.8.2"
+  resolved "https://registry.npmmirror.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea"
+  integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==
+
+file-saver@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
+  integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
+
 fill-range@^7.1.1:
   version "7.1.1"
   resolved "https://mirrors.cloud.tencent.com/npm/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
@@ -813,6 +938,21 @@ follow-redirects@^1.15.6:
   resolved "https://mirrors.cloud.tencent.com/npm/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
   integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
 
+fontkit@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.npmmirror.com/fontkit/-/fontkit-2.0.4.tgz#4765d664c68b49b5d6feb6bd1051ee49d8ec5ab0"
+  integrity sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==
+  dependencies:
+    "@swc/helpers" "^0.5.12"
+    brotli "^1.3.2"
+    clone "^2.1.2"
+    dfa "^1.2.0"
+    fast-deep-equal "^3.1.3"
+    restructure "^3.0.0"
+    tiny-inflate "^1.0.3"
+    unicode-properties "^1.4.0"
+    unicode-trie "^2.0.0"
+
 form-data@^4.0.0:
   version "4.0.1"
   resolved "https://mirrors.cloud.tencent.com/npm/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48"
@@ -882,7 +1022,7 @@ he@^1.2.0:
   resolved "https://mirrors.cloud.tencent.com/npm/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
   integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
 
-html2canvas@^1.4.1:
+html2canvas@^1.0.0, html2canvas@^1.0.0-rc.5, html2canvas@^1.4.1:
   version "1.4.1"
   resolved "https://mirrors.cloud.tencent.com/npm/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
   integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
@@ -890,11 +1030,35 @@ html2canvas@^1.4.1:
     css-line-break "^2.1.0"
     text-segmentation "^1.0.3"
 
+html2pdf.js@^0.14.0:
+  version "0.14.0"
+  resolved "https://registry.npmmirror.com/html2pdf.js/-/html2pdf.js-0.14.0.tgz#dd2fdf2ee3036cb4c0d7c0d4606ee2da7c677e83"
+  integrity sha512-yvNJgE/8yru2UeGflkPdjW8YEY+nDH5X7/2WG4uiuSCwYiCp8PZ8EKNiTAa6HxJ1NjC51fZSIEq6xld5CADKBQ==
+  dependencies:
+    dompurify "^3.3.1"
+    html2canvas "^1.0.0"
+    jspdf "^4.0.0"
+
+immediate@~3.0.5:
+  version "3.0.6"
+  resolved "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
+  integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
+
 immutable@^5.0.2:
   version "5.0.3"
   resolved "https://mirrors.cloud.tencent.com/npm/immutable/-/immutable-5.0.3.tgz#aa037e2313ea7b5d400cd9298fa14e404c933db1"
   integrity sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==
 
+inherits@~2.0.3:
+  version "2.0.4"
+  resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+iobuffer@^5.3.2:
+  version "5.4.0"
+  resolved "https://registry.npmmirror.com/iobuffer/-/iobuffer-5.4.0.tgz#f85dff957fd0579257472f0a4cfe5ed3430e63e1"
+  integrity sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==
+
 is-extglob@^2.1.1:
   version "2.1.1"
   resolved "https://mirrors.cloud.tencent.com/npm/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
@@ -912,6 +1076,11 @@ is-number@^7.0.0:
   resolved "https://mirrors.cloud.tencent.com/npm/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
   integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
 
+isarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+  integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
+
 jiti@^1.21.6:
   version "1.21.6"
   resolved "https://mirrors.cloud.tencent.com/npm/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268"
@@ -922,6 +1091,44 @@ js-base64@^3.7.5:
   resolved "https://mirrors.cloud.tencent.com/npm/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79"
   integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==
 
+jspdf@^4.0.0:
+  version "4.2.0"
+  resolved "https://registry.npmmirror.com/jspdf/-/jspdf-4.2.0.tgz#f5b42a8e1592c3da1531d005adc87ccc19272965"
+  integrity sha512-hR/hnRevAXXlrjeqU5oahOE+Ln9ORJUB5brLHHqH67A+RBQZuFr5GkbI9XQI8OUFSEezKegsi45QRpc4bGj75Q==
+  dependencies:
+    "@babel/runtime" "^7.28.6"
+    fast-png "^6.2.0"
+    fflate "^0.8.1"
+  optionalDependencies:
+    canvg "^3.0.11"
+    core-js "^3.6.0"
+    dompurify "^3.3.1"
+    html2canvas "^1.0.0-rc.5"
+
+jspdf@^4.2.1:
+  version "4.2.1"
+  resolved "https://registry.npmmirror.com/jspdf/-/jspdf-4.2.1.tgz#6ba0d263999313f91f369ee80ecf235046b2acd8"
+  integrity sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==
+  dependencies:
+    "@babel/runtime" "^7.28.6"
+    fast-png "^6.2.0"
+    fflate "^0.8.1"
+  optionalDependencies:
+    canvg "^3.0.11"
+    core-js "^3.6.0"
+    dompurify "^3.3.1"
+    html2canvas "^1.0.0-rc.5"
+
+jszip@^3.10.1:
+  version "3.10.1"
+  resolved "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2"
+  integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==
+  dependencies:
+    lie "~3.3.0"
+    pako "~1.0.2"
+    readable-stream "~2.3.6"
+    setimmediate "^1.0.5"
+
 kolorist@^1.8.0:
   version "1.8.0"
   resolved "https://mirrors.cloud.tencent.com/npm/kolorist/-/kolorist-1.8.0.tgz#edddbbbc7894bc13302cdf740af6374d4a04743c"
@@ -937,6 +1144,13 @@ leaflet@^1.9.4:
   resolved "https://mirrors.cloud.tencent.com/npm/leaflet/-/leaflet-1.9.4.tgz#23fae724e282fa25745aff82ca4d394748db7d8d"
   integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==
 
+lie@~3.3.0:
+  version "3.3.0"
+  resolved "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
+  integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
+  dependencies:
+    immediate "~3.0.5"
+
 local-pkg@^0.5.0:
   version "0.5.1"
   resolved "https://mirrors.cloud.tencent.com/npm/local-pkg/-/local-pkg-0.5.1.tgz#69658638d2a95287534d4c2fff757980100dbb6d"
@@ -1075,6 +1289,21 @@ ollama@^0.5.14:
   dependencies:
     whatwg-fetch "^3.6.20"
 
+pako@^0.2.5:
+  version "0.2.9"
+  resolved "https://registry.npmmirror.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
+  integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==
+
+pako@^1.0.10, pako@^1.0.11, pako@^1.0.6, pako@~1.0.2:
+  version "1.0.11"
+  resolved "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
+  integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
+
+pako@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
+  integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==
+
 path-browserify@^1.0.1:
   version "1.0.1"
   resolved "https://mirrors.cloud.tencent.com/npm/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
@@ -1085,6 +1314,21 @@ pathe@^1.1.2:
   resolved "https://mirrors.cloud.tencent.com/npm/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec"
   integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==
 
+pdf-lib@^1.17.1:
+  version "1.17.1"
+  resolved "https://registry.npmmirror.com/pdf-lib/-/pdf-lib-1.17.1.tgz#9e7dd21261a0c1fb17992580885b39e7d08f451f"
+  integrity sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==
+  dependencies:
+    "@pdf-lib/standard-fonts" "^1.0.0"
+    "@pdf-lib/upng" "^1.0.1"
+    pako "^1.0.11"
+    tslib "^1.11.1"
+
+performance-now@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+  integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
+
 picocolors@^1.1.1:
   version "1.1.1"
   resolved "https://mirrors.cloud.tencent.com/npm/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
@@ -1118,6 +1362,11 @@ postcss@^8.4.27, postcss@^8.4.48:
     picocolors "^1.1.1"
     source-map-js "^1.2.1"
 
+process-nextick-args@~2.0.0:
+  version "2.0.1"
+  resolved "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
+  integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+
 province-city-china@^8.5.8:
   version "8.5.8"
   resolved "https://mirrors.cloud.tencent.com/npm/province-city-china/-/province-city-china-8.5.8.tgz#b1f28cfe917388e67684cfb64f7cf2a5c30ed44a"
@@ -1142,16 +1391,51 @@ queue-microtask@^1.2.2:
   resolved "https://mirrors.cloud.tencent.com/npm/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
   integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
 
+raf@^3.4.1:
+  version "3.4.1"
+  resolved "https://registry.npmmirror.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
+  integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
+  dependencies:
+    performance-now "^2.1.0"
+
+readable-stream@~2.3.6:
+  version "2.3.8"
+  resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
+  integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
+  dependencies:
+    core-util-is "~1.0.0"
+    inherits "~2.0.3"
+    isarray "~1.0.0"
+    process-nextick-args "~2.0.0"
+    safe-buffer "~5.1.1"
+    string_decoder "~1.1.1"
+    util-deprecate "~1.0.1"
+
 readdirp@^4.0.1:
   version "4.0.2"
   resolved "https://mirrors.cloud.tencent.com/npm/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a"
   integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==
 
+regenerator-runtime@^0.13.7:
+  version "0.13.11"
+  resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
+  integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
+
+restructure@^3.0.0:
+  version "3.0.2"
+  resolved "https://registry.npmmirror.com/restructure/-/restructure-3.0.2.tgz#e6b2fad214f78edee21797fa8160fef50eb9b49a"
+  integrity sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==
+
 reusify@^1.0.4:
   version "1.0.4"
   resolved "https://mirrors.cloud.tencent.com/npm/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
   integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
 
+rgbcolor@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d"
+  integrity sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==
+
 rollup@^3.27.1:
   version "3.29.5"
   resolved "https://mirrors.cloud.tencent.com/npm/rollup/-/rollup-3.29.5.tgz#8a2e477a758b520fb78daf04bca4c522c1da8a54"
@@ -1166,6 +1450,11 @@ run-parallel@^1.1.9:
   dependencies:
     queue-microtask "^1.2.2"
 
+safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+  version "5.1.2"
+  resolved "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
 sass@^1.64.2:
   version "1.82.0"
   resolved "https://mirrors.cloud.tencent.com/npm/sass/-/sass-1.82.0.tgz#30da277af3d0fa6042e9ceabd0d984ed6d07df70"
@@ -1194,6 +1483,11 @@ set-function-length@^1.2.2:
     gopd "^1.0.1"
     has-property-descriptors "^1.0.2"
 
+setimmediate@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+  integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
+
 side-channel@^1.0.6:
   version "1.0.6"
   resolved "https://mirrors.cloud.tencent.com/npm/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
@@ -1209,6 +1503,23 @@ side-channel@^1.0.6:
   resolved "https://mirrors.cloud.tencent.com/npm/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
   integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
 
+stackblur-canvas@^2.0.0:
+  version "2.7.0"
+  resolved "https://registry.npmmirror.com/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz#af931277d0b5096df55e1f91c530043e066989b6"
+  integrity sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==
+
+string_decoder@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+  integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+  dependencies:
+    safe-buffer "~5.1.0"
+
+svg-pathdata@^6.0.3:
+  version "6.0.3"
+  resolved "https://registry.npmmirror.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz#80b0e0283b652ccbafb69ad4f8f73e8d3fbf2cac"
+  integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==
+
 swiper@^11.1.15:
   version "11.1.15"
   resolved "https://mirrors.cloud.tencent.com/npm/swiper/-/swiper-11.1.15.tgz#e2258c8d38282e2f115ca463d6e8c5b84cdcf1ca"
@@ -1226,6 +1537,11 @@ three@^0.171.0:
   resolved "https://mirrors.cloud.tencent.com/npm/three/-/three-0.171.0.tgz#3c0dd3f8fa14e78a7f8db6e416b98f264f1185c0"
   integrity sha512-Y/lAXPaKZPcEdkKjh0JOAHVv8OOnv/NDJqm0wjfCzyQmfKxV7zvkwsnBgPBKTzJHToSOhRGQAGbPJObT59B/PQ==
 
+tiny-inflate@^1.0.0, tiny-inflate@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
+  integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
+
 to-regex-range@^5.0.1:
   version "5.0.1"
   resolved "https://mirrors.cloud.tencent.com/npm/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@@ -1238,6 +1554,16 @@ tslib@2.3.0:
   resolved "https://mirrors.cloud.tencent.com/npm/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
   integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
 
+tslib@^1.11.1:
+  version "1.14.1"
+  resolved "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+  integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+
+tslib@^2.8.0:
+  version "2.8.1"
+  resolved "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+  integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
 typescript@5.4.5:
   version "5.4.5"
   resolved "https://mirrors.cloud.tencent.com/npm/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611"
@@ -1253,6 +1579,22 @@ undici-types@~6.19.2:
   resolved "https://mirrors.cloud.tencent.com/npm/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
   integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
 
+unicode-properties@^1.4.0:
+  version "1.4.1"
+  resolved "https://registry.npmmirror.com/unicode-properties/-/unicode-properties-1.4.1.tgz#96a9cffb7e619a0dc7368c28da27e05fc8f9be5f"
+  integrity sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==
+  dependencies:
+    base64-js "^1.3.0"
+    unicode-trie "^2.0.0"
+
+unicode-trie@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.npmmirror.com/unicode-trie/-/unicode-trie-2.0.0.tgz#8fd8845696e2e14a8b67d78fa9e0dd2cad62fec8"
+  integrity sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==
+  dependencies:
+    pako "^0.2.5"
+    tiny-inflate "^1.0.0"
+
 unplugin-element-plus@^0.7.2:
   version "0.7.2"
   resolved "https://mirrors.cloud.tencent.com/npm/unplugin-element-plus/-/unplugin-element-plus-0.7.2.tgz#93408f4c064a102d31d49097144641b3fcc9726c"
@@ -1280,6 +1622,11 @@ unplugin@^1.14.1, unplugin@^1.3.2:
     acorn "^8.14.0"
     webpack-virtual-modules "^0.6.2"
 
+util-deprecate@~1.0.1:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+  integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+
 utrie@^1.0.2:
   version "1.0.2"
   resolved "https://mirrors.cloud.tencent.com/npm/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645"