Prechádzať zdrojové kódy

fix: Merge branch 'xj' of http://192.168.0.115:3000/bill/public-fuse into xj

bill 1 rok pred
rodič
commit
e8526a7790

+ 2 - 0
package.json

@@ -13,6 +13,7 @@
     "@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",
@@ -21,6 +22,7 @@
     "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",

+ 1 - 0
src/app/map/App.vue

@@ -290,6 +290,7 @@ body {
   width: 90%;
   flex-direction: row;
   flex-wrap: wrap;
+  justify-content: center;
 }
 
 .cover {

+ 49 - 3
src/assets/icon/fuse/demo_index.html

@@ -55,6 +55,18 @@
           <ul class="icon_lists dib-box">
           
             <li class="dib">
+              <span class="icon iconfont">&#xe767;</span>
+                <div class="name">fire_firmware</div>
+                <div class="code-name">&amp;#xe767;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe768;</span>
+                <div class="name">fire_app</div>
+                <div class="code-name">&amp;#xe768;</div>
+              </li>
+          
+            <li class="dib">
               <span class="icon iconfont">&#xe74d;</span>
                 <div class="name">fire_statistics</div>
                 <div class="code-name">&amp;#xe74d;</div>
@@ -492,9 +504,9 @@
 <pre><code class="language-css"
 >@font-face {
   font-family: 'iconfont';
-  src: url('iconfont.woff2?t=1706348932330') format('woff2'),
-       url('iconfont.woff?t=1706348932330') format('woff'),
-       url('iconfont.ttf?t=1706348932330') format('truetype');
+  src: url('iconfont.woff2?t=1721888765779') format('woff2'),
+       url('iconfont.woff?t=1721888765779') format('woff'),
+       url('iconfont.ttf?t=1721888765779') format('truetype');
 }
 </code></pre>
           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -521,6 +533,24 @@
         <ul class="icon_lists dib-box">
           
           <li class="dib">
+            <span class="icon iconfont icon-fire_firmware"></span>
+            <div class="name">
+              fire_firmware
+            </div>
+            <div class="code-name">.icon-fire_firmware
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-fire_app"></span>
+            <div class="name">
+              fire_app
+            </div>
+            <div class="code-name">.icon-fire_app
+            </div>
+          </li>
+          
+          <li class="dib">
             <span class="icon iconfont icon-fire_statistics"></span>
             <div class="name">
               fire_statistics
@@ -1179,6 +1209,22 @@
           
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-fire_firmware"></use>
+                </svg>
+                <div class="name">fire_firmware</div>
+                <div class="code-name">#icon-fire_firmware</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-fire_app"></use>
+                </svg>
+                <div class="name">fire_app</div>
+                <div class="code-name">#icon-fire_app</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
                   <use xlink:href="#icon-fire_statistics"></use>
                 </svg>
                 <div class="name">fire_statistics</div>

+ 11 - 3
src/assets/icon/fuse/iconfont.css

@@ -1,8 +1,8 @@
 @font-face {
   font-family: "iconfont"; /* Project id 3549513 */
-  src: url('iconfont.woff2?t=1706348932330') format('woff2'),
-       url('iconfont.woff?t=1706348932330') format('woff'),
-       url('iconfont.ttf?t=1706348932330') format('truetype');
+  src: url('iconfont.woff2?t=1721888765779') format('woff2'),
+       url('iconfont.woff?t=1721888765779') format('woff'),
+       url('iconfont.ttf?t=1721888765779') format('truetype');
 }
 
 .iconfont {
@@ -13,6 +13,14 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-fire_firmware:before {
+  content: "\e767";
+}
+
+.icon-fire_app:before {
+  content: "\e768";
+}
+
 .icon-fire_statistics:before {
   content: "\e74d";
 }

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 1
src/assets/icon/fuse/iconfont.js


+ 14 - 0
src/assets/icon/fuse/iconfont.json

@@ -6,6 +6,20 @@
   "description": "",
   "glyphs": [
     {
+      "icon_id": "41207891",
+      "name": "fire_firmware",
+      "font_class": "fire_firmware",
+      "unicode": "e767",
+      "unicode_decimal": 59239
+    },
+    {
+      "icon_id": "41207890",
+      "name": "fire_app",
+      "font_class": "fire_app",
+      "unicode": "e768",
+      "unicode_decimal": 59240
+    },
+    {
       "icon_id": "39135106",
       "name": "fire_statistics",
       "font_class": "fire_statistics",

BIN
src/assets/icon/fuse/iconfont.ttf


BIN
src/assets/icon/fuse/iconfont.woff


BIN
src/assets/icon/fuse/iconfont.woff2


+ 1 - 0
src/constant/caseFile.ts

@@ -13,3 +13,4 @@ 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;

+ 69 - 1
src/core/Scene.js

@@ -6,6 +6,22 @@ import { Mitt } from "./mitt.js";
 import testData from "./save.json";
 const stats = new Stats();
 
+function sleep(ms) {
+  return new Promise((resolve) => setTimeout(resolve, ms));
+}
+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);
+  }
+};
+
 export default class Scene extends Mitt {
   constructor(domElement) {
     super();
@@ -17,6 +33,7 @@ export default class Scene extends Mitt {
     this.sceneType = 1;
     this.width = 0;
     this.height = 0;
+    this.defaultZoom = 250;
     this.inited = false;
 
     this.init = () => {
@@ -26,6 +43,7 @@ export default class Scene extends Mitt {
         canvas: this.domElement,
         antialias: true,
         autoClear: true,
+        preserveDrawingBuffer: true,
       });
 
       this.width = this.domElement.clientWidth;
@@ -44,7 +62,7 @@ export default class Scene extends Mitt {
         0.1,
         1000
       );
-      this.orthCamera.zoom = 250;
+      this.orthCamera.zoom = this.defaultZoom;
 
       this.orthCamera.position.set(0, 10, 0);
       this.orthCamera.lookAt(0, 0, 0);
@@ -163,6 +181,56 @@ export default class Scene extends Mitt {
   editing(item) {
     this.player.editing(item);
   }
+
+  screenshot(x, index) {
+    var imgData, imgNode;
+    const times = 4;
+    this.orthCamera.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");
+
+    saveFile(dataURL, `${index}.jpg`);
+    this.onResize(this.width, this.height);
+  }
+
+  test() {
+    this.orthCamera.zoom = this.defaultZoom;
+    const object = this.boxManager.model;
+    const total = this.boxManager.imgList.length;
+
+    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) {
+      for (var i = 0; i <= slides; i++) {
+        (function (index, that) {
+          setTimeout(function () {
+            const offset = -(one * 3 * index + 0.21);
+            console.log("Iteration:", offset);
+            that.screenshot(offset, index);
+
+            console.log(`Width: ${offset}`);
+          }, index * 1000);
+        })(i, this); // 传递当前迭代的索引i给setTimeout的回调函数
+      }
+    }
+  }
+
   onBindEvent = () => {
     //window.addEventListener('resize', this.onResize)
   };

+ 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;
+  }
+}

+ 25 - 6
src/core/box/object/PureTextLabel.js

@@ -1,19 +1,31 @@
 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;
-    const height = 50 * res;
+    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 = fontsize * res;
+    let fs = 12 * res * radio;
     var context = canvas.getContext("2d");
+
+    const lines = getWrapText(context, text, 158);
+    console.log("lines", lines);
+    containerHeight = containerHeight * lines.length;
+    canvas.height = height * lines.length;
     context.fillStyle = "transparent";
-    context.rect(0, 0, width, height);
+    // 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);
@@ -21,13 +33,17 @@ export default class PureTextLabel extends THREE.Mesh {
     context.fillStyle = color;
     context.textAlign = "center";
     context.textBaseline = "middle";
-    context.fillText(text, width / 2, height / 2);
+    // 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(1.5, 0.44);
+    const g = new THREE.PlaneGeometry(containerWidth, containerHeight);
+
     g.rotateX(-Math.PI / 2);
 
     // const texture = new THREE.CanvasTexture(canvas_map);
@@ -36,7 +52,9 @@ export default class PureTextLabel extends THREE.Mesh {
       map: canvas_map,
       transparent: true, // 允许材质透明
     });
+
     super(g, m);
+
     if (id) {
       this.uuid = id;
     }
@@ -49,6 +67,7 @@ export default class PureTextLabel extends THREE.Mesh {
       fontsize: fontsize,
     };
     this.position.copy(p);
+
     this.name = "pureText_" + text;
   }
 }

+ 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;
+};

+ 1 - 1
src/request/urls.ts

@@ -19,7 +19,7 @@ export const sendUserMsg = "/fusion-xj/notAuth/getMsgAuthCode";
 // 修改密码
 export const updatePsw = "/fusion-xj/notAuth/changePassword";
 // 重置密码
-export const restPassword = "/fusion-xj/notAuth/restPassword";
+export const restPassword = "/fusion-xj/web/user/restPassword";
 // 新增用户
 export const userAdd = "/fusion-xj/web/user/addUser";
 //修改用户

+ 8 - 2
src/router/config.ts

@@ -101,13 +101,13 @@ export const routes: Routes = [
         name: RouteName.cameraVersion,
         path: "cameraVersion",
         component: () => import("@/view/cameraVersion/index.vue"),
-        meta: { title: "固件管理", icon: "icon-nav-setup" },
+        meta: { title: "固件管理", icon: "icon-fire_firmware" },
       },
       {
         name: RouteName.cameraVersionApp,
         path: "cameraVersionApp",
         component: () => import("@/view/cameraVersionApp/index.vue"),
-        meta: { title: "APP管理", icon: "icon-nav-setup" },
+        meta: { title: "APP管理", icon: "icon-fire_app" },
       },
     ],
   },
@@ -117,4 +117,10 @@ 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

@@ -20,6 +20,7 @@ export const RouteName = {
   // firmware: "firmware",
   cameraVersion: "cameraVersion",
   cameraVersionApp: "cameraVersionApp",
+  noCase: "no-case",
 } as const;
 
 type RouteNamesType = typeof RouteName;

+ 9 - 3
src/store/cameraVersion.ts

@@ -24,10 +24,16 @@ export type CameraVersionType = PaggingReq<{
   type: number;
 }>
 
-export const getCameraVersionList = (params: CameraVersionType) => {
-  return axios.post(cameraVersionList, params) as unknown as PaggingRes<CameraVersionEntity[]>;
+export const getCameraVersionList = async (params: CameraVersionType) => {
+  return (await axios.post(cameraVersionList, params)).data as unknown as PaggingRes<CameraVersionEntity[]>;
 }
 
-export const addCameraVersion = async (params: CameraVersionType) => {
+export const addCameraVersion = async (params: CameraVersionEntity) => {
   return (await axios.post<string>(cameraVersionUpload, { ...params })).data;
 };
+export const editCameraVersion = async (params: CameraVersionEntity) => {
+  return (await axios.post<string>(cameraVersionUpdate, { ...params })).data;
+};
+export const delCameraVersion = async (id: string | number) => {
+  return (await axios.post<string>(cameraVersionDelete, { id })).data;
+};

+ 40 - 0
src/store/cameraVersionApp.ts

@@ -0,0 +1,40 @@
+import {
+  axios,
+  PaggingReq,
+  PaggingRes,
+  cameraVersionAppList,
+  cameraVersionAppUpload,
+  cameraVersionAppDelete,
+  cameraVersionAppUpdate,
+
+} from "@/request";
+
+export type CameraVersionAppEntity = {
+  id?: string;
+  version: string;
+  description: string;
+  status: string;
+  fileUrl: string;
+  name: string;
+  minVersion: string;
+  file: File | undefined
+}
+
+export type CameraVersionAppType = PaggingReq<{
+  version: string;
+  type?: number;
+}>
+
+export const getcameraVersionAppList = async (params: CameraVersionAppType) => {
+  return (await axios.post(cameraVersionAppList, params)).data as unknown as PaggingRes<CameraVersionAppEntity>;
+}
+
+export const addCameraVersionApp = async (params: CameraVersionAppEntity) => {
+  return (await axios.post<string>(cameraVersionAppUpload, { ...params })).data;
+};
+export const editCameraVersionApp = async (params: CameraVersionAppEntity) => {
+  return (await axios.post<string>(cameraVersionAppUpdate, { ...params })).data;
+};
+export const delCameraVersionApp = async (id: string | number) => {
+  return (await axios.post<string>(cameraVersionAppDelete, { id })).data;
+};

+ 2 - 0
src/store/user.ts

@@ -69,9 +69,11 @@ type UpDataPasswordParams = {
 };
 export const updatePassword = async (params: UpDataPasswordParams) => {
   const password = encodePwd(params.password);
+  const oldPassword = encodePwd(params.oldPassword);
   await axios.post(updatePsw, {
     ...params,
     password,
+    oldPassword,
     confirmPwd: password,
   });
 };

+ 57 - 19
src/view/cameraVersion/edit.vue

@@ -23,16 +23,20 @@
       ></el-input>
     </el-form-item>
 
-    <el-form-item label="版本更新说明" class="mandatory">
+    <el-form-item
+      label="版本更新说明"
+      class="mandatory"
+      style="white-space: pre-line"
+    >
       <el-input
         maxlength="200"
         type="textarea"
         :autosize="{ minRows: 4, maxRows: 10 }"
-        v-model.trim="data.description"
+        v-model="data.description"
         placeholder="请输入版本更新说明"
       ></el-input>
     </el-form-item>
-    <el-form-item label="文件" class="mandatory">
+    <el-form-item label="文件" class="mandatory" v-if="isAdd">
       <el-upload
         class="upload-demo"
         :multiple="false"
@@ -51,7 +55,7 @@
         <template v-slot:tip>
           <div class="el-upload__tip">注:可上传{{ size }}以内的文件</div>
         </template>
-        <!-- <template v-slot:file="{ file }: { file: UploadFile }">
+        <template v-slot:file="{ file }: { file: UploadFile }">
           <div class="file" @click.stop="previewFile()">
             <div>
               <el-icon><Document /></el-icon>
@@ -59,22 +63,26 @@
             </div>
             <el-icon @click.stop="removeFile()"><Close /></el-icon>
           </div>
-        </template> -->
+        </template>
       </el-upload>
     </el-form-item>
   </el-form>
 </template>
 
 <script setup lang="ts">
-import { computed, ref, watchEffect } from "vue";
+import { computed, ref, unref, watch, watchEffect } from "vue";
 import { QuiskExpose } from "@/helper/mount";
 import { CameraVersionEntity } from "@/store/cameraVersion";
 import { useUpload } from "@/hook/upload";
-import { maxFileSize } from "@/constant/caseFile";
+import { maxAppFileSize } from "@/constant/caseFile";
 import { CameraType } from "@/store/camera";
 import { cameraTypeDesc } from "@/constant/camera";
-import { addCameraVersion, CameraVersionType } from "@/store/cameraVersion";
-import { ElMessage } from "element-plus";
+import {
+  addCameraVersion,
+  editCameraVersion,
+  CameraVersionType,
+} from "@/store/cameraVersion";
+import { ElMessage, UploadFile } from "element-plus";
 
 const cameraTypes = [CameraType.SWKJ, CameraType.SWSS1, CameraType.SWSS2];
 const props = defineProps<{
@@ -101,10 +109,15 @@ const httpsApi = async ({ file }) => {
   // console.log("httpsApi", file, fileUrl);
 };
 
+watchEffect(() => {
+  if (props.entity) {
+    data.value = { ...props.entity };
+  }
+});
 const { size, fileList, upload, removeFile, previewFile, file, accept } =
   useUpload({
-    maxSize: maxFileSize,
-    formats: [".jpg", ".jpeg"],
+    maxSize: maxAppFileSize,
+    formats: [".zip"],
   });
 defineExpose<QuiskExpose>({
   async submit() {
@@ -112,17 +125,25 @@ defineExpose<QuiskExpose>({
       ElMessage.error("版本号不能为空!");
       throw "版本号不能为空!";
     }
-    if (!data.value.file) {
-      ElMessage.error("文件不能为空!");
-      throw "文件不能为空!";
-    }
-    if (!data.value.description) {
+    if (
+      !data.value.description ||
+      (data.value.description &&
+        String(data.value.description).trim().length === 0)
+    ) {
       ElMessage.error("版本更新说明不能为空!");
       throw "版本更新说明不能为空!";
     }
-    const res = await addCameraVersion(data.value as any as CameraVersionType);
-    console.log("res", res);
-    ElMessage.success("添加成功!");
+    if (unref(isAdd)) {
+      if (!data.value.file) {
+        ElMessage.error("文件不能为空!");
+        throw "文件不能为空!";
+      }
+      await addCameraVersion(data.value as any as CameraVersionEntity);
+      ElMessage.success("添加成功!");
+    } else {
+      await editCameraVersion(data.value as any as CameraVersionEntity);
+      ElMessage.success("保存成功!");
+    }
   },
 });
 </script>
@@ -132,4 +153,21 @@ defineExpose<QuiskExpose>({
   font-size: 20px;
   line-height: 50px;
 }
+.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>

+ 79 - 10
src/view/cameraVersion/index.vue

@@ -3,7 +3,10 @@
     <el-form label-width="94px" inline>
       <slot name="header" />
       <el-form-item label="版本号">
-        <el-input placeholder="请输入"></el-input>
+        <el-input
+          v-model="pageHook.pagging.state.query.version"
+          placeholder="请输入"
+        ></el-input>
       </el-form-item>
       <el-form-item class="searh-btns" style="grid-area: 1 / 4 / 2 / 4">
         <el-button type="primary" @click="pageHook.pagging.refresh"
@@ -16,15 +19,15 @@
     </el-form>
   </com-head>
 
-  <div class="body-layer">
+  <div class="body-layer" style="padding-top: 8px">
     <div class="body-but">
       <el-button type="primary" v-pdpath="'add'" @click="addHandler"
-        >新增</el-button
+        >新增固件</el-button
       >
     </div>
-    {{ pageHook }}
+
     <el-table
-      :data="tableData"
+      :data="pageHook.pagging.state.table.rows"
       tooltip-effect="dark"
       style="width: 100%"
       size="large"
@@ -35,9 +38,43 @@
         prop="description"
       ></el-table-column>
       <el-table-column label="最低版本号" prop="minVersion"></el-table-column>
-      <el-table-column label="创建人" prop="deptLevelStr"></el-table-column>
-      <el-table-column label="创建时间" prop="createTime"></el-table-column>
-      <el-table-column label="状态" prop="status"></el-table-column>
+      <el-table-column label="创建人" prop="createName"></el-table-column>
+      <el-table-column
+        label="创建时间"
+        prop="createTime"
+        v-slot:default="{ row }"
+      >
+        {{ dayjs(row.createTime).format("YYYY-DD-MM HH:MM:ss") }}
+      </el-table-column>
+      <el-table-column label="状态" prop="status" v-slot:default="{ row }">
+        <el-switch
+          v-model:model-value="row.status"
+          active-text="启用"
+          inactive-text="禁用"
+          inline-prompt
+          @update:model-value="handleStatusupdate(row)"
+          active-value="A"
+          inactive-value="I"
+        ></el-switch>
+      </el-table-column>
+
+      <el-table-column
+        label="操作"
+        v-slot:default="{ row }: { row: CameraVersionEntity }"
+        :width="240"
+      >
+        <span class="oper-span" @click="editHandler(row)" v-pdpath="['view']"
+          >编辑</span
+        >
+        <span
+          class="oper-span"
+          @click="delHandler(row)"
+          style="color: var(--primaryColor)"
+          v-pdpath="['del']"
+        >
+          删除
+        </span>
+      </el-table-column>
     </el-table>
     <com-pagination
       @size-change="pageHook.pagging.changPageSize"
@@ -49,13 +86,18 @@
   </div>
 </template>
 <script lang="ts" setup>
-import { ref } from "vue";
+import { computed, ref } from "vue";
 import comHead from "@/components/head/index.vue";
 import comPagination from "@/components/pagination/index.vue";
 import { useScenePaggingParams } from "./paging";
 import { CameraType } from "@/store/camera";
 import { addCameraVersion, editCameraVersion } from "./quisk";
-const tableData = ref();
+import {
+  CameraVersionEntity,
+  delCameraVersion,
+  editCameraVersion as EditCameraVersion,
+} from "@/store/cameraVersion";
+import { dayjs, ElMessage, ElMessageBox } from "element-plus";
 
 const headOptions = [
   { value: CameraType.SWKJ, name: "双目转台" },
@@ -69,6 +111,33 @@ const addHandler = async () => {
   (await addCameraVersion({ type: pageHook.pagging.state.query.type })) &&
     pageHook.pagging.refresh();
 };
+const editHandler = async (row: CameraVersionEntity) => {
+  await editCameraVersion({
+    type: pageHook.pagging.state.query.type,
+    entity: row,
+  });
+  pageHook.pagging.refresh();
+};
+
+const delHandler = async (row: CameraVersionEntity) => {
+  const confirm = await ElMessageBox.confirm("是否确定删除?", "温馨提示", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
+    type: "warning",
+  });
 
+  if (confirm) {
+    row.id && delCameraVersion(row.id);
+    pageHook.pagging.refresh();
+  }
+};
 
+const handleStatusupdate = async (row: CameraVersionEntity) => {
+  console.log("row", row);
+  await EditCameraVersion({
+    ...row,
+    type: pageHook.pagging.state.query.type,
+  });
+  pageHook.pagging.refresh();
+};
 </script>

+ 73 - 0
src/view/cameraVersionApp/download.vue

@@ -0,0 +1,73 @@
+<template>
+  <el-form
+    :model="data"
+    ref="form"
+    label-width="120px"
+    class="cameraVersion-from"
+  >
+    <el-form-item label="App下载地址">
+      <div class="column">
+        <el-input maxlength="30" v-model="data.url" disabled></el-input>
+        <useClipboard v-slot="{ copy, copied }" :source="data.url">
+          <el-button type="primary" style="margin-left: 20px" @click="copy()">
+            {{ copied ? "已复制" : "复制" }}
+          </el-button>
+        </useClipboard>
+    
+      </div>
+    </el-form-item>
+    <div class="qrcode">
+      <qrcode-vue :value="data.url" :size="300"></qrcode-vue>
+    </div>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, unref, watch, watchEffect } from "vue";
+import { QuiskExpose } from "@/helper/mount";
+import QrcodeVue from "qrcode.vue";
+import { UseClipboard } from "@vueuse/components";
+
+const props = withDefaults(
+  defineProps<{
+    url: string;
+  }>(),
+  {
+    url: "",
+  }
+);
+const data = ref<{
+  url: string;
+}>({
+  url: "",
+});
+
+const form = ref();
+
+watchEffect(() => {
+  if (props.url) {
+    data.value.url = props.url;
+  }
+});
+
+defineExpose<QuiskExpose>({
+  async submit() {},
+});
+</script>
+
+<style scoped>
+.icon-style {
+  font-size: 20px;
+  line-height: 50px;
+}
+.qrcode {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+.column {
+  display: flex;
+  width: 100%;
+}
+</style>

+ 172 - 0
src/view/cameraVersionApp/edit.vue

@@ -0,0 +1,172 @@
+<template>
+  <el-form
+    :model="data"
+    ref="form"
+    label-width="120px"
+    class="cameraVersion-from"
+  >
+    <el-form-item label="版本号" class="mandatory">
+      <el-input
+        maxlength="30"
+        v-model.trim="data.version"
+        placeholder="请输入最新版本号"
+      ></el-input>
+    </el-form-item>
+    <el-form-item label="最低版本号">
+      <el-input
+        maxlength="30"
+        v-model.trim="data.minVersion"
+        placeholder="请输入最低版本(低于此版本号强制更新)"
+      ></el-input>
+    </el-form-item>
+
+    <el-form-item label="版本更新说明" class="mandatory">
+      <el-input
+        :maxlength="200"
+        type="textarea"
+        :autosize="{ minRows: 4, maxRows: 10 }"
+        style="white-space: pre-wrap; word-break: break-all"
+        v-model="data.description"
+        placeholder="请输入版本更新说明"
+      ></el-input>
+    </el-form-item>
+    <el-form-item label="文件" class="mandatory" v-if="isAdd">
+      <el-upload
+        class="upload-demo"
+        :multiple="false"
+        :limit="1"
+        :disabled="!!data.file"
+        :before-upload="upload"
+        :file-list="fileList"
+        :http-request="httpsApi"
+        :on-preview="previewFile"
+        :accept="accept"
+        :before-remove="removeFile"
+      >
+        <el-button type="primary" :disabled="!!data.file">
+          <el-icon><Upload /></el-icon>上传
+        </el-button>
+        <template v-slot:tip>
+          <div class="el-upload__tip">注:可上传{{ size }}以内的文件</div>
+        </template>
+        <template v-slot:file="{ file }: { file: UploadFile }">
+          <div class="file" @click.stop="previewFile()">
+            <div>
+              <el-icon><Document /></el-icon>
+              <span class="name">{{ file.name }}</span>
+            </div>
+            <el-icon @click.stop="removeFile() && (data.file = undefined)"
+              ><Close
+            /></el-icon>
+          </div>
+        </template>
+      </el-upload>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, unref, watch, watchEffect } from "vue";
+import { QuiskExpose } from "@/helper/mount";
+import { CameraVersionAppEntity } from "@/store/cameraVersionApp";
+import { useUpload } from "@/hook/upload";
+import { maxAppFileSize } from "@/constant/caseFile";
+import { UploadFile } from "element-plus";
+import {
+  addCameraVersionApp,
+  editCameraVersionApp,
+  CameraVersionAppType,
+} from "@/store/cameraVersionApp";
+import { ElMessage } from "element-plus";
+
+
+const props = defineProps<{
+  // type: string | number;
+  entity?: CameraVersionAppEntity;
+}>();
+const data = ref<CameraVersionAppEntity>({
+  version: "",
+  description: "",
+  minVersion: "",
+  file: undefined,
+  status: "",
+  fileUrl: "",
+  name: "",
+});
+
+const form = ref();
+
+
+
+const isAdd = computed(() => !props.entity?.id);
+
+const httpsApi = async ({ file }) => {
+  console.log("httpsApi", file);
+  // let fileUrl = await uploadFile(file);
+  data.value.file = file;
+  // file.url = fileUrl;
+  // console.log("httpsApi", file, fileUrl);
+};
+
+watchEffect(() => {
+  if (props.entity) {
+    data.value = { ...props.entity };
+  }
+});
+const { size, fileList, upload, removeFile, previewFile, file, accept } =
+  useUpload({
+    maxSize: maxAppFileSize,
+    formats: [".apk"],
+  });
+defineExpose<QuiskExpose>({
+  async submit() {
+    if (!data.value.version) {
+      ElMessage.error("版本号不能为空!");
+      throw "版本号不能为空!";
+    }
+    if (
+      !data.value.description ||
+      (data.value.description &&
+        String(data.value.description).trim().length === 0)
+    ) {
+      ElMessage.error("版本更新说明不能为空!");
+      throw "版本更新说明不能为空!";
+    }
+    if (unref(isAdd)) {
+      if (!data.value.file) {
+        ElMessage.error("文件不能为空!");
+        throw "文件不能为空!";
+      }
+      await addCameraVersionApp(data.value as any as CameraVersionAppEntity);
+      ElMessage.success("添加成功!");
+    } else {
+      await editCameraVersionApp(data.value as any as CameraVersionAppEntity);
+      ElMessage.success("保存成功!");
+    }
+  },
+});
+</script>
+
+<style scoped>
+.icon-style {
+  font-size: 20px;
+  line-height: 50px;
+}
+.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>

+ 126 - 28
src/view/cameraVersionApp/index.vue

@@ -1,48 +1,88 @@
 <template>
-  <com-head
-    :options="[{ name: 'app管理', value: '1' }]"
-    class="app-head frame-head"
-  >
+  <com-head :options="headOptions">
     <el-form label-width="94px" inline>
       <slot name="header" />
       <el-form-item label="版本号">
-        <el-input placeholder="请输入"></el-input>
+        <el-input
+          v-model="pageHook.pagging.state.query.version"
+          placeholder="请输入"
+        ></el-input>
       </el-form-item>
       <el-form-item class="searh-btns" style="grid-area: 1 / 4 / 2 / 4">
-        <el-button type="primary" @click="params.pagging.refresh"
+        <el-button type="primary" @click="pageHook.pagging.refresh"
           >查询</el-button
         >
-        <el-button type="primary" plain @click="params.pagging.queryReset"
+        <el-button type="primary" plain @click="pageHook.pagging.queryReset"
           >重置</el-button
         >
       </el-form-item>
     </el-form>
   </com-head>
 
-  <div class="body-layer">
-    <slot name="content" />
+  <div class="body-layer" style="padding-top: 8px">
+    <div class="body-but">
+      <el-button type="default" @click="codeHandler"> APP下载 </el-button>
+      <el-button type="primary" v-pdpath="'add'" @click="addHandler">
+        新增版本
+      </el-button>
+    </div>
+
     <el-table
-      :data="tableData"
+      :data="pageHook.pagging.state.table.rows"
       tooltip-effect="dark"
       style="width: 100%"
       size="large"
     >
-      <el-table-column label="版本号" prop="deptName"></el-table-column>
+      <el-table-column label="版本号" prop="version"></el-table-column>
       <el-table-column
         label="版本更新说明"
-        prop="deptLevelStr"
+        prop="description"
+        style="white-space: pre-line"
       ></el-table-column>
-      <el-table-column label="最低版本号" prop="deptLevelStr"></el-table-column>
-      <el-table-column label="创建人" prop="deptLevelStr"></el-table-column>
-      <el-table-column label="创建时间" prop="deptLevelStr"></el-table-column>
-      <el-table-column label="状态" prop="deptLevelStr"></el-table-column>
+      <el-table-column label="最低版本号" prop="minVersion"></el-table-column>
+      <el-table-column label="创建人" prop="createName"></el-table-column>
+      <el-table-column
+        label="创建时间"
+        prop="createTime"
+        v-slot:default="{ row }"
+      >
+        {{ dayjs(row.createTime).format("YYYY-DD-MM HH:MM:ss") }}
+      </el-table-column>
+      <el-table-column label="状态" prop="status" v-slot:default="{ row }">
+        <el-switch
+          v-model:model-value="row.status"
+          active-text="启用"
+          inactive-text="禁用"
+          inline-prompt
+          @update:model-value="handleStatusupdate(row)"
+          active-value="A"
+          inactive-value="I"
+        ></el-switch>
+      </el-table-column>
+      <el-table-column
+        label="操作"
+        v-slot:default="{ row }: { row: CameraVersionAppEntity }"
+        :width="240"
+      >
+        <span class="oper-span" @click="editHandler(row)" v-pdpath="['view']"
+          >编辑</span
+        >
+        <span
+          class="oper-span"
+          @click="delHandler(row)"
+          style="color: var(--primaryColor)"
+          v-pdpath="['del']"
+        >
+          删除
+        </span>
+      </el-table-column>
     </el-table>
     <com-pagination
-      @size-change="params.pagging.changPageSize"
-      @current-change="params.pagging.changPageCurrent"
-      :current-page="params.pagging.state.pag.currentPage"
-      :page-size="params.pagging.state.pag.size"
-      :total="params.pagging.state.pag.total"
+      @size-change="pageHook.pagging.changPageSize"
+      @current-change="pageHook.pagging.changPageCurrent"
+      :current-page="pageHook.pagging.state.pag.currentPage"
+      :page-size="pageHook.pagging.state.pag.size"
+      :total="pageHook.pagging.state.pag.total"
     />
   </div>
 </template>
@@ -50,12 +90,70 @@
 import { ref } from "vue";
 import comHead from "@/components/head/index.vue";
 import comPagination from "@/components/pagination/index.vue";
-const tableData = ref();
 import { useScenePaggingParams } from "./paging";
-const headOptions = [
-  { value: 1, name: "双目转台" },
-  { value: 2, name: "激光转台" },
-  { value: 3, name: "激光移动" },
-];
-const params = useScenePaggingParams();
+import {
+  getcameraVersionAppList,
+  delCameraVersionApp,
+} from "@/store/cameraVersionApp";
+
+import {
+  addCameraAppVersion,
+  editCameraAppVersion,
+  downCameraAppVersion,
+} from "./quisk";
+import {
+  CameraVersionAppEntity,
+  editCameraVersionApp,
+} from "@/store/cameraVersionApp";
+import { dayjs, ElMessage, ElMessageBox } from "element-plus";
+
+const headOptions = [{ value: 1, name: "app管理" }];
+
+const pageHook = useScenePaggingParams();
+
+const addHandler = async () => {
+  (await addCameraAppVersion({})) && pageHook.pagging.refresh();
+};
+const editHandler = async (row: CameraVersionAppEntity) => {
+  await editCameraAppVersion({
+    entity: row,
+  });
+  pageHook.pagging.refresh();
+};
+
+const delHandler = async (row: CameraVersionAppEntity) => {
+  const confirm = await ElMessageBox.confirm("是否确定删除?", "温馨提示", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
+    type: "warning",
+  });
+
+  if (confirm) {
+    row.id && delCameraVersionApp(row.id);
+    pageHook.pagging.refresh();
+  }
+};
+const handleStatusupdate = async (row: CameraVersionAppEntity) => {
+  console.log("row", row);
+  await editCameraVersionApp(row);
+  pageHook.pagging.refresh();
+};
+
+const codeHandler = async () => {
+  const all = await getcameraVersionAppList({
+    pageNum: 1,
+    pageSize: 10000,
+    version: "",
+    type: 1,
+  });
+  const activeOne = Array.from(all.list).find((item) => item.status === "A");
+
+  if (activeOne) {
+    const fireUrl =
+      location.protocol + "//" + location.host + activeOne.fileUrl;
+    await downCameraAppVersion({ url: fireUrl });
+  } else {
+    ElMessage.error("暂没启用状态状app,请启用");
+  }
+};
 </script>

+ 9 - 29
src/view/cameraVersionApp/paging.ts

@@ -1,41 +1,21 @@
 import { usePagging } from "@/hook/pagging";
-import { SceneType, getScenePagging } from "@/store/scene";
+// import { SceneType, getScenePagging } from "@/store/scene";
+import { CameraType } from "@/store/camera";
+import { getcameraVersionAppList } from '@/store/cameraVersionApp'
 import { computed, reactive, watch, watchEffect } from "vue";
 
 export const useScenePaggingParams = () => {
+
   const pagging = usePagging({
-    get: getScenePagging,
+    get: getcameraVersionAppList,
     paramsTemlate: {
-      type: SceneType.SWKJ,
-      sceneName: "",
-      modelTitle: "",
-      deptId: "",
-      snCode: "",
-    },
-  });
-
-  const isSwmx = computed(() => pagging.state.query.type === SceneType.SWMX);
-
-  const keyword = computed({
-    get: () =>
-      isSwmx.value
-        ? pagging.state.query.modelTitle
-        : pagging.state.query.sceneName,
-    set: (val: string) => {
-      pagging.state.query.modelTitle = val;
-      pagging.state.query.sceneName = val;
+      type: 1,
+      version: '',
     },
   });
 
-  let oldSnCode = pagging.state.query.snCode;
-
   watchEffect(() => {
-    if (isSwmx.value) {
-      oldSnCode = pagging.state.query.snCode;
-      pagging.state.query.snCode = "";
-    } else {
-      pagging.state.query.snCode = oldSnCode;
-    }
+
   });
 
   watch(
@@ -52,6 +32,6 @@ export const useScenePaggingParams = () => {
     pagging.state.query.type = type;
   };
 
-  return reactive({ pagging, keyword, isSwmx });
+  return reactive({ pagging });
 };
 export type ScenePagging = ReturnType<typeof useScenePaggingParams>["pagging"];

+ 20 - 0
src/view/cameraVersionApp/quisk.ts

@@ -0,0 +1,20 @@
+import { quiskMountFactory } from "@/helper/mount";
+import EditCameraVersionApp from "./edit.vue";
+import downloadApp from "./download.vue";
+
+export const editCameraAppVersion = quiskMountFactory(EditCameraVersionApp, {
+  title: "编辑App",
+  width: 540,
+});
+export const addCameraAppVersion = quiskMountFactory(EditCameraVersionApp, {
+  title: "新增App",
+  width: 540,
+});
+
+export const downCameraAppVersion = quiskMountFactory(downloadApp, {
+  title: "下载App",
+  width: 540,
+  enterText: "确定",
+  showClose: false,
+
+});

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

@@ -173,7 +173,7 @@ onMounted(async () => {
     desc.value = "";
   } else {
     console.error("该案件不存在!");
-    router.replace({ name: RouteName.vrmodel });
+    router.replace({ name: RouteName.noCase });
   }
 });
 

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

@@ -0,0 +1,38 @@
+<template>
+  <div class="no-data">
+    <img :src="emptyBG" />
+    <span>案件不存在</span>
+    <el-button
+      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 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>

+ 15 - 6
src/view/case/photos/index.vue

@@ -238,13 +238,17 @@ const handleDetele = async (item) => {
   }
 };
 const handleSwitchGrid = async () => {
-  const res = await ElMessageBox.confirm("切换模版不包括标注内容,确定要切换吗?", "温馨提示", {
-    confirmButtonText: "确定",
-    cancelButtonText: "取消",
-    type: "default",
-  });
+  const res = await ElMessageBox.confirm(
+    "切换模版不包括标注内容,确定要切换吗?",
+    "温馨提示",
+    {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "default",
+    }
+  );
   if (res) {
-    sortType.value = !sortType.value
+    sortType.value = !sortType.value;
     handleClear();
   }
 };
@@ -417,3 +421,8 @@ onMounted(() => {
   }
 }
 </style>
+<style scoped>
+:global(.body-layer) {
+  padding-right: 0 !important;
+}
+</style>

+ 1 - 1
src/view/user/index.vue

@@ -152,7 +152,7 @@ const resetHandler = async (row: UserInfo) => {
   });
 
   await ElMessageBox.confirm(
-    "当前用户密码已重置为 xj12345678用户可登录自行修改。",
+    "当前用户密码已重置为 Xj12345678用户可登录自行修改。",
     "重置密码",
     {
       confirmButtonText: "确定",

+ 4 - 3
vite.config.ts

@@ -9,7 +9,8 @@ if (process.argv.length > 3) {
 }
 
 const dev = true;
-
+// const devUrl = "https://xj-mix3d.4dkankan.com"
+const devUrl = "https://192.168.0.25";
 export default defineConfig({
   define: {
     VITE_APP_APP: JSON.stringify(app),
@@ -46,13 +47,13 @@ export default defineConfig({
     proxy: {
       "/api": {
         secure: false,
-        target: dev ? "https://192.168.0.25" : "mix3d.4dkankan.com",
+        target: dev ? devUrl : "mix3d.4dkankan.com",
         changeOrigin: true,
         rewrite: (path) => path.replace(new RegExp(`^/api`), ""),
       },
       "/fusion-xj": {
         secure: false,
-        target: dev ? "https://192.168.0.25" : "mix3d.4dkankan.com",
+        target: dev ? devUrl : "mix3d.4dkankan.com",
         changeOrigin: true,
         rewrite: (path) => path.replace(new RegExp(`^/api`), "/fusion-xj"),
       },

+ 15 - 1
yarn.lock

@@ -373,7 +373,16 @@
   resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.4.31.tgz"
   integrity sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA==
 
-"@vueuse/core@^10.11.0":
+"@vueuse/components@^10.11.0":
+  version "10.11.0"
+  resolved "https://registry.npmmirror.com/@vueuse/components/-/components-10.11.0.tgz#fd4191e30070bed3b51bdc4d33a9edb4b5da08de"
+  integrity sha512-ZvLZI23d5ZAtva5fGyYh/jQtZO8l+zJ5tAXyYNqHJZkq1o5yWyqZhENvSv5mfDmN5IuAOp4tq02mRmX/ipFGcg==
+  dependencies:
+    "@vueuse/core" "10.11.0"
+    "@vueuse/shared" "10.11.0"
+    vue-demi ">=0.14.8"
+
+"@vueuse/core@10.11.0", "@vueuse/core@^10.11.0":
   version "10.11.0"
   resolved "https://registry.npmjs.org/@vueuse/core/-/core-10.11.0.tgz#b042585a8bf98bb29c177b33999bd0e3fcd9e65d"
   integrity sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==
@@ -926,6 +935,11 @@ proxy-from-env@^1.1.0:
   resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
   integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
 
+qrcode.vue@^3.4.1:
+  version "3.4.1"
+  resolved "https://registry.npmmirror.com/qrcode.vue/-/qrcode.vue-3.4.1.tgz#dd8141da9c4ea07ee56b111cd13eadf123af822a"
+  integrity sha512-wq/zHsifH4FJ1GXQi8/wNxD1KfQkckIpjK1KPTc/qwYU5/Bkd4me0w4xZSg6EXk6xLBkVDE0zxVagewv5EMAVA==
+
 qs@^6.11.2:
   version "6.12.3"
   resolved "https://registry.npmmirror.com/qs/-/qs-6.12.3.tgz"