Explorar o código

聊天界面图片显示

wangwei %!s(int64=4) %!d(string=hai) anos
pai
achega
2b583c4642
Modificáronse 7 ficheiros con 426 adicións e 109 borrados
  1. 5 0
      config/index.js
  2. 26 0
      package-lock.json
  3. 2 0
      package.json
  4. 242 101
      src/components/ChatFrame.vue
  5. 25 8
      src/main.js
  6. 103 0
      src/utils/common.js
  7. 23 0
      yarn.lock

+ 5 - 0
config/index.js

@@ -11,6 +11,11 @@ module.exports = {
     assetsSubDirectory: 'static',
     assetsPublicPath: '/',
     proxyTable: {
+      '/api/': {
+        target: 'http://192.168.100.8:3000/', //代理的服务地址
+        changeOrigin: true, // needed for virtual hosted sites
+        logLevel: 'debug'
+      },
       '/socket.io': {
         target: 'http://192.168.100.8:3000', //代理的服务地址
         ws: true,

+ 26 - 0
package-lock.json

@@ -12687,6 +12687,11 @@
         }
       }
     },
+    "socketio-file-upload": {
+      "version": "0.7.3",
+      "resolved": "https://registry.npmjs.org/socketio-file-upload/-/socketio-file-upload-0.7.3.tgz",
+      "integrity": "sha512-JUvzi8Vvp2+GBfQtOehPSfecetZOo4g1JTl6+zmKhPiljn+z09lLL8zYeX4AJVSNpmRkGJnypbHkiPjPSkk5UA=="
+    },
     "sockjs": {
       "version": "0.3.19",
       "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz",
@@ -13797,6 +13802,22 @@
       "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
       "dev": true
     },
+    "v-viewer": {
+      "version": "1.6.4",
+      "resolved": "https://registry.npmjs.org/v-viewer/-/v-viewer-1.6.4.tgz",
+      "integrity": "sha512-LVkiUHpmsbsZXebeNXnu8krRCi5i2n07FeLFxoIVGhw8lVvTBO0ffpbDC6mLEuacCjrIh09HjIqpciwUtWE8lQ==",
+      "requires": {
+        "throttle-debounce": "^2.0.1",
+        "viewerjs": "^1.5.0"
+      },
+      "dependencies": {
+        "throttle-debounce": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz",
+          "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ=="
+        }
+      }
+    },
     "validate-npm-package-license": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -13830,6 +13851,11 @@
         "extsprintf": "^1.2.0"
       }
     },
+    "viewerjs": {
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/viewerjs/-/viewerjs-1.10.1.tgz",
+      "integrity": "sha512-Oyzd3JP9dDSd+bBulfnQ+UTfHoobFwkmcT/uKSnQXjmPz7rZU0HJIiKudxPaMsiv17dr4Sm1cHnASJcDlFw1PA=="
+    },
     "vm-browserify": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",

+ 2 - 0
package.json

@@ -17,6 +17,8 @@
     "element-ui": "^2.15.3",
     "socket.io-client": "^4.1.3",
     "socketio-file-upload": "^0.7.3",
+    "v-contextmenu": "^3.0.0",
+    "v-viewer": "^1.6.4",
     "vue": "^2.5.2",
     "vue-router": "^3.0.1",
     "vue-socket.io": "^3.0.10",

+ 242 - 101
src/components/ChatFrame.vue

@@ -21,7 +21,15 @@
               class="record-item"
               :class="item.id !== self.id ? 'receive-record' : 'send-record'"
             >
-              <span>{{ item.msg }}</span>
+              <div>
+                <span>{{ item.msg }}</span>
+              </div>
+              <div>
+                <span>{{ item.msg }}</span>
+              </div>
+              <div>
+                <span>{{ item.msg }}</span>
+              </div>
             </div>
           </div>
         </el-scrollbar>
@@ -47,12 +55,33 @@
                   {{ item.nickname }}
                 </div>
                 <!-- <div class="user">5asfdasdf</div> -->
-                <span>{{ item.msg }}</span>
+                <!-- <span>{{ item.msg }}</span> -->
+                <div class="message-content-text" style="margin-left:2px; margin-top: 0" v-text="_parseText(item.msg)"></div>
               </div>
             </div>
             <div v-if="item.id === self.id" class="record-item send-record">
               <!-- <div class="user">5asfdasdf</div> -->
-              <span>{{ item.msg }}</span>
+              <!-- <span>{{ item.msg }}</span> -->
+              <!-- 链接 -->
+              <!-- <a class="message-content-text" href="https://img2.baidu.com/it/u=1605987499,4009355285&fm=26&fmt=auto&gp=0.jpg" target="_blank">https://img2.baidu.com/it/u=1605987499,4009355285&fm=26&fmt=auto&gp=0.jpg</a> -->
+              <!-- 文本 -->
+              <!-- <div class="message-content-text" v-text="_parseText(item.msg)"></div> -->
+              <!-- 图片 -->
+              <!-- <div class="message-content-image" :style="getImageStyle('https://img2.baidu.com/it/u=1605987499,4009355285&fm=26&fmt=auto&gp=0.jpg')">
+                <viewer style="display: flex; align-items: center">
+                  <img src="https://img2.baidu.com/it/u=1605987499,4009355285&fm=26&fmt=auto&gp=0.jpg" alt="" />
+                </viewer>
+              </div> -->
+              <!-- 视频格式文件 -->
+              <div class="message-content-image">
+                <video style="width: 255px" src="https://media.w3.org/2010/05/sintel/trailer.mp4" controls="controls">您的浏览器不支持 video 标签。</video>
+              </div>
+              <!-- <div class="avatar">
+                <el-avatar
+                  shape="square"
+                  src="https://img2.baidu.com/it/u=1077360284,2857506492&fm=26&fmt=auto&gp=0.jpg"
+                ></el-avatar>
+              </div> -->
             </div>
           </div>
         </el-scrollbar>
@@ -60,9 +89,21 @@
       <div class="send-msg-wrap">
         <div class="send-header">
           <div class="tools" style="margin:10px">
-            <input type="file" name="" id="siofu_input">
-
-            <i id="upload-btn" class="el-icon-folder-opened" @click="upload"></i>
+            <el-upload
+              ref="upload"
+              class="file-uploader"
+              action="/api/upload/"
+              :show-file-list="false"
+              :on-success="handleSuccess"
+              :before-upload="beforeUpload"
+              style="display: inline"
+            >
+              <i
+                id="upload-btn"
+                class="el-icon-folder-opened"
+                @click="upload"
+              ></i>
+            </el-upload>
             <i class="el-icon-folder-add"></i>
             <i class="el-icon-folder-opened"></i>
           </div>
@@ -94,6 +135,7 @@
 
 <script>
 import { addCookie, getCookie } from "@/utils/setCookie.js";
+import { isUrl, parseText, processReturn } from "@/utils/common";
 export default {
   name: "ChatFrame",
   props: ["selected", "tab"],
@@ -105,7 +147,7 @@ export default {
       room_record: {},
       room_record_list: [],
       self: null,
-      uploader: null
+      uploadProgress: false
     };
   },
   computed: {},
@@ -125,105 +167,182 @@ export default {
     }
   },
   methods: {
-    upload() {
-      console.log(`this.$socket`, this.$socket)
-      this.uploader = this.$socketUpload(this.$socket);
-      this.uploader.addEventListener("choose", (event) => {
-        console.info('-----------event----------')
-        console.info(event)
-        
-      })
-
-      this.uploader.addEventListener("start", (event) => {
-        console.info('-----------start----------')
-        console.info(event)
-        console.log(`event.file`, event.file)
-        this.uploader.submitFiles(event.file)
-      })
-
-      this.uploader.addEventListener("progress", (event) => {
-        console.info('-----------progress----------')
-        console.info(event)
-      })
+    handleSuccess(res, file) {
+      console.log(`res`, res);
+      this.imageUrl = URL.createObjectURL(file.raw);
+    },
+    beforeUpload(file) {
+      console.log(`file`, file);
+      // const isJPG = file.type === "image/jpeg";
+      // const isLt2M = file.size / 1024 / 1024 < 2;
 
-      this.uploader.addEventListener("load", (event) => {
-        console.info('-----------load----------')
-        console.info(event)
-      })
+      // if (!isJPG) {
+      //   this.$message.error("上传头像图片只能是 JPG 格式!");
+      // }
+      // if (!isLt2M) {
+      //   this.$message.error("上传头像图片大小不能超过 2MB!");
+      // }
+      // return isJPG && isLt2M;
+      this.uploadProgress = true;
+      this.fileData = file;
+      //  调用函数分割文件 我这里是分割成不超过20M的文件快
+      this.fileDataList = this.createFileChunk(file, 1024 * 1024 * 20);
+      console.log(`this.fileDataList`, this.fileDataList);
+      // return new Promise((reslove, reject)=>{
+      //   this.fileUpLoad(reslove, reject);
+      // })
+      let fd = new FormData();
+      fd.append("filename", file);
+      // fd.append('project_id', this.project_id)
+      // fd.append('version_id', this.version_id)
+      console.log(`fd`, fd);
+      this.$http.post("/api/upload/", fd).then(
+        res => {
+          // this.importDataBtnText='导入成功';
+          console.log(`res1111`, res);
+          console.log("======success====");
+        },
+        res => {
+          console.log("===========shibai====");
+          //  this.importDataBtnText='导入失败';
+          console.log(res);
+        }
+      );
+      return false;
+    },
+    submitUpload() {
+      this.$refs.upload.submit();
+    },
 
-      this.uploader.addEventListener("complete", (event) => {
-        console.info('-----------complete----------')
-        console.info(event)
-      })
+    /**
+     * 文本转译/校验
+     * @params text
+     */
+    _parseText(text) {
+      return parseText(text);
+    },
 
-      this.uploader.addEventListener("error", function(data) {
-        console.log(`data`, data)
-        if (data.code === 1) {
-          alert("Don't upload such a big file");
-        }
-      });
-      this.uploader.prompt()
-      // console.log(`object`, document.getElementById("upload-btn"))
-      // const ob = this.uploader.listenOnSubmit(document.getElementById("upload-btn"), document.getElementById("siofu_input"));
-      console.log('==========')
-      // this.uploader.listenOnInput()
-      // console.log(`uploader`, this.uploader);
+    /**
+     * 是否URL
+     * @params text
+     */
+    _isUrl(text) {
+      return isUrl(text);
     },
-    newFn(event) {
-      console.log(`event`, event);
+
+    // // 初始化上传接口的函数,再上面上传之前调用的
+    // fileUpLoad(reslove, reject) {
+    //   const paramsData = {
+    //     multipartUpload: true, // 是否是文件分步,分块上传
+    //     name: this.fileData.name,
+    //     size: this.fileData.size,
+    //   }
+    //   initFileUpload(paramsData).then(res => {
+    //     const uploadUrl = res.data.data.uploadUrl
+    //     const completeMultipartUrl = res.data.data.completeMultipartUrl
+    //     // 文件上传的请求地址
+    //      this.uploadUrl = uploadUrl
+    //     // 合并文件上传的请求地址
+    //     this.completeMultipartUrl = completeMultipartUrl
+    //     reslove();
+    //   }).catch((err)=>{
+    //     reject(err)
+    //   })
+    // },
+    // 文件分割的方法
+    createFileChunk(file, size = chunkSize) {
+      const fileChunkList = [];
+      let count = 0;
+      let num = 1;
+      while (count < file.size) {
+        fileChunkList.push({
+          file: file.slice(count, count + size),
+          partNumber: num
+        });
+        count += size;
+        num++;
+      }
+      return fileChunkList;
     },
+    /**
+   * 根据图片url设置图片框宽高, 注意是图片框
+   */
+  getImageStyle(src) {
+    const arr = src.split('$');
+    let width = Number(arr[2]);
+    let height = Number(arr[3]);
+    if (this.mobile) {
+      // 如果是移动端,图片最大宽度138, 返回值加12是因为设置的是图片框的宽高要加入padding值
+      if (width > 138) {
+        height = (height * 138) / width;
+        width = 138;
+        return {
+          width: `${width + 12}px`,
+          height: `${height + 12}px`,
+        };
+      }
+    }
+    return {
+      width: `${width + 12}px`,
+      height: `${height + 12}px`,
+    };
+  },
+
+    upload() {},
 
     sendMsg(e) {
-      console.log(`uploader`, this.uploader);
-      // if (e.which === 13) {
-      //   console.log(`e.which`, e.which);
-      //   e.cancelBubble = true;
-      //   e.preventDefault();
-      //   e.stopPropagation();
-      // }
+      if (e.which === 13) {
+        console.log(`e.which`, e.which);
+        e.cancelBubble = true;
+        e.preventDefault();
+        e.stopPropagation();
+      }
 
-      // if (this.msg.trim() === "") {
-      //   this.msg = "";
-      //   return;
-      // }
-      // console.log(`this.self`, this.self);
-      // const msgInfo = {
-      //   id: this.$socket.id,
-      //   msg: this.msg,
-      //   nickname: this.self.nickname
-      // };
-      // this.msg = ""; // 输入enter后置空
-      // if (this.tab === "friends") {
-      //   msgInfo.toid = this.selected.id;
-      //   this.$socket.emit("private message", this.selected.id, msgInfo);
-      //   if (this.friend_record[this.selected.id]) {
-      //     this.friend_record[this.selected.id].push(msgInfo);
-      //     this.friend_record_list = this.friend_record[this.selected.id];
-      //   } else {
-      //     this.friend_record[this.selected.id] = [];
-      //     this.friend_record[this.selected.id].push(msgInfo);
-      //     this.friend_record_list = this.friend_record[this.selected.id];
-      //   }
-      //   addCookie("friend_record", JSON.stringify(this.friend_record));
-      // } else {
-      //   msgInfo.roomid = this.selected.id;
-      //   this.$socket.emit("chat-room", this.selected.id, msgInfo);
-      //   if (this.room_record[this.selected.id]) {
-      //     this.room_record[this.selected.id].push(msgInfo);
-      //     this.room_record_list = this.room_record[this.selected.id];
-      //   } else {
-      //     this.room_record[this.selected.id] = [];
-      //     this.room_record[this.selected.id].push(msgInfo);
-      //     this.room_record_list = this.room_record[this.selected.id];
-      //   }
-      //   addCookie("room_record", JSON.stringify(this.room_record));
-      // }
-      // this.$nextTick(() => {
-      //   // 滚动到底部
-      //   this.$refs["myScrollbar"].wrap.scrollTop = this.$refs[
-      //     "myScrollbar"
-      //   ].wrap.scrollHeight;
-      // });
+      if (this.msg.trim() === "") {
+        this.msg = "";
+        return;
+      }
+      if (!this.self) {
+        this.self = JSON.parse(localStorage.getItem("self"));
+      }
+      console.log(`this.self`, this.self);
+      const msgInfo = {
+        id: this.$socket.id,
+        msg: this.msg,
+        nickname: this.self.nickname
+      };
+      this.msg = ""; // 输入enter后置空
+      if (this.tab === "friends") {
+        msgInfo.toid = this.selected.id;
+        this.$socket.emit("private message", this.selected.id, msgInfo);
+        if (this.friend_record[this.selected.id]) {
+          this.friend_record[this.selected.id].push(msgInfo);
+          this.friend_record_list = this.friend_record[this.selected.id];
+        } else {
+          this.friend_record[this.selected.id] = [];
+          this.friend_record[this.selected.id].push(msgInfo);
+          this.friend_record_list = this.friend_record[this.selected.id];
+        }
+        addCookie("friend_record", JSON.stringify(this.friend_record));
+      } else {
+        msgInfo.roomid = this.selected.id;
+        this.$socket.emit("chat-room", this.selected.id, msgInfo);
+        if (this.room_record[this.selected.id]) {
+          this.room_record[this.selected.id].push(msgInfo);
+          this.room_record_list = this.room_record[this.selected.id];
+        } else {
+          this.room_record[this.selected.id] = [];
+          this.room_record[this.selected.id].push(msgInfo);
+          this.room_record_list = this.room_record[this.selected.id];
+        }
+        addCookie("room_record", JSON.stringify(this.room_record));
+      }
+      this.$nextTick(() => {
+        // 滚动到底部
+        this.$refs["myScrollbar"].wrap.scrollTop = this.$refs[
+          "myScrollbar"
+        ].wrap.scrollHeight;
+      });
     },
 
     closeScoket() {
@@ -355,7 +474,7 @@ export default {
   border-radius: 5px;
 }
 .nickname {
-  padding: 10px 5px;
+  padding: 4px;
   margin-bottom: 2px;
   font-size: 14px;
   color: gray;
@@ -366,9 +485,7 @@ export default {
   background-color: rgb(199, 206, 200);
 }
 .send-record {
-  width: 60%;
   position: relative;
-  float: right;
   justify-content: flex-end;
 }
 .receive-record {
@@ -376,8 +493,32 @@ export default {
   justify-content: flex-start;
 }
 .send-record > span {
+  display: inline-block;
   text-align: right;
+}
+.message-content-text,
+.message-content-image {
+  max-width: 600px;
+  display: inline-block;
+  margin-left: 35px;
+  overflow: hidden;
+  margin-top: 4px;
+  padding: 6px;
   background: rgb(117, 197, 128);
+  font-size: 16px;
+  border-radius: 5px;
+  text-align: left;
+  word-break: break-word;
+}
+.message-content-image {
+  max-height: 255px;
+  max-width: 255px;
+  background: rgb(204, 197, 197);
+}
+.message-content-image img {
+  cursor: pointer;
+  max-width: 225px;
+  max-height: 225px;
 }
 .receive-record > span {
   text-align: left;

+ 25 - 8
src/main.js

@@ -3,24 +3,24 @@
 import Vue from 'vue'
 import App from './App'
 import router from './router'
+import Viewer from 'v-viewer'; // 图片预览插件
 import store from './store/index'
 import VueSocketIO from 'vue-socket.io'
 import SocketIO from 'socket.io-client'
 import ElementUI from 'element-ui';
 import "./assets/icon/iconfont.css"
 import 'element-ui/lib/theme-chalk/index.css';
+import 'viewerjs/dist/viewer.css';
+// import contextmenu from "v-contextmenu";
+// import "v-contextmenu/dist/themes/default.css";
+
 import axios from 'axios'
-import SocketIOFileUpload from 'socketio-file-upload'
 Vue.prototype.$http = axios
-Vue.prototype.$socketUpload = function (socket) {
-  const upload = new SocketIOFileUpload(socket)
-  upload.maxFileSize = 2 * 1024 * 1024
-  upload.chunkSize = 100 * 1024
-  return upload
-}
+
 const socketOptions = {
   autoConnect: false,  // 自动连接
 }
+// Vue.use(contextmenu);
 Vue.use(ElementUI);
 Vue.config.productionTip = false
 Vue.use(new VueSocketIO({
@@ -29,7 +29,24 @@ Vue.use(new VueSocketIO({
   // connection: SocketIO('http://127.0.0.1:3000', socketOptions), // 使用Socket.IO-client 本地
   store
 }))
-
+Vue.use(Viewer, {
+  defaultOptions: {
+    navbar: false,
+    title: false,
+    toolbar: {
+      zoomIn: 1,
+      zoomOut: 1,
+      oneToOne: 4,
+      reset: 4,
+      prev: 0,
+      next: 0,
+      rotateLeft: 4,
+      rotateRight: 4,
+      flipHorizontal: 4,
+      flipVertical: 4,
+    },
+  },
+});
 
 
 /* eslint-disable no-new */

+ 103 - 0
src/utils/common.js

@@ -0,0 +1,103 @@
+import Vue from 'vue';
+import { AxiosResponse } from 'axios';
+
+// 处理所有后端返回的数据
+export function processReturn(res) {
+  // code 0:成功 1:错误 2:后端报错
+  const { code, msg, data } = res.data;
+  if (code) {
+    Vue.prototype.$message.error(msg);
+    return;
+  }
+  if (msg) {
+    Vue.prototype.$message.success(msg);
+  }
+  return data;
+}
+
+// 判断一个字符串是否包含另外一个字符串
+export function isContainStr(str1, str2) {
+  return str2.indexOf(str1) >= 0;
+}
+
+/**
+ * 屏蔽词
+ * @param text 文本
+ */
+export function parseText(text) {
+  return text;
+}
+
+/**
+ * 判断是否URL
+ * @param text 文本
+ */
+export function isUrl(text) {
+  // 解析网址
+  // eslint-disable-next-line no-useless-escape
+  const UrlReg = new RegExp(/http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/);
+  return UrlReg.test(text);
+}
+
+/**
+ * 消息时间格式化
+ * @param time
+ */
+export function formatTime(time) {
+  const moment = Vue.prototype.$moment;
+  // 大于昨天
+  if (moment().add(-1, 'days').startOf('day') > time) {
+    return moment(time).format('M/D HH:mm');
+  }
+  // 昨天
+  if (moment().startOf('day') > time) {
+    return `昨天 ${moment(time).format('HH:mm')}`;
+  }
+  // 大于五分钟不显示秒
+  // if (new Date().valueOf() > time + 300000) {
+  //   return moment(time).format('HH:mm');
+  // }
+  return moment(time).format('HH:mm');
+}
+
+/**
+ * 群名/用户名校验
+ * @param name
+ */
+export function nameVerify(name) {
+  const nameReg = /^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]+$/;
+  if (name.length === 0) {
+    Vue.prototype.$message.error('请输入名字');
+    return false;
+  }
+  if (!nameReg.test(name)) {
+    Vue.prototype.$message.error('名字只含有汉字、字母、数字和下划线 不能以下划线开头和结尾');
+    return false;
+  }
+  if (name.length > 16) {
+    Vue.prototype.$message.error('名字太长');
+    return false;
+  }
+  return true;
+}
+
+/**
+ * 密码校验
+ * @param password
+ */
+export function passwordVerify(password) {
+  const passwordReg = '/^\w+$/gis';
+  if (password.length === 0) {
+    Vue.prototype.$message.error('请输入密码');
+    return false;
+  }
+  if (!passwordReg.test(password)) {
+    Vue.prototype.$message.error('密码只含有字母、数字和下划线');
+    return false;
+  }
+  if (password.length > 16) {
+    Vue.prototype.$message.error('密码最多16位,请重新输入');
+    return false;
+  }
+  return true;
+}

+ 23 - 0
yarn.lock

@@ -8703,6 +8703,11 @@ throttle-debounce@^1.0.1:
   resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-1.1.0.tgz#51853da37be68a155cb6e827b3514a3c422e89cd"
   integrity sha512-XH8UiPCQcWNuk2LYePibW/4qL97+ZQ1AN3FNXwZRBNPPowo/NRU5fAlDCSNBJIYCKbioZfuYtMhG4quqoJhVzg==
 
+throttle-debounce@^2.0.1:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2"
+  integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==
+
 through2@^2.0.0:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
@@ -9124,6 +9129,19 @@ uuid@^3.0.1, uuid@^3.3.2:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
 
+v-contextmenu@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/v-contextmenu/-/v-contextmenu-3.0.0.tgz#d20d3beec9cd0cb18befd231425bce7e650221e4"
+  integrity sha512-zi38JxmTt66TmljgV1JbfEa9WvoQkpzRuEwZK7Tjb2XoRejbWLozQtkyTWXJa6x6Y3FrVDfgT36w01gpTpo41A==
+
+v-viewer@^1.6.4:
+  version "1.6.4"
+  resolved "https://registry.yarnpkg.com/v-viewer/-/v-viewer-1.6.4.tgz#39e36b534baab34076fb816704c6a734de0dc72f"
+  integrity sha512-LVkiUHpmsbsZXebeNXnu8krRCi5i2n07FeLFxoIVGhw8lVvTBO0ffpbDC6mLEuacCjrIh09HjIqpciwUtWE8lQ==
+  dependencies:
+    throttle-debounce "^2.0.1"
+    viewerjs "^1.5.0"
+
 validate-npm-package-license@^3.0.1:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@@ -9151,6 +9169,11 @@ verror@1.10.0:
     core-util-is "1.0.2"
     extsprintf "^1.2.0"
 
+viewerjs@^1.5.0:
+  version "1.10.1"
+  resolved "https://registry.yarnpkg.com/viewerjs/-/viewerjs-1.10.1.tgz#07499ed043d0a29e3002b90f55c5b228bd1a742c"
+  integrity sha512-Oyzd3JP9dDSd+bBulfnQ+UTfHoobFwkmcT/uKSnQXjmPz7rZU0HJIiKudxPaMsiv17dr4Sm1cHnASJcDlFw1PA==
+
 vm-browserify@^1.0.1:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"