浏览代码

feat: 移植case模块

gemercheung 11 月之前
父节点
当前提交
4a323e85c3
共有 59 个文件被更改,包括 7410 次插入191 次删除
  1. 11 3
      package.json
  2. 5 4
      src/app/fire/view/dispatch/header.vue
  3. 88 49
      src/app/fire/view/dispatch/index.vue
  4. 99 11
      src/app/fire/view/dispatch/list.vue
  5. 25 8
      src/app/fire/view/dispatch/pagging.ts
  6. 12 13
      src/components/head/index.vue
  7. 5 2
      src/constant/caseFile.ts
  8. 274 0
      src/core/Scene copy.js
  9. 347 0
      src/core/Scene.js
  10. 95 0
      src/core/box/BoxManager.js
  11. 119 0
      src/core/box/HorizontalBox.js
  12. 107 0
      src/core/box/VerticalBox.js
  13. 66 0
      src/core/box/object/CircleTextLabel.js
  14. 47 0
      src/core/box/object/ImgLabel.js
  15. 118 0
      src/core/box/object/ImgLabelBox.js
  16. 139 0
      src/core/box/object/Line.js
  17. 75 0
      src/core/box/object/LinePoints.js
  18. 54 0
      src/core/box/object/PureTextLabel copy.js
  19. 73 0
      src/core/box/object/PureTextLabel.js
  20. 48 0
      src/core/box/object/SimpleLabel.js
  21. 47 0
      src/core/box/object/TextLabel.js
  22. 24 0
      src/core/box/object/TouchEdge.js
  23. 31 0
      src/core/box/object/marker.js
  24. 501 0
      src/core/controls/FloorplanControls.js
  25. 10 0
      src/core/mitt.js
  26. 969 0
      src/core/player/Player.js
  27. 8 0
      src/core/save.json
  28. 0 0
      src/core/settings/style.js
  29. 92 0
      src/core/utils/cap.js
  30. 14 0
      src/core/utils/text.js
  31. 39 0
      src/core/utils/utils.js
  32. 11 7
      src/hook/pagging.ts
  33. 1 1
      src/main.ts
  34. 30 2
      src/request/urls.ts
  35. 6 0
      src/router/config.ts
  36. 1 0
      src/router/routeName.ts
  37. 87 2
      src/store/case.ts
  38. 1 1
      src/view/case/addCaseFile.vue
  39. 152 0
      src/view/case/addPhotoFile.vue
  40. 193 0
      src/view/case/addPhotoFileAll.vue
  41. 1 1
      src/view/case/addScenes.vue
  42. 129 65
      src/view/case/caseFile.vue
  43. 142 0
      src/view/case/download.vue
  44. 3 0
      src/view/case/draw/c.ts
  45. 1 1
      src/view/case/draw/eshape.vue
  46. 7 4
      src/view/case/draw/header.vue
  47. 1 1
      src/view/case/draw/slider.vue
  48. 24 9
      src/view/case/editMenu.vue
  49. 3 6
      src/view/case/help.ts
  50. 47 0
      src/view/case/no-case.vue
  51. 209 0
      src/view/case/photos/draggable.vue
  52. 154 0
      src/view/case/photos/edit.vue
  53. 552 0
      src/view/case/photos/index.vue
  54. 31 0
      src/view/case/quisk.ts
  55. 561 0
      src/view/case/records/index.vue
  56. 444 0
      src/view/case/records/manifest.vue
  57. 1 1
      src/view/case/share.vue
  58. 2 0
      vite.config.ts
  59. 1074 0
      yarn.lock

+ 11 - 3
package.json

@@ -5,26 +5,34 @@
   "type": "module",
   "scripts": {
     "dev": "vite",
-    "build": " npm run build-quisk",
-    "build-quisk": "vite build ./ fire && vite build ./ criminal && vite build ./ xmfire && vite build ./ ga",
+    "build": "npm run build-quisk",
+    "build-quisk": "vite build ./ --mode=fire",
     "preview": "vite preview"
   },
   "dependencies": {
     "@amap/amap-jsapi-loader": "^1.0.1",
     "@element-plus/icons-vue": "^2.1.0",
     "@types/qs": "^6.9.7",
+    "@vueuse/components": "^10.11.0",
+    "@vueuse/core": "^10.11.0",
+    "@vueuse/router": "^10.11.0",
     "axios": "^1.4.0",
-    "echarts": "^5.5.1",
+    "echarts": "^5.4.3",
     "element-plus": "^2.3.8",
     "js-base64": "^3.7.5",
     "mime": "^3.0.0",
     "mitt": "^3.0.1",
+    "qrcode.vue": "^3.4.1",
     "qs": "^6.11.2",
     "sass": "^1.64.2",
+    "sortablejs": "^1.15.2",
+    "swiper": "^11.1.4",
+    "three": "^0.158.0",
     "unplugin-element-plus": "^0.7.2",
     "unplugin-vue-define-options": "^1.3.12",
     "vue": "^3.3.4",
     "vue-cropper": "^1.0.9",
+    "vue-draggable-plus": "^0.5.0",
     "vue-router": "^4.2.4"
   },
   "devDependencies": {

+ 5 - 4
src/app/fire/view/dispatch/header.vue

@@ -10,7 +10,7 @@
           placeholder="请输入"
         ></el-input>
       </el-form-item>
-      <el-form-item label="起火地址:">
+      <el-form-item label="详细地址:">
         <el-input
           v-model="pagging.state.query.projectAddress"
           placeholder="请输入"
@@ -25,7 +25,8 @@
           :props="{ expandTrigger: 'hover', checkStrictly: true }"
         ></el-cascader>
       </el-form-item>
-      <el-form-item label="承办单位:">
+
+      <el-form-item label="勘验单位:">
         <com-company v-model="pagging.state.query.deptId" />
       </el-form-item>
       <el-form-item label="事故日期:">
@@ -36,7 +37,7 @@
           style="width: 100%"
         />
       </el-form-item>
-      <el-form-item label="火灾原因:">
+      <!-- <el-form-item label="火灾原因:">
         <el-cascader
           style="width: 100%"
           v-model="fireReason"
@@ -44,7 +45,7 @@
           :options="[{ label: '全部', value: UN_REQ_NUM.toString() }].concat(reason)"
           :props="{ expandTrigger: 'hover', checkStrictly: true }"
         ></el-cascader>
-      </el-form-item>
+      </el-form-item> -->
       <el-form-item label="项目状态:">
         <el-select placeholder="请选择" v-model="pagging.state.query.status" showAll>
           <el-option

+ 88 - 49
src/app/fire/view/dispatch/index.vue

@@ -1,29 +1,35 @@
 <template>
   <Header :pagging="pagging" :isTeached="isTeached" />
-  <List :pagging="pagging" :checkPerm="isTeached ? 'cancel' : 'teach'">
+  <List
+    :pagging="pagging"
+    :checkPerm="isTeached ? 'cancel' : 'teach'"
+    :isRecycle="isRecycle"
+  >
     <template v-slot:tableCtrl>
-      <template v-if="isTeached">
-        <el-button
-          type="primary"
-          @click="revokeTeaching()"
-          v-pdpath="'cancel'"
-          :class="{ disable: !pagging.state.table.selectRows.length }"
-        >
-          撤销教学
-        </el-button>
-      </template>
-      <template v-else>
-        <el-button type="primary" @click="addHandler" v-pdpath="'add'">
-          新增火调项目
-        </el-button>
-        <el-button
-          type="primary"
-          @click="setTeaching"
-          v-pdpath="'teach'"
-          :class="{ disable: !pagging.state.table.selectRows.length }"
-        >
-          设为教学项目
-        </el-button>
+      <template v-if="!isRecycle">
+        <template v-if="isTeached">
+          <el-button
+            type="primary"
+            @click="revokeTeaching()"
+            v-pdpath="'cancel'"
+            :class="{ disable: !pagging.state.table.selectRows.length }"
+          >
+            撤销教学
+          </el-button>
+        </template>
+        <template v-else>
+          <el-button type="primary" @click="addHandler" v-pdpath="'add'">
+            新增火调项目
+          </el-button>
+          <el-button
+            type="primary"
+            @click="setTeaching"
+            v-pdpath="'teach'"
+            :class="{ disable: !pagging.state.table.selectRows.length }"
+          >
+            设为教学项目
+          </el-button>
+        </template>
       </template>
     </template>
 
@@ -34,37 +40,64 @@
     </template>
 
     <template v-slot:rowCtrl="{ row }: { row: Fire }">
-      <template v-if="isTeached">
-        <span class="oper-span" @click="gotoQuery(row.caseId)" v-pdpath="['view']">
-          查看
-        </span>
-        <span
-          class="oper-span"
-          @click="showMessageHandler(row)"
-          v-pdpath="['message:look']"
-          >留言
-        </span>
+      <template v-if="!isRecycle">
+        <template v-if="isTeached">
+          <span class="oper-span" @click="gotoQuery(row.caseId)" v-pdpath="['view']">
+            查看
+          </span>
+          <span
+            class="oper-span"
+            @click="showMessageHandler(row)"
+            v-pdpath="['message:look']"
+            >留言
+          </span>
+        </template>
+        <template v-else>
+          <CaseEditMenu
+            :title="row.projectSn"
+            :prev-menu="[
+              {
+                key: 'info',
+                label: '火调信息',
+                onClick: () => editHandler(row),
+              },
+            ]"
+            :caseId="row.caseId"
+            v-if="row.caseId"
+          />
+          <span class="oper-span" @click="gotoQuery(row.caseId)" v-pdpath="['view']">
+            查看
+          </span>
+          <span class="oper-span" @click="copy(row.caseId)" v-pdpath="['view']">
+            复制
+          </span>
+          <span
+            class="oper-span"
+            @click="pagging.del(row)"
+            style="color: var(--primaryColor)"
+            v-pdpath="['del']"
+          >
+            删除
+          </span>
+        </template>
       </template>
       <template v-else>
-        <CaseEditMenu
-          :prev-menu="[
-            {
-              key: 'info',
-              label: '火调信息',
-              onClick: () => editHandler(row),
-            },
-          ]"
-          :caseId="row.caseId"
-          v-if="row.caseId"
-        />
-        <span class="oper-span" @click="gotoQuery(row.caseId)" v-pdpath="['view']">
-          查看
+        <span
+          class="oper-span"
+          @click="async () => {
+            await setFire({...row, isDelete: 0} as any)
+            pagging.refresh();
+          }"
+          v-pdpath="['retruen']"
+          style="color: var(--primaryColor)"
+        >
+          恢复
         </span>
         <span
           class="oper-span"
           @click="pagging.del(row)"
-          style="color: var(--primaryColor)"
           v-pdpath="['del']"
+          style="color: var(--primaryColor)"
         >
           删除
         </span>
@@ -77,7 +110,8 @@
 import Header from "./header.vue";
 import List from "./list.vue";
 import { useFirePagging } from "./pagging";
-import { Fire, revokeFireTeachs, setFireTeachs } from "@/app/fire/store/fire";
+import { Fire, revokeFireTeachs, setFireTeachs, setFire } from "@/app/fire/store/fire";
+import { copyCase } from "@/store/case";
 import CaseEditMenu from "@/view/case/editMenu.vue";
 import { gotoQuery } from "@/view/case/help";
 import { confirm } from "@/helper/message";
@@ -85,7 +119,12 @@ import { addFire, editFire, showLeaveMsgList, addLeaveMsg } from "./quisk";
 import { shareCase } from "@/view/case/quisk";
 import { ElMessage } from "element-plus";
 
-const { pagging, isTeached } = useFirePagging();
+const { pagging, isTeached, isRecycle } = useFirePagging();
+
+const copy = async (caseId: number) => {
+  await copyCase(caseId);
+  pagging.refresh();
+};
 
 // 撤销教学
 const revokeTeaching = async (row?: Fire) => {

+ 99 - 11
src/app/fire/view/dispatch/list.vue

@@ -16,6 +16,7 @@
       @selection-change="pagging.changeSelectRows"
     >
       <el-table-column
+        v-if="!isRecycle"
         type="selection"
         width="50"
         :selectable="() => !!operateIsPermissionByPath(checkPerm)"
@@ -26,27 +27,51 @@
         </div>
       </el-table-column>
       <el-table-column label="项目编号" prop="projectSn"></el-table-column>
+      <el-table-column label="事件分类" prop="field5" v-slot:default="{ row }">
+        <el-tooltip
+          class="item"
+          effect="dark"
+          :content="row.field5"
+          placement="bottom-start"
+          v-if="row.field5 && row.field5.length > 10"
+        >
+          <p class="tip oper-user">{{ row.field5.substring(0, 10) }}...</p>
+        </el-tooltip>
+        <p class="tip" v-else>{{ row.field5 }}</p>
+      </el-table-column>
       <el-table-column label="起火对象" prop="projectName" v-slot:default="{ row }">
         <el-tooltip
           class="item"
           effect="dark"
           :content="row.projectName"
           placement="bottom-start"
-          v-if="row.projectName && row.projectName.length > 15"
+          v-if="row.projectName && row.projectName.length > 10"
         >
-          <p class="tip oper-user">{{ row.projectName }}</p>
+          <p class="tip oper-user">{{ row.projectName.substring(0, 10) }}...</p>
         </el-tooltip>
         <p class="tip" v-else>{{ row.projectName }}</p>
       </el-table-column>
-      <el-table-column label="起火地址" prop="projectAddress" v-slot:default="{ row }">
+      <el-table-column label="分类登记" prop="field6" v-slot:default="{ row }">
+        <el-tooltip
+          class="item"
+          effect="dark"
+          :content="row.field6"
+          placement="bottom-start"
+          v-if="row.field6 && row.field6.length > 10"
+        >
+          <p class="tip oper-user">{{ row.field6.substring(0, 10) }}...</p>
+        </el-tooltip>
+        <p class="tip" v-else>{{ row.field6 }}</p>
+      </el-table-column>
+      <el-table-column label="详细地址" prop="projectAddress" v-slot:default="{ row }">
         <el-tooltip
           class="item"
           effect="dark"
           :content="row.projectAddress"
           placement="bottom-start"
-          v-if="row.projectAddress && row.projectAddress.length > 15"
+          v-if="row.projectAddress && row.projectAddress.length > 10"
         >
-          <p class="tip oper-user">{{ row.projectAddress }}</p>
+          <p class="tip oper-user">{{ row.projectAddress.substring(0, 10) }}...</p>
         </el-tooltip>
         <p class="tip" v-else>{{ row.projectAddress }}</p>
       </el-table-column>
@@ -58,21 +83,84 @@
           placement="bottom-start"
           v-if="row.projectSite && row.projectSite.length > 10"
         >
-          <p class="tip oper-user">{{ row.projectSite }}</p>
+          <p class="tip oper-user">{{ row.projectSite.substring(0, 10) }}...</p>
         </el-tooltip>
         <p class="tip" v-else>{{ row.projectSite }}</p>
       </el-table-column>
-      <el-table-column label="承办单位" prop="organizerDeptName"></el-table-column>
+      <el-table-column label="天气情况" prop="field7" v-slot:default="{ row }">
+        <el-tooltip
+          class="item"
+          effect="dark"
+          :content="row.field7"
+          placement="bottom-start"
+          v-if="row.field7 && row.field7.length > 10"
+        >
+          <p class="tip oper-user">{{ row.field7.substring(0, 10) }}...</p>
+        </el-tooltip>
+        <p class="tip" v-else>{{ row.field7 }}</p>
+      </el-table-column>
+
+      <el-table-column label="勘验地址" prop="field1" v-slot:default="{ row }">
+        <el-tooltip
+          class="item"
+          effect="dark"
+          :content="row.field1"
+          placement="bottom-start"
+          v-if="row.field1 && row.field1.length > 10"
+        >
+          <p class="tip oper-user">{{ row.field1.substring(0, 10) }}...</p>
+        </el-tooltip>
+        <p class="tip" v-else>{{ row.field1 }}</p>
+      </el-table-column>
+      <el-table-column label="全宗名称" prop="caseNewName" v-slot:default="{ row }">
+        <el-tooltip
+          class="item"
+          effect="dark"
+          :content="row.caseNewName"
+          placement="bottom-start"
+          v-if="row.caseNewName && row.caseNewName.length > 10"
+        >
+          <p class="tip oper-user">{{ row.caseNewName.substring(0, 10) }}...</p>
+        </el-tooltip>
+        <p class="tip" v-else>{{ row.caseNewName }}</p>
+      </el-table-column>
+      <el-table-column label="勘验人员" prop="organizerUsers" v-slot:default="{ row }">
+        <el-tooltip
+          class="item"
+          effect="dark"
+          :content="row.organizerUsers"
+          placement="bottom-start"
+          v-if="row.organizerUsers && row.organizerUsers.length > 10"
+        >
+          <p class="tip oper-user">{{ row.organizerUsers.substring(0, 10) }}...</p>
+        </el-tooltip>
+        <p class="tip" v-else>{{ row.organizerUsers }}</p>
+      </el-table-column>
+      <el-table-column label="勘验人勘验人职务" prop="field4" v-slot:default="{ row }">
+        <el-tooltip
+          class="item"
+          effect="dark"
+          :content="row.field4"
+          placement="bottom-start"
+          v-if="row.field4 && row.field4.length > 10"
+        >
+          <p class="tip oper-user">{{ row.field4.substring(0, 10) }}...</p>
+        </el-tooltip>
+        <p class="tip" v-else>{{ row.field4 }}</p>
+      </el-table-column>
+
+      <el-table-column label="勘验单位" prop="organizerDeptName"></el-table-column>
       <el-table-column label="事故日期" prop="accidentDate"></el-table-column>
+      <el-table-column label="勘验起止时间" prop="field8"></el-table-column>
       <el-table-column label="火灾原因" prop="fireReason" v-slot:default="{ row }">
         <el-tooltip
           class="item"
           effect="dark"
           :content="row.fireReason"
           placement="bottom-start"
-          v-if="row.fireReason && row.fireReason.length > 15"
+          v-if="row.fireReason && row.fireReason.length > 10"
         >
-          <p class="tip oper-user">{{ row.fireReason }}</p>
+          <p class="tip oper-user">{{ row.fireReason.substring(0, 10) }}...</p>
         </el-tooltip>
         <p class="tip" v-else>{{ row.fireReason }}</p>
       </el-table-column>
@@ -80,7 +168,7 @@
         {{ fireStatusDesc[row.status as FireStatus] }}
       </el-table-column>
       <slot name="appendColumn" />
-      <el-table-column label="操作" v-slot:default="{ row }" :width="180">
+      <el-table-column label="操作" v-slot:default="{ row }" :width="220" fixed="right">
         <slot name="rowCtrl" :row="row" />
       </el-table-column>
     </el-table>
@@ -102,5 +190,5 @@ import { FireStatus } from "@/app/fire/store/fire";
 import { operateIsPermissionByPath } from "@/directive/permission";
 import { FirePagging } from "./pagging";
 
-defineProps<{ pagging: FirePagging; checkPerm: string }>();
+defineProps<{ pagging: FirePagging; checkPerm: string; isRecycle: boolean }>();
 </script>

+ 25 - 8
src/app/fire/view/dispatch/pagging.ts

@@ -14,18 +14,27 @@ export const useFirePagging = () => {
   const isTeached = computed(
     () => router.currentRoute.value.name === FireRouteName.teaching
   );
+  const isRecycle = computed(
+    () => router.currentRoute.value.name === FireRouteName.recycle
+  );
 
   const pagging = usePagging({
     get: getFirePagging,
-    del: delFire,
+    del: async (raw) => {
+      await delFire(raw, isRecycle.value ? 1 : 2);
+    },
     mapper: {
-      delMsg: "删除火调项目,相关档案也会一并删除,确定要删除吗?",
+      delMsg: () =>
+        isRecycle.value
+          ? "删除后将无法恢复,确定要删除吗?"
+          : "删除火调项目,相关档案也会一并删除,确定要删除吗?",
     },
     paramsTemlate: {
       projectSn: "",
       projectName: "",
       projectAddress: "",
       deptId: "",
+      isDelete: 0,
       accidentDate: "",
       status: FireStatus.all,
       projectSiteCode: "",
@@ -35,23 +44,31 @@ export const useFirePagging = () => {
   });
 
   watch(
-    () => [pagging.state.query.queryType, isTeached.value],
+    () => [pagging.state.query.queryType, isTeached.value, isRecycle.value],
     () => {
-      pagging.state.query.queryType = isTeached.value
-        ? FirePaggingRoute.teached
-        : FirePaggingRoute.fire;
+      console.log(isRecycle.value);
+      if (isRecycle.value) {
+        pagging.state.query.queryType = FirePaggingRoute.fire;
+        pagging.state.query.isDelete = 2;
+      } else {
+        pagging.state.query.isDelete = 0;
+        pagging.state.query.queryType = isTeached.value
+          ? FirePaggingRoute.teached
+          : FirePaggingRoute.fire;
+      }
     },
-    { flush: "sync", immediate: true }
+    { flush: "post", immediate: true }
   );
 
   const params = [{ ...pagging.state.query }, { ...pagging.state.query }];
   watchEffect(
     () => {
       pagging.state.query = params[isTeached.value ? 0 : 1];
+      // pagging.state.query = params[isRecycle.value ? 0 : 1];
     },
     { flush: "sync" }
   );
 
-  return { pagging, isTeached };
+  return { pagging, isTeached, isRecycle };
 };
 export type FirePagging = ReturnType<typeof useFirePagging>["pagging"];

+ 12 - 13
src/components/head/index.vue

@@ -1,16 +1,11 @@
 <template>
   <div class="head-layer">
-    <el-tabs
-      :modelValue="modelValue"
-      @update:modelValue="(str: any) => updateModelValue(str)"
-    >
-      <el-tab-pane
-        v-for="item in options"
-        :key="item.value"
-        :label="item.name"
-        :name="item.value"
-      >
+    <el-tabs :modelValue="modelValue" @update:modelValue="(str: any) => updateModelValue(str)">
+
+      <el-tab-pane v-for="item in options" :key="item.value" :label="item.name"
+        :name="item.value">
       </el-tab-pane>
+
     </el-tabs>
     <div class="head-content-layer" :class="{ show: show }" v-if="!notContent">
       <div class="head-content">
@@ -19,11 +14,15 @@
       <div class="display" @click="show = !show" v-if="showCtrl">
         <template v-if="show">
           <span>收起</span>
-          <el-icon><ArrowUp /></el-icon>
+          <el-icon>
+            <ArrowUp />
+          </el-icon>
         </template>
         <template v-else>
           <span>展开</span>
-          <el-icon><ArrowDown /></el-icon>
+          <el-icon>
+            <ArrowDown />
+          </el-icon>
         </template>
       </div>
     </div>
@@ -51,7 +50,6 @@ const activeValue = ref(props.modelValue || props.options[0].value);
 const emit = defineEmits<{ (e: "update:modelValue", modelValue: string): void }>();
 const updateModelValue = (modelValue: string) => {
   if ("modelValue" in props) {
-    console.log(modelValue, props);
     emit("update:modelValue", modelValue);
   } else {
     activeValue.value = modelValue;
@@ -74,6 +72,7 @@ const show = ref(true);
     overflow: hidden;
     height: 42px;
   }
+  
 
   &.show {
     padding-bottom: 16px;

+ 5 - 2
src/constant/caseFile.ts

@@ -1,13 +1,16 @@
 import { BoardType } from "@/store/caseFile";
 
 export const FileDrawType = 1;
-export const DrawFormats = [".jpg", ".jpeg", ".png"];
+export const DrawFormats = [".jpg", ".jpeg", ".png", ".raw", ".dcm"];
+export const photoFormats = [".jpg", ".jpeg", ".png"];
 export const OtherFormats = [".pdf", ".jpeg", ".doc", ".docx", ".jpg", ".png"];
 export const BoardTypeDesc = {
   [BoardType.scene]: "现场图",
   [BoardType.map]: "方位图",
 };
-export const DrawFormatDesc = "jpg、png等格式的文件";
+export const DrawFormatDesc = "jpg、png、raw、dcm等格式的文件";
 export const OtherFormatDesc = "pdf、word、jpg、png等格式的文件";
+export const photoFormatDesc = "jpg、jpeg、png等格式的文件";
 
 export const maxFileSize = 100 * 1024 * 1024;
+export const maxAppFileSize = 300 * 1024 * 1024;

+ 274 - 0
src/core/Scene copy.js

@@ -0,0 +1,274 @@
+import * as THREE from "three";
+import Stats from "three/examples/jsm/libs/stats.module.js";
+import Player from "./player/Player.js";
+import BoxManager from "./box/BoxManager.js";
+import { Mitt } from "./mitt.js";
+import testData from "./save.json";
+import { dataURItoBlob, saveFile } from "./utils/utils.js";
+const stats = new Stats();
+
+function sleep(ms) {
+  return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export default class Scene extends Mitt {
+  constructor(domElement) {
+    super();
+    this.domElement = domElement;
+    this.scene = null;
+    this.renderer = null;
+    this.orthCamera = null;
+    this.player = null;
+    this.sceneType = 1;
+    this.width = 0;
+    this.height = 0;
+    this.defaultZoom = 250;
+    this.initCamPView = new THREE.Vector3();
+    this.initCamRView = new THREE.Vector3();
+    this.blobScreens = [];
+    this.inited = false;
+
+    this.init = () => {
+      this.scene = new THREE.Scene();
+      this.scene.background = new THREE.Color(0xf0f2f5);
+      this.renderer = new THREE.WebGLRenderer({
+        canvas: this.domElement,
+        antialias: true,
+        autoClear: true,
+        preserveDrawingBuffer: true,
+      });
+
+      this.width = this.domElement.clientWidth;
+      this.height = this.domElement.clientHeight;
+      this.renderRes = window.devicePixelRatio;
+
+      this.renderer.setSize(this.width, this.height);
+      this.renderer.setPixelRatio(this.renderRes);
+      console.log(this.width, this.height, this.renderRes);
+
+      this.orthCamera = new THREE.OrthographicCamera(
+        -this.width / 2,
+        this.width / 2,
+        this.height / 2,
+        -this.height / 2,
+        0.1,
+        1000
+      );
+      this.orthCamera.zoom = this.defaultZoom;
+
+      this.orthCamera.position.set(0, 10, 0);
+      this.orthCamera.lookAt(0, 0, 0);
+      // this.orthCamera.setViewOffset(this.width, this.height, 0, 0);
+      this.orthCamera.updateProjectionMatrix();
+
+      //player
+      this.player = new Player(this);
+
+      //stats
+      domElement.parentNode.appendChild(stats.dom);
+      stats.dom.style.pointerEvents = "none";
+      stats.dom.style.left = "15%";
+      stats.dom.style.display = "none";
+
+      this.onBindEvent();
+
+      this.inited = true;
+      this.load();
+      this.animate();
+    };
+  }
+
+  load = (list, type, data) => {
+    if (!list) return;
+    // console.log("scene: ", list, type, data);
+    //axesHeloer
+    this.clearScene();
+    this.sceneType = type;
+    // const axesHelper = new THREE.AxesHelper(1);
+    // this.scene.add(axesHelper);
+    this.boxManager = new BoxManager(this);
+    this.boxManager.load(list, type);
+    //light
+    this.loadLight();
+    this.player.load(type, data || []);
+    this.initCamPView.copy(this.orthCamera.position);
+    this.initCamRView.copy(this.orthCamera.rotation);
+  };
+
+  clearScene() {
+    for (var i = this.scene.children.length - 1; i >= 0; i--) {
+      let obj = this.scene.children[i];
+      this.scene.remove(obj);
+    }
+  }
+  clearDrawScene() {
+    for (var i = this.scene.children.length - 1; i >= 0; i--) {
+      let obj = this.scene.children[i];
+      if (
+        String(obj.name).includes("marker_") ||
+        String(obj.name).includes("line_") ||
+        String(obj.name).includes("line_point_") ||
+        String(obj.name).includes("circle_") ||
+        String(obj.name).includes("pureText_")
+      ) {
+        this.scene.remove(obj);
+      }
+    }
+  }
+
+  deleteItemById(uuid, type) {
+    for (var i = this.scene.children.length - 1; i >= 0; i--) {
+      let obj = this.scene.children[i];
+      if (obj.uuid === uuid) {
+        console.log("deleteItemById-userData", obj.userData);
+        this.player.deleteItemByType(type, obj.userData);
+        this.scene.remove(obj);
+      }
+    }
+  }
+  deleteImageDataByIds(ids) {
+    this.player.deleteImageDataByIds(ids);
+  }
+  loadLight = () => {
+    const light = new THREE.AmbientLight(0xffffff, 1.5); // 柔和的白光
+    this.scene.add(light);
+  };
+
+  setCamera = () => {};
+
+  toHorizontal = () => {};
+
+  toVertical = () => {};
+
+  lockView(open) {
+    if (!open) {
+      this.player.floorplanControls.enablePan = true;
+      this.player.floorplanControls.enableZoom = true;
+    } else {
+      this.player.floorplanControls.enablePan = false;
+      this.player.floorplanControls.enableZoom = false;
+    }
+  }
+  setMode(mode) {
+    this.player.setMode(mode);
+  }
+  onResize = (width, height) => {
+    this.width = width !== undefined ? width : this.domElement.clientWidth;
+    this.height = height !== undefined ? height : this.domElement.clientHeight;
+    console.log("resize", this.width, this.height);
+
+    (this.orthCamera.left = -this.width / 2),
+      (this.orthCamera.right = this.width / 2),
+      (this.orthCamera.bottom = -this.height / 2),
+      (this.orthCamera.top = this.height / 2),
+      this.orthCamera.updateProjectionMatrix();
+
+    this.renderer.setSize(this.width, this.height);
+  };
+  render = () => {
+    if (this.player) {
+      this.player.update();
+      this.renderer.render(this.scene, this.orthCamera);
+    }
+  };
+  animate = () => {
+    stats.begin();
+    this.render();
+    stats.end();
+    requestAnimationFrame(this.animate);
+  };
+
+  resetCameraView() {
+    this.orthCamera.zoom = this.defaultZoom;
+    // this.orthCamera.position.set(0, 0, 0);
+    // this.orthCamera.rotation.set(0, 10, 0);
+    // this.orthCamera.updateMatrixWorld();
+  }
+
+  editing(item) {
+    this.player.editing(item);
+  }
+  endScreenshot() {
+    this.lockView(false);
+    this.blobScreens = [];
+    this.player.floorplanControls.reset();
+    this.onResize();
+    this.renderer.setSize(this.width, this.height);
+  }
+  screenshot(x, zoom) {
+    var imgData, imgNode;
+    const times = 4;
+    this.orthCamera.zoom = zoom || this.defaultZoom;
+    this.scene.position.x = x || 0;
+
+    this.renderer.setSize(this.width * times, this.height * times);
+    this.orthCamera.aspect = this.width / this.height;
+
+    // this.player.floorplanControls.minZoom = 1;
+    // this.orthCamera.zoom = 50;
+    this.orthCamera.updateProjectionMatrix();
+    this.renderer.render(this.scene, this.orthCamera, null, false);
+
+    const dataURL = this.renderer.domElement.toDataURL("image/jpeg");
+    this.blobScreens.push(dataURItoBlob(dataURL));
+    // saveFile(dataURL, `${index}.jpg`);
+    this.onResize(this.width, this.height);
+  }
+
+  exportScreenshot() {
+    if (window.devicePixelRatio !== 1) {
+      this.emit("devicePixelRatio");
+      return;
+    }
+    this.player.floorplanControls.reset();
+    this.lockView(true);
+    this.setMode(0);
+    // await sleep(500);
+    this.orthCamera.zoom = this.defaultZoom;
+    const object = this.boxManager.model;
+    const total = object.children.length;
+    if (total === 0) {
+      return;
+      console.error("没数据");
+    }
+
+    object.updateMatrixWorld();
+    this.orthCamera.updateProjectionMatrix();
+    const boundingBox = new THREE.Box3().setFromObject(object);
+    // 计算宽度、高度和深度
+    const width = boundingBox.max.x - boundingBox.min.x;
+    const one = width / total;
+    let slides = Math.floor(total / 3);
+    // debugger;
+    console.log("slides", slides);
+    if (slides >= 1) {
+      for (var i = 0; i <= slides; i++) {
+        (function (index, that) {
+          setTimeout(function () {
+            const offset = -(one * 3 * index);
+            console.log("Iteration:", offset);
+            that.screenshot(offset);
+            console.log(`Width: ${offset}`);
+            if (index === slides) {
+              console.log("last");
+              that.scene.position.x = 0;
+              that.emit("submitScreenshot");
+            }
+          }, index * 500);
+        })(i, this); // 传递当前迭代的索引i给setTimeout的回调函数
+      }
+    } else {
+      // 只有一个或0个
+      if (total >= 1 && total <= 3) {
+        console.log("total", total);
+        this.player.floorplanControls.reset();
+        this.screenshot(-0.3, 227);
+        this.emit("submitScreenshot");
+      }
+    }
+  }
+
+  onBindEvent = () => {
+    window.addEventListener("resize", this.onResize, false);
+  };
+}

+ 347 - 0
src/core/Scene.js

@@ -0,0 +1,347 @@
+import * as THREE from "three";
+import Stats from "three/examples/jsm/libs/stats.module.js";
+import Player from "./player/Player.js";
+import BoxManager from "./box/BoxManager.js";
+import { Mitt } from "./mitt.js";
+import testData from "./save.json";
+import { dataURItoBlob, saveFile } from "./utils/utils.js";
+import { screenshotObject } from "./utils/cap.js";
+
+const stats = new Stats();
+
+function sleep(ms) {
+  return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export default class Scene extends Mitt {
+  constructor(domElement) {
+    super();
+    this.domElement = domElement;
+    this.scene = null;
+    this.renderer = null;
+    this.orthCamera = null;
+    this.camera = null;
+    this.player = null;
+    this.sceneType = 1;
+    this.width = 0;
+    this.height = 0;
+    this.defaultZoom = 250;
+    this.defaultUseZoom = 250 / window.devicePixelRatio;
+    this.initCamPView = new THREE.Vector3();
+    this.initCamRView = new THREE.Vector3();
+    this.blobScreens = [];
+    this.inited = false;
+
+    this.init = () => {
+      this.scene = new THREE.Scene();
+      this.scene.background = new THREE.Color(0xf0f2f5);
+      this.renderer = new THREE.WebGLRenderer({
+        canvas: this.domElement,
+        antialias: true,
+        autoClear: true,
+        preserveDrawingBuffer: true,
+      });
+
+      this.width = this.domElement.clientWidth;
+      this.height = this.domElement.clientHeight;
+      this.renderRes = window.devicePixelRatio;
+      this.defaultUseZoom = this.defaultZoom / window.devicePixelRatio;
+      this.renderer.setSize(this.width, this.height);
+      this.renderer.setPixelRatio(this.renderRes);
+
+      console.log(
+        "init",
+        this.width,
+        this.height,
+        this.renderRes,
+        this.defaultZoom
+      );
+
+      this.camera = new THREE.PerspectiveCamera(
+        70,
+        this.domElement.clientWidth / this.domElement.clientHeight,
+        0.1,
+        1000
+      );
+
+      this.orthCamera = new THREE.OrthographicCamera(
+        -this.width / 2,
+        this.width / 2,
+        this.height / 2,
+        -this.height / 2,
+        0.1,
+        1000
+      );
+      this.orthCamera.zoom = this.defaultUseZoom;
+      // 影响画线
+      this.orthCamera.position.set(0, 10, 0);
+      this.orthCamera.lookAt(0, 0, 0);
+      // this.orthCamera.setViewOffset(this.width, this.height, 0, 0);
+      this.orthCamera.updateProjectionMatrix();
+      //player
+      this.player = new Player(this);
+
+      domElement.parentNode.appendChild(stats.dom);
+      stats.dom.style.pointerEvents = "none";
+      stats.dom.style.left = "15%";
+      stats.dom.style.display = "none";
+
+      this.onBindEvent();
+
+      this.inited = true;
+      this.load();
+      this.animate();
+    };
+  }
+
+  load = (list, type, data) => {
+    if (!list) return;
+    // console.log("scene: ", list, type, data);
+    //axesHeloer
+    this.clearScene();
+    this.sceneType = type;
+    // const axesHelper = new THREE.AxesHelper(1);
+    // this.scene.add(axesHelper);
+    this.boxManager = new BoxManager(this);
+    this.boxManager.load(list, type);
+    //light
+    this.loadLight();
+    this.player.load(type, data || []);
+    this.initCamPView.copy(this.orthCamera.position);
+    this.initCamRView.copy(this.orthCamera.rotation);
+  };
+
+  clearScene() {
+    for (var i = this.scene.children.length - 1; i >= 0; i--) {
+      let obj = this.scene.children[i];
+      this.scene.remove(obj);
+    }
+  }
+  clearDrawScene() {
+    for (var i = this.scene.children.length - 1; i >= 0; i--) {
+      let obj = this.scene.children[i];
+      if (
+        String(obj.name).includes("marker_") ||
+        String(obj.name).includes("line_") ||
+        String(obj.name).includes("line_point_") ||
+        String(obj.name).includes("circle_") ||
+        String(obj.name).includes("pureText_")
+      ) {
+        this.scene.remove(obj);
+      }
+    }
+  }
+
+  deleteItemById(uuid, type) {
+    for (var i = this.scene.children.length - 1; i >= 0; i--) {
+      let obj = this.scene.children[i];
+      if (obj.uuid === uuid) {
+        console.log("deleteItemById-userData", obj.userData);
+        this.player.deleteItemByType(type, obj.userData);
+        this.scene.remove(obj);
+      }
+    }
+  }
+  deleteImageDataByIds(ids) {
+    this.player.deleteImageDataByIds(ids);
+  }
+  loadLight = () => {
+    const light = new THREE.AmbientLight(0xffffff, 1.5); // 柔和的白光
+    this.scene.add(light);
+  };
+
+  setCamera = () => {
+    // const object = this.boxManager.model;
+    // const boundingBox = new THREE.Box3().setFromObject(object);
+    // let size = boundingBox.getSize(new THREE.Vector3());
+    // const fov = this.camera.fov * (Math.PI / 180);
+    // const fovh = 2 * Math.atan(Math.tan(fov / 2) * this.camera.aspect);
+    // let dx = Math.abs(size.x / 2 / Math.tan(fovh / 2));
+    // let dz = Math.abs(size.z / 2 / Math.tan(fov / 2));
+    // this.orthCamera.zoom = this.floorplanZoom;
+  };
+
+  toHorizontal = () => {};
+
+  toVertical = () => {};
+
+  lockView(open) {
+    if (!open) {
+      this.player.floorplanControls.enablePan = true;
+      this.player.floorplanControls.enableZoom = true;
+    } else {
+      this.player.floorplanControls.enablePan = false;
+      this.player.floorplanControls.enableZoom = false;
+    }
+  }
+  setMode(mode) {
+    this.player.setMode(mode);
+  }
+  onResize = (rWidth, rHeight) => {
+    this.width =
+      typeof rWidth === "number" ? rWidth : this.domElement.clientWidth;
+    this.height =
+      typeof rHeight === "height" ? rHeight : this.domElement.clientHeight;
+
+    const container = document.querySelector("#canvas");
+    const { width, height } = container.getBoundingClientRect();
+
+    if (width !== this.domElement.clientWidth) {
+      this.width = width;
+    }
+    if (height !== this.domElement.clientHeight) {
+      this.height = height;
+    }
+
+    console.warn("onResize", this.width, this.height);
+
+    (this.orthCamera.left = -this.width / 2),
+      (this.orthCamera.right = this.width / 2),
+      (this.orthCamera.bottom = -this.height / 2),
+      (this.orthCamera.top = this.height / 2),
+      this.orthCamera.updateProjectionMatrix();
+
+    this.renderer.setSize(this.width, this.height);
+    this.renderRes = window.devicePixelRatio;
+    this.defaultUseZoom = this.defaultZoom / this.renderRes;
+    this.renderer.setPixelRatio(this.renderRes);
+  };
+  render = () => {
+    if (this.player) {
+      this.player.update();
+      this.renderer.render(this.scene, this.orthCamera);
+    }
+  };
+  animate = () => {
+    stats.begin();
+    this.render();
+    stats.end();
+    requestAnimationFrame(this.animate);
+  };
+
+  resetCameraView() {
+    this.orthCamera.zoom = this.defaultUseZoom;
+    // this.orthCamera.position.set(0, 0, 0);
+    // this.orthCamera.rotation.set(0, 10, 0);
+    this.orthCamera.updateMatrixWorld();
+  }
+
+  editing(item) {
+    this.player.editing(item);
+  }
+  endScreenshot() {
+    this.lockView(false);
+    this.blobScreens = [];
+    this.scene.position.x = 0;
+    this.player.floorplanControls.reset();
+    this.onResize(this.width, this.height);
+  }
+  screenshot(x, zoom, index) {
+    var imgData, imgNode;
+    const times = 4;
+    this.orthCamera.zoom = zoom || this.defaultUseZoom;
+    this.scene.position.x = x || 0;
+
+    this.renderer.setSize(this.width * times, this.height * times);
+    this.renderer.setPixelRatio(this.renderRes);
+    this.orthCamera.aspect = this.width / this.height;
+
+    this.orthCamera.updateProjectionMatrix();
+    this.renderer.render(this.scene, this.orthCamera, null, false);
+
+    const dataURL = this.renderer.domElement.toDataURL("image/jpeg");
+    this.blobScreens.push(dataURItoBlob(dataURL));
+    console.log(this.width, this.height);
+    if (typeof index === "number") {
+      saveFile(dataURL, `${index}.jpg`);
+    }
+
+    this.renderer.setSize(this.width, this.height);
+  }
+
+  exportScreenshot(isSaveJpg = true) {
+    if (window.devicePixelRatio !== 1) {
+      this.emit("devicePixelRatio");
+      return;
+    }
+
+    this.player.floorplanControls.reset();
+    this.onResize();
+    this.lockView(true);
+    this.setMode(0);
+    // await sleep(500);
+
+    const object = this.boxManager.model;
+    const total = object.children.length;
+    if (total === 0) {
+      return;
+      console.error("没数据");
+    }
+
+    object.updateMatrixWorld();
+    this.orthCamera.updateProjectionMatrix();
+    const boundingBox = new THREE.Box3().setFromObject(object);
+    // 计算宽度、高度和深度
+    const width = boundingBox.max.x - boundingBox.min.x;
+    const one = width / total;
+    let slides = Math.floor(total / 3);
+
+    console.log("slides", slides);
+
+    if (slides >= 1 && total > 3) {
+      for (var i = 0; i <= slides; i++) {
+        (function (index, that) {
+          setTimeout(function () {
+            const offset = -(one * 3 * index);
+            console.log("Iteration:", one, index, offset, that.defaultUseZoom);
+            that.screenshot(offset, that.defaultUseZoom);
+            console.log(`Width: ${offset}`);
+            if (index === slides) {
+              console.log("last");
+              that.emit("submitScreenshot", true);
+            }
+          }, index * 500);
+        })(i, this); // 传递当前迭代的索引i给setTimeout的回调函数
+      }
+    } else {
+      // 只有一个
+      if (total >= 1) {
+        console.log("total", total);
+        this.player.floorplanControls.reset();
+        this.screenshot(-0.3, 227);
+        this.emit("submitScreenshot", true);
+      }
+    }
+  }
+  test() {
+    const object = this.boxManager.model;
+    const boundingBox = new THREE.Box3().setFromObject(object);
+    this.lockView(true);
+    // let size = new THREE.Vector3();
+    // boundingBox.getSize(size);
+    const width = boundingBox.max.x - boundingBox.min.x;
+    const height = boundingBox.max.y - boundingBox.min.y;
+    const z = boundingBox.max.z - boundingBox.min.z;
+    const radio = width / height;
+    // const dynamicWidth = this.height * radio;
+
+    // (this.orthCamera.left = -dynamicWidth / 2),
+    //   (this.orthCamera.right = dynamicWidth / 2),
+    //   (this.orthCamera.bottom = -this.height / 2),
+    //   (this.orthCamera.top = this.height / 2),
+    //   this.orthCamera.updateProjectionMatrix();
+
+    // this.renderer.setSize(dynamicWidth, this.height);
+    // this.renderRes = window.devicePixelRatio;
+    // this.defaultUseZoom = this.defaultZoom / this.renderRes;
+    // this.renderer.setPixelRatio(this.renderRes);
+
+    // console.log("dynamicWidth", dynamicWidth);
+
+    // console.log("height", height);
+  }
+
+  onBindEvent = () => {
+    window.addEventListener("resize", this.onResize, false);
+  };
+}

+ 95 - 0
src/core/box/BoxManager.js

@@ -0,0 +1,95 @@
+import * as THREE from "three";
+import HorizontalBox from "./HorizontalBox";
+import VerticalBox from "./VerticalBox";
+import SimpleLabel from "./object/SimpleLabel";
+
+// import { Line2 } from "three/examples/jsm/lines/Line2.js";
+// import { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js";
+// import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
+
+export default class BoxManager {
+  constructor(scene) {
+    this.scene = scene;
+    this.loadingManager = new THREE.LoadingManager();
+    this.loader = new THREE.TextureLoader(this.loadingManager);
+    this.model = new THREE.Group();
+    this.model.name = "boxManager";
+    this.maps = {};
+    this.obb = new THREE.Box3();
+    this.imgList = [];
+    this.opacity = 1;
+
+    this.onBindEvent();
+  }
+
+  load = (list, type) => {
+    console.log("this.model.name", this.model.name);
+    const total = list.length;
+    list.forEach((item, index) => {
+      if (type === 1) {
+        //横排
+        console.log("横排");
+        const box = new HorizontalBox(this, item, index, total);
+        this.model.add(box);
+      }
+      if (type === 2) {
+        //竖排
+        const box = new VerticalBox(this, item, index, total);
+        // console.log("竖排");
+        this.model.add(box);
+      }
+    });
+    // this.model.position.y += 0.3;
+    // this.model.visible =false;
+    this.scene.scene.add(this.model);
+    this.setArea();
+    // console.log("this.scene.scene", this.scene.scene);
+  };
+
+  onBindEvent = () => {
+    const _this = this;
+    this.loadingManager.onStart = (url, itemsLoaded, itemsTotal) => {
+      // console.log( 'Started loading file: ' + url + '.\nLoaded ' + itemsLoaded + ' of ' + itemsTotal + ' files.' );
+      console.log("loading_manager: loading...");
+ 
+    };
+    this.loadingManager.onLoad = () => {
+      console.log("loading_manager: loading complete!");
+      this.scene.emit("loaded");
+
+    };
+    this.loadingManager.onProgress = function (url, itemsLoaded, itemsTotal) {
+      // console.log( 'Loading file: ' + url + '.\nLoaded ' + itemsLoaded + ' of ' + itemsTotal + ' files.' );
+    };
+    this.loadingManager.onError = function (url) {
+      console.error("loading_manager: error loading " + url);
+    };
+  };
+  setArea() {
+    const object = this.model;
+    const boundingBox = new THREE.Box3().setFromObject(object);
+    // boundingBox.expandByScalar(1.3);
+    const size = boundingBox.getSize(new THREE.Vector3());
+    this.obb = boundingBox;
+    console.log("boundingBox-size", size);
+    // const topLine = [
+    //   -size.x,
+    //   0,
+    //   (size.z + 0.3) / -2,
+    //   size.x + 0.1,
+    //   0,
+    //   (size.z + 0.3) / -2,
+    // ];
+
+    // helper.scale.set(1.2, 1.2, 1.2);
+    // helper.update();
+  }
+  setVisible = (val) => {
+    if (!this.model) return;
+    this.model.visible = val;
+  };
+
+  setOpacity = (val) => {
+    this.material.opacity = val;
+  };
+}

+ 119 - 0
src/core/box/HorizontalBox.js

@@ -0,0 +1,119 @@
+import * as THREE from "three";
+import TextLabel from "./object/TextLabel";
+import SimpleLabel from "./object/SimpleLabel";
+import ImgLabel from "./object/ImgLabel";
+import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
+
+export default class HorizontalBox extends THREE.Group {
+  constructor(manager, data, index, total) {
+    super();
+    this.manager = manager;
+    this.name = "horizontal_box";
+    this.total = total;
+    this.getStyle();
+    this.load(data, index);
+  }
+  getStyle() {
+    this.width = 2;
+    this.height = (2 * 710) / 500;
+    this.color = 0xffffff;
+  }
+  cover(texture, aspect) {
+    var imageAspect = texture.image.width / texture.image.height;
+
+    if (aspect < imageAspect) {
+      texture.matrix.setUvTransform(0, 0, aspect / imageAspect, 1, 0, 0.5, 0.5);
+    } else {
+      texture.matrix.setUvTransform(0, 0, 1, imageAspect / aspect, 0, 0.5, 0.5);
+    }
+  }
+  load(data, index) {
+    //box
+    const geometry = new THREE.PlaneGeometry(1, 1);
+    geometry.rotateX(-Math.PI / 2);
+
+    const bm = new THREE.MeshBasicMaterial({
+      color: this.color,
+    });
+
+    const box = new THREE.Mesh(geometry, bm);
+    box.scale.set(this.width, 1, this.height);
+
+    this.add(box);
+    this.position.x = (this.width + 0.125) * index - 1.8;
+
+    const matLine = new LineMaterial({
+      color: 0xe44d54,
+      linewidth: 4, // in world units with size attenuation, pixels otherwise
+      dashed: false,
+      alphaToCoverage: true,
+    });
+    matLine.resolution = new THREE.Vector2(
+      this.manager.scene.width,
+      this.manager.scene.height
+    );
+    //content
+    data.forEach((i, j) => {
+      //img
+      let img;
+      this.manager.loader.load(i.imgUrl, (texture) => {
+        let imgRatio = texture.image.width / texture.image.height;
+        texture.matrixAutoUpdate = false;
+        let planeRatio = 1.5 / 0.85;
+        // let ratio = planeRatio / imgRatio;
+        texture.matrixAutoUpdate = false;
+        if (planeRatio < imgRatio) {
+          texture.matrix.setUvTransform(
+            0,
+            0,
+            planeRatio / imgRatio,
+            1,
+            0,
+            0.5,
+            0.5
+          );
+        } else {
+          texture.matrix.setUvTransform(
+            0,
+            0,
+            1,
+            imgRatio / planeRatio,
+            0,
+            0.5,
+            0.5
+          );
+        }
+        // texture.wrapS = THREE.RepeatWrapping;
+        // texture.wrapS = THREE.RepeatWrapping;
+        // texture.wrapT = THREE.ClampToEdgeWrapping;
+        // texture.repeat.x = ratio;
+        // texture.offset.x = 0.5 * (1 - ratio);
+        // console.log("texture", texture);
+        texture.colorSpace = THREE.SRGBColorSpace;
+
+        img = new ImgLabel(texture, matLine);
+
+        img.userData = i.id;
+        img.position.y += 1;
+        if (j === 0) {
+          img.position.z -= 0.8;
+        } else {
+          img.position.z += 0.43;
+        }
+        this.add(img);
+        this.manager.imgList.push(img);
+        const textlabel = new TextLabel(i.imgInfo, true);
+        this.add(textlabel);
+        textlabel.position.copy(img.position);
+        textlabel.position.z += textlabel.scale.z * 0.5 + 0.1;
+      });
+    });
+    //页脚
+    const f_txt_label = ` 第 ${index + 1} 页  共 ${this.total} 页`;
+    const footlabel = new SimpleLabel(f_txt_label, true);
+    footlabel.renderOrder = 100;
+    footlabel.position.z += 1.26;
+
+    this.add(footlabel);
+  }
+}

+ 107 - 0
src/core/box/VerticalBox.js

@@ -0,0 +1,107 @@
+import * as THREE from "three";
+import TextLabel from "./object/TextLabel";
+import ImgLabel from "./object/ImgLabel";
+import ImgLabelBox from "./object/ImgLabelBox";
+import SimpleLabel from "./object/SimpleLabel";
+import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
+
+export default class VerticalBox extends THREE.Group {
+  constructor(manager, data, index, total) {
+    super();
+    this.manager = manager;
+    this.total = total;
+    this.name = "vertical_box";
+    this.getStyle();
+    this.load(data, index);
+  }
+  getStyle() {
+    this.width = 2;
+    this.height = (2 * 710) / 500;
+    this.color = 0xffffff;
+  }
+
+  load(data, index) {
+    //box
+    const geometry = new THREE.PlaneGeometry(1, 1);
+    geometry.rotateX(-Math.PI / 2);
+
+    const bm = new THREE.MeshBasicMaterial({
+      color: this.color,
+    });
+
+    const box = new THREE.Mesh(geometry, bm);
+    box.scale.set(this.width, 1, this.height);
+
+    this.add(box);
+    this.position.x = (this.width + 0.125) * index - 1.8;
+
+    const matLine = new LineMaterial({
+      color: 0xe44d54,
+      linewidth: 4, // in world units with size attenuation, pixels otherwise
+      dashed: false,
+      alphaToCoverage: true,
+    });
+    matLine.resolution = new THREE.Vector2(
+      this.manager.scene.width,
+      this.manager.scene.height
+    );
+    //content
+    data.forEach((i, j) => {
+      //img
+      let img;
+      this.manager.loader.load(i.imgUrl, (texture) => {
+        let imgRatio = texture.image.width / texture.image.height;
+        let planeRatio = 1.5 / 2;
+        texture.matrixAutoUpdate = false;
+        //放开所有uv的比例
+        if (planeRatio > imgRatio) {
+          // texture.matrix.setUvTransform(
+          //   0,
+          //   0,
+          //   planeRatio / imgRatio,
+          //   1,
+          //   0,
+          //   0.5,
+          //   0.5
+          // );
+        } else {
+          // debugger
+          // texture.matrix.setUvTransform(
+          //   0,
+          //   0,
+          //   1,
+          //   imgRatio / planeRatio,
+          //   0,
+          //   0.5,
+          //   0.5
+          // );
+        }
+
+        texture.colorSpace = THREE.SRGBColorSpace;
+
+        // texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
+        // texture.repeat.set(2, 2); // 纹理平铺两次
+        // texture.offset.set(0.5, 0.5); // 纹理偏移到中心
+        img = new ImgLabelBox(texture, matLine, false);
+
+        img.userData = i.id;
+        img.position.y += 1;
+        img.position.z -= 0.2;
+        this.add(img);
+        this.manager.imgList.push(img);
+        const textlabel = new TextLabel(i.imgInfo, true);
+        this.add(textlabel);
+        textlabel.position.copy(img.position);
+        textlabel.position.z += textlabel.scale.z * 0.5 + 0.7;
+      });
+    });
+
+    //页脚
+    const f_txt_label = ` 第 ${index + 1} 页  共 ${this.total} 页`;
+    const footlabel = new SimpleLabel(f_txt_label, true);
+    footlabel.renderOrder = 100;
+    footlabel.position.z += 1.26;
+
+    this.add(footlabel);
+  }
+}

+ 66 - 0
src/core/box/object/CircleTextLabel.js

@@ -0,0 +1,66 @@
+import * as THREE from "three";
+import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
+import { LineSegments2 } from "three/examples/jsm/lines/LineSegments2.js";
+import { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js";
+
+export default class CircleTextLabel extends THREE.Mesh {
+  constructor(text, pos) {
+    let res = 5;
+    let point = new THREE.Vector3().copy(pos);
+    const canvas = document.createElement("canvas");
+    canvas.width = 128 * res;
+    canvas.height = 128 * res;
+    let fontSize = 68 * res;
+    const ctx = canvas.getContext("2d");
+    ctx.font = `800 ${fontSize}px Arial`; // 设置字体大小和类型
+    ctx.textAlign = "center";
+    ctx.textBaseline = "middle";
+    ctx.fillStyle = "#e44d54"; // 设置文字颜色和透明度
+    ctx.fillText(text, canvas.width / 2, canvas.height / 2);
+
+    // 步骤3: 将画布转换为纹理
+    const texture = new THREE.CanvasTexture(canvas);
+
+    // 步骤4: 创建材质并应用纹理
+    const m = new THREE.MeshBasicMaterial({
+      map: texture,
+      transparent: true, // 允许材质透明
+    });
+
+    // const canvas_map = new THREE.Texture(canvas);
+    texture.colorSpace = THREE.SRGBColorSpace;
+    texture.needsUpdate = true;
+    texture.anisotropy = 4;
+
+    const g = new THREE.CircleGeometry(0.08, 128);
+    g.rotateX(-Math.PI / 2);
+
+    super(g, m);
+
+    this.userData = text;
+
+    const edges = new THREE.EdgesGeometry(g);
+
+    const geometry = new LineGeometry();
+    geometry.fromEdgesGeometry(edges);
+
+    const line_m = new LineMaterial({
+      color: 0xe44d54,
+      linewidth: 5,
+      dashed: false,
+      dashScale : 0.1,
+      alphaToCoverage: false,
+    });
+
+    line_m.resolution.set(window.innerWidth, window.innerHeight);
+
+    const line_n = new LineSegments2(geometry, line_m);
+
+    line_n.position.y += 0.5;
+
+    this.add(line_n);
+    this.userData = point.toArray();
+    this.position.copy(point);
+    this.name = "circle_" + text;
+  }
+}

+ 47 - 0
src/core/box/object/ImgLabel.js

@@ -0,0 +1,47 @@
+import * as THREE from "three";
+import TouchEdge from "./TouchEdge";
+
+export default class ImgLabel extends THREE.Mesh {
+  constructor(texture, matLine, isHorizontal = true) {
+    let width, height, p;
+    if (isHorizontal) {
+      width = 1.5;
+      height = 0.85;
+      p = [
+        [-0.75, 0, -0.425, 0.75, 0, -0.425],
+        [-0.75, 0, -0.425, -0.75, 0, 0.425],
+        [-0.75, 0, 0.425, 0.75, 0, 0.425],
+        [0.75, 0, 0.425, 0.75, 0, -0.425],
+      ];
+    } else {
+      width = 1.5;
+      height = 2;
+      p = [
+        [-0.75, 0, -1, 0.75, 0, -1],
+        [-0.75, 0, -1, -0.75, 0, 1],
+        [-0.75, 0, 1, 0.75, 0, 1],
+        [0.75, 0, 1, 0.75, 0, -1],
+      ];
+    }
+    const g = new THREE.PlaneGeometry(width, height);
+    g.rotateX(-Math.PI / 2);
+
+    const m = new THREE.MeshBasicMaterial({
+      map: texture,
+      transparent: true,
+    });
+    super(g, m);
+
+  
+
+    this.width = width;
+    this.height = height;
+    this.touchLines = new TouchEdge(p, matLine);
+
+    this.touchLines.position.y += 0.5;
+    this.add(this.touchLines);
+    // this.touchLines.children.forEach((child) => (child.visible = true));
+
+    this.name = "imglabel";
+  }
+}

+ 118 - 0
src/core/box/object/ImgLabelBox.js

@@ -0,0 +1,118 @@
+import * as THREE from "three";
+import { TriangleBlurShader } from "three/addons/shaders/TriangleBlurShader.js";
+import TouchEdge from "./TouchEdge";
+
+function makeTriangleBlurShader(iterations = 10) {
+  // Remove texture, because texture is a reserved word in WebGL 2
+  const { texture, ...uniforms } = TriangleBlurShader.uniforms;
+
+  const TriangleBlurShader2 = {
+    ...TriangleBlurShader,
+
+    name: "TriangleBlurShader2",
+
+    uniforms: {
+      ...uniforms,
+
+      // Replace texture with blurTexture for WebGL 2
+      blurTexture: { value: null },
+    },
+  };
+
+  // Replace texture with blurTexture for WebGL 2
+  TriangleBlurShader2.fragmentShader =
+    TriangleBlurShader2.fragmentShader.replace(
+      "uniform sampler2D texture;",
+      "uniform sampler2D blurTexture;"
+    );
+  TriangleBlurShader2.fragmentShader =
+    TriangleBlurShader2.fragmentShader.replace(
+      "texture2D( texture",
+      "texture2D( blurTexture"
+    );
+
+  // Make iterations configurable.
+  TriangleBlurShader2.fragmentShader =
+    TriangleBlurShader2.fragmentShader.replace(
+      "#define ITERATIONS 10.0",
+      "#define ITERATIONS " + iterations + ".0"
+    );
+
+  console.log("shader:", TriangleBlurShader2.fragmentShader);
+
+  return TriangleBlurShader2;
+}
+
+export default class ImgLabel extends THREE.Mesh {
+  constructor(texture, matLine, isHorizontal = true) {
+    let width, height, p;
+    if (isHorizontal) {
+      width = 1.5;
+      height = 0.85;
+      p = [
+        [-0.75, 0, -0.425, 0.75, 0, -0.425],
+        [-0.75, 0, -0.425, -0.75, 0, 0.425],
+        [-0.75, 0, 0.425, 0.75, 0, 0.425],
+        [0.75, 0, 0.425, 0.75, 0, -0.425],
+      ];
+    } else {
+      width = 1.5;
+      height = 2;
+      p = [
+        [-0.75, 0, -1, 0.75, 0, -1],
+        [-0.75, 0, -1, -0.75, 0, 1],
+        [-0.75, 0, 1, 0.75, 0, 1],
+        [0.75, 0, 1, 0.75, 0, -1],
+      ];
+    }
+    const g = new THREE.PlaneGeometry(width, height);
+    g.rotateX(-Math.PI / 2);
+
+    // const m = new THREE.MeshBasicMaterial({
+    //   map: texture,
+    //   transparent: true,
+    // });
+
+    // const shader = makeTriangleBlurShader(12);
+
+    // const blurMaterial = new THREE.ShaderMaterial({
+    //   vertexShader: shader.vertexShader,
+    //   fragmentShader: shader.fragmentShader,
+    //   uniforms: THREE.UniformsUtils.clone(shader.uniforms),
+    // });
+    // // console.log("blurMaterial", blurMaterial.uniforms);
+    // blurMaterial.uniforms.blurTexture.value = texture;
+    // blurMaterial.uniforms.delta.value = new THREE.Vector2(0.5, 0.9);
+
+    const bg = new THREE.MeshBasicMaterial({
+      color: 0xf2f2f2,
+      transparent: false,
+    });
+
+    super(g, bg);
+
+    let imgRatio = texture.image.width / texture.image.height;
+    const imgHeight = width / imgRatio >= 2 ? 2 : width / imgRatio;
+    const imageG = new THREE.PlaneGeometry(width, imgHeight);
+
+    imageG.rotateX(-Math.PI / 2);
+
+    const im = new THREE.MeshBasicMaterial({
+      map: texture,
+      transparent: true,
+    });
+    const imageMesh = new THREE.Mesh(imageG, im);
+
+    imageMesh.renderOrder = 10;
+    this.add(imageMesh);
+
+    this.width = width;
+    this.height = height;
+    this.touchLines = new TouchEdge(p, matLine);
+
+    this.touchLines.position.y += 0.5;
+    this.add(this.touchLines);
+    // this.touchLines.children.forEach((child) => (child.visible = true));
+    this.name = "imglabel";
+  }
+}

+ 139 - 0
src/core/box/object/Line.js

@@ -0,0 +1,139 @@
+import * as THREE from "three";
+import { Line2 } from "three/examples/jsm/lines/Line2.js";
+import { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js";
+import gotoPic from "@/assets/image/goto.png";
+const offset = 0.25;
+
+function pointsToArray(arr) {
+  let res = [];
+  arr.forEach((i) => {
+    res = res.concat(i.toArray());
+  });
+  return res;
+}
+let m = new THREE.MeshBasicMaterial({
+  map: new THREE.TextureLoader().load(gotoPic),
+  color: 0x26559b,
+  transparent: true,
+});
+export default class Line extends Line2 {
+  constructor(startPoint, endPoint, endEdge, matLine) {
+    let points;
+
+    let g = new THREE.PlaneGeometry(0.1, 0.1);
+    g.rotateX(-Math.PI / 2);
+    let cross = new THREE.Mesh(g, m);
+
+    if (endEdge.name === 0) {
+      // top
+      let a = startPoint.clone();
+      let b = new THREE.Vector3(
+        startPoint.x,
+        startPoint.y,
+        endEdge.y + endEdge.parent.parent.position.z - offset
+      );
+      let c = new THREE.Vector3(
+        endPoint.x,
+        endPoint.y,
+        endEdge.y + endEdge.parent.parent.position.z - offset
+      );
+      let d = new THREE.Vector3(
+        endPoint.x,
+        endPoint.y,
+        endEdge.y + endEdge.parent.parent.position.z
+      );
+      cross.position.copy(d);
+      cross.rotation.y = -Math.PI / 2;
+      cross.position.z -= 0.02;
+
+      points = pointsToArray([a, b, c, d]);
+    } else if (endEdge.name === 1) {
+      //left
+
+      let a = startPoint.clone();
+      let b = new THREE.Vector3(
+        (startPoint.x + endPoint.x) / 2,
+        startPoint.y,
+        startPoint.z
+      );
+      let c = new THREE.Vector3(
+        (startPoint.x + endPoint.x) / 2,
+        startPoint.y,
+        endPoint.z
+      );
+      let d = new THREE.Vector3(
+        endEdge.x + endEdge.parent.parent.parent.position.x,
+        startPoint.y,
+        endPoint.z
+      );
+      cross.position.copy(d);
+
+      const diff = c.x < d.x;
+      cross.rotation.y = diff ? 0 : -Math.PI;
+      diff ? (cross.position.x -= 0.02) : (cross.position.x += 0.02);
+
+      points = pointsToArray([a, b, c, d]);
+    } else if (endEdge.name === 2) {
+      //bottom
+      let a = startPoint.clone();
+      let b = new THREE.Vector3(
+        startPoint.x,
+        startPoint.y,
+        endEdge.y + endEdge.parent.parent.position.z + offset
+      );
+      let c = new THREE.Vector3(
+        endPoint.x,
+        endPoint.y,
+        endEdge.y + endEdge.parent.parent.position.z + offset
+      );
+      let d = new THREE.Vector3(
+        endPoint.x,
+        endPoint.y,
+        endEdge.y + endEdge.parent.parent.position.z
+      );
+      cross.rotation.y = Math.PI / 2;
+      cross.position.copy(d);
+      cross.position.z += 0.02;
+      points = pointsToArray([a, b, c, d]);
+    } else {
+      //right
+      let a = startPoint.clone();
+      let b = new THREE.Vector3(
+        (startPoint.x + endPoint.x) / 2,
+        startPoint.y,
+        startPoint.z
+      );
+      let c = new THREE.Vector3(
+        (startPoint.x + endPoint.x) / 2,
+        startPoint.y,
+        endPoint.z
+      );
+      let d = new THREE.Vector3(
+        endEdge.x + endEdge.parent.parent.parent.position.x,
+        startPoint.y,
+        endPoint.z
+      );
+      const diff = c.x < d.x;
+      cross.position.copy(d);
+
+      cross.rotation.y = diff ? 0 : Math.PI;
+      diff ? (cross.position.x -= 0.02) : (cross.position.x += 0.02);
+
+      points = pointsToArray([a, b, c, d]);
+    }
+
+    const geometry = new LineGeometry();
+
+    cross.visible = false;
+    geometry.setPositions(points);
+    super(geometry, matLine);
+    this.name = "line_" + this.uuid;
+    this.userData = {
+      dir: endEdge.name,
+      points: points,
+    };
+    this.scale.set(1, 1, 1);
+    this.position.y += 0.5;
+    this.add(cross);
+  }
+}

+ 75 - 0
src/core/box/object/LinePoints.js

@@ -0,0 +1,75 @@
+import * as THREE from "three";
+import { Line2 } from "three/examples/jsm/lines/Line2.js";
+import { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js";
+import gotoPic from "@/assets/image/goto.png";
+const offset = 0.25;
+
+function pointsToArray(arr) {
+  let res = [];
+  arr.forEach((i) => {
+    res = res.concat(i.toArray());
+  });
+  return res;
+}
+let m = new THREE.MeshBasicMaterial({
+  map: new THREE.TextureLoader().load(gotoPic),
+  color: 0x26559b,
+  transparent: true,
+});
+export default class LinePoints extends Line2 {
+  constructor(points, matLine, dir, imgId, id) {
+    let g = new THREE.PlaneGeometry(0.1, 0.1);
+    g.rotateX(-Math.PI / 2);
+    let cross = new THREE.Mesh(g, m);
+    let a = new THREE.Vector3(points[0], points[1], points[2]);
+    let b = new THREE.Vector3(points[3], points[4], points[5]);
+    let c = new THREE.Vector3(points[6], points[7], points[8]);
+    let d = new THREE.Vector3(points[9], points[10], points[11]);
+    let diff;
+    switch (dir) {
+      case 0:
+        //top
+        cross.rotation.y = -Math.PI / 2;
+        cross.position.copy(d);
+        break;
+      case 1:
+        //left
+        diff = c.x < d.x;
+        cross.rotation.y = diff ? 0 : -Math.PI;
+        diff ? (cross.position.x -= 0.02) : (cross.position.x += 0.02);
+
+        break;
+      case 2:
+        //bottom
+        cross.position.copy(d);
+        cross.rotation.y = Math.PI / 2;
+        cross.position.z += 0.02;
+        break;
+      case 3:
+        //right
+        diff = c.x < d.x;
+        cross.position.copy(d);
+        cross.rotation.y = diff ? 0 : Math.PI;
+        diff ? (cross.position.x -= 0.02) : (cross.position.x += 0.02);
+        break;
+    }
+    const geometry = new LineGeometry();
+    cross.visible = false;
+    // console.log("points", points);
+    geometry.setPositions(points);
+    super(geometry, matLine);
+    this.name = "line_point_" + this.uuid;
+    this.scale.set(1, 1, 1);
+    this.position.y += 0.5;
+    if (id) {
+      this.uuid = id;
+    }
+    this.userData = {
+      id: id || this.uuid,
+      dir: dir,
+      points,
+      imgId: imgId || null,
+    };
+    this.add(cross);
+  }
+}

+ 54 - 0
src/core/box/object/PureTextLabel copy.js

@@ -0,0 +1,54 @@
+import * as THREE from "three";
+
+export default class PureTextLabel extends THREE.Mesh {
+  constructor(text, point, fontsize = 12, color = "#000000", id) {
+    let res = 2;
+    const width = 168 * res;
+    const height = 50 * res;
+    var canvas = document.createElement("canvas");
+    canvas.width = width;
+    canvas.height = height;
+
+    let fontFamily = "Arial";
+    let fs = fontsize * res;
+    var context = canvas.getContext("2d");
+    context.fillStyle = "transparent";
+    context.rect(0, 0, width, height);
+    context.fill();
+    let fontStyle = "normal " + fs + "px " + fontFamily;
+    // console.log("fontStyle", fontStyle);
+    context.font = fontStyle;
+    context.fillStyle = color;
+    context.textAlign = "center";
+    context.textBaseline = "middle";
+    context.fillText(text, width / 2, height / 2);
+    const canvas_map = new THREE.Texture(canvas);
+    canvas_map.colorSpace = THREE.SRGBColorSpace;
+    canvas_map.needsUpdate = true;
+    canvas_map.anisotropy = 4;
+
+    const g = new THREE.PlaneGeometry(1.5, 0.44);
+    g.rotateX(-Math.PI / 2);
+
+    // const texture = new THREE.CanvasTexture(canvas_map);
+
+    const m = new THREE.MeshBasicMaterial({
+      map: canvas_map,
+      transparent: true, // 允许材质透明
+    });
+    super(g, m);
+    if (id) {
+      this.uuid = id;
+    }
+    const p = new THREE.Vector3().copy(point);
+    this.userData = {
+      id: this.uuid,
+      text: text,
+      color: color,
+      pos: p.toArray(),
+      fontsize: fontsize,
+    };
+    this.position.copy(p);
+    this.name = "pureText_" + text;
+  }
+}

+ 73 - 0
src/core/box/object/PureTextLabel.js

@@ -0,0 +1,73 @@
+import * as THREE from "three";
+import { getWrapText } from "../../utils/text";
+
+export default class PureTextLabel extends THREE.Mesh {
+  constructor(text, point, fontsize = 12, color = "#000000", id) {
+    const radio = fontsize / 12;
+
+    let containerWidth = 1.5 * radio;
+    let containerHeight = 0.12 * radio;
+    const containerRadio = containerWidth / containerHeight;
+    let res = 2;
+    const width = 168 * res * radio;
+    const height = width / containerRadio;
+    var canvas = document.createElement("canvas");
+    canvas.width = width;
+    canvas.height = height;
+
+    let fontFamily = "Arial";
+    let fs = 12 * res * radio;
+    var context = canvas.getContext("2d");
+
+    const lines = getWrapText(context, text, 140);
+    console.log("lines", lines);
+    containerHeight = containerHeight * lines.length;
+    canvas.height = height * lines.length;
+    context.fillStyle = "transparent";
+    // context.fillStyle = "rgba(255,255,255,0.5)";
+    context.rect(0, 0, width, height * lines.length);
+    context.fill();
+    let fontStyle = "normal " + fs + "px " + fontFamily;
+    // console.log("fontStyle", fontStyle);
+    context.font = fontStyle;
+    context.fillStyle = color;
+    context.textAlign = "center";
+    context.textBaseline = "middle";
+    // context.fillText(text, width / 2, height / 2);
+    lines.forEach((txt, index) => {
+      context.fillText(txt, width / 2, height / 2 + height * index);
+    });
+    const canvas_map = new THREE.Texture(canvas);
+    canvas_map.colorSpace = THREE.SRGBColorSpace;
+    canvas_map.needsUpdate = true;
+    canvas_map.anisotropy = 4;
+
+    const g = new THREE.PlaneGeometry(containerWidth, containerHeight);
+
+    g.rotateX(-Math.PI / 2);
+
+    // const texture = new THREE.CanvasTexture(canvas_map);
+
+    const m = new THREE.MeshBasicMaterial({
+      map: canvas_map,
+      transparent: true, // 允许材质透明
+    });
+
+    super(g, m);
+
+    if (id) {
+      this.uuid = id;
+    }
+    const p = new THREE.Vector3().copy(point);
+    this.userData = {
+      id: this.uuid,
+      text: text,
+      color: color,
+      pos: p.toArray(),
+      fontsize: fontsize,
+    };
+    this.position.copy(p);
+
+    this.name = "pureText_" + text;
+  }
+}

+ 48 - 0
src/core/box/object/SimpleLabel.js

@@ -0,0 +1,48 @@
+import * as THREE from "three";
+
+export default class SimpleLabel extends THREE.Mesh {
+  constructor(text, outline) {
+    let res = 5;
+    const width = 150 * res;
+    const height = 15 * res;
+    var canvas = document.createElement("canvas");
+    canvas.width = width;
+    canvas.height = height;
+    let fontFamily = "Arial";
+    let fontSize = 5.2 * res;
+    let offsetX = 75 * res;
+    let offsetY = 10 * res;
+    var context = canvas.getContext("2d");
+
+    context.fillStyle = "#ffffff";
+    context.rect(0, 0, width, height);
+    context.fill();
+    context.font = "normal " + fontSize + "px " + fontFamily;
+    context.fillStyle = "#000000";
+    context.textAlign = "center";
+    context.fillText(text, offsetX, offsetY);
+    const canvas_map = new THREE.Texture(canvas);
+    canvas_map.colorSpace = THREE.SRGBColorSpace;
+    canvas_map.needsUpdate = true;
+    canvas_map.anisotropy = 4;
+
+
+    const g = new THREE.PlaneGeometry(1.5, 0.15);
+    g.rotateX(-Math.PI / 2);
+
+    const m = new THREE.MeshBasicMaterial({
+      map: canvas_map,
+    });
+    super(g, m);
+
+    // const edges = new THREE.EdgesGeometry(g);
+    // const line = new THREE.LineSegments(
+    //   edges,
+    //   new THREE.LineBasicMaterial({ color: 0xcccccc })
+    // );
+    // line.position.y += 0.5;
+    // this.add(line);
+
+    this.name = "SimpleLabel_" + text;
+  }
+}

+ 47 - 0
src/core/box/object/TextLabel.js

@@ -0,0 +1,47 @@
+import * as THREE from "three";
+
+export default class TextLabel extends THREE.Mesh {
+  constructor(text, outline) {
+    let res = 5;
+    const width = 150 * res;
+    const height = 15 * res;
+    var canvas = document.createElement("canvas");
+    canvas.width = width;
+    canvas.height = height;
+    let fontFamily = "Arial";
+    let fontSize = 7 * res;
+    let offsetX = 75 * res;
+    let offsetY = 10 * res;
+    var context = canvas.getContext("2d");
+
+    context.fillStyle = "#ffffff";
+    context.rect(0, 0, width, height);
+    context.fill();
+    context.font = "normal " + fontSize + "px " + fontFamily;
+    context.fillStyle = "#000000";
+    context.textAlign = "center";
+    context.fillText(text, offsetX, offsetY);
+    const canvas_map = new THREE.Texture(canvas);
+    canvas_map.colorSpace = THREE.SRGBColorSpace;
+    canvas_map.needsUpdate = true;
+    canvas_map.anisotropy = 4;
+
+    const g = new THREE.PlaneGeometry(1.5, 0.15);
+    g.rotateX(-Math.PI / 2);
+
+    const m = new THREE.MeshBasicMaterial({
+      map: canvas_map,
+    });
+    super(g, m);
+
+    const edges = new THREE.EdgesGeometry(g);
+    const line = new THREE.LineSegments(
+      edges,
+      new THREE.LineBasicMaterial({ color: 0xcccccc })
+    );
+    line.position.y += 0.5;
+    this.add(line);
+
+    this.name = "textlabel_" + text;
+  }
+}

+ 24 - 0
src/core/box/object/TouchEdge.js

@@ -0,0 +1,24 @@
+import * as THREE from "three";
+import { Line2 } from "three/examples/jsm/lines/Line2.js";
+import { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js";
+
+export default class TouchEdge extends THREE.Group {
+  constructor(positions, matLine) {
+    super();
+    positions.forEach((i, j) => {
+      //top left bottom right
+      const geometry = new LineGeometry();
+      geometry.setPositions(i);
+
+      const line = new Line2(geometry, matLine);
+      line.scale.set(1, 1, 1);
+      line.position.y += 0.5;
+      line.name = j;
+      line.visible = false;
+      line.x = i[0];
+      line.y = i[2];
+      this.add(line);
+    });
+    // console.log(this);
+  }
+}

+ 31 - 0
src/core/box/object/marker.js

@@ -0,0 +1,31 @@
+import * as THREE from "three";
+import gotoPic from "@/assets/image/arrow.svg";
+const m = new THREE.MeshBasicMaterial({
+  map: new THREE.TextureLoader().load(gotoPic),
+  color: 0xe44d54,
+  transparent: true,
+});
+
+export default class Marker extends THREE.Mesh {
+  constructor(startPoint, imageId) {
+    const g = new THREE.PlaneGeometry(0.15, 0.15);
+    g.rotateX(-Math.PI / 2);
+    super(g, m);
+    const a = startPoint.clone();
+    this.position.copy(a);
+
+    this.rotation.y = 0;
+    this.position.y = 5;
+    this.position.z -= 0.02;
+    this.userData = {
+      imageId: imageId || null,
+      point: a.toArray(),
+    };
+    this.visible = true;
+    this.scale.set(1, 1, 1);
+    this.position.y += 0.5;
+    this.name = "marker_" + this.uuid;
+    this.renderOrder = 1000;
+    // console.log(this, this.position);
+  }
+}

+ 501 - 0
src/core/controls/FloorplanControls.js

@@ -0,0 +1,501 @@
+import { Vector2, Vector3 } from "three";
+
+const STATE = {
+  NONE: -1,
+  ROTATE: 0,
+  PAN: 1,
+  ZOOM: 2,
+  ZOOM_PAN: 3,
+  ZOOM_ROTATE: 4,
+};
+const HANDLE = {
+  ROTATE: 0,
+  PAN: 1,
+  ZOOM: 2,
+  ZOOM_PAN: 3,
+  ZOOM_ROTATE: 4,
+};
+
+const pointers = [];
+const pointerPositions = {};
+
+export default class FloorplanControls {
+  constructor(camera, dom, player) {
+    this.camera = camera;
+    this.domElement = dom;
+    this.domElement.style.touchAction = "none"; // disable touch scroll
+    this.player = player;
+
+    this.panSpeed = 1;
+    this.zoomSpeed = 1;
+    this.rotateSpeed = 1;
+
+    this.maxDistance = 100;
+    this.minDistance = 0.1;
+    this.maxZoom = 500;
+    this.minZoom = 5;
+
+    this.target = new Vector3();
+
+    this.state = STATE.NONE;
+
+    this.rotateStart = new Vector2();
+    this.rotateEnd = new Vector2();
+
+    this.panStart = new Vector2();
+    this.panEnd = new Vector2();
+
+    this.zoomStart = new Vector2();
+
+    this.locked = false; //禁止用户操作
+    this.enabled = true; //禁止update
+    this.enablePan = true;
+    this.enableRotate = true;
+    this.enableZoom = true;
+
+    this.touchesEvent = {
+      ONE: HANDLE.PAN,
+      TWO: HANDLE.ZOOM,
+    };
+    this.mouseEvent = {
+      LEFT: HANDLE.PAN,
+      RIGHT: HANDLE.ROTATE,
+      WHEEL: HANDLE.ZOOM,
+    };
+
+    this.onBindEvent();
+  }
+
+  onBindEvent = () => {
+    this.domElement.addEventListener(
+      "pointerdown",
+      this.onPointerDown.bind(this)
+    );
+    this.domElement.addEventListener("pointerup", this.onPointerUp.bind(this));
+    this.domElement.addEventListener(
+      "pointermove",
+      this.onPointerMove.bind(this)
+    );
+    this.domElement.addEventListener(
+      "pointercancel",
+      this.onPointerUp.bind(this)
+    );
+
+    this.domElement.addEventListener(
+      "mousewheel",
+      this.onMouseWheel.bind(this),
+      { passive: false }
+    );
+
+    this.domElement.addEventListener("contextmenu", this.onPreventDefault);
+  };
+
+  addPointer = (event) => {
+    pointers.push(event);
+  };
+  removePointer = (event) => {
+    for (let i = 0; i < pointers.length; i++) {
+      if (pointers[i].pointerId == event.pointerId) {
+        pointers.splice(i, 1);
+        return;
+      }
+    }
+  };
+  isTrackingPointer = (event) => {
+    for (let i = 0; i < pointers.length; i++) {
+      if (pointers[i] == event.pointerId) return true;
+    }
+    return false;
+  };
+  trackPointer = (event) => {
+    let position = pointerPositions[event.pointerId];
+    if (position === undefined) {
+      position = new Vector2();
+      pointerPositions[event.pointerId] = position;
+    }
+    position.set(event.pageX, event.pageY);
+  };
+  getSecondPointerPosition = (event) => {
+    const pointerId =
+      event.pointerId === pointers[0].pointerId
+        ? pointers[1].pointerId
+        : pointers[0].pointerId;
+    return pointerPositions[pointerId];
+  };
+
+  // pointer event
+  onPointerDown = (event) => {
+    if (this.locked) return;
+    if (pointers.length === 0) {
+      this.domElement.setPointerCapture(event.pointerId);
+    }
+    if (this.isTrackingPointer(event)) return;
+    this.addPointer(event);
+    if (event.pointerType === "touch") {
+      this.onTouchStart(event);
+    } else {
+      this.onMouseDown(event);
+    }
+  };
+  onPointerUp = (event) => {
+    if (this.locked) return;
+    this.removePointer(event);
+    if (pointers.length === 0) {
+      this.domElement.releasePointerCapture(event.pointerId);
+      this.state = STATE.NONE;
+    } else if (pointers.length === 1) {
+      const pointerId = pointers[0].pointerId;
+      const position = pointerPositions[pointerId];
+      this.onTouchStart({
+        pointerId: pointerId,
+        pageX: position.x,
+        pageY: position.y,
+      });
+    }
+  };
+  onPointerMove = (event) => {
+    if (this.locked) return;
+    if (event.pointerType === "touch") {
+      this.onTouchMove(event);
+    } else {
+      this.onMouseMove(event);
+    }
+  };
+
+  //touch event
+  onTouchStart = (event) => {
+    this.trackPointer(event);
+    switch (pointers.length) {
+      case 1:
+        switch (this.touchesEvent.ONE) {
+          case HANDLE.ROTATE: //rotate
+            if (this.enableRotate === false) return;
+            this.handleTouchStartRotate();
+            this.state = STATE.ROTATE;
+            break;
+          case HANDLE.PAN: //pan
+            if (this.enablePan === false) return;
+            this.handleTouchStartPan();
+            this.state = STATE.PAN;
+            break;
+          default:
+            state = STATE.NONE;
+        }
+        break;
+      case 2:
+        switch (this.touchesEvent.TWO) {
+          case HANDLE.ZOOM: //zoom
+            if (this.enableZoom === false) return;
+            this.handleTouchStartZoom();
+            this.state = STATE.ZOOM;
+            break;
+          case HANDLE.ZOOM_PAN: //zoom_pan
+            if (this.enableZoom === false && this.enablePan === false) return;
+            this.handleTouchStartZoom();
+            this.handleTouchStartPan();
+            this.state = STATE.ZOOM_PAN;
+            break;
+          //todo case HANDLE.ZOOM_ROTATE:
+          default:
+            state = STATE.NONE;
+        }
+        break;
+      default:
+        this.state = STATE.NONE;
+    }
+  };
+  onTouchMove = (event) => {
+    this.trackPointer(event);
+    switch (this.state) {
+      case STATE.ROTATE:
+        if (this.enableRotate === false) return;
+        this.handleTouchMoveRotate(event);
+        break;
+      case STATE.PAN:
+        if (this.enablePan === false) return;
+        this.handleTouchMovePan(event);
+        break;
+      case STATE.ZOOM:
+        if (this.enableZoom === false) return;
+        this.handleTouchMoveZoom(event);
+        break;
+      case STATE.ZOOM_PAN:
+        if (this.enableZoom) this.handleTouchMoveZoom(event);
+        if (this.enablePan) this.handleTouchMovePan(event);
+        break;
+      //todo case STATE.ZOOM_ROTATE:
+      default:
+        this.state = STATE.NONE;
+    }
+  };
+
+  //mouse event
+  onMouseDown = (event) => {
+    if (this.locked) return;
+    switch (event.button) {
+      case 0: //left
+        switch (this.mouseEvent.LEFT) {
+          case HANDLE.PAN:
+            if (this.enablePan === false) return;
+            this.handleMouseDownPan(event);
+            this.state = STATE.PAN;
+            break;
+          case HANDLE.ROTATE:
+            if (this.enablePan === false) return;
+            this.handleMouseDownRotate(event);
+            this.state = STATE.ROTATE;
+            break;
+          default:
+            this.state = STATE.NONE;
+        }
+        break;
+      case 2: //right
+        switch (this.mouseEvent.RIGHT) {
+          case HANDLE.PAN:
+            if (this.enablePan === false) return;
+            this.handleMouseDownPan(event);
+            this.state = STATE.PAN;
+            break;
+          case HANDLE.ROTATE:
+            if (this.enablePan === false) return;
+            this.handleMouseDownRotate(event);
+            this.state = STATE.ROTATE;
+            break;
+          default:
+            this.state = STATE.NONE;
+        }
+        break;
+      default:
+        this.state = STATE.NONE;
+    }
+  };
+  onMouseMove = (event) => {
+    if (this.locked) return;
+    switch (this.state) {
+      case STATE.PAN:
+        if (this.enablePan === false) return;
+        this.handleMouseMovePan(event);
+        break;
+      case STATE.ROTATE:
+        if (this.enableRotate === false) return;
+        this.handleMouseMoveRotate(event);
+        break;
+      default:
+        this.state = STATE.NONE;
+    }
+  };
+  onMouseWheel = (event) => {
+    // console.log("this", this);
+    if (this.locked) return;
+    if (this.enableZoom === false) return;
+    event.preventDefault();
+    this.handleMouseWheelZoom(event);
+  };
+  onPreventDefault = (event) => {
+    event.preventDefault();
+  };
+
+  //================================handle================================
+  //-------------------------rotate-------------------------
+  handleTouchStartRotate = () => {
+    const position = pointerPositions[pointers[0].pointerId];
+    this.rotateStart.set(position.x, position.y);
+  };
+  handleTouchMoveRotate = (event) => {
+    this.rotateEnd.set(event.pageX, event.pageY);
+    let rotateDelta = this.rotateEnd
+      .clone()
+      .sub(this.rotateStart)
+      .multiplyScalar(this.rotateSpeed);
+    let element = this.domElement;
+    let rotateX = (2 * Math.PI * rotateDelta.x) / element.clientHeight;
+    let rotateY = (2 * Math.PI * rotateDelta.y) / element.clientHeight;
+
+    this.rotate(rotateX, rotateY);
+
+    this.rotateStart.copy(this.rotateEnd);
+  };
+  handleMouseDownRotate = (event) => {
+    this.rotateStart.set(event.pageX, event.pageY);
+  };
+  handleMouseMoveRotate = (event) => {
+    this.rotateEnd.set(event.pageX, event.pageY);
+    let rotateDelta = this.rotateEnd
+      .clone()
+      .sub(this.rotateStart)
+      .multiplyScalar(this.rotateSpeed);
+    let element = this.domElement;
+    let rotateX = (2 * Math.PI * rotateDelta.x) / element.clientHeight;
+    let rotateY = (2 * Math.PI * rotateDelta.y) / element.clientHeight;
+
+    this.rotate(rotateX, rotateY);
+
+    this.rotateStart.copy(this.rotateEnd);
+  };
+  //-------------------------zoom-------------------------
+  handleTouchStartZoom = () => {
+    const dx = pointers[0].pageX - pointers[1].pageX;
+    const dy = pointers[0].pageY - pointers[1].pageY;
+    const distance = Math.sqrt(dx * dx + dy * dy);
+    this.zoomStart.set(0, distance);
+  };
+  handleTouchMoveZoom = (event) => {
+    const position = this.getSecondPointerPosition(event);
+    const dx = event.pageX - position.x;
+    const dy = event.pageY - position.y;
+    const distance = Math.sqrt(dx * dx + dy * dy);
+    let delta = Math.pow(distance / this.zoomStart.y, this.zoomSpeed);
+
+    this.zoom(1 / delta);
+
+    this.zoomStart.set(0, distance);
+  };
+  handleMouseWheelZoom = (event) => {
+    if (event.deltaY > 0) {
+      //zoom out
+      this.zoom(1.05 * this.zoomSpeed);
+    } else {
+      //zoom in
+      this.zoom(0.95 * this.zoomSpeed);
+    }
+  };
+  //-------------------------pan-------------------------
+  handleTouchStartPan = () => {
+    if (pointers.length === 1) {
+      const position = pointerPositions[pointers[0].pointerId];
+      this.panStart.set(position.x, position.y);
+    } else {
+      const x = 0.5 * (pointers[0].pageX + pointers[1].pageX);
+      const y = 0.5 * (pointers[0].pageY + pointers[1].pageY);
+      this.panStart.set(x, y);
+    }
+  };
+  handleTouchMovePan = (event) => {
+    if (pointers.length === 1) {
+      this.panEnd.set(event.pageX, event.pageY);
+    } else {
+      const position = this.getSecondPointerPosition(event);
+      const x = 0.5 * (event.pageX + position.x);
+      const y = 0.5 * (event.pageY + position.y);
+      this.panEnd.set(x, y);
+    }
+    let panDelta = this.panEnd.clone().sub(this.panStart);
+
+    this.pan(panDelta);
+
+    this.panStart.copy(this.panEnd);
+  };
+  handleMouseDownPan = (event) => {
+    this.panStart.set(event.pageX, event.pageY);
+  };
+  handleMouseMovePan = (event) => {
+    this.panEnd.set(event.pageX, event.pageY);
+    let panDelta = this.panEnd.clone().sub(this.panStart);
+
+    this.pan(panDelta);
+
+    this.panStart.copy(this.panEnd);
+  };
+
+  rotate(x, y) {
+    let r = y;
+    if (Math.abs(x) > Math.abs(y)) r = x;
+
+    let cameraRZ = this.camera.rotation.z;
+    cameraRZ += r;
+    if (Math.abs(cameraRZ) >= Math.PI * 2) {
+      cameraRZ -= Math.sign(cameraRZ) * Math.PI * 2;
+    }
+    this.camera.rotation.z = cameraRZ;
+    this.cameraUpdate();
+  }
+  zoom(delta) {
+    // if(this.camera.isPerspectiveCamera) {
+    //   let cameraY = this.camera.position.y
+    //   cameraY *= delta
+    //   cameraY = Math.max(cameraY, this.minDistance)
+    //   cameraY = Math.min(cameraY, this.maxDistance)
+    //   this.camera.position.y = cameraY //handle
+    // } else if(this.camera.isOrthographicCamera) {
+    //   let zoom = this.camera.zoom
+    //   zoom *= 1/delta
+    //   console.log(zoom)
+    //   this.camera.zoom = zoom
+    //   this.camera.updateProjectionMatrix()
+    // }
+
+    let cameraY = this.camera.position.y;
+    cameraY *= delta;
+    cameraY = Math.max(cameraY, this.minDistance);
+    cameraY = Math.min(cameraY, this.maxDistance);
+    this.camera.position.y = cameraY; //handle
+    if (this.camera.isOrthographicCamera) {
+      let zoom = this.camera.zoom;
+      zoom *= 1 / delta;
+      zoom = Math.max(zoom, this.minZoom);
+      zoom = Math.min(zoom, this.maxZoom);
+      this.camera.zoom = zoom;
+      this.camera.updateProjectionMatrix();
+    }
+
+    this.cameraUpdate();
+  }
+  pan(delta) {
+    const element = this.domElement;
+    const matrix = this.camera.matrix.clone();
+    const left = new Vector3();
+    const up = new Vector3();
+    let panDelta = delta.multiplyScalar(this.panSpeed);
+
+    if (this.camera.isPerspectiveCamera) {
+      let scalar =
+        (2 *
+          this.camera.position.y *
+          Math.tan(((this.camera.fov / 2) * Math.PI) / 180.0)) /
+        element.clientHeight;
+      panDelta.multiplyScalar(scalar);
+
+      left.setFromMatrixColumn(matrix, 0);
+      left.multiplyScalar(-panDelta.x);
+      up.setFromMatrixColumn(matrix, 1);
+      up.multiplyScalar(panDelta.y);
+    } else if (this.camera.isOrthographicCamera) {
+      (panDelta.x =
+        (panDelta.x * (this.camera.right - this.camera.left)) /
+        this.camera.zoom /
+        element.clientWidth),
+        this.camera.matrix;
+      (panDelta.y =
+        (panDelta.y * (this.camera.top - this.camera.bottom)) /
+        this.camera.zoom /
+        element.clientHeight),
+        this.camera.matrix;
+      left.setFromMatrixColumn(matrix, 0);
+      left.multiplyScalar(-panDelta.x);
+      up.setFromMatrixColumn(matrix, 1);
+      up.multiplyScalar(panDelta.y);
+    } else {
+      return;
+    }
+    this.camera.position.add(left).add(up);
+    this.target.set(this.camera.position.x, 0, this.camera.position.z);
+    this.cameraUpdate();
+  }
+
+  lookAt(target, height) {
+    if (!target) return;
+    height = height !== undefined ? height : this.camera.position.y;
+    this.camera.position.set(target.x, height, target.z);
+    this.target.set(target.x, 0, target.z);
+    this.camera.lookAt(this.target);
+  }
+
+  cameraUpdate = () => {
+    this.camera.updateMatrix();
+    this.camera.updateProjectionMatrix();
+  };
+
+  update = () => {
+    if (!this.enabled) return;
+  };
+}

+ 10 - 0
src/core/mitt.js

@@ -0,0 +1,10 @@
+import mitt from "mitt";
+export class Mitt {
+  constructor() {
+    const emitter = mitt();
+
+    Object.keys(emitter).forEach((method) => {
+      this[method] = emitter[method];
+    });
+  }
+}

+ 969 - 0
src/core/player/Player.js

@@ -0,0 +1,969 @@
+import * as THREE from "three";
+
+import FloorplanControls from "../controls/FloorplanControls.js";
+import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
+
+import { TrackballControls } from "three/examples/jsm/controls/TrackballControls.js";
+import Line from "../box/object/Line";
+import LinePoints from "../box/object/LinePoints.js";
+import Marker from "../box/object/marker.js";
+import CircleTextLabel from "../box/object/CircleTextLabel.js";
+import PureTextLabel from "../box/object/PureTextLabel.js";
+import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
+
+const convertScreenToNDC = function (event, domElement) {
+  let x = (event.offsetX / domElement.clientWidth) * 2 - 1;
+  let y = -(event.offsetY / domElement.clientHeight) * 2 + 1;
+  return new THREE.Vector2(x, y);
+};
+export default class Player {
+  constructor(scene) {
+    this.scene = scene;
+    this.orthCamera = scene.orthCamera;
+
+    this.floorplanControls = null;
+    this.raycaster = null;
+
+    this.position = new THREE.Vector3();
+
+    this.pointerdown = new THREE.Vector2();
+    this.pointerup = new THREE.Vector2();
+    this.pointer = new THREE.Vector2();
+    this.markPosition = new THREE.Vector3();
+
+    this.touchImg = null;
+    this.activeEdge = null;
+    this.drawLine = null;
+    this.startObj = null;
+    this.marker = null;
+    this.symbol = null;
+    this.symbolIndex = 0;
+    this.text = null;
+    this.showText = "文本";
+    this.selectItem = null;
+
+    this.drawing = false;
+    this.inited = false;
+    this.renderLines = [];
+    this.renderMarkers = [];
+    this.activeEdges = [];
+    this.renderSymbols = [];
+    this.renderTexts = [];
+
+    this.matLine = null;
+    this.lineColor = 0xe44d54;
+    // 1是画线,2是标方向, 3符号, 4文本
+    this.mode = 0;
+    this.init();
+  }
+
+  setMode(mode) {
+    this.mode = mode;
+
+    if (mode !== 0) {
+      this.reset();
+      this.setEditMode();
+    }
+    // 2方向
+    if (mode === 2) {
+      let pos = new THREE.Vector3(0, 0, -1);
+      pos.unproject(this.orthCamera);
+      pos.y = 5;
+
+      this.marker = new Marker(pos);
+      this.marker.visible = false;
+      this.scene.scene.add(this.marker);
+      this.drawing = true;
+    }
+    //符号
+    if (mode === 3) {
+      let pos = new THREE.Vector3(0, 0, -1);
+      pos.unproject(this.orthCamera);
+      pos.y = 5;
+      const lastIndex = this.getLaSybIndex();
+      this.symbolIndex = lastIndex + 1;
+
+      this.symbol = new CircleTextLabel(this.symbolIndex, pos);
+      this.symbol.visible = false;
+      this.scene.scene.add(this.symbol);
+      console.log("this.symbol", this.symbol);
+      this.drawing = true;
+    }
+
+    if (mode === 4) {
+      let pos = new THREE.Vector3(0, 0, -1);
+      pos.unproject(this.orthCamera);
+      pos.y = 5;
+      this.text = new PureTextLabel(this.showText, pos);
+      this.text.visible = false;
+      this.showText = "文本";
+      this.scene.scene.add(this.text);
+      this.drawing = true;
+    }
+
+    if (mode === 0) {
+      this.setFreeMode();
+    }
+    this.scene.emit("mode", this.mode);
+  }
+  getLaSybIndex() {
+    const maxIndexObject = this.renderSymbols.reduce(
+      (max, current) => {
+        return current.index > max.index ? current : max;
+      },
+      { index: 0, point: [] }
+    );
+    return maxIndexObject.index;
+  }
+
+  setFreeMode() {
+    this.floorplanControls.enablePan = true;
+    this.floorplanControls.mouseButtons = {
+      LEFT: THREE.MOUSE.PAN,
+      MIDDLE: THREE.MOUSE.DOLLY,
+      RIGHT: THREE.MOUSE.PAN,
+    };
+    this.reset();
+  }
+
+  setEditMode() {
+    this.floorplanControls.enablePan = true;
+    this.floorplanControls.mouseButtons = {
+      LEFT: THREE.MOUSE.ROTATE,
+      MIDDLE: THREE.MOUSE.DOLLY,
+      RIGHT: THREE.MOUSE.PAN,
+    };
+  }
+
+  setRotateMode() {
+    this.floorplanControls.enableRotate = true;
+    this.floorplanControls.mouseButtons = {
+      LEFT: THREE.MOUSE.ROTATE,
+      MIDDLE: THREE.MOUSE.DOLLY,
+      RIGHT: THREE.MOUSE.PAN,
+    };
+  }
+  init = () => {
+    // //floorplanControls
+    // this.floorplanControls = new FloorplanControls(
+    //   this.orthCamera,
+    //   this.scene.domElement,
+    //   this
+    // );
+    this.floorplanControls = new OrbitControls(
+      this.orthCamera,
+      this.scene.domElement
+    );
+
+    this.floorplanControls.enablePan = true;
+    // this.floorplanControls.target.set(0, 1, 0);
+    // this.floorplanControls.rotateSpeed = 0.5;
+    // this.floorplanControls.panSpeed = 0.75
+
+    this.floorplanControls.maxDistance = 100;
+    this.floorplanControls.minDistance = 3.5;
+    this.floorplanControls.maxZoom = 500;
+    this.floorplanControls.minZoom = 100;
+
+    // this.floorplanControls.mouseButtons = {
+    //   LEFT: THREE.MOUSE.PAN,
+    //   MIDDLE: THREE.MOUSE.DOLLY,
+    //   RIGHT: THREE.MOUSE.PAN
+    // }
+    this.setMode(0);
+    this.floorplanControls.enableRotate = false;
+    this.raycaster = new THREE.Raycaster();
+    this.onBindEvent();
+    this.inited = true;
+    this.matLine = new LineMaterial({
+      color: this.lineColor,
+      linewidth: 4, // in world units with size attenuation, pixels otherwise
+      dashed: false,
+      alphaToCoverage: true,
+    });
+    this.matLine.resolution = new THREE.Vector2(
+      this.scene.width,
+      this.scene.height
+    );
+  };
+
+  onPointerMove = (e) => {
+    // console.log("intersects", intersects);
+    // if (this.mode === 0) {
+    //   const intersects = this.raycaster.intersectObjects(
+    //     this.scene.scene.children,
+    //     true
+    //   );
+    //   intersects.forEach((i) => {
+    //     if (String(i.object.name).includes("marker")) {
+    //       // console.log("i.object.name", i.object);
+    //       // debugger
+    //     }
+    //   });
+    // }
+    this.pointermove = convertScreenToNDC(e, this.scene.domElement);
+    this.raycaster.setFromCamera(this.pointermove, this.orthCamera);
+
+    if (!this.drawing) return;
+
+    if (this.mode === 1) {
+      let intersectArr = this.scene.boxManager.imgList;
+      // if(this.startObj) {
+      //   let i = intersectArr.indexOf(this.startObj)
+      //   intersectArr.splice(i, 1)
+      // }
+      const intersects = this.raycaster.intersectObjects(intersectArr, false);
+      if (intersects[0] && intersects[0].object !== this.startObj) {
+        this.touchImg = intersects[0];
+        this.setActiveLine(this.touchImg);
+      }
+    }
+    if (this.mode === 2) {
+      if (this.marker) {
+        this.marker.visible = true;
+        let pos = new THREE.Vector3(this.pointermove.x, this.pointermove.y, -1);
+        pos.unproject(this.orthCamera);
+        pos.y = 5;
+        this.marker.position.copy(pos);
+      }
+    }
+
+    if (this.mode === 3) {
+      if (this.symbol) {
+        this.symbol.visible = true;
+        let pos = new THREE.Vector3(this.pointermove.x, this.pointermove.y, -1);
+        pos.unproject(this.orthCamera);
+        pos.y = 5;
+        const clamp = new THREE.Vector3();
+        this.scene.boxManager.obb.clampPoint(pos, clamp);
+        clamp.y = 5;
+        this.symbol.position.copy(clamp);
+      }
+    }
+
+    if (this.mode === 4) {
+      if (this.text) {
+        this.text.visible = true;
+        let pos = new THREE.Vector3(this.pointermove.x, this.pointermove.y, -1);
+        pos.unproject(this.orthCamera);
+        pos.y = 5;
+
+        const clamp = new THREE.Vector3();
+        this.scene.boxManager.obb.clampPoint(pos, clamp);
+        clamp.y = 5;
+        this.text.position.copy(clamp);
+        this.text.userData.pos = clamp.toArray();
+      }
+    }
+  };
+
+  onPointerDown = (e) => {
+    console.log("start draw");
+
+    this.pointerdown = convertScreenToNDC(e, this.scene.domElement);
+
+    if (this.mode === 0) {
+      const intersects = this.raycaster.intersectObjects(
+        this.scene.scene.children,
+        true
+      );
+      intersects.forEach((i) => {
+        if (
+          String(i.object.name).includes("marker") ||
+          String(i.object.name).includes("line") ||
+          String(i.object.name).includes("circle") ||
+          String(i.object.name).includes("pureText")
+        ) {
+          let type;
+          switch (true) {
+            case String(i.object.name).includes("marker"):
+              type = 1;
+              break;
+            case String(i.object.name).includes("line"):
+              type = 2;
+              break;
+            case String(i.object.name).includes("circle"):
+              type = 3;
+              break;
+            case String(i.object.name).includes("pureText"):
+              type = 4;
+              break;
+          }
+
+          this.selectItem = i.object;
+          this.scene.emit("confirmDelete", {
+            id: i.object.uuid,
+            type,
+          });
+        }
+      });
+    }
+
+    if (this.mode === 1) {
+      this.raycaster.setFromCamera(this.pointerdown, this.orthCamera);
+      let intersectArr = this.scene.boxManager.imgList;
+      const intersects = this.raycaster.intersectObjects(intersectArr, false);
+      console.log("intersects", intersects);
+      if (intersects[0]) {
+        this.startObj = intersects[0].object;
+        this.drawing = true;
+      } else {
+        this.startObj = null;
+        this.drawing = false;
+      }
+    }
+
+    if (this.mode === 2) {
+      if (this.marker) {
+        this.raycaster.setFromCamera(this.pointerdown, this.orthCamera);
+        let intersectArr = this.scene.boxManager.imgList;
+        const intersects = this.raycaster.intersectObjects(intersectArr, false);
+
+        if (intersects[0]) {
+          // this.drawing = false;
+          const imageId = intersects[0].object.userData;
+          let lasPos = new THREE.Vector3(
+            this.pointerdown.x,
+            this.pointerdown.y,
+            -1
+          );
+          lasPos.unproject(this.orthCamera);
+          lasPos.y = 5;
+          const marker = new Marker(lasPos, imageId);
+
+          const activeMarkeritem = {
+            id: imageId,
+            point: lasPos.toArray(),
+          };
+          const exist = this.renderMarkers.find((item) => item.id === imageId);
+
+          if (!exist) {
+            this.scene.scene.add(marker);
+            this.renderMarkers.push(activeMarkeritem);
+            this.scene.scene.remove(this.marker);
+            this.marker = null;
+          } else {
+            this.scene.emit("markerExist");
+          }
+
+          console.log("activeMarkeritem", activeMarkeritem);
+          this.setMode(0);
+        }
+      }
+    }
+
+    if (this.mode === 3) {
+      if (this.symbol) {
+        let lasPos = new THREE.Vector3(
+          this.pointerdown.x,
+          this.pointerdown.y,
+          -1
+        );
+        lasPos.unproject(this.orthCamera);
+        lasPos.y = 5;
+
+        const activeSymbolItem = {
+          index: this.symbolIndex,
+          point: lasPos.toArray(),
+        };
+        this.renderSymbols.push(activeSymbolItem);
+        console.log("activeSymbolItem", activeSymbolItem);
+        this.setMode(0);
+      }
+    }
+    if (this.mode === 4) {
+      if (this.text) {
+        let lasPos = new THREE.Vector3(
+          this.pointerdown.x,
+          this.pointerdown.y,
+          -1
+        );
+        this.scene.emit("edit", {
+          type: 4,
+          text: this.showText,
+          id: this.text.uuid,
+          ...this.text.userData,
+        });
+        this.drawing = true;
+        // const activeSymbolItem = {
+        //   id: this.symbolIndex,
+        //   point: lasPos.toArray(),
+        // };
+
+        // this.setMode(0);
+      }
+    }
+  };
+  onPointerUp = (e) => {
+    this.pointerup = convertScreenToNDC(e, this.scene.domElement);
+    // console.log("onPointerUp", this.pointerup);
+    if (this.mode === 1) {
+      this.drawing = false;
+      this.floorplanControls.enabled = true;
+
+      if (this.drawLine) {
+        const points = this.drawLine.userData.points;
+        const dir = this.drawLine.userData.dir;
+        const imageId = this.touchImg.object.userData;
+        const finishLine = new LinePoints(points, this.matLine, dir, imageId);
+        // console.log("startObj", this.startObj);
+        this.scene.scene.add(finishLine);
+        const activeLineItem = {
+          id: finishLine.uuid,
+          imgId: imageId,
+          startId: this.startObj.userData,
+          dir: dir,
+          points: points,
+        };
+        const activeEdgeItem = {
+          id: imageId,
+          startId: this.startObj.userData,
+          dir: [dir],
+        };
+
+        this.renderLines.push(activeLineItem);
+        // console.log("this.touchImg", activeLineItem, points);
+        this.insertActiveEdge(activeEdgeItem);
+        this.scene.scene.remove(this.drawLine);
+        this.drawLine = null;
+        this.startObj = null;
+      }
+    }
+    if (this.mode === 2) {
+      // this.drawing = false;
+    }
+    if (this.mode === 4) {
+      if (this.text) {
+        let pos = new THREE.Vector3(this.pointerup.x, this.pointerup.y, -1);
+        pos.unproject(this.orthCamera);
+        pos.y = 5;
+        const clamp = new THREE.Vector3();
+        this.scene.boxManager.obb.clampPoint(pos, clamp);
+        clamp.y = 5;
+        console.log("pos", pos);
+        console.log("clamp", clamp);
+        this.text.position.copy(clamp);
+        this.text.userData.pos = clamp.toArray();
+        const activeTextItem = this.text.userData;
+        this.drawing = false;
+        console.log("activeTextItem", activeTextItem);
+        this.insertrenderTexts(activeTextItem);
+      }
+    }
+    this.syncDrawData();
+  };
+
+  Listener = {
+    onPointerDown: this.onPointerDown.bind(this),
+    onPointerMove: this.onPointerMove.bind(this),
+    onPointerUp: this.onPointerUp.bind(this),
+  };
+
+  onBindEvent = () => {
+    this.scene.domElement.addEventListener(
+      "pointerdown",
+      this.Listener.onPointerDown
+    );
+    this.scene.domElement.addEventListener(
+      "pointermove",
+      this.Listener.onPointerMove,
+      false
+    );
+    this.scene.domElement.addEventListener(
+      "pointerup",
+      this.Listener.onPointerUp
+    );
+  };
+  unbindEvent = () => {
+    this.scene.domElement.removeEventListener(
+      "pointerdown",
+      this.Listener.onPointerDown
+    );
+    this.scene.domElement.removeEventListener(
+      "pointermove",
+      this.Listener.onPointerMove
+    );
+    this.scene.domElement.removeEventListener(
+      "pointerup",
+      this.Listener.onPointerUp
+    );
+  };
+
+  buildLine = () => {
+    if (this.drawLine) {
+      this.drawLine.removeFromParent();
+    }
+    let s = new THREE.Vector3(this.pointerdown.x, this.pointerdown.y, -1);
+    let e = new THREE.Vector3(this.pointermove.x, this.pointermove.y, -1);
+    s.unproject(this.orthCamera);
+    e.unproject(this.orthCamera);
+    s.y = 5;
+    e.y = 5;
+    const matLine = new LineMaterial({
+      color: this.lineColor,
+      linewidth: 4, // in world units with size attenuation, pixels otherwise
+      dashed: false,
+      alphaToCoverage: true,
+    });
+    matLine.resolution = new THREE.Vector2(this.scene.width, this.scene.height);
+    this.drawLine = new Line(s, e, this.activeEdge, matLine);
+    this.scene.scene.add(this.drawLine);
+  };
+
+  setActiveLine = (obj) => {
+    function getTouchLine(x, y) {
+      // [0 - 1]
+      x -= 0.5;
+      y -= 0.5;
+      // console.log(x, y);
+      if (x >= 0 && y >= 0) {
+        if (x > y) {
+          return 3;
+        } else {
+          return 0;
+        }
+      } else if (x >= 0 && y <= 0) {
+        if (x > Math.abs(y)) {
+          return 3;
+        } else {
+          return 2;
+        }
+      } else if (x <= 0 && y >= 0) {
+        if (Math.abs(x) > y) {
+          return 1;
+        } else {
+          return 0;
+        }
+      } else if (x <= 0 && y <= 0) {
+        if (-x > -y) {
+          return 1;
+        } else {
+          return 2;
+        }
+      }
+    }
+
+    if (this.activeEdge) {
+      this.activeEdge.visible = false;
+      this.activeEdge = null;
+    }
+
+    let num = getTouchLine(obj.uv.x, obj.uv.y);
+    this.activeEdge = obj.object.touchLines.getObjectByName(num);
+    this.activeEdge.visible = true;
+    this.buildLine();
+  };
+
+  insertActiveEdge(item) {
+    const exist = this.activeEdges.find((s) => item.id === s.id);
+    if (exist) {
+      exist.dir = [...new Set([...exist.dir, ...item.dir])];
+    } else {
+      this.activeEdges.push(item);
+    }
+  }
+
+  // insertActiveMarker(item) {
+  //   const exist = this.activeEdges.find((s) => item.id === s.id);
+  //   if (exist) {
+  //     exist.dir = [...new Set([...exist.dir, ...item.dir])];
+  //   } else {
+  //     this.activeEdges.push(item);
+  //   }
+  // }
+
+  insertrenderTexts(item) {
+    const index = this.renderTexts.findIndex((s) => item.id === s.id);
+    if (index > -1) {
+      this.renderTexts[index] = item;
+    } else {
+      this.renderTexts.push(item);
+    }
+  }
+
+  showAllActiveEdges() {
+    if (this.inited) {
+      let imgList = this.scene.boxManager.imgList;
+      if (this.activeEdges.length > 0) {
+        this.activeEdges.forEach((edge) => {
+          const exist = imgList.find((item) => item.userData === edge.id);
+          if (exist) {
+            let others = [0, 1, 2, 3].filter((x) => !edge.dir.includes(x));
+            // console.log("others", others);
+            edge.dir.forEach((dir) => {
+              exist.touchLines.children[dir].visible = true;
+            });
+            // console.log("others", others);
+            others.forEach((dir) => {
+              exist.touchLines.children[dir].visible = false;
+            });
+          }
+        });
+      } else {
+        imgList.forEach((img) => {
+          // console.log("img", img);
+          img.touchLines.children.forEach((line) => {
+            if (line.visible) {
+              line.visible = false;
+            }
+          });
+        });
+      }
+    }
+  }
+  deleteItemByType(type, data) {
+    if (type === 1) {
+      const index = this.renderMarkers.findIndex((mk) => {
+        const p = new THREE.Vector3().fromArray(mk.point);
+        const v = new THREE.Vector3().fromArray(data.point);
+        return p.equals(v);
+      });
+      this.renderMarkers.splice(index, 1);
+    }
+    if (type === 2) {
+      const { imgId, id, dir, points } = data;
+      const index = this.renderLines.findIndex((item) => item.id === id);
+      index > -1 && this.renderLines.splice(index, 1);
+      //线段处理完成
+      const egIndex = this.activeEdges.findIndex((eg) => eg.id === imgId);
+      if (egIndex > -1) {
+        //存在activeEdge 再找renderLines的sibling
+        const cluEgArr = this.renderLines
+          .filter((l) => l.imgId === this.activeEdges[egIndex].id)
+          .reduce((pre, curr) => pre.concat(curr["dir"]), []);
+        const uni_dir = [...new Set(cluEgArr)];
+        console.log("uni_dir", uni_dir);
+        if (uni_dir.length > 0) {
+          this.activeEdges[egIndex].dir = uni_dir;
+        } else {
+          console.log("全空", this.activeEdges[egIndex].id);
+          let imgList = this.scene.boxManager.imgList;
+          const image = imgList.find(
+            (item) => item.userData === this.activeEdges[egIndex].id
+          );
+          image.touchLines.children.forEach((line) => {
+            line.visible = false;
+          });
+          this.activeEdges.splice(egIndex, 1);
+          this.update();
+        }
+      }
+    }
+    if (type === 3) {
+      const index = this.renderSymbols.findIndex((syb) => {
+        const p = new THREE.Vector3().fromArray(syb.point);
+        const v = new THREE.Vector3().fromArray(data);
+        return p.equals(v);
+      });
+      this.renderSymbols.splice(index, 1);
+    }
+    if (type === 4) {
+      const { id } = data;
+      const index = this.renderTexts.findIndex((item) => item.id === id);
+      index > -1 && this.renderTexts.splice(index, 1);
+    }
+  }
+  editing(item) {
+    if (item.type === 4) {
+      if (this.text) {
+        const { pos } = this.text.userData;
+        const newP = new THREE.Vector3().fromArray(pos);
+        this.scene.scene.remove(this.text);
+        this.text = null;
+        this.showText = item.text;
+        console.log("editing", item, newP);
+        // // console.log("this.text", lastPos, newP, item);
+        this.text = new PureTextLabel(
+          item.text,
+          newP,
+          item.fontsize,
+          item.color,
+          item.id
+        );
+        this.scene.scene.add(this.text);
+        const activeTextItem = this.text.userData;
+        console.log("activeTextItem", activeTextItem);
+        this.insertrenderTexts(activeTextItem);
+      }
+    }
+  }
+  getDrawData() {
+    let data;
+    if (this.scene.sceneType === 1) {
+      data = {
+        hor_lines: this.renderLines,
+        hor_activeEdges: this.activeEdges,
+        hor_markers: this.renderMarkers,
+        hor_symbols: this.renderSymbols,
+        hor_texts: this.renderTexts,
+        vir_lines: [],
+        vir_activeEdges: [],
+        vir_markers: [],
+        vir_symbols: [],
+        vir_texts: [],
+      };
+    } else {
+      data = {
+        hor_lines: [],
+        hor_activeEdges: [],
+        hor_markers: [],
+        hor_symbols: [],
+        hor_texts: [],
+        vir_lines: this.renderLines,
+        vir_activeEdges: this.activeEdges,
+        vir_markers: this.renderMarkers,
+        vir_symbols: this.renderSymbols,
+        vir_texts: this.renderTexts,
+      };
+    }
+
+    // console.log("sceneType", this.scene.sceneType);
+    return data;
+  }
+
+  syncDrawData() {
+    const data = this.getDrawData();
+    this.scene.emit("data", data);
+  }
+  load(type, data) {
+    if (type === 1) {
+      console.log("data1", data);
+      const {
+        hor_activeEdges,
+        hor_lines,
+        hor_markers,
+        hor_symbols,
+        hor_texts,
+      } = data;
+      hor_activeEdges && (this.activeEdges = hor_activeEdges);
+      if (hor_lines && Array.isArray(hor_lines)) {
+        this.renderLines = hor_lines;
+        hor_lines.forEach((line) => {
+          const finishLine = new LinePoints(
+            line.points,
+            this.matLine,
+            line.dir,
+            line.imgId,
+            line.id
+          );
+          this.scene.scene.add(finishLine);
+        });
+      }
+      if (hor_markers && Array.isArray(hor_markers)) {
+        this.renderMarkers = hor_markers;
+        hor_markers.forEach((pos) => {
+          console.log("pos");
+          const p = new THREE.Vector3().fromArray(pos.point);
+          const marker = new Marker(p, pos.id);
+          this.scene.scene.add(marker);
+        });
+      }
+
+      if (hor_symbols && Array.isArray(hor_symbols)) {
+        this.renderSymbols = hor_symbols;
+        hor_symbols.forEach((syb) => {
+          console.log("pos");
+          const p = new THREE.Vector3().fromArray(syb.point);
+          const symbol = new CircleTextLabel(syb.index, p);
+          this.scene.scene.add(symbol);
+        });
+      }
+      if (hor_texts && Array.isArray(hor_texts)) {
+        this.renderTexts = hor_texts;
+        hor_texts.forEach((txt) => {
+          console.log("pos");
+          const p = new THREE.Vector3().fromArray(txt.pos);
+          const text = new PureTextLabel(
+            txt.text,
+            p,
+            txt.fontsize,
+            txt.color,
+            txt.id
+          );
+          this.scene.scene.add(text);
+        });
+      }
+    }
+
+    if (type === 2) {
+      const {
+        vir_activeEdges,
+        vir_lines,
+        vir_markers,
+        vir_symbols,
+        vir_texts,
+      } = data;
+      vir_activeEdges && (this.activeEdges = vir_activeEdges);
+      if (vir_lines && Array.isArray(vir_lines)) {
+        this.renderLines = vir_lines;
+        vir_lines.forEach((line) => {
+          const finishLine = new LinePoints(
+            line.points,
+            this.matLine,
+            line.dir,
+            line.imgId,
+            line.id
+          );
+          this.scene.scene.add(finishLine);
+        });
+      }
+      if (vir_markers && Array.isArray(vir_markers)) {
+        this.renderMarkers = vir_markers;
+        vir_markers.forEach((pos) => {
+          const p = new THREE.Vector3().fromArray(pos.point);
+          const marker = new Marker(p);
+          this.scene.scene.add(marker);
+        });
+      }
+      if (vir_symbols && Array.isArray(vir_symbols)) {
+        this.renderSymbols = vir_symbols;
+        vir_symbols.forEach((syb) => {
+          // console.log("pos", syb);
+          const p = new THREE.Vector3().fromArray(syb.point);
+          const symbol = new CircleTextLabel(syb.index, p);
+          this.scene.scene.add(symbol);
+        });
+      }
+      if (vir_texts && Array.isArray(vir_texts)) {
+        this.renderTexts = vir_texts;
+        vir_texts.forEach((txt) => {
+          console.log("pos");
+          const p = new THREE.Vector3().fromArray(txt.pos);
+          const text = new PureTextLabel(
+            txt.text,
+            p,
+            txt.fontsize,
+            txt.color,
+            txt.id
+          );
+          this.scene.scene.add(text);
+        });
+      }
+    }
+    this.syncDrawData();
+  }
+  reset() {
+    if (this.marker) {
+      this.scene.scene.remove(this.marker);
+      this.marker = null;
+    }
+    if (this.drawLine) {
+      this.scene.scene.remove(this.drawLine);
+      this.drawLine = null;
+    }
+    if (this.touchImg) {
+      this.touchImg = null;
+    }
+    if (this.activeEdge) {
+      this.activeEdge = null;
+    }
+    if (this.text) {
+      this.text = null;
+      this.scene.scene.remove(this.text);
+    }
+    this.showText = "文本";
+    this.drawing = false;
+  }
+
+  clear() {
+    this.activeEdges = [];
+    this.renderLines = [];
+    this.renderMarkers = [];
+    this.renderSymbols = [];
+    this.renderTexts = [];
+    this.reset();
+    this.scene.clearDrawScene();
+    this.syncDrawData();
+  }
+
+  //单多张图片删除时要删除相关数据
+  deleteImageDataByIds(ids) {
+    setTimeout(() => {
+      console.warn("单多张图片删除时要删除相关数据", ids);
+      this.clear();
+      this.scene.emit("autoSave");
+      // this.t_deleteImageDataByIds(ids);
+    }, 500);
+  }
+  t_deleteImageDataByIds(ids) {
+    ids.forEach((id) => {
+      const makerIndex = this.renderMarkers.findIndex((item) => item.id === id);
+      if (makerIndex > -1) {
+        this.renderMarkers.splice(makerIndex, 1);
+      }
+      const lines = this.renderLines.filter(
+        (item) => item.imgId === id || item.startId === id
+      );
+
+      lines.forEach((line) => {
+        const edge = this.activeEdges.find(
+          (item) => item.id === line.imgId || item.startId === line.imgId
+        );
+        if (edge) {
+          if (edge.dir.length > 0) {
+            const dirIndex = Array.from(edge.dir).findIndex(
+              (d) => d === line.dir
+            );
+            edge.dir.splice(dirIndex, 1);
+          } else {
+            const indexEdge = this.activeEdges.findIndex(
+              (a) => a.id === edge.id
+            );
+            console.warn("edge没dir", indexEdge);
+            indexEdge > -1 && this.activeEdges.splice(indexEdge, 1);
+          }
+        }
+
+        const lineIndex = this.renderLines.findIndex(
+          (item) => item.imageId === line.imageId
+        );
+        console.log("lineIndex", lineIndex);
+        if (lineIndex > -1) {
+          this.renderLines.splice(lineIndex, 1);
+        }
+      });
+      console.log("lines", lines);
+    });
+
+    setTimeout(() => {
+      this.syncDrawData();
+      this.scene.emit("autoSave");
+    }, 2500);
+  }
+
+  checkDeleteing() {
+    const makers = this.scene.scene.children.filter((obj) =>
+      String(obj.name).includes("marker_")
+    );
+    Array.from(makers).forEach((marker) => {
+      const { imageId, point } = marker.userData;
+      const image = this.scene.boxManager.imgList.find(
+        (item) => item.userData === imageId
+      );
+      if (image) {
+        const nPoint = new THREE.Vector3().fromArray(point);
+        nPoint.project(this.orthCamera);
+        console.log("nPoint", nPoint, point);
+
+        this.raycaster.setFromCamera(nPoint, this.orthCamera);
+        const intersects = this.raycaster.intersectObjects(
+          this.scene.boxManager.imgList,
+          false
+        );
+
+        if (image.userData !== intersects[0].object.userData) {
+          console.log("相交不正确");
+          this.scene.scene.remove(marker);
+        }
+      }
+      // const
+    });
+  }
+
+  update = () => {
+    if (this.floorplanControls.enabled) {
+      this.floorplanControls && this.floorplanControls.update();
+      this.scene.boxManager && this.showAllActiveEdges();
+    }
+  };
+}

+ 8 - 0
src/core/save.json

@@ -0,0 +1,8 @@
+{
+    "hor_lines": [],
+    "vir_lines": [],
+    "hor_markers": [],
+    "vir_markers": [],
+    "hor_activeEdges": [],
+    "vir_activeEdges": []
+}

+ 0 - 0
src/core/settings/style.js


+ 92 - 0
src/core/utils/cap.js

@@ -0,0 +1,92 @@
+import * as THREE from "three";
+
+export function screenshotObject(obj, camera, renderer) {
+  this.obj = obj;
+  this.camera = camera;
+  this.renderer = renderer;
+  this.box = new THREE.Box3().setFromObject(obj);
+  this.size = { w: 0, h: 0 };
+  this.pos = { x: 0, y: 0 };
+
+  var distance = this.distance();
+  this.size = this.getSizeInPixel(distance);
+  this.pos = this.getPositionInPixel();
+  debugger;
+  this.getImage(this.size.w, this.size.h, this.pos.x, this.pos.y);
+}
+
+screenshotObject.prototype.distance = function () {
+  var self = this;
+  var size = new THREE.Vector3();
+  self.box.getSize(size);
+  var z = self.camera.position.z - self.obj.position.z - size.z / 2;
+  // or use self.camera.position.distanceTo( self.obj.position );
+  return z;
+};
+screenshotObject.prototype.getSizeInPixel = function (distance) {
+  var self = this;
+  var size = new THREE.Vector3();
+  self.box.getSize(size);
+
+  // Calc visible height and width
+  var vFOV = THREE.MathUtils.degToRad(self.camera.far); // convert vertical fov to radians
+  var height = 2 * Math.tan(vFOV / 2) * Math.abs(distance); // visible height
+  var width =
+    height * (self.renderer.domElement.width / self.renderer.domElement.height); // visible width
+  // Calc ratio between pixel and visible z-unit of threejs
+  var ratio = self.renderer.domElement.height / height;
+
+  var width = size.x * ratio;
+  var height = size.y * ratio;
+  return { w: width, h: height };
+};
+screenshotObject.prototype.getPositionInPixel = function () {
+  var self = this;
+  var vector = new THREE.Vector3();
+  var viewProjectionMatrix = new THREE.Matrix4();
+  var viewMatrix = new THREE.Matrix4();
+  viewMatrix.copy(self.camera.matrixWorldInverse);
+  viewProjectionMatrix.multiplyMatrices(
+    self.camera.projectionMatrix,
+    viewMatrix
+  );
+  var widthHalf = 0.5 * self.renderer.domElement.width;
+  var heightHalf = 0.5 * self.renderer.domElement.height;
+  self.obj.updateMatrixWorld();
+  vector.setFromMatrixPosition(self.obj.matrixWorld);
+  //vector.project(camera);
+  vector.applyMatrix4(viewProjectionMatrix);
+
+  vector.x = vector.x * widthHalf + widthHalf;
+  vector.y = -(vector.y * heightHalf) + heightHalf;
+
+  var x = vector.x - self.size.w / 2;
+  var y = vector.y - self.size.h / 2;
+  return { x: x, y: y };
+};
+screenshotObject.prototype.getImage = function (w, h, x, y) {
+  var self = this;
+  var oldCanvas = self.renderer.domElement;
+  var newCanvas = document.createElement("canvas");
+  newCanvas.width = w;
+  newCanvas.height = h;
+  var newContext = newCanvas.getContext("2d");
+  newContext.drawImage(oldCanvas, x, y, w, h, 0, 0, w, h);
+
+  var fileName = "test.png";
+  var strMime = "image/png";
+  var strDownloadMime = "image/octet-stream";
+  var imgData = newCanvas.toDataURL(strMime);
+  var base64str = imgData.replace(strMime, strDownloadMime);
+
+  var link = document.createElement("a");
+  if (typeof link.download === "string") {
+    document.body.appendChild(link); //Firefox requires the link to be in the body
+    link.download = fileName;
+    link.href = base64str;
+    link.click();
+    document.body.removeChild(link); //remove the link when done
+  } else {
+    window.location.replace(uri);
+  }
+};

+ 14 - 0
src/core/utils/text.js

@@ -0,0 +1,14 @@
+export const getWrapText = (ctx, text = "", maxWidth = 200) => {
+  let txtList = [];
+  let str = "";
+  for (let i = 0, len = text.length; i < len; i++) {
+    str += text.charAt(i);
+    if (ctx.measureText(str).width > maxWidth) {
+      txtList.push(str.substring(0, str.length - 1));
+      str = "";
+      i--;
+    }
+  }
+  txtList.push(str);
+  return txtList;
+};

+ 39 - 0
src/core/utils/utils.js

@@ -0,0 +1,39 @@
+
+
+export function dataURItoBlob(dataURI) {
+  // convert base64 to raw binary data held in a string
+  // doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
+  var byteString = atob(dataURI.split(",")[1]);
+
+  // separate out the mime component
+  var mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0];
+
+  // write the bytes of the string to an ArrayBuffer
+  var ab = new ArrayBuffer(byteString.length);
+
+  // create a view into the buffer
+  var ia = new Uint8Array(ab);
+
+  // set the bytes of the buffer to the correct values
+  for (var i = 0; i < byteString.length; i++) {
+    ia[i] = byteString.charCodeAt(i);
+  }
+
+  // write the ArrayBuffer to a blob, and you're done
+  var blob = new Blob([ab], { type: mimeString });
+  return blob;
+}
+
+export const saveFile = function (strData, filename) {
+  var link = document.createElement("a");
+  if (typeof link.download === "string") {
+    document.body.appendChild(link); //Firefox requires the link to be in the body
+    link.download = filename;
+    link.href = strData;
+    link.click();
+    document.body.removeChild(link); //remove the link when done
+  } else {
+    location.replace(uri);
+  }
+};
+

+ 11 - 7
src/hook/pagging.ts

@@ -65,7 +65,7 @@ type Operate<P, RAW, NRAW> = {
   set?: (param: RAW) => void;
   add?: (param: NRAW) => void;
   del?: (param: RAW) => void;
-  mapper?: { delMsg?: string };
+  mapper?: { delMsg?: string | (() => string) };
 };
 
 type PaggingProps<P, RAW, NRAW> = Operate<P, RAW, NRAW> & {
@@ -148,21 +148,25 @@ export const usePagging = <PARAM, RAW, NRAW>(
 
   return {
     async del(data: RAW) {
-      if (await confirm(props.mapper?.delMsg || "确定要删除此数据吗?")) {
+      const msg =
+        typeof props.mapper?.delMsg === "function"
+          ? props.mapper?.delMsg()
+          : props.mapper?.delMsg;
+      if (await confirm(msg || "确定要删除此数据吗?")) {
         const result = await itemAPI.del!(data);
         ElMessage.success("删除成功");
         return result;
       }
     },
     async deleteSelected() {
+      const msg =
+        typeof props.mapper?.delMsg === "function"
+          ? props.mapper?.delMsg()
+          : props.mapper?.delMsg;
       const rows = tableOperate.tableState.value.selectRows;
       if (rows.length === 0) {
         ElMessage.error("请勾选数据后再删除数据!");
-      } else if (
-        await confirm(
-          props.mapper?.delMsg || `确定要删除这${rows.length}条数据吗?`
-        )
-      ) {
+      } else if (await confirm(msg || `确定要删除这${rows.length}条数据吗?`)) {
         await Promise.all(rows.map((item) => itemAPI?.del!(item as any)));
         ElMessage.success("删除成功");
         refresh();

+ 1 - 1
src/main.ts

@@ -27,5 +27,5 @@ $ico.setAttribute("type", "image/svg+xml");
 $ico.setAttribute("href", appConstant.ico);
 document.head.appendChild($ico);
 
-import "./setSystem";
+// import "./setSystem";
 

+ 30 - 2
src/request/urls.ts

@@ -96,6 +96,7 @@ export const deleteScene = "/web/scene/delete";
 /** ------------------------------------------ */
 
 // ---------example案件接口--------
+export const copyExample = "/fusion/case/copyCase";
 export const exampleList = "/fusion/case/list";
 export const setExample = "/fusion/case/addOrUpdate";
 export const deleteExample = "/fusion/case/delete";
@@ -177,6 +178,20 @@ export const insertCaseFile = "/fusion/caseFiles/add";
 export const deleteCaseFile = "/fusion/caseFiles/delete";
 export const updateCaseFile = "/fusion/caseFiles/updateTitle";
 
+//勘验笔录信息
+export const caseInquestInfo = "/fusion/caseInquest/info";
+export const caseInquestOpt = "/fusion/caseInquest/saveOrUpdate";
+export const caseInquestExport = "/fusion/caseInquest/downDocx";
+
+//提取清单
+export const caseExtractDetail = "/fusion/caseExtractDetail/info";
+export const caseExtractDetailOpt = "/fusion/caseExtractDetail/saveOrUpdate";
+export const caseExtractDetailExport = "/fusion/caseExtractDetail/downDocx";
+
+//标注
+export const getCaseImgTag = "/fusion/caseImgTag/info";
+export const saveCaseImgTag = "/fusion/caseImgTag/saveOrUpdate";
+
 // 火调链接地址设置密码
 export const setCasePsw = "/fusion/web/fireProject/updateRandomCode";
 export const getCasePsw = "/fusion/web/fireProject/getRandCode";
@@ -204,6 +219,9 @@ export const uploadAttachFile = "/web/fireProject/uploadFile";
 export const uploadAttachImage = "/web/fireProject/uploadImage";
 /** ------------------------------------------ */
 
+export const checkCaseHasDownload = `/fusion/offlinePackage/checkDown`;
+export const getCaseHasDownloadProcess = `/fusion/offlinePackage/process`;
+export const downloadCaseScene = "/fusion/offlinePackage/down";
 // 下载校验
 export const checkHasDownload = "/fusion/sceneDownLog/checkDownLoad";
 // 下载获取进度条
@@ -216,6 +234,16 @@ export const offLine = "/web/fireProject/offLine"; //{roomId}
 export const onLine = "/web/fireProject/onLine"; //{roomId}
 export const onLineCheck = "/web/fireProject/onLineCheck";
 
+// 照片制卷
+export const caseApiList = "/fusion/caseImg/list";
+export const saveApiOrUpdate = "/fusion/caseImg/saveOrUpdate";
+export const uploadImagesAndSave = "/fusion/caseImg/addBatch";
+export const caseApiDel = "/fusion/caseImg/delete";
+export const caseApiUpdateSort = "/fusion/caseImg/updateSort";
+
 //settting
-export const getSysSetting = `/fusion-xj/systemSetting/info`;
-export const updateSysSetting = `/fusion-xj/systemSetting/save`;
+export const getSysSetting = `/fusion/systemSetting/info`;
+export const updateSysSetting = `/fusion/systemSetting/save`;
+
+//相片合成
+export const ffmpegMergeImage = `/fusion/caseImg/ffmpegImage`;

+ 6 - 0
src/router/config.ts

@@ -105,5 +105,11 @@ export const routes: Routes = [
     component: () => import("@/view/case/draw/index.vue"),
     meta: { title: "绘制卷宗图" },
   },
+  {
+    name: RouteName.noCase,
+    path: "/no-case",
+    component: () => import("@/view/case/no-case.vue"),
+    meta: { title: "案件不存在" },
+  },
 
 ];

+ 1 - 0
src/router/routeName.ts

@@ -16,6 +16,7 @@ export const RouteName = {
   sceneInitiator: "sceneInitiator",
   sceneVisitor: "sceneVisitor",
   setting: "setting",
+  noCase: "no-case",
 } as const;
 
 type RouteNamesType = typeof RouteName;

+ 87 - 2
src/store/case.ts

@@ -7,21 +7,55 @@ import {
   setCasePsw,
   syncInfo,
   updateCaseFile,
+  caseApiList,
+  saveApiOrUpdate,
+  uploadImagesAndSave,
+  caseApiDel,
+  caseApiUpdateSort,
+  caseInquestInfo,
+  caseInquestOpt,
+  caseInquestExport,
+  caseExtractDetail,
+  caseExtractDetailOpt,
+  caseExtractDetailExport,
+  copyExample,
+  saveCaseImgTag,
+  getCaseImgTag,
+  ffmpegMergeImage
 } from "@/request";
 import { ModelScene, QuoteScene, Scene, SceneType } from "./scene";
 import { CaseFile } from "./caseFile";
 
 export type Case = {
+  value: Case;
   caseId: number;
   caseTitle: string;
-  mapUrl: string;
-  latAndLong: string;
   createTime: string;
   name: string;
   tbStatus: string;
   userName: number;
 };
 
+export type CaseImg = {
+  id: number;
+  caseId: number;
+  imgInfo: string | null;
+  imgUrl: string | null;
+  status: number | null;
+  sort: number | null;
+};
+
+export type filesItem = {
+  caseId: number;
+  imgInfo: string | null;
+  imgUrl: string | null;
+};
+
+export type AllSaveFile = {
+  imgUrls: filesItem[];
+};
+
+
 export const setCaseSharePWD = (params: { caseId: number; randCode: string }) =>
   axios.post(setCasePsw, params);
 
@@ -31,6 +65,8 @@ export const getCaseSharePWD = async (params: { caseId: number }) =>
 export const getCaseInfo = async (caseId: number) =>
   (await axios.get<Case>(caseInfo, { params: { caseId } })).data;
 
+export const copyCase = (caseId: number) => axios.post(copyExample, { caseId });
+
 export const updateCaseInfo = async (caseFile: CaseFile) =>
   await axios.post(updateCaseFile, {
     filesId: caseFile.filesId,
@@ -71,3 +107,52 @@ export const getCaseScenes = (scenes: Scene[]) => {
 
 export const replaceCaseScenes = (caseId: number, caseScenes: CaseScenes) =>
   axios.post(repCaseScenes, { sceneNumParam: caseScenes, caseId });
+
+export const caseImgList = (caseId: number, orderBy: string | null) =>
+  axios.post(caseApiList, { orderBy: orderBy || "", caseId });
+
+export const saveOrUpdate = (params: CaseImg) =>
+  axios.post(saveApiOrUpdate, { ...params });
+
+export const saveOrAndSave = (params: AllSaveFile) =>
+  axios.post(uploadImagesAndSave, { ...params });
+
+export const caseDel = (id: number) => axios.post(caseApiDel, { id });
+
+export const caseUpdateSort = (list: [CaseImg]) =>
+  axios.post(caseApiUpdateSort, { paramList: list });
+
+export const getCaseInquestInfo = (caseId: number) =>
+  axios.get(caseInquestInfo, { params: { caseId } });
+
+export const saveCaseInquestInfo = (caseId: number, data) =>
+  axios.post(caseInquestOpt, { caseId, ...data });
+
+export const exportCaseInquestInfo = (caseId: number) =>
+  axios.get(caseInquestExport, {
+    params: { caseId, ingoreRes: true },
+    responseType: "blob",
+  });
+
+export const getCaseDetailInfo = (caseId: number) =>
+  axios.get(caseExtractDetail, { params: { caseId } });
+
+export const saveCaseDetailInfo = (caseId: number, data) =>
+  axios.post(caseExtractDetailOpt, { caseId, ...data });
+
+export const exportCaseDetailInfo = (caseId: number) =>
+  axios.get(caseExtractDetailExport, {
+    params: { caseId, ingoreRes: true },
+    responseType: "blob",
+  });
+
+// 
+
+export const saveCaseImgTagData = (params: any) =>
+  axios.post(saveCaseImgTag, { ...params });
+
+export const getCaseImgTagData = (caseId: number) =>
+  axios.get(getCaseImgTag, { params: { caseId } });
+
+
+export const submitMergePhotos = (data) => axios.post(ffmpegMergeImage, { ...data })

+ 1 - 1
src/view/case/addCaseFile.vue

@@ -13,7 +13,7 @@
         :disabled="!!file"
         :before-upload="upload"
         :file-list="fileList"
-        :http-request="() => {}"
+        :http-request="async() => {}"
         :on-preview="previewFile"
         :accept="accept"
         :before-remove="removeFile"

+ 152 - 0
src/view/case/addPhotoFile.vue

@@ -0,0 +1,152 @@
+<template>
+  <el-form
+    ref="form"
+    :model="caseFile"
+    label-width="90px"
+    class="camera-from dispatch-file-from"
+  >
+    <el-form-item label="附件:" class="mandatory">
+      <el-upload
+        class="upload-demo"
+        :multiple="false"
+        :limit="1"
+        :disabled="!!file"
+        :before-upload="upload"
+        :file-list="fileList"
+        :http-request="httpsApi"
+        :on-preview="previewFile"
+        :accept="photoFormats"
+        :before-remove="removeFile"
+      >
+        <el-button type="primary" :disabled="!!file">
+          <el-icon><Upload /></el-icon>上传
+        </el-button>
+        <template v-slot:tip>
+          <div class="el-upload__tip">注:可上传{{ size }}以内的{{ photoFormatDesc }}</div>
+        </template>
+        <template v-slot:file="{ file }">
+          <div class="file" @click.stop="previewFile()">
+            <div>
+              <el-icon><Document /></el-icon>
+              <span class="name">{{ file.name }}</span>
+            </div>
+            <el-icon v-if="!caseFile.id" @click.stop="removeFile()"><Close /></el-icon>
+          </div>
+        </template>
+      </el-upload>
+    </el-form-item>
+    <el-form-item label="附件标题:" class="mandatory">
+      <el-input
+        v-model="caseFile.imgInfo"
+        placeholder="请输入最多不能超过50字"
+        maxlength="50"
+        show-word-limit
+      ></el-input>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import {
+  DrawFormatDesc,
+  DrawFormats,
+  photoFormats,
+  photoFormatDesc,
+  FileDrawType,
+  OtherFormatDesc,
+  OtherFormats,
+} from "@/constant/caseFile";
+import { uploadFile } from "@/store/system";
+import { maxFileSize } from "@/constant/caseFile";
+import { useUpload } from "@/hook/upload";
+import { saveOrUpdate, CaseImg } from "@/store/case";
+import { ElMessage } from "element-plus";
+import { computed, ref, watch, watchEffect } from "vue";
+import { QuiskExpose } from "@/helper/mount";
+
+const props = defineProps<{
+  caseId: number;
+  data: CaseImg;
+}>();
+
+const caseFile = ref<CaseImg>({
+  caseId: props.caseId,
+  id: props.data?.id,
+  imgUrl: props.data.imgUrl,
+  imgInfo: props.data.imgInfo,
+  sort: props.data?.sort || '',
+} as any);
+
+const { size, fileList, upload, removeFile, previewFile, file, accept } = useUpload({
+  maxSize: maxFileSize,
+  formats: photoFormats,
+});
+
+const formatDesc = computed(() =>
+DrawFormatDesc
+);
+watch(props, newValue => {
+  caseFile.value.id = newValue.data.id;
+  caseFile.value.imgInfo = newValue.data.imgInfo;
+  caseFile.value.imgUrl = newValue.data.imgUrl;
+  caseFile.value.sort = newValue.data.sort;
+  if(newValue.data.imgUrl){
+    file.value = {
+    name: newValue.data.imgInfo || '',
+    url: newValue.data.imgUrl || '',
+  }
+  }
+},{ immediate: true })
+watchEffect(() => {
+  if (file.value?.name) {
+    caseFile.value.imgInfo = file.value?.name.substring(0, 50);
+  }
+});
+
+const httpsApi = async ({file})=> {
+  console.log('httpsApi', file)
+  let fileUrl = await uploadFile(file);
+
+  file.value = {
+    name: file.name,
+    url: fileUrl,
+  }
+  console.log('httpsApi', file, fileUrl)
+}
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    if (!file.value) {
+      ElMessage.error("请上传附件");
+      throw "请上传附件";
+    } else if (!caseFile.value.imgInfo.trim()) {
+      ElMessage.error("附件标题不能为空!");
+      throw "附件标题不能为空!";
+    }
+    console.log('defineExpose', caseFile.value, file.value)
+    let imgUrl = file.value && file.value.value ? file.value.value?.url : file.value?.url
+    await saveOrUpdate({ ...caseFile.value, imgUrl });
+    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;
+  }
+}
+</style>

+ 193 - 0
src/view/case/addPhotoFileAll.vue

@@ -0,0 +1,193 @@
+<template>
+  <el-form
+    ref="form"
+    :model="caseFile"
+    label-width="90px"
+    class="camera-from dispatch-file-from"
+  >
+    <el-form-item label="照片:" class="mandatory">
+      <el-upload
+        class="upload-demo"
+        :multiple="true"
+        :limit="10"
+        :before-upload="upload"
+        v-model:file-list="fileList"
+        :http-request="httpsApi"
+        :on-preview="previewFile"
+        :on-exceed="handleExceed"
+        :accept="photoFormats"
+        :on-remove="handleRemove"
+      >
+        <el-button type="primary" :disabled="!!file">
+          <el-icon><Upload /></el-icon>上传
+        </el-button>
+        <template v-slot:tip>
+          <div class="el-upload__tip">注:可上传{{ size }}以内的{{ photoFormatDesc }}</div>
+        </template>
+        <template v-slot:file="{ file }">
+          <div class="file" @click.stop="previewFile()">
+            <div>
+              <el-icon><Document /></el-icon>
+              <span class="name">{{ file.name }}</span>
+            </div>
+            <el-icon @click.stop="removeFile(file)"><Close /></el-icon>
+          </div>
+        </template>
+      </el-upload>
+    </el-form-item>
+    <!-- <el-form-item label="附件标题:" class="mandatory">
+      <el-input
+        v-model="caseFile.imgInfo"
+        placeholder="请输入最多不能超过50字"
+        maxlength="50"
+        show-word-limit
+      ></el-input>
+    </el-form-item> -->
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import {
+  DrawFormatDesc,
+  DrawFormats,
+  photoFormats,
+  photoFormatDesc,
+  FileDrawType,
+  OtherFormatDesc,
+  OtherFormats,
+} from "@/constant/caseFile";
+import { uploadFile } from "@/store/system";
+import { maxFileSize } from "@/constant/caseFile";
+import { useUpload } from "@/hook/upload";
+import { saveOrAndSave, CaseImg } from "@/store/case";
+import { ElMessage } from "element-plus";
+import { computed, ref, watch, watchEffect } from "vue";
+import { QuiskExpose } from "@/helper/mount";
+import type { UploadProps } from 'element-plus'
+
+const props = defineProps<{
+  caseId: number;
+  data: CaseImg;
+}>();
+
+const defaultUpload = (
+  file: File,
+  onPercentage: (percentage: number) => void
+) => {
+  onPercentage(100);
+};
+
+const caseFile = ref<CaseImg>({
+  caseId: props.caseId,
+  id: props.data?.id,
+  imgUrl: props.data.imgUrl,
+  imgInfo: props.data.imgInfo,
+  sort: props.data?.sort || '',
+} as any);
+
+const { size, previewFile, file, accept, percentage, format } = useUpload({
+  maxSize: maxFileSize,
+  formats: DrawFormats,
+});
+const handleExceed = () => {
+  return ElMessage.error(`最大上传数量为10个`);
+}
+const upload = async (file: File) => {
+    const fileType = file.name
+      .substring(file.name.lastIndexOf("."))
+      .toUpperCase();
+
+    if (!photoFormats.some((type) => type.toUpperCase() === fileType)) {
+      ElMessage.error(`请上传${photoFormatDesc}`);
+      return false;
+    } else if (file.size > maxFileSize) {
+      ElMessage.error(`请上传${size.value}以内的文件`);
+      return false;
+    } else {
+      console.log('file', file)
+      fileList.value.push(file);
+      await defaultUpload(
+        file,
+        (val) => (percentage.value = val)
+      );
+      if (fileType === ".RAW") {
+      }
+      percentage.value = undefined;
+      return true;
+    }
+  };
+const fileList = ref([])
+const formatDesc = computed(() =>
+DrawFormatDesc
+);
+watch(props, newValue => {
+  caseFile.value.id = newValue.data.id;
+  caseFile.value.imgInfo = newValue.data.imgInfo;
+  caseFile.value.imgUrl = newValue.data.imgUrl;
+  caseFile.value.sort = newValue.data.sort;
+  if(newValue.data.imgUrl){
+    file.value = {
+    name: newValue.data.imgInfo || '',
+    url: newValue.data.imgUrl || '',
+  }
+  }
+},{ immediate: true })
+watchEffect(() => {
+  if (file.value?.name) {
+    caseFile.value.imgInfo = file.value?.name.substring(0, 50);
+  }
+});
+const handleRemove: UploadProps['onRemove'] = (uploadFile, uploadFiles) => {
+  console.log(uploadFile, uploadFiles)
+}
+const removeFile = (file) => {
+  fileList.value = fileList.value.filter((item) => item.raw.url !== file.raw.url);
+};
+const httpsApi = async ({file})=> {
+  console.log('httpsApi', file)
+  let fileUrl = await uploadFile(file);
+
+  file.url = fileUrl
+  console.log('httpsApi', file, fileUrl)
+}
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    console.log('defineExpose', fileList.value)
+    if (!fileList.value.length) {
+      ElMessage.error("请上传照片");
+      throw "请上传照片";
+    }
+    console.log('defineExpose', caseFile.value, file.value)
+    let imgUrls = fileList.value.map(item => {
+      return {
+        imgUrl: item.raw && item.raw.url,
+        imgInfo: item.name.replace(/\.[^/.]+$/, ""),
+        caseId: props.caseId,
+      }
+    })
+    await saveOrAndSave({ imgUrls });
+    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;
+  }
+}
+</style>

+ 1 - 1
src/view/case/addScenes.vue

@@ -52,7 +52,7 @@ watch(
   () => params.pagging.state.query,
   () => {
     params.pagging.state.query.status = 2;
-    // params.pagging.state.query.caseId = props.caseId;
+    params.pagging.state.query.caseId = props.caseId;
   },
   { immediate: true, deep: true }
 );

+ 129 - 65
src/view/case/caseFile.vue

@@ -1,64 +1,93 @@
 <template>
-  <com-head :options="options" v-model="currentTypeId" notContent v-if="options.length" />
+  <com-head
+    :options="options"
+    v-model="currentTypeId"
+    notContent
+    v-if="options.length"
+  />
 
   <div class="body-layer">
-    <div class="body-head">
-      <h3 style="visibility: hidden">场景管理</h3>
-      <div>
-        <template v-if="isDraw">
-          <el-button type="primary" @click="gotoDraw(BoardType.map, -1)">
-            创建{{ BoardTypeDesc[BoardType.map] }}
+    <template v-if="currentTypeId === 2">
+      <Photos :caseId="caseId" :title="caseInfoData.caseTitle" />
+    </template>
+    <template v-else-if="currentTypeId === 3">
+      <Records :caseId="caseId" :title="caseInfoData.caseTitle" />
+    </template>
+    <template v-else-if="currentTypeId === 4">
+      <Manifest :caseId="caseId" :title="caseInfoData.caseTitle" />
+    </template>
+    <template v-else>
+      <div class="body-head">
+        <h3 style="visibility: hidden">场景管理</h3>
+        <div>
+          <template v-if="isDraw">
+            <el-button type="primary" @click="gotoDraw(BoardType.map, -1)">
+              创建{{ BoardTypeDesc[BoardType.map] }}
+            </el-button>
+            <el-button type="primary" @click="gotoDraw(BoardType.scene, -1)">
+              创建{{ BoardTypeDesc[BoardType.scene] }}
+            </el-button>
+          </template>
+          <el-button type="primary" @click="addCaseFileHandler">
+            上传
           </el-button>
-          <el-button type="primary" @click="gotoDraw(BoardType.scene, -1)">
-            创建{{ BoardTypeDesc[BoardType.scene] }}
-          </el-button>
-        </template>
-        <el-button type="primary" @click="addCaseFileHandler"> 上传 </el-button>
+        </div>
       </div>
-    </div>
 
-    <el-table :data="files" tooltip-effect="dark" style="width: 100%" size="large">
-      <el-table-column label="序号" width="70" v-slot:default="{ $index }">
-        <div style="text-align: center">
-          {{ $index + 1 }}
-        </div>
-      </el-table-column>
-      <el-table-column label="名称" v-slot:default="{ row }: { row: CaseFile }">
-        <span v-if="!inputCaseTitles.includes(row)">
-          {{ row.filesTitle }}
-          <el-icon class="edit-title" @click="inputCaseTitles.push(row)">
-            <EditPen />
-          </el-icon>
-        </span>
-        <template v-else>
-          <ElInput
-            v-model="row.filesTitle"
-            placeholder="请输入文件名"
-            focus
-            :maxlength="50"
-            style="width: 280px"
-          >
-            <template #append>
-              <el-button type="primary" plain @click="updateFileTitle(row)">
-                确定
-              </el-button>
-            </template>
-          </ElInput>
-        </template>
-      </el-table-column>
-      <el-table-column label="创建时间" prop="createTime"></el-table-column>
-      <el-table-column label="操作" v-slot:default="{ row }: { row: CaseFile }">
-        <span class="oper-span" @click="query(row)"> 查看 </span>
-        <span
-          class="oper-span"
-          @click="gotoDraw(row.imgType!, row.filesId)"
-          v-if="row.imgType !== null"
+      <el-table
+        :data="files"
+        tooltip-effect="dark"
+        style="width: 100%"
+        size="large"
+      >
+        <el-table-column label="序号" width="70" v-slot:default="{ $index }">
+          <div style="text-align: center">
+            {{ $index + 1 }}
+          </div>
+        </el-table-column>
+        <el-table-column
+          label="名称"
+          v-slot:default="{ row }: { row: CaseFile }"
+        >
+          <span v-if="!inputCaseTitles.includes(row)">
+            {{ row.filesTitle }}
+            <el-icon class="edit-title" @click="inputCaseTitles.push(row)">
+              <EditPen />
+            </el-icon>
+          </span>
+          <template v-else>
+            <ElInput
+              v-model="row.filesTitle"
+              placeholder="请输入文件名"
+              focus
+              :maxlength="50"
+              style="width: 280px"
+            >
+              <template #append>
+                <el-button type="primary" plain @click="updateFileTitle(row)">
+                  确定
+                </el-button>
+              </template>
+            </ElInput>
+          </template>
+        </el-table-column>
+        <el-table-column label="创建时间" prop="createTime"></el-table-column>
+        <el-table-column
+          label="操作"
+          v-slot:default="{ row }: { row: CaseFile }"
         >
-          编辑
-        </span>
-        <span class="oper-span delBtn" @click="del(row)"> 删除 </span>
-      </el-table-column>
-    </el-table>
+          <span class="oper-span" @click="query(row)"> 查看 </span>
+          <span
+            class="oper-span"
+            @click="gotoDraw(row.imgType!, row.filesId)"
+            v-if="row.imgType !== null"
+          >
+            编辑
+          </span>
+          <span class="oper-span delBtn" @click="del(row)"> 删除 </span>
+        </el-table-column>
+      </el-table>
+    </template>
   </div>
 </template>
 
@@ -81,6 +110,9 @@ import {
 import { getCaseInfo, updateCaseInfo } from "@/store/case";
 import { appConstant } from "@/app";
 import { ElIcon, ElInput, ElMessage } from "element-plus";
+import Photos from "./photos/index.vue";
+import Records from "./records/index.vue";
+import Manifest from "./records/manifest.vue";
 
 const caseId = computed(() => {
   const caseId = router.currentRoute.value.params.caseId;
@@ -88,6 +120,7 @@ const caseId = computed(() => {
     return Number(caseId);
   }
 });
+const caseInfoData = ref<any>();
 
 const inputCaseTitles = ref<CaseFile[]>([]);
 
@@ -96,13 +129,18 @@ const updateFileTitle = async (caseFile: CaseFile) => {
     return ElMessage.error("卷宗标题不能为空!");
   }
   await updateCaseInfo(caseFile);
-  inputCaseTitles.value = inputCaseTitles.value.filter((item) => item !== caseFile);
+  inputCaseTitles.value = inputCaseTitles.value.filter(
+    (item) => item !== caseFile
+  );
 };
 
 const currentTypeId = ref<number>();
 const types = ref<CaseFileType[]>([]);
 const options = computed(() =>
-  types.value.map((item) => ({ name: item.filesTypeName, value: item.filesTypeId }))
+  types.value.map((item) => ({
+    name: item.filesTypeName,
+    value: item.filesTypeId,
+  }))
 );
 const isDraw = computed(() => currentTypeId.value === FileDrawType);
 
@@ -115,7 +153,19 @@ const refresh = async () => {
 };
 watchEffect(() => caseId.value && currentTypeId.value && refresh());
 
-const query = (file: CaseFile) => window.open(file.filesUrl + "?time=" + Date.now());
+const query = (file: CaseFile) => {
+  const ext = file.filesUrl
+    .substring(file.filesUrl.lastIndexOf("."))
+    .toLocaleLowerCase();
+  if ([".raw", ".dcm"].includes(ext)) {
+    window.open(
+      `/xfile-viewer/index.html?file=${file.filesUrl}&name=${file.filesTitle}&time=` +
+        Date.now()
+    );
+  } else {
+    window.open(file.filesUrl + "?time=" + Date.now());
+  }
+};
 const del = async (file: CaseFile) => {
   if (await confirm("确定要删除此数据?")) {
     await delCaseFile({ caseId: caseId.value!, filesId: file.filesId });
@@ -136,15 +186,29 @@ const gotoDraw = (type: BoardType, id: number) => {
 };
 
 onMounted(async () => {
-  types.value = await getCaseFileTypes();
-  currentTypeId.value = types.value[0].filesTypeId;
-  const caseInfo = await getCaseInfo(caseId.value!);
-  if (caseInfo) {
-    title.value = (await getCaseInfo(caseId.value!)).caseTitle + " | 卷宗管理";
-    desc.value = "";
-  } else {
+  try {
+    types.value = await getCaseFileTypes();
+    currentTypeId.value = types.value[0].filesTypeId;
+    const caseInfo = await getCaseInfo(caseId.value!);
+    if (caseInfo) {
+      caseInfoData.value = caseInfo;
+      title.value =
+        (await getCaseInfo(caseId.value!)).caseTitle + " | 卷宗管理";
+      desc.value = "";
+    } else {
+      console.error("该案件不存在!");
+      throw "该案件不存在!";
+    }
+  } catch (error) {
     console.error("该案件不存在!");
-    router.replace({ name: RouteName.vrmodel });
+    // debugger;
+    //TODO 由于没有登录状态可以判断或hook插入,只能延时进入no-case router当前的router
+    setTimeout(() => {
+      console.log("current-router", router.currentRoute.value.name);
+      if (router.currentRoute.value.name !== "login") {
+        router.replace({ name: RouteName.noCase });
+      }
+    }, 1000);
   }
 });
 

+ 142 - 0
src/view/case/download.vue

@@ -0,0 +1,142 @@
+<template>
+  <!-- hideFloor: state === State.package -->
+  <div>
+    <div class="title">
+      {{ stateTitle[state] }}
+    </div>
+
+    <div v-if="state === State.package">
+      <div
+        class="text"
+        style="display: flex; justify-content: space-between; margin-top: 15px"
+      >
+        <span>{{ filename }}</span>
+        <span>{{ percent }}%</span>
+      </div>
+      <div style="pointer-events: none">
+        <el-slider v-model="percent" :show-tooltip="false" />
+      </div>
+    </div>
+    <div v-else-if="state === State.readDown">
+      <span>正在下载中……</span>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, onUnmounted, ref } from "vue";
+import saveAs from "@/util/file-serve";
+import {
+  checkCaseHasDownload as checkHasDownload,
+  getCaseHasDownloadProcess as getDownloadProcess,
+  downloadCaseScene as downloadScene,
+  axios,
+} from "@/request";
+import { ElLoading, ElMessage } from "element-plus";
+import { QuoteScene, SceneType } from "@/store/scene";
+import { QuiskExpose } from "@/helper/mount";
+
+const props = defineProps<{ caseId: number; title: string }>();
+enum State {
+  uncreate,
+  package,
+  readDown,
+}
+const getState = (type: number) => {
+  const stateTypes = [
+    { codes: [0, 2], state: State.uncreate },
+    { codes: [1], state: State.package },
+    { codes: [3], state: State.readDown },
+  ];
+  return (
+    stateTypes.find((stateType) => stateType.codes.includes(type))?.state ||
+    State.uncreate
+  );
+};
+
+const state = ref<State>(State.uncreate);
+const count = ref<number>(0);
+const filename = ref<string>(props.title + ".zip");
+const downloadURL = ref<string>();
+const percent = ref(0);
+
+const stateTitle = {
+  [State.uncreate]: "下载案件离线数据包,可在本地运行查看。",
+  [State.package]: "正在打包案件离线数据",
+  [State.readDown]: filename.value,
+};
+
+const params = {
+  caseId: props.caseId,
+};
+// 初始化
+const initial = async () => {
+  const res = await axios.get(checkHasDownload, { params });
+  state.value = getState(res.data.downloadStatus);
+  count.value = res.data.count;
+  downloadURL.value = res.data.downloadUrl;
+
+  if (state.value === State.uncreate) {
+    const downRes = await axios.get(downloadScene, { params });
+    state.value = getState(downRes.data.downloadStatus);
+    // const unCountFlag =
+    //   count.value == 0 ||(await axios.get(downloadScene, { params })).data.downloadStatus !== 1;
+    if (state.value === State.uncreate) {
+      ElMessage.error("下载失败,请联系管理员");
+      throw "暂无剩余下载次数";
+    }
+  }
+
+  if (state.value === State.package) {
+    await new Promise<void>((resolve) => requestUpdateURL(resolve));
+  } else {
+    downloadURL.value = res.data.downloadUrl;
+  }
+};
+
+// 下载
+const download = () => {
+  if (!downloadURL.value) {
+    ElMessage.error("下载链接未生成,请稍等!");
+    throw "下载链接未生成,请稍等!";
+  } else {
+    if (!downloadURL.value.startsWith("/")) {
+      downloadURL.value = "/" + downloadURL.value;
+    }
+    console.error("downloadURL.value", downloadURL.value);
+    return saveAs(downloadURL.value, filename.value);
+  }
+};
+
+// 进度请求
+let timer: any;
+const requestUpdateURL = async (callback: () => void) => {
+  const res = await axios.get(getDownloadProcess, { params });
+
+  percent.value = parseInt(res.data.percent);
+
+  downloadURL.value = percent.value === 100 ? res.data.url : null;
+  if (downloadURL.value) {
+    state.value = State.readDown;
+    callback();
+  } else {
+    timer = setTimeout(() => requestUpdateURL(callback), 1000);
+  }
+};
+
+onUnmounted(() => clearTimeout(timer));
+
+defineExpose<QuiskExpose>({
+  submit: async () => {
+    await initial();
+    const loading = ElLoading.service({
+      lock: true,
+      text: "下载中",
+      background: "rgba(255, 255, 255, 0.4)",
+    });
+    await download();
+    loading.close();
+    ElMessage.success("下载完成");
+  },
+});
+</script>

+ 3 - 0
src/view/case/draw/c.ts

@@ -0,0 +1,3 @@
+import { ref } from "vue";
+
+export const inputIng = ref(false);

+ 1 - 1
src/view/case/draw/eshape.vue

@@ -21,6 +21,7 @@
 import { computed, onMounted, onUnmounted, ref } from "vue";
 import { BoardShape, compass, title } from "./board";
 import { components } from "./edit-shape";
+import { inputIng } from "./c";
 
 const props = defineProps<{ shape: BoardShape }>();
 const emit = defineEmits<{
@@ -33,7 +34,6 @@ const editComponent = computed(() => {
     return components[type];
   }
 });
-const inputIng = ref(false);
 
 const delHandler = () => {
   props.shape.delete();

+ 7 - 4
src/view/case/draw/header.vue

@@ -45,6 +45,7 @@
 import { BoardType } from "@/store/caseFile";
 import { BoardTypeDesc } from "@/constant/caseFile";
 import { onMounted, onUnmounted } from "vue";
+import { inputIng } from "./c";
 
 const props = defineProps<{
   type: BoardType;
@@ -67,10 +68,12 @@ const keydownHandler = (ev: KeyboardEvent) => {
   } else if (["Control", "Ctrl"].includes(ev.key)) {
     const downKey = ev.key;
     const secondaryHandler = (ev: KeyboardEvent) => {
-      if (ev.key.toUpperCase() === "Y" && !props.forwardDisabled) {
-        emit("forward");
-      } else if (ev.key.toUpperCase() === "Z" && !props.backDisabled) {
-        emit("back");
+      if (!inputIng.value) {
+        if (ev.key.toUpperCase() === "Y" && !props.forwardDisabled) {
+          emit("forward");
+        } else if (ev.key.toUpperCase() === "Z" && !props.backDisabled) {
+          emit("back");
+        }
       }
     };
     window.addEventListener("keydown", secondaryHandler);

+ 1 - 1
src/view/case/draw/slider.vue

@@ -107,7 +107,7 @@ const emit = defineEmits<{
 const cover = reactive(
   useUpload({
     maxSize: maxFileSize,
-    formats: [".jpg", ".png"],
+    formats: [".jpg", ".png", ".raw"],
   })
 );
 

+ 24 - 9
src/view/case/editMenu.vue

@@ -8,12 +8,8 @@
     </span>
     <template #dropdown>
       <el-dropdown-menu>
-        <el-dropdown-item
-          v-for="menu in menus"
-          :key="menu.key"
-          @click="menu.onClick()"
-          :disabled="!operateIsPermissionByPath('edit', menu.key)"
-        >
+        <!-- :disabled="!operateIsPermissionByPath('edit', menu.key)" -->
+        <el-dropdown-item v-for="menu in menus" :key="menu.key" @click="menu.onClick()">
           {{ menu.label }}
         </el-dropdown-item>
       </el-dropdown-menu>
@@ -24,14 +20,14 @@
 <script setup lang="ts">
 import { computed } from "vue";
 import { getFuseCodeLink, checkScenesOpen, MenuItem, getSWKKSyncLink } from "./help";
-import { showCaseScenes, addCaseScenes, shareCase } from "./quisk";
+import { showCaseScenes, addCaseScenes, shareCase, downloadCase } from "./quisk";
 import { RouteName, router } from "@/router";
-import { getCaseSceneList } from "@/store/case";
+import { copyCase, getCaseSceneList } from "@/store/case";
 import { alert } from "@/helper/message";
-import { operateIsPermissionByPath } from "@/directive/permission";
 
 const props = defineProps<{
   caseId: number;
+  title: string;
   prevMenu?: MenuItem[];
   lastMenu?: MenuItem[];
 }>();
@@ -101,6 +97,25 @@ const menus = computed(() => {
         }
       },
     },
+    {
+      key: "fj",
+      label: "分镜配置",
+      onClick: async () => {
+        window.open(`mirror.html?caseId=${caseId}`);
+      },
+    },
+    {
+      key: "download",
+      label: "下载",
+      onClick: async () => {
+        const scenes = await getCaseSceneList(caseId);
+        if (!scenes.length) {
+          alert("当前案件下无场景,请先添加场景。");
+        } else {
+          downloadCase({ caseId, title: props.title });
+        }
+      },
+    },
     ...(props.lastMenu || []).map((item) => ({
       ...item,
       onClick: () => item.onClick(caseId),

+ 3 - 6
src/view/case/help.ts

@@ -39,12 +39,7 @@ export const getFuseCodeLink = (caseId: number, query?: boolean) => {
 
 export const getSWKKSyncLink = async (caseId: number) => {
   const scenes = await getCaseSceneList(caseId);
-  const supportTypes = [
-    SceneType.SWKK,
-    SceneType.SWKJ,
-    SceneType.SWSSMX,
-    SceneType.SWYDMX,
-  ];
+  const supportTypes = [SceneType.SWKJ, SceneType.SWSSMX, SceneType.SWYDMX];
   const kkScenes = scenes.filter((scene) =>
     supportTypes.includes(scene.type)
   ) as QuoteScene[];
@@ -301,6 +296,8 @@ export enum OpenType {
 export const openSceneUrl = async (scene: Scene, type: OpenType) => {
   const pathname = SceneTypePaths[scene.type][type];
   const url = new URL(pathname || "", window.location.href);
+
+  url.searchParams.append("lang", "zh");
   if (scene.type === SceneType.SWMX) {
     url.searchParams.append(
       "modelId",

+ 47 - 0
src/view/case/no-case.vue

@@ -0,0 +1,47 @@
+<template>
+  <div class="no-data">
+    <img :src="emptyBG" />
+    <span>案件不存在</span>
+    <el-button
+      v-if="showBtn"
+      class="btn"
+      link
+      text
+      plain
+      style="margin-top: 20px"
+      @click="toHome"
+      >回到首页</el-button
+    >
+  </div>
+</template>
+<script lang="ts" setup>
+import emptyBG from "@/assets/image/empty__empty.png";
+import { RouteName, router } from "@/router";
+const props = withDefaults(
+  defineProps<{
+    showBtn: boolean;
+  }>(),
+  {
+    showBtn: true,
+  }
+);
+const toHome = () => {
+  router.replace({ name: RouteName.vrmodel });
+};
+</script>
+<style lang="scss" scoped>
+.no-data {
+  width: 100%;
+  height: 100%;
+  /* background: red; */
+  min-height: 530px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  .btn,
+  span {
+    color: #999;
+  }
+}
+</style>

+ 209 - 0
src/view/case/photos/draggable.vue

@@ -0,0 +1,209 @@
+<template>
+  <div class="VueDraggable">
+    <el-input
+      @change="handleSearch"
+      clearable
+      size="medium"
+      placeholder="请输入内容"
+      suffix-icon="search"
+      v-model="search"
+      class="input-with-select"
+    >
+    </el-input>
+    <VueDraggable
+      v-if="list.length"
+      :move="search ? false : null"
+      class="draggable"
+      ref="el"
+      v-model="list"
+      @sort="onChange"
+    >
+      <div
+        class="item"
+        v-for="(item, index) in search ? searchList : list"
+        :key="item.id"
+        @click="handleItem(item.id)"
+      >
+        <img class="itemImg" :src="item.imgUrl" alt="" />
+        <div class="text">
+          <div :title="item.imgInfo">{{ item.imgInfo }}</div>
+          <EditPen @click.stop="handleEdit(item)" class="EditPen" />
+        </div>
+        <CircleCloseFilled
+          @click.stop="handleDet(index, item.id)"
+          class="itemIcon"
+        />
+      </div>
+    </VueDraggable>
+    <el-empty class="empty" v-else description="请上传现场照片" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, watch, computed } from "vue";
+import { caseImgList, CaseImg, caseDel, caseUpdateSort } from "@/store/case";
+import { VueDraggable } from "vue-draggable-plus";
+import { openErrorMsg } from "@/request/errorMsg.js";
+import { addCaseImgFile } from "../quisk";
+import { ElMessage, ElMessageBox } from "element-plus";
+// import { IconRabbish } from '@element-plus/icons-vue'
+const props = defineProps({ sortType: Boolean, caseId: Number });
+const emit = defineEmits<{
+  (e: "changeList", value: CaseImg[] | null): void;
+  (e: "handleItem", value: Number | null): void;
+}>();
+const list = ref<CaseImg[]>([]);
+const search = ref("");
+const searchList = computed(() => {
+  let searchText = search.value.toLowerCase();
+  return list.value.filter((item) => {
+    return item.imgInfo?.toLowerCase().includes(searchText);
+  });
+});
+
+watch(
+  () => props.sortType,
+  (newValue, oldValue) => {
+    emit("changeList", list.value);
+  },
+  { deep: true, immediate: true }
+);
+
+async function onChange({ newIndex, oldIndex }) {
+  if (search.value) {
+    openErrorMsg("搜索状态禁止拖动");
+    return;
+  }
+  const imageIDs = Array.from(list.value)
+    .filter((i, index) => index === newIndex || index === oldIndex)
+    .reduce((prev, current) => prev.concat(current["id"]), []);
+  console.log("draggable-imageIDs", imageIDs);
+  emit("delImage", imageIDs);
+  setTimeout(async () => {
+    let apiList = searchList.value.map((item, index) => {
+      return { ...item, sort: index + 1 };
+    });
+    console.log(apiList);
+    await caseUpdateSort(apiList);
+    emit("changeList", apiList);
+  }, 500);
+}
+function handleItem(id) {
+  setTimeout(() => {
+    let index = list.value.findIndex((item) => item.id === id);
+    console.log(index, list.value);
+    emit("handleItem", index);
+  }, 500);
+}
+function handleSearch(val: string) {
+  console.log("handleSearch", val, search.value);
+}
+async function getList() {
+  let lists = await caseImgList(props.caseId, "desc");
+  list.value = lists.data;
+  emit("changeList", list.value);
+}
+async function handleDet(index: Number, id: Number) {
+  const res = await ElMessageBox.confirm(
+    "删除图像后会重新排版并清空标记数据,是否继续?",
+    "温馨提示",
+    {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "default",
+    }
+  );
+  if (res) {
+    caseDel(id).then((res) => {
+      emit("delImage", [id]);
+      list.value.splice(index, 1);
+      emit("changeList", list.value);
+    });
+  }
+}
+async function handleEdit(params) {
+  await addCaseImgFile({
+    caseId: props.caseId,
+    data: {
+      ...params,
+    },
+  });
+  getList();
+}
+function filterItem() {
+  let searchText = search.value.toLowerCase();
+  return list.value.filter((item) => {
+    return item.imgInfo?.toLowerCase().includes(searchText);
+  });
+}
+onMounted(() => {
+  getList();
+  // emit("update:list", props.list.value);
+});
+defineExpose({
+  getList,
+});
+</script>
+<style lang="scss" scoped>
+.empty {
+  width: 200px;
+}
+.input-with-select {
+  margin-top: 16px;
+}
+.draggable {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+  .item {
+    position: relative;
+    // flex: 0 0 50%; /* 每个子元素占用50%的宽度 */
+    width: calc(50% - 4px);
+    margin-top: 16px;
+    .itemImg {
+      width: 100%;
+      height: 62px;
+      object-fit: cover;
+    }
+    .text {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      height: 20px;
+      div {
+        width: 100%;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        overflow: hidden;
+      }
+      .EditPen {
+        width: 20px;
+        height: 20px;
+        display: none;
+      }
+      &:hover {
+        div {
+          width: calc(100% - 20px);
+        }
+        .EditPen {
+          display: block;
+        }
+      }
+    }
+    .itemIcon {
+      width: 20px;
+      height: 20px;
+      color: var(--el-color-primary);
+      position: absolute;
+      right: -10px;
+      top: -10px;
+      display: none;
+    }
+    &:hover {
+      .itemIcon {
+        display: block;
+      }
+    }
+  }
+}
+</style>

+ 154 - 0
src/view/case/photos/edit.vue

@@ -0,0 +1,154 @@
+<template>
+  <div class="layout" v-if="isShow">
+    <el-icon class="close" @click="handleClose">
+      <Close />
+    </el-icon>
+    <el-form :inline="true" :model="form" label-width="auto">
+      <el-form-item label="内容">
+        <el-input type="input" :maxlength="40" v-model="form.text" />
+      </el-form-item>
+
+      <el-form-item label="字号:">
+        <el-select
+          v-model="form.fontsize"
+          placeholder="选择字号"
+          style="width: 200px"
+        >
+          <el-option
+            v-for="item in fontSizeOptions"
+            v-bind="item"
+            :key="item.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="颜色:">
+        <el-color-picker
+          v-model="form.color"
+          color-format="rgba"
+          show-alpha
+          :predefine="predefineColors"
+        />
+      </el-form-item>
+      <el-form-item label="删除:">
+        <el-button type="primary" @click="handleDel">删除</el-button>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+<script setup>
+import { reactive, ref, watch } from "vue";
+
+const isShow = ref(false);
+const props = defineProps({ show: Boolean, data: Object });
+const emit = defineEmits(["update", "del", "close"]);
+
+const predefineColors = [
+  "#ff0f00",
+  "#ffbe00",
+  "#1a9bff",
+  "#1aad19",
+  "#000000",
+  "#ffffff",
+  "#666666",
+];
+
+watch(
+  props,
+  ({ show, data }) => {
+    isShow.value = show;
+    form.text = data.text;
+    form.id = data.id;
+    form.type = data.type;
+    form.pos = data.pos;
+    form.fontsize = data.fontsize || 12;
+    console.log("data", data);
+  },
+  {
+    deep: true,
+  }
+);
+
+// do not use same name with ref
+const defaultfrom = {
+  id: "",
+  text: "",
+  fontsize: 12,
+  type: null,
+  pos: null,
+  color: "#000000",
+};
+let form = reactive(defaultfrom);
+
+watch(
+  form,
+  () => {
+    handleUpdate();
+  },
+  {
+    deep: true,
+  }
+);
+
+const fontSizeRange = [8, 30];
+const fontSizeOptions = [];
+for (let i = fontSizeRange[0]; i <= fontSizeRange[1]; i++) {
+  fontSizeOptions.push({ value: i, label: i.toString() });
+}
+
+const handleClose = () => {
+  isShow.value = false;
+  emit("close", form);
+  form = reactive(defaultfrom);
+};
+const handleUpdate = () => {
+  emit("update", form);
+};
+
+const handleDel = () => {
+  isShow.value = false;
+  emit("del", form);
+  form = reactive(defaultfrom);
+};
+</script>
+
+<style>
+.layout {
+  position: absolute;
+  top: 0;
+  left: 50%;
+  transform: translateX(-50%);
+  right: 0;
+  background: #fff;
+  box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
+  z-index: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 15px 25px 0 10px;
+  width: fit-content;
+  /* width: 300px; */
+}
+.layout .el-form {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: flex-start;
+  max-width: 300px;
+}
+.layout .el-form-item {
+  margin-left: 10px;
+  margin-right: 10px;
+  flex: 0 0 auto;
+  display: flex;
+  align-items: center;
+}
+.close {
+  align-self: start;
+  font-size: 14px;
+  color: rgba(0, 0, 0, 0.85);
+  position: absolute !important;
+  right: 10px;
+  top: 10px;
+  cursor: pointer;
+}
+</style>

+ 552 - 0
src/view/case/photos/index.vue

@@ -0,0 +1,552 @@
+<template>
+  <div class="photo">
+    <div class="left">
+      <div class="upload my-photo-upload">
+        <!-- <el-upload
+          v-model:file-list="fileList"
+          class="upload-demo"
+          multiple
+          :show-file-list="false"
+          :http-request="handleRequest"
+          :on-change="handleChange"
+          :before-upload="handleUpload"
+          :limit="10"
+        >
+          <el-button type="primary">上传照片</el-button>
+        </el-upload> -->
+        <el-button type="primary" @click="addCaseFileHandlerAll">
+          上传照片
+        </el-button>
+        <el-button
+          type="primary"
+          @click="handleSwitchGrid"
+          :icon="sortType ? FullScreen : Menu"
+          >{{ sortType ? "横排" : "竖排" }}</el-button
+        >
+      </div>
+      <draggable
+        ref="childRef"
+        :caseId="caseId"
+        :sortType="sortType"
+        @changeList="changeList"
+        @handleItem="handleItem"
+        @delImage="handleImageDel"
+      />
+    </div>
+    <div class="right">
+      <div class="tools">
+        <el-button @click="handleMark">
+          <i class="iconfont icon-arrows1" />
+          箭头
+        </el-button>
+        <el-button @click="handleLine">
+          <i class="iconfont icon-index" />
+          标引
+        </el-button>
+        <el-button @click="handleSymbol">
+          <i class="iconfont icon-symbol" />
+          符号
+        </el-button>
+        <el-button @click="handleText">
+          <i class="iconfont icon-text1" />
+          文本</el-button
+        >
+        <el-button @click="handleSave" class="save">保存</el-button>
+        <el-button
+          @click="handleExport"
+          class="opt"
+          :loading="!isSenseLoaded"
+          :disabled="!isSenseLoaded"
+          >导出</el-button
+        >
+
+        <el-button @click="handleClear" v-if="hasDrawData" class="opt"
+          >清空</el-button
+        >
+        <el-button @click="handleFree" v-if="isShowExitEdit" class="opt"
+          >退出编辑</el-button
+        >
+      </div>
+
+      <canvas id="canvas" v-show="true"></canvas>
+      <edit
+        :show="editing.show"
+        :data="editing.data"
+        @update="handleEditingUpdate"
+        @del="handleEditingDel"
+        @close="handleEditingClose"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, ref, computed, onUnmounted, reactive } from "vue";
+import { Menu, FullScreen } from "@element-plus/icons-vue";
+import { Swiper, SwiperSlide } from "swiper/vue";
+import "swiper/css";
+// import { addCaseFile } from "@/store/caseFile";
+import { addCaseImgFile, addCaseImgFileAll } from "../quisk";
+import {
+  saveCaseImgTagData,
+  getCaseImgTagData,
+  submitMergePhotos,
+} from "@/store/case";
+import Scene from "@/core/Scene.js";
+import draggable from "./draggable.vue";
+import edit from "./edit.vue";
+import saveAs from "@/util/file-serve";
+import { ElMessage, ElMessageBox } from "element-plus";
+
+const props = defineProps({ caseId: Number, title: String });
+
+const editing = ref({
+  show: false,
+  data: {},
+});
+const newlist = ref([]);
+const fileList = ref([]);
+const swiperRef = ref(null);
+const childRef = ref(null);
+const isSenseLoaded = ref(false);
+const caseId = ref(props.caseId);
+const sortType = ref(false);
+const drawMode = ref(0);
+const isShowExitEdit = computed(() => drawMode.value > 0);
+const loadedDrawData = ref();
+const hasDrawData = ref(false);
+let scene = null;
+
+const addCaseFileHandler = async () => {
+  await addCaseImgFile({
+    caseId: caseId.value,
+    data: {
+      imgUrl: "",
+      imgInfo: "",
+      id: "",
+      sort: "",
+    },
+  });
+  refresh();
+};
+
+const addCaseFileHandlerAll = async () => {
+  await addCaseImgFileAll({
+    caseId: caseId.value,
+    data: {
+      imgUrl: "",
+      imgInfo: "",
+      id: "",
+      sort: "",
+    },
+  });
+  refresh();
+};
+
+function refresh() {
+  console.log("changeList", childRef.value);
+
+  if (childRef.value) {
+    childRef.value.getList();
+  }
+}
+const changeList = async (list) => {
+  //同步数据
+  if (!loadedDrawData.value) {
+    const res = await getCaseImgTagData(caseId.value);
+    if (res.data) {
+      if (res.data.data) {
+        loadedDrawData.value = res.data.data;
+      }
+      if ("isHorizontal" in res.data) {
+        // console.error("sortType.value", sortType.value, !res.data.isHorizontal);
+        sortType.value = !res.data.isHorizontal;
+      }
+    } else {
+      loadedDrawData.value = [];
+    }
+  }
+
+  let newList = [];
+  list.map((item, index) => {
+    if (sortType.value) {
+      newList.push([item]);
+    } else {
+      if (index % 2 == 0) {
+        let newItem = list[index + 1] ? [item, list[index + 1]] : [item];
+        newList.push(newItem);
+      }
+    }
+  });
+  newlist.value = newList;
+  const arr = [];
+  newList.map((i) => arr.push(JSON.parse(JSON.stringify(i))));
+
+  const type = sortType.value ? 2 : 1;
+
+  if (scene) {
+    scene.load(arr, type, loadedDrawData.value || []);
+    console.log("changeList", arr, type, loadedDrawData.value);
+  }
+};
+const renderCanvas = () => {
+  const canvas = document.getElementById("canvas");
+
+  scene = new Scene(canvas);
+  scene.init();
+  window.scene = scene;
+  scene.on("mode", (mode) => {
+    console.warn("mode", mode);
+    drawMode.value = mode;
+  });
+  scene.on("markerExist", () => {
+    ElMessage.error("该案件已有方向标注!");
+  });
+  scene.on("confirmDelete", async ({ id, type }) => {
+    const res = await ElMessageBox.confirm("是否删除该部件?", "温馨提示", {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "default",
+    });
+    if (res) {
+      window.scene.deleteItemById(id, type);
+    }
+  });
+  scene.on("data", (data) => {
+    let hasData = false;
+    Object.keys(data).forEach((key) => {
+      if (Array.isArray(data[key])) {
+        if (data[key].length > 0) {
+          hasData = true;
+        }
+      }
+    });
+    hasDrawData.value = hasData;
+    console.log("sync", data, hasData);
+    // editing.value.data = data;
+    loadedDrawData.value = data;
+  });
+  scene.on("edit", (editData) => {
+    console.log("editData", editData);
+    editing.value.show = true;
+    editing.value.data = editData;
+  });
+  scene.on("devicePixelRatio", () => {
+    ElMessage.error(
+      `当前浏览器缩放${Math.floor(
+        window.devicePixelRatio * 100
+      )}%比例,请设置缩放比例为100%,方可正常导出`
+    );
+  });
+  scene.on("loaded", () => {
+    isSenseLoaded.value = true;
+  });
+  scene.on("autoSave", () => {
+    console.log("autoSave");
+    handleAutoSave();
+  });
+  scene.on("submitScreenshot", (save) => {
+    if (window.scene) {
+      const params = {
+        files: window.scene.blobScreens.map(
+          (b, index) => new File([b], `${Date.now()}-${index}.jpg`)
+        ),
+        caseId: caseId.value,
+      };
+
+      setTimeout(async () => {
+        try {
+          const res = await submitMergePhotos(params);
+          console.log("res", res);
+          const { data, code } = res;
+          const title = `${props.title}-照片卷.jpg`;
+          if (data && data.imgUrl) {
+            if (save) {
+              // debugger;
+              saveAs(data.imgUrl, title);
+            }
+          }
+          window.scene.endScreenshot();
+        } catch (error) {
+          window.scene.endScreenshot();
+        }
+      }, 500);
+    }
+  });
+};
+const onSwiper = (swiper) => {
+  console.log("onSwiper");
+  swiperRef.value = swiper;
+};
+const onSlideChange = (swiper) => {
+  console.log(swiper);
+};
+const handleChange = (val, list) => {
+  fileList.value = list;
+  console.log("handleChange", val, list, fileList.value);
+};
+const handleRequest = (val, list) => {
+  console.log("handleRequest", val, list);
+};
+const handleUpload = (val) => {
+  console.log("handleUpload", val);
+};
+const handleItem = (item) => {
+  let active = sortType.value ? item : Math.floor(item / 2);
+  // swiperRef.value.slideTo(active);
+  console.log("handleItem", item, active);
+};
+const handleDetele = async (item) => {
+  if (
+    await confirm("删除该场景,将同时从案件和融合模型中移除,确定要删除吗?")
+  ) {
+    const scenes = getCaseScenes(list.value.filter((item) => item !== scene));
+    await replaceCaseScenes(props.caseId, scenes);
+    refresh();
+  }
+};
+const handleSwitchGrid = async () => {
+  const res = await ElMessageBox.confirm(
+    "切换模版不包括标注内容,确定要切换吗?",
+    "温馨提示",
+    {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "default",
+    }
+  );
+  if (res) {
+    sortType.value = !sortType.value;
+    window.scene.setMode(0);
+    window.scene.clearScene();
+    handleClear();
+  }
+};
+
+const handleLine = () => {
+  if (window.scene) {
+    window.scene.setMode(1);
+  }
+};
+const handleMark = () => {
+  if (window.scene) {
+    window.scene.setMode(2);
+  }
+};
+
+const handleSymbol = () => {
+  if (window.scene) {
+    window.scene.setMode(3);
+  }
+};
+const handleText = () => {
+  if (window.scene) {
+    window.scene.setMode(4);
+  }
+};
+const handleSave = async () => {
+  if (window.scene) {
+    const data = scene.player.getDrawData();
+    scene.player.syncDrawData();
+    console.log("data", data);
+    const res = await saveCaseImgTagData({
+      caseId: caseId.value,
+      data: data,
+      isHorizontal: !sortType.value,
+    });
+
+    console.log("res", res);
+    if (newlist.value.length > 0) {
+      window.scene.exportScreenshot(false);
+    }
+    ElMessage.success("保存成功!");
+  }
+};
+
+const handleAutoSave = async () => {
+  if (window.scene) {
+    const data = scene.player.getDrawData();
+    scene.player.syncDrawData();
+    await saveCaseImgTagData({
+      caseId: caseId.value,
+      data: data,
+      isHorizontal: !sortType.value,
+    });
+  }
+};
+const handleFree = () => {
+  if (window.scene) {
+    window.scene.setMode(0);
+  }
+};
+const handleClear = () => {
+  if (window.scene) {
+    window.scene.player.clear();
+  }
+};
+
+const handleEditingUpdate = (data) => {
+  // console.log("update", data);
+  if (window.scene) {
+    window.scene.editing(data);
+  }
+};
+const handleEditingDel = (form) => {
+  if (window.scene) {
+    const { id, type } = form;
+    console.log("handleEditingDel", form);
+    window.scene.deleteItemById(id, type);
+    window.scene.setMode(0);
+  }
+};
+const handleEditingClose = () => {
+  window.scene.setMode(0);
+};
+
+const handleImageDel = (ids) => {
+  console.log("handleImageDel", ids);
+  if (window.scene) {
+    window.scene.deleteImageDataByIds(ids);
+  }
+};
+const handleExport = () => {
+  if (window.scene) {
+    window.scene.exportScreenshot();
+  }
+};
+
+onMounted(() => {
+  renderCanvas();
+  console.warn("renderCanvas");
+});
+</script>
+<style lang="scss">
+.my-photo-upload {
+  .upload-demo {
+    display: inline-block;
+    margin-right: 20px;
+    position: relative;
+    bottom: -1px;
+    .el-upload-list {
+      display: none;
+    }
+  }
+}
+</style>
+<style lang="scss" scoped>
+#canvas {
+  width: 100%;
+  height: 100%;
+}
+
+.photo {
+  display: flex;
+  height: 100%;
+
+  .left {
+    width: 260px;
+    padding: 16px 24px 30px 0;
+    height: calc(100% - 46.16px);
+    overflow-y: auto;
+    background: #ffffff;
+    box-shadow: 10px 0 10px -10px rgba(0, 0, 0, 0.15);
+    // box-shadow: 0px 2px 8px 0px rgba(0,0,0,0.15);
+  }
+
+  .right {
+    width: calc(100% - 260px);
+    background-color: var(--bgColor);
+    padding-left: 24px;
+    height: 100%;
+    position: relative;
+    display: block;
+
+    .tools {
+      position: absolute;
+      top: 15px;
+      left: 30px;
+    }
+
+    .swiperItem {
+      height: calc(100vh - 155.16px);
+      width: calc((100vh - 156.16px) * 0.707);
+      background: #ffffff;
+
+      .swiperList {
+        padding: 0 60px;
+        height: 100%;
+
+        .page {
+          font-weight: 400;
+          font-size: 12px;
+          color: rgba(0, 0, 0, 0.85);
+          line-height: 22px;
+          text-align: right;
+          margin-top: 30px;
+        }
+
+        .itemper {
+          height: calc(50% - 100px);
+          padding: 60px 0 0 0;
+
+          .text {
+            margin-top: 16px;
+            border-radius: 0px 0px 0px 0px;
+            border: 1px dotted #cccccc;
+            text-align: center;
+            font-family: Microsoft YaHei, Microsoft YaHei;
+            font-weight: 400;
+            font-size: 14px;
+            line-height: 30px;
+            color: rgba(0, 0, 0, 0.85);
+          }
+
+          .itemImg {
+            width: 100%;
+            height: calc(100% - 48px);
+            display: block;
+            object-fit: cover;
+          }
+        }
+
+        .oneItemper {
+          height: calc(100% - 120px);
+
+          .itemImg {
+          }
+        }
+      }
+    }
+  }
+}
+</style>
+<style scoped>
+:global(.body-layer) {
+  padding-right: 0 !important;
+  overflow: hidden;
+}
+.save {
+  background-color: #24569e;
+  color: white;
+  &:hover {
+    background-color: #14396c;
+    color: white;
+    border-color: #14396c;
+  }
+}
+.opt {
+  background-color: #deeafe;
+  color: #24569e;
+  border: #24569e 1px solid;
+  &:hover {
+    background-color: #bdcce6;
+    color: #14396c;
+    border-color: #14396c;
+  }
+}
+.tools {
+  i {
+    margin-right: 4px;
+  }
+}
+</style>

+ 31 - 0
src/view/case/quisk.ts

@@ -1,3 +1,5 @@
+import addPhotoFile from "./addPhotoFile.vue";
+import addPhotoFileAll from "./addPhotoFileAll.vue";
 import AddCaseFile from "./addCaseFile.vue";
 import AddScenes from "./addScenes.vue";
 import ShareCase from "./share.vue";
@@ -9,12 +11,24 @@ import SelectFuseImage, { FuseImage } from "./draw/selectFuseImage.vue";
 import SelectMapImage, { MapImage } from "./draw/selectMapImage.vue";
 import { quiskMountFactory } from "@/helper/mount";
 import { nextTick } from "vue";
+import { axios, checkCaseHasDownload, checkHasDownload } from "@/request";
+import CaseDownload from "./download.vue";
 
 export const addCaseFile = quiskMountFactory(AddCaseFile, {
   title: "上传附件",
   width: 500,
 });
 
+export const addCaseImgFile = quiskMountFactory(addPhotoFile, {
+  title: "上传照片",
+  width: 500,
+});
+
+export const addCaseImgFileAll = quiskMountFactory(addPhotoFileAll, {
+  title: "上传照片",
+  width: 500,
+});
+
 export const addCaseScenes = quiskMountFactory(AddScenes, {
   title: "添加场景",
   width: 800,
@@ -51,3 +65,20 @@ export const shareCase = quiskMountFactory(ShareCase, {
   title: "分享",
   enterText: "复制链接及密码",
 })<string>;
+
+export type caseDownloadProps = { caseId: number; title: string };
+export const downloadCase = async (props: caseDownloadProps) => {
+  const params = {
+    caseId: props.caseId,
+  };
+  const res = await axios.get(checkCaseHasDownload, { params });
+  const hideFloor = Number(res.data.downloadStatus) !== 3;
+
+  const sceneDownloadDialog = quiskMountFactory(CaseDownload, {
+    title: "案件离线包下载",
+    width: 500,
+    hideFloor: hideFloor,
+    enterText: "下 载",
+  });
+  return await sceneDownloadDialog(props);
+};

+ 561 - 0
src/view/case/records/index.vue

@@ -0,0 +1,561 @@
+<template>
+  <!-- 勘验笔录{{ props.caseId }} -->
+  <div class="records">
+    <div class="header">
+      <el-button type="primary" @click="handleSave">保存</el-button>
+      <el-button :disabled="isDisableExport" @click="handleExport"
+        >导出</el-button
+      >
+    </div>
+    <h3 class="title">基本信息</h3>
+    <div class="content">
+      <div class="line">
+        <span>勘验次数:</span>
+        <span>第</span>
+        <el-input
+          class="input"
+          v-model="data.count"
+          placeholder=""
+          style="width: 80px"
+        />
+        <span>次勘验</span>
+      </div>
+
+      <div class="line">
+        <span>勘验时间:</span>
+        <div>
+          <el-input
+            class="input"
+            :maxlength="4"
+            type="text"
+            v-model="data.startTime.year"
+            placeholder=""
+            style="width: 80px"
+          />
+          <span>年</span>
+          <el-input
+            class="input"
+            :maxlength="2"
+            type="text"
+            v-model="data.startTime.month"
+            placeholder=""
+            style="width: 80px"
+          />
+          <span>月</span>
+          <el-input
+            class="input"
+            :maxlength="2"
+            type="text"
+            v-model="data.startTime.day"
+            placeholder=""
+            style="width: 80px"
+          />
+          <span>日</span>
+          <el-input
+            class="input"
+            :maxlength="2"
+            type="text"
+            v-model="data.startTime.hour"
+            placeholder=""
+            style="width: 80px"
+          />
+          <span>时</span>
+          <el-input
+            class="input"
+            :maxlength="2"
+            type="text"
+            v-model="data.startTime.min"
+            placeholder=""
+            style="width: 80px"
+          />
+          <span>分</span>
+        </div>
+        <span style="width: 60px; text-align: center">至</span>
+        <div>
+          <el-input
+            class="input"
+            :maxlength="4"
+            v-model="data.endTime.year"
+            placeholder=""
+            style="width: 80px"
+          />
+          <span>年</span>
+          <el-input
+            class="input"
+            :maxlength="2"
+            v-model="data.endTime.month"
+            placeholder=""
+            style="width: 80px"
+          />
+          <span>月</span>
+          <el-input
+            class="input"
+            :maxlength="2"
+            v-model="data.endTime.day"
+            placeholder=""
+            style="width: 80px"
+          />
+          <span>日</span>
+          <el-input
+            class="input"
+            :maxlength="2"
+            type="text"
+            v-model="data.endTime.hour"
+            placeholder=""
+            style="width: 80px"
+          />
+          <span>时</span>
+          <el-input
+            class="input"
+            :maxlength="2"
+            type="text"
+            v-model="data.endTime.min"
+            placeholder=""
+            style="width: 80px"
+          />
+          <span>分</span>
+        </div>
+      </div>
+
+      <div class="line">
+        <span>勘验地点:</span>
+        <el-input
+          class="input"
+          type="tel"
+          v-model="data.address"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+      <div class="line">
+        <span>勘验人员姓名、勘验人职务(含技术职务):</span>
+        <el-input
+          class="input"
+          type="tel"
+          v-model="data.userInfo"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+
+      <div class="line">
+        <span>勘验气象条件(天气、风力、温度):</span>
+        <el-input
+          class="input"
+          type="tel"
+          v-model="data.weather"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+
+      <div class="textarea">
+        <span>勘验情况:</span>
+        <el-input
+          type="textarea"
+          :rows="4"
+          v-model="data.situation"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+      <div class="textarea">
+        <span>一、环境勘验</span>
+        <el-input
+          type="textarea"
+          :rows="4"
+          v-model="data.environment"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+
+      <div class="textarea">
+        <span>二、初步勘验</span>
+        <el-input
+          type="textarea"
+          :rows="4"
+          v-model="data.firstInquest"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+
+      <div class="textarea">
+        <span>三、细项勘验</span>
+        <el-input
+          type="textarea"
+          :rows="4"
+          v-model="data.carefulInquest"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+
+      <div class="textarea">
+        <span>四、专项勘验</span>
+        <el-input
+          type="textarea"
+          :rows="6"
+          v-model="data.specialInquest"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+
+      <div class="textarea">
+        <span>提取物品描述:</span>
+        <el-input
+          type="textarea"
+          :rows="6"
+          v-model="data.itemDescription"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+
+      <div class="textarea">
+        <span>现场拍照制图描述:</span>
+        <el-input
+          type="textarea"
+          :rows="6"
+          v-model="data.imgDescription"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+
+      <div class="info">
+        <span class="sub-tit">勘验信息:</span>
+        <div class="inner">
+          <div class="sec">
+            <span>勘验负责人</span>
+            <el-input class="input" v-model="data.leader" placeholder="" />
+          </div>
+
+          <div class="sec">
+            <span> 记录人</span>
+            <el-input class="input" v-model="data.recorder" placeholder="" />
+          </div>
+
+          <div class="sec">
+            <span>勘验人</span>
+            <el-input class="input" v-model="data.inspector" placeholder="" />
+          </div>
+        </div>
+      </div>
+
+      <div class="gap"></div>
+      <!-- 证人 -->
+      <template v-for="item of data.witnessInfo">
+        <div class="witnessInfo">
+          <span class="sub-tit">证人信息:</span>
+          <div class="line">
+            <span>证人或当事人:</span>
+            <el-input
+              class="input"
+              v-model="item.name"
+              placeholder=""
+              style="width: 180px"
+            />
+            <div>
+              <el-input
+                class="input"
+                v-model="item.year"
+                placeholder=""
+                style="width: 80px"
+              />
+              <span>年</span>
+              <el-input
+                class="input"
+                v-model="item.month"
+                placeholder=""
+                style="width: 80px"
+              />
+              <span>月</span>
+              <el-input
+                class="input"
+                v-model="item.day"
+                placeholder=""
+                style="width: 80px"
+              />
+              <span>日</span>
+            </div>
+
+            <span style="margin-left: 50px">身份证件号码:</span>
+            <el-input
+              class="input"
+              v-model="item.id"
+              placeholder=""
+              style="width: 280px"
+            />
+          </div>
+          <div class="line">
+            <span>单位或住址:</span>
+            <el-input
+              class="input"
+              v-model="item.address"
+              placeholder=""
+              style="width: 100%"
+            />
+          </div>
+        </div>
+      </template>
+
+      <div class="btn-container">
+        <el-button class="btn" @click="addwitnessInfo">+新增</el-button>
+      </div>
+
+      <div></div>
+    </div>
+  </div>
+</template>
+<script setup>
+import { onMounted, ref, watch } from "vue";
+import { reactive } from "vue";
+import {
+  getCaseInquestInfo,
+  saveCaseInquestInfo,
+  exportCaseInquestInfo,
+} from "@/store/case";
+import { ElMessage } from "element-plus";
+import saveAs from "@/util/file-serve";
+const props = defineProps({ caseId: Number, title: String });
+
+console.log(props);
+const isDisableExport = ref(false);
+const data = reactive({
+  count: "",
+  startTime: {
+    year: "",
+    month: "",
+    day: "",
+    hour: "",
+    min: "",
+  },
+  endTime: {
+    year: "",
+    month: "",
+    day: "",
+    hour: "",
+    min: "",
+  },
+  address: "",
+  userInfo: "",
+  weather: "",
+  situation: "",
+  environment: "", //环境勘验
+  firstInquest: "", //初步勘验
+  carefulInquest: "", //细项勘验
+  specialInquest: "", //专项勘验
+  itemDescription: "",
+  imgDescription: "",
+  leader: "",
+  recorder: "",
+  inspector: "",
+  witnessInfo: [
+    {
+      name: "",
+      year: "",
+      month: "",
+      day: "",
+      id: "",
+      address: "",
+    },
+    {
+      name: "",
+      year: "",
+      month: "",
+      day: "",
+      id: "",
+      address: "",
+    },
+  ],
+});
+
+watch(
+  data,
+  (newValue) => {
+    // data.userName = newValue.userName.replace(/[^0-9]/g, '');
+    const sMonth = newValue.startTime.month.replace(/[^0-9]/g, "");
+    const sDay = newValue.startTime.day.replace(/[^0-9]/g, "");
+    const sHour = newValue.startTime.hour.replace(/[^0-9]/g, "");
+    const sMin = newValue.startTime.min.replace(/[^0-9]/g, "");
+
+    const eMonth = newValue.endTime.month.replace(/[^0-9]/g, "");
+    const eDay = newValue.endTime.day.replace(/[^0-9]/g, "");
+    const eHour = newValue.endTime.hour.replace(/[^0-9]/g, "");
+    const eMin = newValue.endTime.min.replace(/[^0-9]/g, "");
+
+    data.startTime.year = newValue.startTime.year.replace(/[^0-9]/g, "");
+    data.startTime.month = Number(sMonth) > 12 ? "12" : sMonth;
+    data.startTime.day = Number(sDay) > 31 ? "31" : sDay;
+    data.startTime.hour = Number(sDay) > 24 ? "24" : sHour;
+    data.startTime.min = Number(sMin) > 60 ? "0" : sMin;
+
+    data.endTime.year = newValue.endTime.year.replace(/[^0-9]/g, "");
+    data.endTime.month = Number(eMonth) > 12 ? "12" : eMonth;
+    data.endTime.day = Number(eDay) > 31 ? "31" : eDay;
+    data.endTime.hour = Number(eHour) > 24 ? "24" : eHour;
+    data.endTime.min = Number(eMin) > 60 ? "0" : eMin;
+
+    newValue.witnessInfo.forEach((item, key) => {
+      const year = newValue.witnessInfo[key].year.replace(/[^0-9]/g, "");
+      const month = newValue.witnessInfo[key].month.replace(/[^0-9]/g, "");
+      const day = newValue.witnessInfo[key].day.replace(/[^0-9]/g, "");
+      data.witnessInfo[key].year = year;
+      data.witnessInfo[key].month = Number(month) > 12 ? "12" : month;
+      data.witnessInfo[key].day = Number(day) > 31 ? "31" : day;
+    });
+  },
+  {
+    immediate: true,
+    deep: true,
+  }
+);
+
+const initInfo = async () => {
+  const res = await getCaseInquestInfo(props.caseId);
+
+  if (!res.data) {
+    isDisableExport.value = true;
+  } else {
+    isDisableExport.value = false;
+  }
+  for (var k in data) {
+    if (res.data && res.data.hasOwnProperty(k)) {
+      console.log("Key is " + k);
+      data[k] = res.data[k];
+    }
+  }
+};
+
+onMounted(() => {
+  initInfo();
+});
+
+const addwitnessInfo = () => {
+  // witnessInfoes.value += 1
+  data.witnessInfo.push({
+    name: "",
+    year: "",
+    month: "",
+    day: "",
+    id: "",
+  });
+};
+
+const handleSave = async () => {
+  console.log("data", data);
+  const res = await saveCaseInquestInfo(props.caseId, data);
+  if (res.code === 0) {
+    ElMessage.success("保存成功!");
+    initInfo();
+  }
+};
+const handleExport = async () => {
+  await saveCaseInquestInfo(props.caseId, data);
+  const res = await exportCaseInquestInfo(props.caseId);
+  console.log("res", res);
+  saveAs(res, `${props.title}_勘验笔录.docx`);
+};
+</script>
+
+<style lang="scss">
+.records {
+  max-width: 1280px;
+  margin: 0 auto;
+  padding: 20px 0;
+
+  .header {
+    display: flex;
+    justify-content: flex-endTime;
+  }
+
+  .input {
+    height: 32px;
+    line-height: 32px;
+    margin: 0 8px;
+  }
+
+  .textarea {
+    margin-right: 8px;
+    margin-bottom: 20px;
+
+    span {
+      padding: 10px 0;
+      display: inline-block;
+    }
+
+    // margin: 0 8px;
+  }
+
+  .line {
+    display: inline-flex;
+    width: 100%;
+    flex-direction: row;
+    align-items: center;
+    margin-bottom: 25px;
+    line-height: 38px;
+
+    span {
+      white-space: nowrap;
+    }
+  }
+}
+
+.title {
+  text-align: center;
+}
+
+.sub-tit {
+  display: inline-block;
+  padding-bottom: 20px;
+}
+
+.info {
+  display: block;
+
+  .inner {
+    display: flex;
+    flex-direction: row;
+    width: 100%;
+
+    .input {
+      flex: 1;
+    }
+
+    .sec {
+      flex: 1;
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+    }
+  }
+}
+
+.witnessInfo {
+  background: #f5f5f5;
+  padding: 15px;
+  margin-top: 20px;
+  margin-right: 8px;
+}
+
+.gap {
+  margin: 15px 0;
+}
+
+.btn-container {
+  padding: 20px 0;
+
+  .btn {
+    color: #26559b;
+    width: 100%;
+
+    &:hover {
+      background: #f5f5f5;
+      border-color: #dcdfe6;
+    }
+  }
+}
+</style>

+ 444 - 0
src/view/case/records/manifest.vue

@@ -0,0 +1,444 @@
+<template>
+  <div class="records">
+    <div class="header">
+      <el-button type="primary" @click="handleSave">保存</el-button>
+      <el-button :disabled="isDisableExport" @click="handleExport"
+        >导出</el-button
+      >
+    </div>
+
+    <div class="content">
+      <div class="line">
+        <span>起火单位/地址:</span>
+        <el-input
+          class="input"
+          v-model="data.address"
+          placeholder=""
+          style="width: 100%"
+        />
+      </div>
+
+      <div class="line">
+        <span>提取日期:</span>
+        <div>
+          <el-input
+            class="input"
+            :maxlength="4"
+            type="text"
+            v-model="data.time.year"
+            placeholder=""
+            style="width: 80px"
+          />
+          <span>年</span>
+          <el-input
+            class="input"
+            :maxlength="2"
+            type="text"
+            v-model="data.time.month"
+            placeholder=""
+            style="width: 80px"
+          />
+          <span>月</span>
+          <el-input
+            class="input"
+            :maxlength="2"
+            type="text"
+            v-model="data.time.day"
+            placeholder=""
+            style="width: 80px"
+          />
+          <span>日</span>
+        </div>
+      </div>
+
+      <div class="detail">
+        <span class="sub-tit">提取清单:</span>
+        <template v-for="(item, index) in data.detail">
+          <div class="con">
+            <span class="sub-tit">编号 {{ index + 1 }}: </span>
+            <div class="info">
+              <div class="inner">
+                <div class="sec">
+                  <span>名称: </span>
+                  <el-input class="input" v-model="item.name" placeholder="" />
+                </div>
+
+                <div class="sec">
+                  <span>规格: </span>
+                  <el-input class="input" v-model="item.spec" placeholder="" />
+                </div>
+
+                <div class="sec">
+                  <span>数量: </span>
+                  <el-input class="input" v-model="item.num" placeholder="" />
+                </div>
+              </div>
+              <div class="inner">
+                <div class="sec">
+                  <span>提取部位: </span>
+                  <el-input class="input" v-model="item.part" placeholder="" />
+                </div>
+              </div>
+              <div class="inner">
+                <div class="sec">
+                  <span>特征: </span>
+                  <el-input class="input" v-model="item.desc" placeholder="" />
+                </div>
+              </div>
+            </div>
+          </div>
+        </template>
+      </div>
+      <div class="btn-container">
+        <el-button class="btn" @click="addItem">+新增</el-button>
+      </div>
+      <div class="gap"></div>
+
+      <div class="extractUser">
+        <span class="sub-tit">提取人:</span>
+        <template v-for="extractUser in data.extractUser">
+          <div class="line">
+            <span>姓名:</span>
+            <el-input
+              class="input"
+              v-model="extractUser.name"
+              placeholder=""
+              style="width: 20%"
+            />
+            <span>工作单位:</span>
+            <el-input
+              class="input"
+              v-model="extractUser.address"
+              placeholder=""
+              style="width: 70%"
+            />
+          </div>
+        </template>
+      </div>
+      <div class="btn-container">
+        <el-button class="btn" @click="addextractUser">+新增</el-button>
+      </div>
+      <!-- 证人 -->
+      <div>
+        <span>证人或当事人:</span>
+        <template v-for="wit in data.witnessInfo">
+          <div class="witnessInfo">
+            <!-- <span class="sub-tit">证人信息:</span> -->
+            <div class="line">
+              <span>姓名:</span>
+              <el-input
+                class="input"
+                v-model="wit.name"
+                placeholder=""
+                style="width: 180px"
+              />
+              <span style="margin-left: 50px">身份证件号码:</span>
+              <el-input
+                class="input"
+                v-model="wit.id"
+                placeholder=""
+                style="width: 280px"
+              />
+              <span style="margin-left: 50px">联系电话:</span>
+              <el-input
+                class="input"
+                v-model="wit.phone"
+                placeholder=""
+                style="width: 280px"
+              />
+            </div>
+            <div class="line">
+              <span>单位或住址:</span>
+              <el-input
+                class="input"
+                v-model="wit.address"
+                placeholder=""
+                style="width: 100%"
+              />
+            </div>
+          </div>
+        </template>
+      </div>
+      <div class="btn-container">
+        <el-button class="btn" @click="addwitnessInfo">+新增</el-button>
+      </div>
+      <div></div>
+    </div>
+  </div>
+</template>
+<script setup>
+import { onMounted, ref, watch } from "vue";
+import { reactive } from "vue";
+import {
+  getCaseDetailInfo,
+  saveCaseDetailInfo,
+  exportCaseDetailInfo,
+} from "@/store/case";
+import saveAs from "@/util/file-serve";
+import { ElMessage } from "element-plus";
+
+const props = defineProps({ caseId: Number, title: String });
+
+const isDisableExport = ref(false);
+
+console.log(props);
+
+const data = reactive({
+  address: "",
+  time: {
+    year: "",
+    month: "",
+    day: "",
+  },
+
+  location: "",
+  detail: [
+    {
+      // id: "1",
+      name: "",
+      spec: "",
+      num: "",
+      part: "",
+      desc: "",
+    },
+    {
+      // id: "2",
+      name: "",
+      spec: "",
+      num: "",
+      part: "",
+      desc: "",
+    },
+  ],
+  extractUser: [
+    {
+      name: "",
+      workplace: "",
+      id: "",
+    },
+
+    {
+      name: "",
+      address: "",
+      id: "",
+    },
+  ],
+
+  witnessInfo: [
+    {
+      name: "",
+      address: "",
+      phone: "",
+      id: "",
+    },
+    {
+      name: "",
+      address: "",
+      phone: "",
+      id: "",
+    },
+  ],
+});
+
+watch(
+  data,
+  (newValue) => {
+    // data.userName = newValue.userName.replace(/[^0-9]/g, '');
+    const sMonth = newValue.time.month.replace(/[^0-9]/g, "");
+    const sDay = newValue.time.day.replace(/[^0-9]/g, "");
+
+    data.time.year = newValue.time.year.replace(/[^0-9]/g, "");
+    data.time.month = Number(sMonth) > 12 ? "12" : sMonth;
+    data.time.day = Number(sDay) > 31 ? "31" : sDay;
+  },
+  {
+    immediate: true,
+    deep: true,
+  }
+);
+
+onMounted(() => {});
+
+const addwitnessInfo = () => {
+  data.witnessInfo.push({
+    name: "",
+    address: "",
+    phone: "",
+    id: "",
+  });
+};
+const addItem = () => {
+  data.detail.push({
+    // id: "1",
+    name: "",
+    spec: "",
+    num: "",
+    part: "",
+    desc: "",
+  });
+};
+const addextractUser = () => {
+  data.extractUser.push({
+    name: "",
+    address: "",
+    id: "",
+  });
+};
+const handleSave = async () => {
+  console.log("data", data);
+  const res = await saveCaseDetailInfo(props.caseId, data);
+  if (res.code === 0) {
+    ElMessage.success("保存成功!");
+    initInfo();
+  }
+};
+const handleExport = async () => {
+  await saveCaseDetailInfo(props.caseId, data);
+  const res = await exportCaseDetailInfo(props.caseId);
+  console.log("res", res);
+  saveAs(res, `${props.title}_提取清单.docx`);
+};
+const initInfo = async () => {
+  const res = await getCaseDetailInfo(props.caseId);
+
+  console.log("res", res);
+  for (var k in data) {
+    if (!res.data) {
+      isDisableExport.value = true;
+    } else {
+      isDisableExport.value = false;
+    }
+    if (res.data && res.data.hasOwnProperty(k)) {
+      // console.log("Key is " + k)
+      data[k] = res.data[k];
+    }
+  }
+};
+onMounted(() => {
+  initInfo();
+});
+</script>
+
+<style lang="scss">
+.records {
+  max-width: 1280px;
+  margin: 0 auto;
+  padding: 20px 0;
+
+  .header {
+    display: flex;
+    justify-content: flex-end;
+    margin-bottom: 50px;
+  }
+
+  .input {
+    height: 32px;
+    line-height: 32px;
+    margin: 0 8px;
+  }
+
+  .textarea {
+    margin-right: 8px;
+    margin-bottom: 20px;
+
+    span {
+      padding: 10px 0;
+      display: inline-block;
+    }
+
+    // margin: 0 8px;
+  }
+
+  .line {
+    display: inline-flex;
+    width: 100%;
+    flex-direction: row;
+    align-items: center;
+    margin-bottom: 25px;
+    line-height: 38px;
+
+    span {
+      white-space: nowrap;
+    }
+  }
+}
+
+.title {
+  text-align: center;
+}
+
+.sub-tit {
+  display: inline-block;
+  padding-bottom: 20px;
+}
+
+.info {
+  display: block;
+
+  .inner {
+    display: flex;
+    flex-direction: row;
+    width: 100%;
+
+    .input {
+      flex: 1;
+    }
+
+    .sec {
+      flex: 1;
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+    }
+  }
+}
+.witnessInfo {
+  background: #f5f5f5;
+  padding: 15px;
+  margin-top: 20px;
+  // margin-right: 8px;
+}
+
+.gap {
+  margin: 15px 0;
+}
+
+.btn-container {
+  padding: 20px 0;
+
+  .btn {
+    color: #26559b;
+    width: 100%;
+
+    &:hover {
+      background: #f5f5f5;
+      border-color: #dcdfe6;
+    }
+  }
+}
+
+.detail {
+  .con {
+    padding: 20px;
+    background-color: #f5f5f5;
+  }
+
+  .info {
+    .inner {
+      margin-bottom: 20px;
+    }
+  }
+}
+
+.extractUser {
+  margin-right: 0px;
+
+  .line {
+    background-color: #f5f5f5;
+    padding: 15px;
+    width: calc(100% - 30px);
+    display: inline-flex;
+    margin-bottom: 15px;
+  }
+}
+</style>

+ 1 - 1
src/view/case/share.vue

@@ -1,7 +1,7 @@
 <template>
   <el-form ref="form" label-width="80px" class="share-from">
     <el-form-item label="链接">
-      <el-input :modelValue="shareLink" placeholder="请输入项目编号" disabled />
+      <el-input :modelValue="shareLink" disabled />
     </el-form-item>
     <el-form-item label="密码">
       <el-input

+ 2 - 0
vite.config.ts

@@ -36,8 +36,10 @@ export default defineConfig({
   css: {
     preprocessorOptions: {
       scss: {
+        api: 'modern-compiler',// or "modern"
         additionalData: `@use "@/app/${app}/useStyle.scss" as *;`,
       },
+
     },
   },
   plugins: [

文件差异内容过多而无法显示
+ 1074 - 0
yarn.lock