Browse Source

fix: Merge branch 'tag' of http://face3d.4dage.com:7005/4dkankan/4dkankan_bim into tag

bill 2 years ago
parent
commit
5bb69920bc
48 changed files with 15965 additions and 625 deletions
  1. 12731 0
      package-lock.json
  2. 6 2
      package.json
  3. 173 0
      public/__langs/css/index.css
  4. 126 0
      public/__langs/index.html
  5. 105 0
      public/__langs/locales/en.json
  6. 105 0
      public/__langs/locales/ja.json
  7. 105 0
      public/__langs/locales/zh.json
  8. 34 0
      public/__langs/script/saveLange.js
  9. 4 2
      public/smart-kankan.html
  10. 1 1
      public/smart-viewer.html
  11. 52 0
      scripts/update-i18n.js
  12. BIN
      src/assets/img/pic_bg.png
  13. 4 4
      src/assets/scss/_base-vars.scss
  14. 2 2
      src/assets/scss/components/_button.scss
  15. 116 0
      src/components/audio/index.vue
  16. 47 0
      src/components/button/index.vue
  17. 24 13
      src/components/calendar/mobile.vue
  18. 1 1
      src/components/datepicker/PickerDay.vue
  19. 14 5
      src/components/dialog/Confirm.vue
  20. 175 47
      src/components/files/TagEditor.vue
  21. 5 39
      src/components/files/TagItem.vue
  22. 0 144
      src/components/files/TagLaserItem.vue
  23. 22 124
      src/components/files/TagManager.vue
  24. 242 32
      src/components/files/TagView.vue
  25. 427 0
      src/components/files/content/Comment.vue
  26. 276 0
      src/components/files/content/TagMsg.vue
  27. 347 47
      src/components/files/index.vue
  28. 3 1
      src/components/form/Area.vue
  29. 20 10
      src/components/form/Input.vue
  30. 48 11
      src/components/form/medias/Audio.vue
  31. 74 22
      src/components/form/medias/Image.vue
  32. 13 6
      src/components/form/medias/Link.vue
  33. 52 13
      src/components/form/medias/Video.vue
  34. 18 3
      src/components/form/medias/index.vue
  35. 4 4
      src/components/header/CopyLink.vue
  36. 13 9
      src/components/header/Login.vue
  37. 51 29
      src/components/header/index.vue
  38. 68 0
      src/i18n/index.js
  39. 105 0
      src/locales/en.json
  40. 105 0
      src/locales/ja.json
  41. 105 0
      src/locales/zh.json
  42. 12 10
      src/pages/SViewer.vue
  43. 77 38
      src/pages/Viewer.vue
  44. 12 2
      src/pages/sviewer.js
  45. 18 2
      src/pages/viewer.js
  46. 4 1
      src/utils/ClickOutSide.js
  47. 14 0
      src/utils/browser.js
  48. 5 1
      src/utils/request.js

File diff suppressed because it is too large
+ 12731 - 0
package-lock.json


+ 6 - 2
package.json

@@ -1,16 +1,20 @@
 {
   "name": "@kankan/smart-bim",
   "private": true,
-  "version": "4.4.4",
+  "version": "4.7.0-alpha.14",
   "scripts": {
     "serve": "vue-cli-service serve",
     "build": "vue-cli-service build",
-    "build:test": "vue-cli-service build --mode test"
+    "build:test": "vue-cli-service build --mode test",
+    "langs": "node ./scripts/update-i18n.js"
   },
   "dependencies": {
     "axios": "^0.21.1",
     "clipboard": "^2.0.11",
     "core-js": "^3.6.5",
+    "cors": "^2.8.5",
+    "express": "^4.18.2",
+    "quill": "^1.3.6",
     "vant": "^3.6.4",
     "vue": "^3.2.26",
     "vue-i18n": "9",

+ 173 - 0
public/__langs/css/index.css

@@ -0,0 +1,173 @@
+.locales-setting {
+  position: fixed;
+  z-index: 20000;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.5);
+  pointer-events: all;
+}
+.locales-setting > div {
+  padding: 40px 20px;
+  display: flex;
+  align-items: flex-start;
+  position: absolute;
+  left: 30px;
+  top: 30px;
+  bottom: 30px;
+  right: 30px;
+  background: #efefef;
+  box-shadow: 0 0 8px #666;
+}
+.locales-setting > div aside {
+  width: 200px;
+  height: 100%;
+  border-right: solid 1px #999;
+}
+.locales-setting > div aside li {
+  margin-bottom: 10px;
+  cursor: pointer;
+}
+.locales-setting > div aside li.active {
+  color: #f60;
+}
+.locales-setting > div main {
+  flex: 1;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  overflow-y: auto;
+}
+.locales-setting > div main li {
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+}
+.locales-setting > div main li input {
+  height: 24px;
+  border: solid 1px #666;
+  width: 100%;
+  padding: 0 4px;
+}
+.locales-setting > div main li > div:first-child {
+  width: 400px;
+  /* text-align: right; */
+  padding-right: 3px;
+}
+.locales-setting > div main li > div:last-child {
+  width: 100%;
+}
+.locales-setting .save {
+  position: absolute;
+  right: 5px;
+  top: 5px;
+}
+.locales-setting .save select {
+  border: solid 1px #666;
+  width: 70px;
+  height: 24px;
+  text-align: center;
+  background: #fff;
+}
+.locales-setting .save button {
+  border: solid 1px #666;
+  width: 70px;
+  height: 24px;
+  text-align: center;
+  background: #fff;
+  margin-left: 10px;
+}
+
+
+
+
+
+.loader {
+  position: relative;
+  margin: 0 auto;
+  width: 100px
+
+}
+.loader:before {
+  content: '';
+  display: block;
+  padding-top: 100%;
+}
+
+.circular {
+  animation: rotate 2s linear infinite;
+  height: 100%;
+  transform-origin: center center;
+  width: 100%;
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  margin: auto;
+}
+
+.path {
+  stroke-dasharray: 1, 200;
+  stroke-dashoffset: 0;
+  animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite;
+  stroke-linecap: round;
+}
+
+@keyframes rotate {
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+@keyframes dash {
+  0% {
+    stroke-dasharray: 1, 200;
+    stroke-dashoffset: 0;
+  }
+  50% {
+    stroke-dasharray: 89, 200;
+    stroke-dashoffset: -35px;
+  }
+  100% {
+    stroke-dasharray: 89, 200;
+    stroke-dashoffset: -124px;
+  }
+}
+
+@keyframes color {
+  100%,
+  0% {
+    stroke: #d62d20;
+  }
+  40% {
+    stroke: #0057e7;
+  }
+  66% {
+    stroke: #008744;
+  }
+  80%,
+  90% {
+    stroke: #ffa700;
+  }
+}
+
+
+body {
+  background-color: #eee;
+}
+.showbox {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  padding: 5%;
+  z-index: 100000;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+

+ 126 - 0
public/__langs/index.html

@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="UTF-8" />
+        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+        <title>配置国际化</title>
+    </head>
+    <link rel="stylesheet" href="./css/index.css" />
+    <body>
+        <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
+        <div id="app">
+            <div class="locales-setting">
+                <div>
+                    <div class="save">
+                        <select v-model="locale" @change="onLocaleChange">
+                            <option value="zh">中文</option>
+                            <option value="en">英文</option>
+                            <option value="kr">韩文</option>
+                            <option value="fr">法语</option>
+                            <option value="ja">日语</option>
+                        </select>
+                        <button @click="onSave">保存</button>
+                    </div>
+
+                    <aside>
+                        <ul>
+                            <li v-for="menu in menus" @click="onModuleChange(menu.name)" :class="{ active: menu.name == module }">{{ menu.text }}</li>
+                        </ul>
+                    </aside>
+                    <main>
+                        <ul>
+                            <li v-for="locale in locales">
+                                <div>{{ locale.key }}:</div>
+                                <div><input type="text" @input="onChangeVal(locale)" v-model="locale.value" /></div>
+                            </li>
+                        </ul>
+                    </main>
+                </div>
+            </div>
+            <div class="showbox" v-if="loading">
+                <div class="loader">
+                    <svg class="circular" viewBox="25 25 50 50">
+                        <circle class="path" cx="50" cy="50" r="20" fill="none" stroke-width="2" stroke-miterlimit="10" />
+                    </svg>
+                </div>
+            </div>
+        </div>
+
+        <script>
+            const { createApp } = Vue
+            createApp({
+                data() {
+                    return {
+                        locale: 'zh',
+                        locales: [],
+                        info: {},
+                        menus: [],
+                        module: 'home',
+                        respone: null,
+                        loading: false,
+                    }
+                },
+                methods: {
+                    async onSave() {
+                        this.loading = true
+
+                        let params = this.respone
+                        fetch('http://192.168.0.130:9091/save', {
+                            method: 'post',
+                            headers: {
+                                'Content-Type': 'application/x-www-form-urlencoded',
+                            },
+                            body: `q=${JSON.stringify(params)}&locale=${this.locale}`,
+                        })
+                            .then(async res => {
+                                this.loading = false
+
+                                let json = await res.json()
+                                alert('保存成功')
+                            })
+                            .catch(err => {
+                                this.loading = false
+                                alert('保存失败,请联系管理员')
+                            })
+                    },
+                    onChangeVal(item) {
+                        this.respone[this.module][item.key] = item.value
+                    },
+                    async onLocaleChange() {
+                        this.locales = []
+                        this.respone = await this.fetchLocale()
+                        this.initData(this.respone)
+                    },
+                    onModuleChange(name) {
+                        this.locales = []
+                        this.module = name
+                        this.initData(this.respone)
+                    },
+                    initData(data, init = false) {
+                        for (let key in data) {
+                            if (key.split('.').pop() == 'name') {
+                                if (init) {
+                                    this.menus.push({ name: key.split('.')[0], text: data[key] })
+                                }
+                            } else if (typeof data[key] == 'object' && key == this.module) {
+                                this.initData(data[key])
+                            } else if (typeof data[key] == 'string') {
+                                this.locales.push({ key: key.split('.')[0], value: data[key] })
+                            }
+                        }
+                    },
+                    async fetchLocale(locale) {
+                        return await (await fetch(`./locales/${this.locale}.json?${Date.now()}`)).json()
+                    },
+                },
+                mounted() {
+                    this.fetchLocale('zh').then(res => {
+                        this.respone = res
+                        this.initData(res, true)
+                    })
+                },
+            }).mount('#app')
+        </script>
+    </body>
+</html>

+ 105 - 0
public/__langs/locales/en.json

@@ -0,0 +1,105 @@
+{
+    "home": {
+        "thickness": "密度选择",
+        "dateScene": "选择日期未上传场景",
+        "notChoosePoint": "您还未选择关联位置",
+        "notFindFile": "未发现BIM文件",
+        "notFindScene": "未发现对比场景",
+        "sceneDelete": "当前场景已被删除,无法进行查看",
+        "tag": "tag",
+        "splitScreen": "分屏",
+        "fullScreen": "全屏"
+    },
+    "home.name": "首页",
+    "header": {
+        "passwordText1": "密码不能为空",
+        "phoneText1": "手机号码不能为空",
+        "phoneText2": "请输入正确手机号",
+        "copyLink": "复制链接",
+        "shareLink": "分享链接",
+        "setting": "设为",
+        "reset": "重设",
+        "userInfo": "个人信息",
+        "loginout": "退出登录",
+        "adhustText1": "为场景设置关联位置",
+        "adhustText2": "请选择位置,确认左右视图中的场景在同一位置后,单击右侧按钮将其设为关联位置。",
+        "userLogin": "用户登录",
+        "rememberPassword": "记住密码",
+        "forgetPassword": "忘记密码",
+        "resigter": "官网注册",
+        "setPointfaidText": "匹配失败,请选择不同点位进行同步",
+        "pointUpdate": "关联位置已更新"
+    },
+    "components": {
+        "uploadVideo": "上传视频",
+        "limitFileSizeBit": "支持 {file} 文件:≤ {size}MB,≤ {bit}Mbps",
+        "linkView": "网页展示区",
+        "continueAdd": "继续添加",
+        "uploadImg": "上传图片",
+        "limitImgLength": "支持JPG、PNG等图片格式,单张不超过5MB,最多支持上传9张。",
+        "TipsImgLength": "图片数量最多支持上传9张",
+        "limitFileSize": "支持 {file} 文件:≤ {size}MB",
+        "FileSizeTips": "请上传 {size}MB 以内的 {file} 文件",
+        "uploadAudio": "上传音频",
+        "year": "年",
+        "month": "月",
+        "day": "日",
+        "chooseTime": "选择时间"
+    },
+    "tag": {
+        "addComment": "发一条评论吧",
+        "addCommentTips": "请输入内容",
+        "deletetCommentTips": "确定要删除吗?",
+        "noComment": "暂无评论",
+        "unkownUser": "未知用户",
+        "reply": "reply",
+        "comment": "comment",
+        "creater": "创建人",
+        "createTime": "创建时间",
+        "statusText1": "待处理",
+        "statusText2": "进行中",
+        "statusText3": "未解决",
+        "statusText4": "已解决",
+        "uploadFile": "上传附件",
+        "desc": "描述",
+        "member": "涉及的成员",
+        "status": "状态",
+        "inputDesc": "请输入描述",
+        "inputMember": "请选择需要通知的项目人员",
+        "inputStatus": "请选择处理状态",
+        "inputTagName": "请输入标注名称",
+        "tagName": "标注名称",
+        "creatTag": "新建标注",
+        "addTag": "添加标注",
+        "isAddTag": "已添加标注",
+        "deleteTagText": "确定要删除资料吗?"
+    },
+    "tag.name": "标注",
+    "components.name": "组件",
+    "header.name": "头部",
+    "common": {
+        "high": "高",
+        "middle": "中",
+        "low": "低",
+        "input": "请输入",
+        "publish": "发布",
+        "submit": "提交",
+        "exit": "退出",
+        "delete": "删除",
+        "edit": "编辑",
+        "confirm": "confirm",
+        "tips": "tips",
+        "login": "login",
+        "cancel": "cancel",
+        "sync": "sync",
+        "deleteSuccess": "删除成功",
+        "copySuccess": "复制成功",
+        "syncSuccess": "同步成功"
+    },
+    "common.name": "通用",
+    "code": {
+        "4008": "用户未登录",
+        "failed": "连接服务器失败"
+    },
+    "code.name": "状态码"
+}

+ 105 - 0
public/__langs/locales/ja.json

@@ -0,0 +1,105 @@
+{
+    "home": {
+        "thickness": "密度选择",
+        "dateScene": "选择日期未上传场景",
+        "notChoosePoint": "您还未选择关联位置",
+        "notFindFile": "未发现BIM文件",
+        "notFindScene": "未发现对比场景",
+        "sceneDelete": "当前场景已被删除,无法进行查看",
+        "tag": "标注",
+        "splitScreen": "分屏",
+        "fullScreen": "全屏"
+    },
+    "home.name": "首页",
+    "header": {
+        "passwordText1": "密码不能为空",
+        "phoneText1": "手机号码不能为空",
+        "phoneText2": "请输入正确手机号",
+        "copyLink": "复制链接",
+        "shareLink": "分享链接",
+        "setting": "设为",
+        "reset": "重设",
+        "userInfo": "个人信息",
+        "loginout": "退出登录",
+        "adhustText1": "为场景设置关联位置",
+        "adhustText2": "请选择位置,确认左右视图中的场景在同一位置后,单击右侧按钮将其设为关联位置。",
+        "userLogin": "用户登录",
+        "rememberPassword": "记住密码",
+        "forgetPassword": "忘记密码",
+        "resigter": "官网注册",
+        "setPointfaidText": "匹配失败,请选择不同点位进行同步",
+        "pointUpdate": "关联位置已更新"
+    },
+    "components": {
+        "uploadVideo": "上传视频",
+        "limitFileSizeBit": "支持 {file} 文件:≤ {size}MB,≤ {bit}Mbps",
+        "linkView": "网页展示区",
+        "continueAdd": "继续添加",
+        "uploadImg": "上传图片",
+        "limitImgLength": "支持JPG、PNG等图片格式,单张不超过5MB,最多支持上传9张。",
+        "TipsImgLength": "图片数量最多支持上传9张",
+        "limitFileSize": "支持 {file} 文件:≤ {size}MB",
+        "FileSizeTips": "请上传 {size}MB 以内的 {file} 文件",
+        "uploadAudio": "上传音频",
+        "year": "年",
+        "month": "月",
+        "day": "日",
+        "chooseTime": "选择时间"
+    },
+    "tag": {
+        "addComment": "发一条评论吧",
+        "addCommentTips": "请输入内容",
+        "deletetCommentTips": "确定要删除吗?",
+        "noComment": "暂无评论",
+        "unkownUser": "未知用户",
+        "reply": "回复",
+        "comment": "评论",
+        "creater": "创建人",
+        "createTime": "创建时间",
+        "statusText1": "待处理",
+        "statusText2": "进行中",
+        "statusText3": "未解决",
+        "statusText4": "已解决",
+        "uploadFile": "上传附件",
+        "desc": "描述",
+        "member": "涉及的成员",
+        "status": "状态",
+        "inputDesc": "请输入描述",
+        "inputMember": "请选择需要通知的项目人员",
+        "inputStatus": "请选择处理状态",
+        "inputTagName": "请输入标注名称",
+        "tagName": "标注名称",
+        "creatTag": "新建标注",
+        "addTag": "添加标注",
+        "isAddTag": "已添加标注",
+        "deleteTagText": "确定要删除资料吗?"
+    },
+    "tag.name": "标注",
+    "components.name": "组件",
+    "header.name": "头部",
+    "common": {
+        "high": "高",
+        "middle": "中",
+        "low": "低",
+        "input": "请输入",
+        "publish": "发布",
+        "submit": "提交",
+        "exit": "退出",
+        "delete": "删除",
+        "edit": "编辑",
+        "confirm": "确定",
+        "tips": "提示",
+        "login": "登录",
+        "cancel": "取消",
+        "sync": "同步",
+        "deleteSuccess": "删除成功",
+        "copySuccess": "复制成功",
+        "syncSuccess": "同步成功"
+    },
+    "common.name": "通用",
+    "code": {
+        "4008": "用户未登录",
+        "failed": "连接服务器失败"
+    },
+    "code.name": "状态码"
+}

+ 105 - 0
public/__langs/locales/zh.json

@@ -0,0 +1,105 @@
+{
+    "home": {
+        "thickness": "密度选择",
+        "dateScene": "选择日期未上传场景",
+        "notChoosePoint": "您还未选择关联位置",
+        "notFindFile": "未发现BIM文件",
+        "notFindScene": "未发现对比场景",
+        "sceneDelete": "当前场景已被删除,无法进行查看",
+        "tag": "标注",
+        "splitScreen": "分屏",
+        "fullScreen": "全屏"
+    },
+    "home.name": "首页",
+    "header": {
+        "passwordText1": "密码不能为空",
+        "phoneText1": "手机号码不能为空",
+        "phoneText2": "请输入正确手机号",
+        "copyLink": "复制链接",
+        "shareLink": "分享链接",
+        "setting": "设为",
+        "reset": "重设",
+        "userInfo": "个人信息",
+        "loginout": "退出登录",
+        "adhustText1": "为场景设置关联位置",
+        "adhustText2": "请选择位置,确认左右视图中的场景在同一位置后,单击右侧按钮将其设为关联位置。",
+        "userLogin": "用户登录",
+        "rememberPassword": "记住密码",
+        "forgetPassword": "忘记密码",
+        "resigter": "官网注册",
+        "setPointfaidText": "匹配失败,请选择不同点位进行同步",
+        "pointUpdate": "关联位置已更新"
+    },
+    "components": {
+        "uploadVideo": "上传视频",
+        "limitFileSizeBit": "支持 {file} 文件:≤ {size}MB,≤ {bit}Mbps",
+        "linkView": "网页展示区",
+        "continueAdd": "继续添加",
+        "uploadImg": "上传图片",
+        "limitImgLength": "支持JPG、PNG等图片格式,单张不超过5MB,最多支持上传9张。",
+        "TipsImgLength": "图片数量最多支持上传9张",
+        "limitFileSize": "支持 {file} 文件:≤ {size}MB",
+        "FileSizeTips": "请上传 {size}MB 以内的 {file} 文件",
+        "uploadAudio": "上传音频",
+        "year": "年",
+        "month": "月",
+        "day": "日",
+        "chooseTime": "选择时间"
+    },
+    "tag": {
+        "addComment": "发一条评论吧",
+        "addCommentTips": "请输入内容",
+        "deletetCommentTips": "确定要删除吗?",
+        "noComment": "暂无评论",
+        "unkownUser": "未知用户",
+        "reply": "回复",
+        "comment": "评论",
+        "creater": "创建人",
+        "createTime": "创建时间",
+        "statusText1": "待处理",
+        "statusText2": "进行中",
+        "statusText3": "未解决",
+        "statusText4": "已解决",
+        "uploadFile": "上传附件",
+        "desc": "描述",
+        "member": "涉及的成员",
+        "status": "状态",
+        "inputDesc": "请输入描述",
+        "inputMember": "请选择需要通知的项目人员",
+        "inputStatus": "请选择处理状态",
+        "inputTagName": "请输入标注名称",
+        "tagName": "标注名称",
+        "creatTag": "新建标注",
+        "addTag": "添加标注",
+        "isAddTag": "已添加标注",
+        "deleteTagText": "确定要删除资料吗?"
+    },
+    "tag.name": "标注",
+    "components.name": "组件",
+    "header.name": "头部",
+    "common": {
+        "high": "高",
+        "middle": "中",
+        "low": "低",
+        "input": "请输入",
+        "publish": "发布",
+        "submit": "提交",
+        "exit": "退出",
+        "delete": "删除",
+        "edit": "编辑",
+        "confirm": "确定",
+        "tips": "提示",
+        "login": "登录",
+        "cancel": "取消",
+        "sync": "同步",
+        "deleteSuccess": "删除成功",
+        "copySuccess": "复制成功",
+        "syncSuccess": "同步成功"
+    },
+    "common.name": "通用",
+    "code": {
+        "4008": "用户未登录",
+        "failed": "连接服务器失败"
+    },
+    "code.name": "状态码"
+}

+ 34 - 0
public/__langs/script/saveLange.js

@@ -0,0 +1,34 @@
+const express = require('express')
+const fs = require('fs')
+const path = require('path')
+const app = express()
+
+// 配置解析表单数据的中间件
+app.use(express.urlencoded({ extended: false }))
+
+// 一定要在路由之前,配置 cors 这个中间件,从而解决接口跨域的问题
+const cors = require('cors')
+app.use(cors())
+
+// 必须在配置 cors 中间件之前,配置 JSONP 的接口
+app.post('/save', (req, res) => {
+    const data = JSON.parse(req.body.q)
+    const locale = req.body.locale
+    fs.writeFile(path.join(__dirname, '..', 'locales', locale + '.json'), JSON.stringify(data, null, 4), err => {
+        if (err) {
+            return
+        }
+        fs.writeFile(path.join(__dirname, '..', '..', '..', 'src', 'locales', locale + '.json'), JSON.stringify(data, null, 4), () => {
+            res.send({ success: true, code: 1, data }).end()
+        })
+    })
+})
+// 导入路由模块
+// const router = require('./16.apiRouter')
+// // 把路由模块,注册到 app 上
+// app.use('/api', router)
+
+// 启动服务器
+app.listen(9091, () => {
+    console.log('express server running at http://127.0.0.1')
+})

+ 4 - 2
public/smart-kankan.html

@@ -25,9 +25,11 @@
             <strong>We're sorry but doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
         </noscript>
         <div id="app"></div>
-        <!-- <script src="<%= BASE_URL %><%= VUE_APP_SDK_DIR %>kankan-sdk-deps.js?v=<%= VUE_APP_VERSION %>"></script>
-        <script src="<%= BASE_URL %><%= VUE_APP_SDK_DIR %>kankan-sdk.js?v=<%= VUE_APP_VERSION %>"></script> -->
+        <script src="<%= BASE_URL %><%= VUE_APP_SDK_DIR %>kankan-sdk-deps.js?v=<%= VUE_APP_VERSION %>"></script>
+        <script src="<%= BASE_URL %><%= VUE_APP_SDK_DIR %>kankan-sdk.js?v=<%= VUE_APP_VERSION %>"></script>
+        <!-- 
         <script src="http://192.168.0.80:3099/dist/sdk/kankan-sdk-deps.js?v=<%= VUE_APP_VERSION %>"></script>
         <script src="http://192.168.0.80:3099/dist/sdk/kankan-sdk.js?v=<%= VUE_APP_VERSION %>"></script>
+        -->
     </body>
 </html>

+ 1 - 1
public/smart-viewer.html

@@ -7,7 +7,7 @@
         <link rel="icon" type="image/svg+xml" href="//4dkk.4dage.com/FDKKIMG/icon/kankan_icon.ico" />
         <link rel="stylesheet" href="<%= VUE_APP_STATIC_DIR %>/ext/iconfont/iconfont.css" />
         <link rel="stylesheet" href="<%= VUE_APP_STATIC_DIR %>/ext/swiper/swiper.min.css" />
-        <link rel="stylesheet" href="//at.alicdn.com/t/c/font_3693743_hxuk44ksxw9.css" />
+        <link rel="stylesheet" href="//at.alicdn.com/t/c/font_3693743_nvvps2z7q3.css" />
         <script src="<%= VUE_APP_STATIC_DIR %>/ext/mobile-detect.js"></script>
         <script src="<%= VUE_APP_STATIC_DIR %>/ext/base64.min.js"></script>
         <script src="<%= VUE_APP_STATIC_DIR %>/ext/swiper/swiper.min.js"></script>

+ 52 - 0
scripts/update-i18n.js

@@ -0,0 +1,52 @@
+const fs = require('fs')
+const path = require('path')
+const locales = ['en', 'ja']
+
+const merge = (source, resource, target) => {
+    for (let key in source) {
+        if (typeof source[key] === 'string') {
+            target[key] = resource[key] == void 0 ? source[key] : resource[key]
+        } else {
+            target[key] = {}
+            resource[key] = resource[key] || {}
+            merge(source[key], resource[key], target[key])
+        }
+    }
+}
+
+const combine = (source, resource, target) => {
+    merge(source, resource, target)
+}
+
+function exec() {
+    var source = null
+    fs.readFile(path.join(__dirname, '..', 'src', 'locales', 'zh.json'), (err, data) => {
+        if (err) {
+            return
+        }
+        source = JSON.parse(data.toString())
+        fs.writeFile(path.join(__dirname, '..', 'public', '__langs', 'locales', 'zh.json'), JSON.stringify(source, null, 4), () => {})
+
+        locales.forEach(locale => {
+            try {
+                fs.readFile(path.join(__dirname, '..', 'src', 'locales', locale + '.json'), (err, response) => {
+                    fs.readFile(path.join(__dirname, '..', 'src', 'locales', 'zh.json'), (err, data) => {
+                        if (err) {
+                            return
+                        }
+                        var target = {}
+                        // var source = JSON.parse(data.toString())
+                        var resource = JSON.parse(response.toString())
+                        combine(source, resource, target)
+
+                        fs.writeFile(path.join(__dirname, '..', 'src', 'locales', locale + '.json'), JSON.stringify(target, null, 4), () => {})
+                        fs.writeFile(path.join(__dirname, '..', 'public', '__langs', 'locales', locale + '.json'), JSON.stringify(target, null, 4), () => {})
+                    })
+                })
+            } catch (error) {
+                console.log(error)
+            }
+        })
+    })
+}
+exec()

BIN
src/assets/img/pic_bg.png


+ 4 - 4
src/assets/scss/_base-vars.scss

@@ -1,12 +1,12 @@
 
 :root {
   --colors-primary-fill: 255, 255, 255;
-  --colors-primary-base-fill: 0, 200, 175;
+  --colors-primary-base-fill:0, 118, 246;
   --colors-primary-base: rgb(var(--colors-primary-base-fill));
-  --colors-primary-hover: #4DD8C7;
+  --colors-primary-hover: #0076F6;
   // --colors-primary-hover: #008B7A;
-  --colors-primary-active: #008B7A;
-  --colors-primary-click: #005046;
+  --colors-primary-active:#0076F6;
+  --colors-primary-click: #0076F6;
   --colors-warn: #FA3F48;
   // --colors-color: #999;
   --colors-color: rgba(255, 255, 255, 0.7);

+ 2 - 2
src/assets/scss/components/_button.scss

@@ -69,8 +69,8 @@
     // &:active,
     &:hover {
         // opacity: 0.8;
-        // background: var(--colors-primary-hover) !important;
-        background: #4DD8C7 !important;
+        background: var(--colors-primary-hover) !important;
+        // background: #4DD8C7 !important;
     }
     &:active {
         background-color: var(--colors-primary-active) !important;

+ 116 - 0
src/components/audio/index.vue

@@ -0,0 +1,116 @@
+<template>
+    <div class="ui-audio audio" @click="clickHandler">
+        <audio loop @play="rotation" ref="audio">
+            <source :src="src" />
+        </audio>
+        <span v-for="random in randoms" :style="{ '--percent': random }" />
+    </div>
+</template>
+
+<script setup>
+import { defineProps, ref, watchEffect, defineExpose } from 'vue'
+defineProps({
+    src: String,
+    autoplay: {
+        type: Boolean,
+        default: true,
+    },
+})
+const audio = ref()
+const randoms = ref([1, 0.5, 1, 0.5])
+const playIng = ref(false)
+
+let timeout
+const rotation = () => {
+    if (!playIng.value) return
+    for (let i = 0; i < randoms.value.length; i++) {
+        randoms.value[i] = Math.random()
+    }
+    timeout = setTimeout(rotation, 200)
+}
+
+watchEffect(() => {
+    if (audio.value) {
+        if (playIng.value) {
+            audio.value.play()
+        } else {
+            audio.value.pause()
+        }
+        clearTimeout(timeout)
+        rotation()
+    }
+})
+
+const clickHandler = () => {
+    playIng.value = !playIng.value
+}
+
+defineExpose({
+    play() {
+        playIng.value = true
+    },
+    pause() {
+        playIng.value = false
+    },
+})
+// 播放音乐
+const audioAutoPlay = () => {
+    if (window.WeixinJSBridge) {
+        WeixinJSBridge.invoke(
+            'getNetworkType',
+            {},
+            function (e) {
+                audio.value.play()
+            },
+            false
+        )
+    } else {
+        document.addEventListener(
+            'touchstart',
+            function () {
+                WeixinJSBridge.invoke('getNetworkType', {}, function (e) {
+                    audio.value.play()
+                })
+            },
+            false
+        )
+    }
+}
+const detectWeixin = () => {
+    //微信 包括PC的微信
+    return window.navigator.userAgent.toLowerCase().match(/MicroMessenger/i) == 'micromessenger'
+}
+if (detectWeixin()) {
+    console.log('detectWeixin')
+    audioAutoPlay()
+}
+// 自动播放
+clickHandler()
+</script>
+
+<script>
+export default { name: 'ui-audio' }
+</script>
+<style lang="scss" scoped>
+.audio {
+    display: inline-block;
+    cursor: pointer;
+
+    > span {
+        --height: 18px;
+        width: 3px;
+        height: calc(var(--height) * var(--percent));
+        background: var(--colors-primary-base);
+        display: inline-block;
+        transition: height 0.2s linear;
+
+        &:not(:last-child) {
+            margin-right: 2px;
+        }
+    }
+
+    audio {
+        display: none;
+    }
+}
+</style>

+ 47 - 0
src/components/button/index.vue

@@ -0,0 +1,47 @@
+<template>
+    <button class="ui-button" :class="className" :style="style">
+        <!-- <UIIcon :type="icon" v-if="icon" class="ui-button-icon" /> -->
+        <slot></slot>
+    </button>
+</template>
+
+<script setup>
+import { defineProps, computed } from 'vue'
+import { normalizeUnitToStyle } from '@/utils/dom.js'
+// import UIIcon from '../icon'
+
+const props = defineProps({
+    type: {
+        type: String,
+        default: 'normal',
+    },
+    color: {
+        type: String,
+    },
+    width: {
+        type: [String, Number],
+    },
+    icon: {
+        type: String,
+    },
+})
+
+const custom = `customize`
+const className = computed(() => (props.color ? custom : props.type))
+
+const style = computed(() => {
+    const style = {
+        width: normalizeUnitToStyle(props.width),
+    }
+
+    if (className.value === custom) {
+        style['--color'] = props.color
+    }
+    return style
+})
+</script>
+
+<script>
+export default { name: 'ui-button' }
+</script>
+

+ 24 - 13
src/components/calendar/mobile.vue

@@ -1,17 +1,28 @@
 <template>
     <div class="calendar" v-show="!showCalendar">
         <span class="prev" @click="emits('prev')"><i class="iconfont icon-arrows_left"></i></span>
-        <span class="cale" @click="onPickDate()">{{ date }}<i style="display: none;" class="iconfont icon-date"></i></span>
+        <span class="cale" @click="onPickDate()">{{ date }}<i style="display: none" class="iconfont icon-date"></i></span>
         <span class="next" @click="emits('next')"><i class="iconfont icon-arrows_right"></i></span>
     </div>
     <div class="calendar-list" v-if="showCalendar" @click="showCalendar = false">
         <div @click.stop>
-            <van-datetime-picker v-model="props.value" type="date" title="选择时间" confirm-button-text="确定" :filter="onFilter" :formatter="onFormatter" @confirm="onConfirm" @cancel="onCancel" />
+            <van-datetime-picker
+                v-model="props.value"
+                type="date"
+                :title="$t('components.chooseTime')"
+                :confirm-button-text="$t('common.confirm')"
+                :filter="onFilter"
+                :formatter="onFormatter"
+                @confirm="onConfirm"
+                @cancel="onCancel"
+            />
         </div>
     </div>
 </template>
 <script setup>
 import { ref, defineProps, computed } from 'vue'
+import { useI18n, getLocale } from '@/i18n'
+const { t } = useI18n({ useScope: 'global' })
 const props = defineProps({
     value: Date,
     highlighted: Array,
@@ -30,34 +41,34 @@ const onPickDate = () => {
 }
 const onFormatter = (type, val) => {
     if (type === 'year') {
-        return `${val}年`
+        return val + t('components.year')
     }
     if (type === 'month') {
-        return `${val}月`
+        return val + t('components.month')
     }
     if (type === 'day') {
-        return `${val}日`
+        return val + t('components.day')
     }
     return val
 }
-const onFilter = (type, options)=>{
+const onFilter = (type, options) => {
     const days = props.highlighted
-    if(!days.year.length){
+    if (!days.year.length) {
         return options
     }
-    if(type == 'year') {
+    if (type == 'year') {
         return days.year
-    }else if(type == 'month'){
+    } else if (type == 'month') {
         return days.month
-    } else if(type == 'day'){
+    } else if (type == 'day') {
         return days.day
     }
     return options
 }
-const onCancel = ()=>{
+const onCancel = () => {
     showCalendar.value = false
 }
-const onConfirm = payload=>{
+const onConfirm = payload => {
     emits('selected', { payload })
     showCalendar.value = false
 }
@@ -99,7 +110,7 @@ const onConfirm = payload=>{
     bottom: 0;
     height: 100vh;
     z-index: 1000;
-    background: rgba(0,0,0,0.5);
+    background: rgba(0, 0, 0, 0.5);
     display: flex;
     align-items: flex-end;
     justify-content: center;

+ 1 - 1
src/components/datepicker/PickerDay.vue

@@ -16,7 +16,7 @@
           >&lt;</span
         >
         <span class="day__month_btn" @click="showMonthCalendar" :class="allowedToShowView('month') ? 'up' : ''"
-          >{{ isYmd ? currMonthName : currYearName }} {{ isYmd ? currYearName : currMonthName }}</span
+          >{{ isYmd ? currMonthName : currYearName }}{{$t('components.year')}} {{ isYmd ? currYearName : currMonthName }}</span
         >
         <span @click="isRtl ? previousMonth() : nextMonth()" class="next" :class="{ disabled: isRightNavDisabled }"
           >&gt;</span

+ 14 - 5
src/components/dialog/Confirm.vue

@@ -17,9 +17,12 @@
     </ui-dialog>
 </template>
 <script>
-import { defineComponent } from 'vue'
+import { defineComponent, defineEmits } from 'vue'
 import { isFunction, omit } from '../../utils'
-
+import UiDialog from './Dialog.vue'
+import UiButton from '../button/index.vue'
+// console.error(UiDialog)
+const emits = defineEmits(['ok', 'no'])
 export default defineComponent({
     name: 'ui-confirm',
     props: {
@@ -49,15 +52,21 @@ export default defineComponent({
     },
     setup: function (props, ctx) {
         const close = result => {
-            if (isFunction(props.func) && props.func(result) === false) {
-                return
+            if (result == 'ok') {
+                ctx.emit('ok', 'ok')
+            } else {
+                ctx.emit('no', 'no')
             }
-            isFunction(props.destroy) && props.destroy()
+            // if (isFunction(props.func) && props.func(result) === false) {
+            //     return
+            // }
+            // isFunction(props.destroy) && props.destroy()
         }
         return {
             ...omit(props, 'destroy', 'func'),
             close,
         }
     },
+    components: { UiDialog, UiButton },
 })
 </script>

+ 175 - 47
src/components/files/TagEditor.vue

@@ -2,49 +2,59 @@
     <div class="tag-editor">
         <div class="tag-editor-content" :style="{ height: height + 'px' }">
             <header>
-                <span>新建标注</span>
-                <i class="iconfont icon-close" @click="emits('action', null)"></i>
+                <span>{{ $t('tag.creatTag') }}</span>
+                <i class="iconfont icon-close" @click="onClose"></i>
             </header>
             <article>
                 <div>
-                    <h4><span>*</span>资料名称</h4>
-                    <UiInput v-model="form.title" type="text" placeholder="请输入资料名称" :maxlength="20" />
+                    <h4><span>*</span>{{ $t('tag.tagName') }}</h4>
+                    <UiInput v-model="form.title" type="text" :placeholder="$t('tag.inputTagName')" :maxlength="20" />
                 </div>
                 <div>
-                    <h4><span>*</span>状态</h4>
-                    <UiInput v-model="form.status" type="select" placeholder="请选择处理状态" :data="data.status" />
+                    <h4><span>*</span>{{ $t('tag.status') }}</h4>
+                    <UiInput v-model="form.status" type="select" :placeholder="$t('tag.inputStatus')" :data="data.status" />
                 </div>
                 <div>
-                    <h4>涉及的成员</h4>
-                    <UiSelectList v-model="form.members" placeholder="请选择需要通知的项目人员" :data="data.members" />
+                    <h4>{{ $t('tag.member') }}</h4>
+                    <UiSelectList v-model="form.members" :placeholder="$t('tag.inputMember')" :data="data.members" />
                 </div>
                 <div>
-                    <h4>描述</h4>
-                    <UiArea v-model="form.describe" type="text" placeholder="请输入描述" :maxlength="50" />
+                    <h4>{{ $t('tag.desc') }}</h4>
+                    <UiArea v-model="form.describe" type="text" :placeholder="$t('tag.inputDesc')" :maxlength="50" />
                 </div>
                 <div>
-                    <h4>上传附件</h4>
+                    <h4>{{ $t('tag.uploadFile') }}</h4>
                     <UiMedias />
                 </div>
             </article>
             <footer>
-                <button @click="onSubmit">提交</button>
+                <button @click="onSubmit">{{ $t('common.submit') }}</button>
             </footer>
         </div>
+        <Toast v-if="showTips" type="warn" :content="showTips" :close="() => (showTips = null)" />
+        <Loading v-if="showLoading"></Loading>
     </div>
 </template>
 <script setup>
-import { ref, onMounted, onBeforeUnmount } from 'vue'
-import browser from '@/utils/browser'
+import { ref, inject, onMounted, onBeforeUnmount, watch } from 'vue'
+import { convertBlob2File } from '@/utils/file'
 import { http } from '@/utils/request'
+import browser from '@/utils/browser'
+import Toast from '@/components/dialog/Toast'
+import Loading from '@/components/loading/Loading.vue'
 import UiArea from '../form/Area.vue'
 import UiInput from '../form/Input.vue'
 import UiMedias from '../form/medias'
 import UiSelectList from '../form/SelectList.vue'
-
+import { from } from 'readable-stream'
+import i18n from '@/i18n'
+const { t } = i18n.global
+const showLoading = ref(false)
+const showTips = ref(null)
 const projectId = browser.valueFromUrl('projectId') || 1
-const props = defineProps(['notify'])
-const emits = defineEmits(['action'])
+const notify = inject('notify')
+const tags = inject('tags')
+const isEdit = inject('isEdit')
 const height = ref(0)
 const form = ref({
     title: '',
@@ -52,53 +62,171 @@ const form = ref({
     status: '',
     members: [],
 })
+const typeList = ['image', 'video', 'audio', 'link']
+let mediaList = []
+let tag = null
 const data = ref({
     status: [
-        { text: '待处理', value: 1 },
-        { text: '进行中', value: 2 },
-        { text: '未解决', value: 3 },
-        { text: '已解决', value: 4 },
+        { text: t('tag.statusText1'), value: 1 },
+        { text: t('tag.statusText2'), value: 2 },
+        { text: t('tag.statusText3'), value: 3 },
+        { text: t('tag.statusText4'), value: 4 },
     ],
     members: [],
 })
-const onSubmit = () => {
-    const tag = { ...props.notify.tag }
-    tag.title = form.value.title
-    tag.content = form.value.describe
-    tag.visiblePanos = tag.visiblePanos
-    console.log(tag)
-    // http.post(`smart-site/marking/addOrUpdate`, {
-    //     projectId,
-    //     userIds: form.value.members.map(item => item.value),
-    //     markingStatus: form.value.status,
-    //     markingTitle: form.value.title,
-    //     hotData:tag
-    // }).then(response => {
-    //     debugger
-    // })
+const onClose = () => {
+    if (notify.value.__temp) {
+        if (window.kankan) {
+            kankan.TagManager.remove(notify.value.sid)
+        } else {
+            tags.value.splice(tags.value.length - 1, 1)
+        }
+    }
+
+    isEdit.value = false
+    notify.value = null
+}
+let pushData = null
+const onSubmit = async () => {
+    if (!form.value.title) {
+        return (showTips.value = t('tag.inputTagName'))
+    }
+    if (!form.value.status) {
+        return (showTips.value = t('tag.inputStatus'))
+    }
+    notify.value.title = form.value.title
+    notify.value.content = form.value.describe
+    //提交前删除别的数据
+    typeList.forEach(item => {
+        if (notify.value.type != item) {
+            delete notify.value.media[item]
+        }
+    })
+    if (notify.value._add) {
+        pushData = notify.value
+    }
+    tag = JSON.stringify(notify.value, (key, value) => {
+        if (key === 'visiblePanos') {
+            return value.map(item => item.id)
+        }
+
+        if (key === 'media' && notify.value.media[notify.value.type]) {
+            mediaList = notify.value.media[notify.value.type]
+        }
+
+        return value
+    })
+    tag = JSON.parse(tag)
+    tag.status = form.value.status
+    tag.members = form.value.members.map(item => item.value)
+    delete tag.__temp
+    await handlerUpload(tag)
+}
+const handlerUpload = async data => {
+    showLoading.value = true
+    if (mediaList.length) {
+        for (let i = 0; i < mediaList.length; i++) {
+            if (mediaList[i].file) {
+                let res = await http.postFile(`smart-site/upload/${projectId}/${notify.value.type}/file/`, {
+                    file: mediaList[i].file,
+                })
+                if (res.success) {
+                    delete tag.media[notify.value.type][i].file
+                    delete notify.value.media[notify.value.type][i].file
+                    tag.media[notify.value.type][i].src = res.data
+                    notify.value.media[notify.value.type][i].src = res.data
+                } else if (res.code == 4008) {
+                    showTips.value = t('code.4008')
+                } else {
+                    showTips.value = res.message
+                }
+            }
+        }
+
+        mediaList = []
+    }
+    let params = {
+        markingId: null,
+        projectId,
+        userIds: tag.members,
+        markingStatus: form.value.status,
+        markingTitle: form.value.title,
+        hotData: tag,
+    }
+    if (tag.id) {
+        params.markingId = tag.id
+    }
+
+    http.post(`smart-site/marking/addOrUpdate`, params).then(response => {
+        showLoading.value = false
+        if (response.success) {
+            notify.value.status = tag.status
+            notify.value.members = tag.members
+            if (pushData && notify.value._add) {
+                delete notify.value._add
+                pushData.status = tag.status
+                pushData.content = tag.content
+                pushData.members = tag.members
+                tags.value.push(pushData)
+                pushData = null
+            }
+            isEdit.value = false
+            delete notify.value.__temp
+            notify.value.id = response.data
+            notify.value = null
+        } else if (response.code == 4008) {
+            showTips.value = t('code.4008')
+        } else {
+            showTips.value = response.message
+        }
+    })
 }
 const onResize = () => {
     height.value = window.innerHeight - 90
 }
+let markingId = null
 onMounted(() => {
-    window.kankan.TagManager.focusTag(props.notify.sid, {
-        direction: 'left',
-        attrs: {
-            width: 450,
-            height: 400,
-        },
-    })
-    onResize()
-    window.addEventListener('resize', onResize)
-
     http.post(`smart-site/projectTeam/select`, { projectId }).then(response => {
         data.value.members = response.data.map(item => {
             return {
-                text: item.userName,
+                text: item.nickName,
                 value: item.userId,
             }
         })
+
+        if (notify.value) {
+            form.value.status = String(notify.value.status || '')
+            form.value.title = notify.value.title || ''
+            form.value.describe = notify.value.content || ''
+            form.value.members = []
+            if (notify.value.members && notify.value.members.length) {
+                notify.value.members.forEach(item => {
+                    let find = data.value.members.find(c => c.value == item)
+                    if (find) {
+                        form.value.members.push(find)
+                    }
+                })
+            }
+        }
     })
+
+    if (window.kankan) {
+        window.kankan.TagManager.focusTag(notify.value.sid, {
+            direction: 'left',
+            attrs: {
+                width: 0,
+                height: 400,
+            },
+        })
+    } else if (window.laser) {
+        window.laser.then(sdk => {
+            // let pos = notify.value.tag.position
+            let pos = notify.value.position
+            sdk.scene.comeToTag(new THREE.Vector3(pos.x, pos.y, pos.z))
+        })
+    }
+    onResize()
+    window.addEventListener('resize', onResize)
 })
 
 onBeforeUnmount(() => {

+ 5 - 39
src/components/files/TagItem.vue

@@ -1,14 +1,9 @@
 <template>
-    <div v-if="props.scene == 'laser'"  :data-tag-id="props.tag.sid"  class="tag-item" ref="dom">
-        <div class="tag-icon">
-            <span>{{ props.index }}</span>
-        </div>
-    </div>
     <div
-        v-else
+        v-if="props.tag"
         @click="onClick"
         :data-tag-id="props.tag.sid"
-        :style="{ lefts: `${props.tag.x}px`, tops: `${props.tag.y}px`, transform: `translate(${props.tag.x}px,${props.tag.y}px)`, display: props.tag.visible ? 'block' : 'none' }"
+        :style="{ transform: `translate(${props.tag.x}px,${props.tag.y}px)`, display: props.tag.visible ? 'block' : 'none' }"
         class="tag-item"
     >
         <div class="tag-icon">
@@ -17,7 +12,7 @@
     </div>
 </template>
 <script setup>
-import { ref,onMounted } from 'vue'
+import { inject } from 'vue'
 const props = defineProps({
     tag: {
         type: Object,
@@ -28,40 +23,11 @@ const props = defineProps({
         required: true,
     },
 })
-const emits = defineEmits(['action'])
-
-const dom = ref(null)
 
+const notify = inject('notify')
 const onClick = () => {
-    emits('action', { event: 'focus', sid: props.tag.sid, tag: props.tag })
+    notify.value = props.tag //{ event: 'focus', sid: props.tag.sid, tag: props.tag }
 }
-onMounted(() => {
-    if (window.laser) {
-        window.laser.then(sdk => {
-            const posInfo = sdk.scene.getPointByScreen({ x: e.clientX, y: e.clientY })
-            sdk.addMouseDownEvent(e => {
-                if (e.button == 2) {
-                    const posInfo = sdk.scene.getPointByScreen({ x: e.clientX, y: e.clientY })
-                    const tag = {
-                        panoId: '0',
-                        createTime: 1658223413176,
-                        icon: 'R7PTZK233714.png',
-                        x: e.clientX,
-                        y: e.clientY,
-                        position: { x: -0.7253925749728442, y: -0.12276942990074809, z: 2.3097074481849633 },
-                        media: { link: [{ src: 'https://test.4dkankan.com/#/scene' }] },
-                        type: 'link',
-                        title: '链接热点',
-                        content: '',
-                        sid: 'ctKxOW233013',
-                        visible: true,
-                    }
-                    tags.value.push(tag)
-                }
-            })
-        })
-    }
-})
 </script>
 <style lang="scss" scoped>
 .tag-item {

+ 0 - 144
src/components/files/TagLaserItem.vue

@@ -1,144 +0,0 @@
-<template>
-    <div :data-tag-id="props.tag.sid"  class="tag-item">
-        <div class="tag-icon">
-            <span>{{ props.index }}</span>
-        </div>
-    </div>
-</template>
-<script setup>
-const props = defineProps({
-    tag: {
-        type: Object,
-        required: true,
-    },
-    index: {
-        type: Number,
-        required: true,
-    },
-})
-console.log(props)
-const emits = defineEmits(['action'])
-
-const onClick = () => {
-    emits('action', { event: 'focus', sid: props.tag.sid, tag: props.tag })
-}
-
-</script>
-<style lang="scss" scoped>
-.tag-item {
-    cursor: pointer;
-    pointer-events: all;
-    display: none;
-    position: absolute;
-    width: 26px;
-    height: 26px;
-    margin-left: -13px;
-    margin-top: -13px;
-    z-index: 1;
-    &.active {
-        .tag-icon {
-            background-color: green;
-        }
-    }
-}
-
-.tag-item.focus {
-    z-index: 2;
-}
-.tag-item.focus .tag-body {
-    transform: translateY(-50%) scale(1);
-}
-.tag-item.fixed {
-    z-index: 3;
-}
-.tag-item.active {
-    z-index: 4;
-}
-
-.tag-item .tag-icon {
-    position: relative;
-    display: block;
-    width: 26px;
-    height: 26px;
-    border-radius: 0 50% 50% 50%;
-    border: 1px solid #fff;
-    transform: rotate(-135deg);
-    background-color: #0076f6;
-    span {
-        display: block;
-        transform: rotate(135deg);
-        margin-top: 3px;
-        margin-left: -1px;
-        text-align: center;
-    }
-}
-
-.tag-item .tag-icon.animate {
-    animation: tag-animate-zoom 3s -1s linear infinite;
-}
-
-.tag-item .tag-body {
-    position: fixed;
-    right: 0;
-    top: 50%;
-    margin-right: 70px;
-    width: 200px;
-    height: 200px;
-    transform: translateY(-50%) scale(1);
-    transform-origin: calc(100% + 40px) -50%;
-    background: rgba(27, 27, 28, 0.8);
-    border-radius: 4px;
-    min-width: 400px;
-    padding: 30px 20px;
-}
-
-.tag-item .tag-body::before {
-    content: '';
-    position: absolute;
-    width: 40px;
-    height: 100%;
-    top: 0;
-    right: -40px;
-}
-
-.tag-item .tag-body::after {
-    content: '';
-    position: absolute;
-    top: 50%;
-    right: -39px;
-    width: 0;
-    height: 0;
-    border-top: 15px solid transparent;
-    border-bottom: 15px solid transparent;
-    border-left: 40px solid rgba(27, 27, 28, 0.8);
-    transform: translateY(-50%);
-}
-
-@keyframes tag-animate-zoom {
-    0% {
-        transform: scale(1);
-    }
-    50% {
-        transform: scale(0.7);
-    }
-    100% {
-        transform: scale(1);
-    }
-}
-
-.tag-item .v-enter-from,
-.tag-item .v-leave-to {
-    opacity: 0;
-    transform: translateY(50%) scale(0);
-}
-.tag-item .v-enter-active,
-.tag-item .v-leave-active {
-    will-change: transform;
-    transition: all 0.25s cubic-bezier(0.35, 0.32, 0.65, 0.63);
-}
-.tag-item .v-enter-to,
-.tag-item .v-leave-from {
-    opacity: 1;
-    transform: translateY(-50%) scale(1);
-}
-</style>

File diff suppressed because it is too large
+ 22 - 124
src/components/files/TagManager.vue


+ 242 - 32
src/components/files/TagView.vue

@@ -1,45 +1,65 @@
 <template>
-    <div class="tag-view">
-        <div class="tag-view-content" :style="{ height: height + 'px' }">
+    <div class="tag-view" v-show="canShow">
+        <!-- <div class="tag-view-content" :style="{ height: height + 'px' }"> -->
+        <div class="tag-view-content">
             <header>
-                <span>场景标注名称 </span>
-                <i class="iconfont icon-close" @click="emits('action', null)"></i>
+                <span>{{ notify.title }}</span>
+                <!-- <i class="iconfont icon-close" @click="emits('action', null)"></i> -->
+                <i class="iconfont icon-close" @click="onClose"></i>
             </header>
             <article>
-                <div>
-                    <div>
-                        
-                    </div>
-                </div>
-                <div></div>
+                <TagMsg @setShow="setShow"></TagMsg>
+                <Comment :slideHeigt="slideHeigt"></Comment>
             </article>
-            <footer></footer>
+            <!-- <footer></footer> -->
         </div>
     </div>
 </template>
 <script setup>
-import { ref, onMounted, onBeforeUnmount } from 'vue'
+import { ref, onMounted, onBeforeUnmount, computed, inject } from 'vue'
 
-const props = defineProps(['notify'])
+import TagMsg from './content/TagMsg.vue'
+import Comment from './content/Comment.vue'
+import { nextTick } from 'process'
+const notify = inject('notify')
 const emits = defineEmits(['action'])
 const height = ref(0)
-const form = ref({
-    title: '',
-    describe: '',
-    status: '',
-    members: [],
-})
+const canShow = ref(false)
 const onResize = () => {
     height.value = window.innerHeight - 90
 }
-onMounted(() => {
-    window.kankan.TagManager.focusTag(props.notify.sid, {
-        direction: 'left',
-        // attrs: {
-        //     width: 450,
-        //     height: 400,
-        // },
+const onClose = () => {
+    if (window.kankan) {
+        if (notify.value.__temp) {
+            kankan.TagManager.remove(notify.value.sid)
+        }
+    } else {
+    }
+    notify.value = null
+}
+const slideHeigt = ref(0)
+const setShow = data => {
+    canShow.value = true
+
+    nextTick(() => {
+        slideHeigt.value = document.querySelector('.left-item').offsetHeight
     })
+}
+onMounted(() => {
+    if (window.kankan) {
+        window.kankan.TagManager.focusTag(notify.value.sid, {
+            direction: 'left',
+            attrs: {
+                width: 0,
+                // height: 400,
+            },
+        })
+    } else if (window.laser) {
+        window.laser.then(sdk => {
+            let pos = notify.value.position
+            sdk.scene.comeToTag(new THREE.Vector3(pos.x, pos.y, pos.z))
+        })
+    }
     onResize()
     window.addEventListener('resize', onResize)
 })
@@ -49,6 +69,191 @@ onBeforeUnmount(() => {
 })
 </script>
 <style lang="scss" scoped>
+.aside-item {
+    padding: 20px;
+    box-sizing: border-box;
+    line-height: 28px;
+    .content-item {
+        margin-bottom: 20px;
+        // &:last-of-type {
+        //     margin-bottom: 0;
+        // }
+    }
+    // &.left-item {
+    //     width: 400px;
+    //     .item-title {
+    //         font-size: 14px;
+    //         color: #999;
+    //     }
+    //     .content-desc {
+    //         font-size: 14px;
+    //         line-height: 28px;
+    //     }
+    //     .media-box {
+    //         width: 360px;
+    //         height: 225px;
+    //         border-radius: 4px 4px 4px 4px;
+    //         opacity: 1;
+    //         border: 1px solid rgba(255, 255, 255, 0.2);
+    //         position: relative;
+    //         // margin-bottom: 30px;
+    //         // &.zoom-in {
+    //         //     cursor: zoom-in;
+    //         // }
+    //     }
+    // }
+
+    &.right-item {
+        position: relative;
+        .input-content {
+            width: 100%;
+            height: 34px;
+            margin: 20px 0;
+            padding: 0 20px;
+            position: absolute;
+            bottom: 0;
+            left: 0;
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            .input-box {
+                width: 226px;
+                height: 34px;
+                background: rgba(255, 255, 255, 0.1);
+                border-radius: 4px;
+                opacity: 1;
+                border: 1px solid rgba(255, 255, 255, 0.2);
+                input {
+                    line-height: 34px;
+                    padding: 0 10px;
+                    color: #fff;
+                }
+            }
+            .send-btn {
+                width: 60px;
+                height: 34px;
+                background: #0076f6;
+                border-radius: 4px;
+                opacity: 1;
+                text-align: center;
+                line-height: 34px;
+                cursor: pointer;
+            }
+        }
+        .comment-content {
+            .comment-header {
+                font-size: 16px;
+                font-weight: bold;
+                color: #999;
+            }
+            .comment-msg {
+                .comment-item {
+                    display: flex;
+                    align-items: flex-start;
+                    justify-content: flex-start;
+                    margin-top: 14px;
+                    .avatar-box {
+                        width: 24px;
+                        height: 24px;
+                        border-radius: 50%;
+                        background: #f2f2f2;
+                        margin-right: 6px;
+                    }
+                    .comment-box {
+                        flex: 1;
+                        > div {
+                            display: flex;
+                            align-items: center;
+                            justify-content: space-between;
+                        }
+                        .view-top {
+                            .user-name {
+                                font-size: 14px;
+                                color: #999;
+                            }
+                            .iconfont {
+                                color: #999;
+                                font-size: 1em;
+                                cursor: pointer;
+                            }
+                        }
+                        .view-middle {
+                            .comment-text {
+                                font-size: 14px;
+                                color: #fff;
+                            }
+                        }
+                        .view-bottom {
+                            font-size: 12px;
+                            .comment-time {
+                                color: #999;
+                            }
+                            .reply-btn {
+                                color: #0076f6;
+                                cursor: pointer;
+                            }
+                        }
+                        .reply-content {
+                            .reply-item {
+                                display: flex;
+                                align-items: flex-start;
+                                justify-content: flex-start;
+                                margin-top: 14px;
+                                width: 100%;
+                                .avatar-box {
+                                    width: 24px;
+                                    height: 24px;
+                                    border-radius: 50%;
+                                    background: #f2f2f2;
+                                    margin-right: 6px;
+                                }
+                                .reply-box {
+                                    flex: 1;
+                                    > div {
+                                        display: flex;
+                                        align-items: center;
+                                        justify-content: space-between;
+                                    }
+                                    .view-top {
+                                        .user-name {
+                                            font-size: 14px;
+                                            color: #999;
+                                        }
+                                        .iconfont {
+                                            color: #999;
+                                            font-size: 1em;
+                                            cursor: pointer;
+                                        }
+                                    }
+                                    .view-middle {
+                                        .reply-text {
+                                            font-size: 14px;
+                                            color: #fff;
+                                            .reply-tips {
+                                                color: #999;
+                                            }
+                                        }
+                                    }
+                                    .view-bottom {
+                                        font-size: 12px;
+                                        .reply-time {
+                                            color: #999;
+                                        }
+                                        .reply-btn {
+                                            color: #0076f6;
+                                            cursor: pointer;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
 .tag-view {
     pointer-events: all;
     position: absolute;
@@ -65,8 +270,11 @@ onBeforeUnmount(() => {
 .tag-view-content {
     display: flex;
     flex-direction: column;
-    width: 740px;
-    height: 400px;
+    // width: 740px;
+    // height: 400px;
+    width: 741px;
+    max-height: 776px;
+    min-height: 70px;
     background: rgba(27, 27, 28, 0.8);
     box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
     border-radius: 4px 4px 4px 4px;
@@ -94,12 +302,14 @@ onBeforeUnmount(() => {
     article {
         display: flex;
         width: 100%;
-        height: 100%;
+        // max-height: 711px;
+        overflow: hidden;
         > div {
             width: 50%;
-            &:first-child {
-                border-right: solid 1px rgba(255, 255, 255, 0.16);
-            }
+            
+            // &.left-item {
+            //     border-right: solid 1px rgba(255, 255, 255, 0.16);
+            // }
         }
     }
 

+ 427 - 0
src/components/files/content/Comment.vue

@@ -0,0 +1,427 @@
+<!--  -->
+<template>
+    <div class="aside-item right-item">
+        <div class="comment-content" ref="slider" v-if="slideHeigt" :style="`height:${slideHeigt - 84}px;`">
+            <div class="comment-header">
+                <span>{{ $t('tag.comment') }}</span>
+            </div>
+            <div class="comment-msg">
+                <div class="comment-item" v-for="(i, index) in commentList">
+                    <div class="avatar-box" :style="i.head ? `background-image:url(${i.head});` : `background-image:url(${emptyAvatar});`"></div>
+                    <div class="comment-box">
+                        <div class="view-box view-top">
+                            <span class="user-name">{{ i.nickName || $t('tag.unkownUser') }}</span>
+                            <i class="iconfont icon-del" v-if="i.userId == userId" @click="delComment({ commentId: i.commentId, index })"></i>
+                        </div>
+                        <div class="view-box view-middle">
+                            <span class="comment-text">{{ i.content }}</span>
+                        </div>
+                        <div class="view-box view-bottom">
+                            <span class="comment-time">{{ i.createTime }}</span>
+                            <span class="reply-btn" @click="handlerReply({ parentId: i.commentId, nickName: i.nickName }, index)">{{ $t('tag.reply') }}</span>
+                        </div>
+                        <div class="reply-content" v-if="i.children">
+                            <div class="reply-item" v-for="(j, j_index) in i.children">
+                                <div class="avatar-box" :style="j.head ? `background-image:url(${j.head});` : `background-image:url(${emptyAvatar});`"></div>
+                                <div class="reply-box">
+                                    <div class="view-box view-top">
+                                        <span class="user-name">{{ j.nickName || $t('tag.unkownUser') }}</span>
+                                        <i class="iconfont icon-del" v-if="j.userId == userId" @click="delComment({ commentId: j.commentId, index: j_index, parentIndex: index })"></i>
+                                    </div>
+                                    <div class="view-box view-middle">
+                                        <span class="reply-text"
+                                            ><span v-if="j.replyId"
+                                                >{{ $t('tag.reply') }}<span class="reply-tips">@{{ j.replyNickName || $t('tag.unkownUser') }}</span></span
+                                            >
+                                            {{ j.content }}</span
+                                        >
+                                    </div>
+                                    <div class="view-box view-bottom">
+                                        <span class="reply-time">{{ j.createTime }}</span>
+                                        <span class="reply-btn" @click="handlerReply({ parentId: i.commentId, replyId: j.commentId, parentUserId: j.userId, nickName: j.nickName }, j_index)">{{
+                                            $t('tag.reply')
+                                        }}</span>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="empty-box" v-if="!commentList.length">
+                <div class="pic"></div>
+                <div>{{ $t('tag.noComment') }}</div>
+            </div>
+        </div>
+
+        <div class="input-content">
+            <div class="input-box"><input ref="input$" @input="handlerInput" v-model="inputText" :placeholder="placeholderText" type="text" /></div>
+            <div class="send-btn" @click="hanlderSubmit">{{ $t('common.publish') }}</div>
+        </div>
+    </div>
+    <Toast v-if="showTips" :type="tipsType" :content="showTips" :close="() => (showTips = null)" />
+
+    <ui-confirm v-if="delComfirm" :title="$t('common.tips')" :noText="$t('common.cancel')" :okText="$t('common.confirm')" @ok="handlerDel" @no="handlerDel">
+        <template #content>
+            <div>{{ $t('tag.deletetCommentTips') }}</div>
+        </template>
+    </ui-confirm>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, computed, inject, nextTick, defineProps } from 'vue'
+import Toast from '@/components/dialog/Toast'
+import { http } from '@/utils/request'
+import avatar from '@/assets/img/avatar@2x.png'
+import UiConfirm from '@/components/dialog/Confirm.vue'
+import i18n from '@/i18n'
+const { t } = i18n.global
+const props = defineProps({
+    slideHeigt: Number,
+})
+let canPut = true
+const delComfirm = ref(null)
+const emptyAvatar = ref(avatar)
+const notify = inject('notify')
+const emits = defineEmits(['action'])
+const input$ = ref(null)
+const inputText = ref('')
+const placeholderText = ref(t('tag.addComment'))
+const replyInfo = ref(null)
+const tipsType = ref('warn')
+const showTips = ref(null)
+const slider = ref(null)
+
+const handlerReply = (data, index) => {
+    inputText.value = ''
+    let name = data.nickName ? data.nickName : t('tag.unkownUser')
+    placeholderText.value = '@' + name
+    delete data.nickName
+    replyInfo.value = data
+}
+
+const handlerInput = () => {
+    // console.log(inputText.value.length)
+    // if (replyInfo.value && inputText.value.length) {
+    //     // console.log(1)
+    // }
+}
+let parentId = null
+let commentId = null
+const userId = ref(localStorage.getItem('userId') - 0)
+const commentList = ref([])
+const hanlderSubmit = () => {
+    if (inputText.value == '') {
+        tipsType.value = 'warn'
+        showTips.value = t('tag.addCommentTips')
+        return
+    }
+
+    let params = {
+        markingId: notify.value.id,
+        content: inputText.value,
+        userId: userId.value,
+        parentId: parentId,
+    }
+
+    if (replyInfo.value) {
+        for (let key in replyInfo.value) {
+            if (replyInfo.value[key]) {
+                params[key] = replyInfo.value[key]
+            }
+        }
+    }
+
+    if (canPut) {
+        canPut = false
+        http.post(`smart-site/comment/reply`, params)
+            .then(response => {
+                if (response.success) {
+                    getAllComments()
+
+                    // if (replyInfo.value) {
+                    // } else {
+                    //     // slider.value.
+                    //     slider.value.scrollTo({
+                    //         top: 0,
+                    //         behavior: 'smooth',
+                    //     })
+                    // }
+
+                    replyInfo.value = null
+                    inputText.value = ''
+                    placeholderText.value = t('tag.addComment')
+                } else {
+                    tipsType.value = 'error'
+                    showTips.value = response.message
+                }
+                canPut = true
+            })
+            .catch(err => {
+                canPut = true
+            })
+    }
+}
+const onClose = () => {
+    if (window.kankan) {
+        if (notify.value.__temp) {
+            kankan.TagManager.remove(notify.value.sid)
+        }
+    } else {
+    }
+    notify.value = null
+}
+const getAllComments = () => {
+    http.post(`smart-site/comment/tree/all`, { markingId: notify.value.id }).then(response => {
+        if (response.success) {
+            commentList.value = response.data
+        } else {
+        }
+    })
+}
+const handlerDel = status => {
+    if (status == 'ok') {
+        http.post(`smart-site/comment/del`, { commentId: delComfirm.value.commentId }).then(response => {
+            if (response.success) {
+                // if (!delComfirm.value.parentIndex) {
+                //     commentList.value.splice(delComfirm.value.index, 1)
+                // } else {
+                //     commentList.value[delComfirm.value.parentIndex].children.splice(delComfirm.value.index, 1)
+                // }
+                getAllComments()
+                if (replyInfo.value?.parentId == delComfirm.value.commentId) {
+                    replyInfo.value = null
+                    inputText.value = ''
+                    placeholderText.value = t('tag.addComment')
+                }
+                tipsType.value = 'success'
+                showTips.value = t('common.deleteSuccess')
+            } else {
+                tipsType.value = 'error'
+                showTips.value = response.message
+            }
+            delComfirm.value = null
+        })
+    } else {
+        delComfirm.value = null
+    }
+}
+const delComment = data => {
+    delComfirm.value = data
+}
+onMounted(() => {
+    getAllComments()
+    if (window.kankan) {
+        window.kankan.TagManager.focusTag(notify.value.sid, {
+            direction: 'left',
+            attrs: {
+                width: 0,
+                // height: 400,
+            },
+        })
+    } else if (window.laser) {
+        window.laser.then(sdk => {
+            let pos = notify.value.position
+            sdk.scene.comeToTag(new THREE.Vector3(pos.x, pos.y, pos.z))
+        })
+    }
+    nextTick(() => {
+        input$.value.addEventListener('keydown', function (e) {
+            if (e.keyCode == 8) {
+                if (replyInfo.value && !inputText.value.length) {
+                    replyInfo.value = null
+                    placeholderText.value = t('tag.addComment')
+                }
+            }
+        })
+    })
+})
+
+onBeforeUnmount(() => {})
+</script>
+<style lang="scss" scoped>
+.aside-item {
+    padding: 20px 0 20px 20px;
+    box-sizing: border-box;
+    line-height: 28px;
+    flex: 1;
+    &.right-item {
+        position: relative;
+        .input-content {
+            width: 100%;
+            height: 34px;
+            margin: 20px 0;
+            padding: 0 20px;
+            position: absolute;
+            bottom: 0;
+            left: 0;
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            .input-box {
+                width: 226px;
+                height: 34px;
+                background: rgba(255, 255, 255, 0.1);
+                border-radius: 4px;
+                opacity: 1;
+                border: 1px solid rgba(255, 255, 255, 0.2);
+                input {
+                    line-height: 34px;
+                    padding: 0 10px;
+                    color: #fff;
+                }
+            }
+            .send-btn {
+                width: 60px;
+                height: 34px;
+                background: #0076f6;
+                border-radius: 4px;
+                opacity: 1;
+                text-align: center;
+                line-height: 34px;
+                cursor: pointer;
+            }
+        }
+        .empty-box {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            flex-flow: column;
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+            .pic {
+                width: 134px;
+                height: 134px;
+                background: url('~@/assets/img/pic_bg.png') no-repeat;
+                background-size: 100% 100%;
+            }
+            div {
+                margin-top: 5px;
+                color: #999;
+            }
+        }
+        .comment-content {
+            // height: calc(100% - 54px);
+            overflow-y: auto;
+            padding: 0 20px 0 0;
+            position: relative;
+            .comment-header {
+                font-size: 16px;
+                font-weight: bold;
+                color: #999;
+            }
+            .comment-msg {
+                .comment-item {
+                    display: flex;
+                    align-items: flex-start;
+                    justify-content: flex-start;
+                    margin-top: 14px;
+                    .avatar-box {
+                        width: 24px;
+                        height: 24px;
+                        border-radius: 50%;
+                        margin-right: 6px;
+                        background-size: 100% 100%;
+
+                        background-repeat: no-repeat;
+                    }
+                    .comment-box {
+                        flex: 1;
+                        > div.view-box {
+                            display: flex;
+                            align-items: center;
+                            justify-content: space-between;
+                        }
+                        .view-top {
+                            .user-name {
+                                font-size: 14px;
+                                color: #999;
+                            }
+                            .iconfont {
+                                color: #999;
+                                font-size: 1em;
+                                cursor: pointer;
+                            }
+                        }
+                        .view-middle {
+                            .comment-text {
+                                font-size: 14px;
+                                color: #fff;
+                            }
+                        }
+                        .view-bottom {
+                            font-size: 12px;
+                            .comment-time {
+                                color: #999;
+                            }
+                            .reply-btn {
+                                color: #0076f6;
+                                cursor: pointer;
+                            }
+                        }
+                        .reply-content {
+                            .reply-item {
+                                display: flex;
+                                align-items: flex-start;
+                                justify-content: flex-start;
+                                margin-top: 14px;
+                                width: 100%;
+                                .avatar-box {
+                                    width: 24px;
+                                    height: 24px;
+                                    border-radius: 50%;
+                                    margin-right: 6px;
+                                    background-size: 100% 100%;
+
+                                    background-repeat: no-repeat;
+                                }
+                                .reply-box {
+                                    flex: 1;
+                                    > div {
+                                        display: flex;
+                                        align-items: center;
+                                        justify-content: space-between;
+                                    }
+                                    .view-top {
+                                        .user-name {
+                                            font-size: 14px;
+                                            color: #999;
+                                        }
+                                        .iconfont {
+                                            color: #999;
+                                            font-size: 1em;
+                                            cursor: pointer;
+                                        }
+                                    }
+                                    .view-middle {
+                                        .reply-text {
+                                            font-size: 14px;
+                                            color: #fff;
+                                            .reply-tips {
+                                                color: #0076f6;
+                                                margin: 0 2px;
+                                            }
+                                        }
+                                    }
+                                    .view-bottom {
+                                        font-size: 12px;
+                                        .reply-time {
+                                            color: #999;
+                                        }
+                                        .reply-btn {
+                                            color: #0076f6;
+                                            cursor: pointer;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+</style>

+ 276 - 0
src/components/files/content/TagMsg.vue

@@ -0,0 +1,276 @@
+<!--  -->
+<template>
+    <div class="aside-item left-item">
+        <UiAudio v-if="notify.type == 'audio'" :src="notify.media?.[notify.type][0].src" />
+
+        <div class="content-item">
+            <div class="item-title">{{$t('tag.createTime')}}</div>
+            <span class="content-desc">{{ notify.createTime }}</span>
+        </div>
+        <div class="content-item">
+            <div class="item-title">{{$t('tag.creater')}}</div>
+            <span class="content-desc">{{ notify.lastCreateBy }}</span>
+        </div>
+        <div class="content-item">
+            <div class="item-title">{{$t('tag.status')}}</div>
+            <span class="content-desc" v-for="i in data.status">
+                <span v-if="i.value == form.status">{{ i.text }}</span></span
+            >
+        </div>
+        <div class="content-item">
+            <div class="item-title">{{$t('tag.member')}}</div>
+            <span class="content-desc" v-for="(i, index) in form.members"
+                ><span>{{ i.text }}</span>
+                <span v-if="index < form.members.length - 1">、</span>
+            </span>
+        </div>
+        <div class="content-item">
+            <div class="item-title">{{$t('tag.desc')}}</div>
+            <span class="content-desc">{{ form.describe }}</span>
+        </div>
+        <div class="media-box" :class="{ nor: notify.type == 'link', 'zoom-in': notify.type != 'video' }" @click="openView = true" v-if="notify.media?.[notify.type]?.length && notify.type != 'audio'">
+            <component :is="component"></component>
+        </div>
+
+        <!-- <audio preload="true" autoplay>
+            <source :src="notify.media?.[notify.type][0].src" type="audio/mpeg" />
+        </audio> -->
+        <teleport to="body">
+            <div class="mask-layer" v-if="openView">
+                <div class="close-btn">
+                    <i class="iconfont icon-close" @click="openView = false"></i>
+                </div>
+                <div class="layer-content">
+                    <component :is="component"></component>
+                </div>
+            </div>
+        </teleport>
+    </div>
+    <Toast v-if="showTips" :type="showTips?.type" :content="showTips.text" :close="closeTips" />
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, computed, inject } from 'vue'
+import { http } from '@/utils/request'
+import browser from '@/utils/browser'
+import Image from '@/components/form/medias/Image.vue'
+import Video from '@/components/form/medias/Video.vue'
+import Audio from '@/components/form/medias/Audio.vue'
+import Link from '@/components/form/medias/Link.vue'
+import UiAudio from '@/components/audio/index.vue'
+import Toast from '@/components/dialog/Toast'
+import { nextTick } from 'process'
+import i18n from '@/i18n'
+const { t } = i18n.global
+const projectId = browser.valueFromUrl('projectId') || 1
+const notify = inject('notify')
+// const props = defineProps(['notify'])
+const emits = defineEmits(['action', 'setShow'])
+const height = ref(0)
+const form = ref({
+    title: '',
+    describe: '',
+    status: '',
+    members: [],
+})
+const openView = ref(false)
+const data = ref({
+    status: [
+        { text: t('tag.statusText1'), value: 1 },
+        { text: t('tag.statusText2'), value: 2 },
+        { text: t('tag.statusText3'), value: 3 },
+        { text: t('tag.statusText4'), value: 4 },
+    ],
+    members: [],
+})
+const showTips = ref(null)
+const media = computed(() => {
+    return notify.value.type || 'image'
+})
+const component = computed(() => {
+    switch (media.value) {
+        case 'image':
+            return Image
+        case 'video':
+            return Video
+        case 'audio':
+            return Audio
+        case 'link':
+            return Link
+    }
+})
+
+const onClose = () => {
+    if (window.kankan) {
+        if (notify.value.__temp) {
+            kankan.TagManager.remove(notify.value.sid)
+        }
+    } else {
+    }
+    notify.value = null
+}
+const closeTips = () => {
+    notify.value = null
+    showTips.value = null
+}
+onMounted(() => {
+    http.post(`smart-site/projectTeam/select`, { projectId }).then(response => {
+        if (response.success) {
+            data.value.members = response.data.map(item => {
+                return {
+                    text: item.nickName,
+                    value: item.userId,
+                }
+            })
+
+            if (notify.value) {
+                form.value.status = String(notify.value.status || '')
+                form.value.title = notify.value.title || ''
+                form.value.describe = notify.value.content || ''
+                form.value.members = []
+                if (notify.value.members && notify.value.members.length) {
+                    notify.value.members.forEach(item => {
+                        let find = data.value.members.find(c => c.value == item)
+                        if (find) {
+                            form.value.members.push(find)
+                        }
+                    })
+                }
+            }
+            nextTick(() => {
+                emits('setShow')
+            })
+        } else {
+            if (response.code == 4008) {
+                showTips.value = {
+                    type: 'warn',
+                    text: t('code.4008'),
+                }
+            } else {
+                showTips.value = {
+                    type: 'warn',
+                    text: response.message,
+                }
+            }
+        }
+    })
+
+    if (window.kankan) {
+        window.kankan.TagManager.focusTag(notify.value.sid, {
+            direction: 'left',
+            attrs: {
+                width: 0,
+                // height: 400,
+            },
+        })
+    } else if (window.laser) {
+        window.laser.then(sdk => {
+            let pos = notify.value.position
+            sdk.scene.comeToTag(new THREE.Vector3(pos.x, pos.y, pos.z))
+        })
+    }
+})
+
+onBeforeUnmount(() => {})
+</script>
+<style lang="scss">
+.mask-layer {
+    .swiper-button-prev,
+    .swiper-button-next {
+        width: 48px !important;
+        height: 48px !important;
+        background: rgba(0, 0, 0, 0.5) !important;
+        color: #fff;
+        &::after {
+            font-size: 18px !important;
+        }
+    }
+}
+</style>
+<style lang="scss" scoped>
+.ui-audio {
+    position: absolute;
+    right: 20px;
+    top: 20px;
+}
+.mask-layer {
+    width: 100%;
+    height: 100%;
+    position: fixed;
+    top: 0;
+    left: 0;
+    background: #292929;
+    z-index: 10000;
+    .layer-content {
+        width: 90%;
+        height: 80%;
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+    }
+    .close-btn {
+        position: absolute;
+        top: 20px;
+        right: 60px;
+        cursor: pointer;
+        z-index: 100;
+        i {
+            color: #fff;
+            font-size: 24px;
+        }
+    }
+}
+.aside-item {
+    padding: 20px;
+    box-sizing: border-box;
+    line-height: 28px;
+    position: relative;
+    .content-item {
+        margin-bottom: 20px;
+        &:last-of-type {
+            margin-bottom: 0;
+        }
+    }
+    &.left-item {
+        width: 400px;
+        border-right: solid 1px rgba(255, 255, 255, 0.16);
+        .item-title {
+            font-size: 14px;
+            color: #999;
+        }
+        .content-desc {
+            font-size: 14px;
+            line-height: 28px;
+        }
+        .media-box {
+            width: 360px;
+            height: 225px;
+            border-radius: 4px 4px 4px 4px;
+            opacity: 1;
+            border: 1px solid rgba(255, 255, 255, 0.2);
+            position: relative;
+            &.zoom-in {
+                cursor: zoom-in;
+            }
+            &.nor {
+                position: relative;
+                &::after {
+                    content: '';
+                    position: absolute;
+                    width: 100%;
+                    height: 100%;
+                    z-index: 10;
+                    opacity: 0;
+                    top: 0;
+                    left: 0;
+                }
+            }
+            // margin-bottom: 30px;
+            // &.zoom-in {
+            //     cursor: zoom-in;
+            // }
+        }
+    }
+}
+</style>

+ 347 - 47
src/components/files/index.vue

@@ -1,66 +1,346 @@
 <template>
     <transition name="slide-right" mode="in-out">
-        <div class="files" v-if="showFiles">
-            <ul>
-                <li>
-                    <button class="add" @click="onAdd">添加标注</button>
-                </li>
-            </ul>
+        <div class="files" v-if="showFiles && !isEdit">
+            <div class="info" ref="add$">
+                <button @click="onAdd">{{ $t('tag.addTag') }}</button>
+                <div>
+                    {{ $t('tag.isAddTag') }}(<span>{{ tags.length }}</span
+                    >)
+                </div>
+            </div>
+            <div class="list" :style="`height:${listStyle};`">
+                <ul>
+                    <li v-for="tag in tags" @click="onClick(tag)" :class="{ active: notify?.sid == tag.sid }">
+                        <div class="title"><i></i>{{ tag.title }}</div>
+                        <div class="more" @click.stop="onShowMore(tag)">
+                            <i class="iconfont icon-more"></i>
+                            <div v-if="showMoreSid == tag.id" v-click-outside="onOutside">
+                                <div @click.stop="onMoreHandler('modify', tag)">{{ $t('common.edit') }}</div>
+                                <div @click.stop="onMoreHandler('delete', tag)">{{ $t('common.delete') }}</div>
+                            </div>
+                        </div>
+                    </li>
+                </ul>
+            </div>
+            <div class="exit" ref="exit$">
+                <button type="button" @click="emits('exit')">{{ $t('common.exit') }}</button>
+            </div>
         </div>
     </transition>
     <transition name="slide-up" mode="in-out">
         <div class="toolbar" v-if="showToolbar">
-            <button type="button" @click="onAddCancel">取消</button>
-            <button type="submit" @click="onAddConfirm">确定</button>
+            <button type="button" @click="onAddCancel">{{ $t('common.cancel') }}</button>
+            <button type="submit" @click="onAddConfirm">{{ $t('common.confirm') }}</button>
         </div>
     </transition>
+    <ui-confirm v-if="delComfirm" @ok="handlerDel" @no="handlerDel">
+        <template #content>
+            <div>{{ $t('tag.deleteTagText') }}</div>
+        </template>
+    </ui-confirm>
 </template>
 <script setup>
-import { ref, watchEffect } from 'vue'
-
+import { ref, inject, watchEffect, onMounted, nextTick } from 'vue'
+import { http } from '@/utils/request'
+import UiConfirm from '@/components/dialog/Confirm.vue'
+let editTag = null
+let tempTag = null
+const exit$ = ref(null)
+const add$ = ref(null)
 const props = defineProps(['show'])
-const emits = defineEmits(['add'])
-
+const emits = defineEmits(['add', 'exit'])
+const handlerDel = status => {
+    if (status == 'ok') {
+        http.post(`smart-site/marking/del`, {
+            markingId: delComfirm.value.id,
+        }).then(response => {
+            if (response.success) {
+                console.log(delComfirm.value)
+                let index = tags.value.findIndex(item => item.sid == delComfirm.value.sid)
+                if (index != -1) {
+                    tags.value.splice(index, 1)
+                }
+                if (notify.value && notify.value.id == delComfirm.value.id) {
+                    notify.value = null
+                }
+            } else {
+            }
+            delComfirm.value = null
+        })
+    } else {
+        delComfirm.value = null
+    }
+}
+const delComfirm = ref(null)
 const showFiles = ref(false)
 const showToolbar = ref(false)
+const showMoreSid = ref('')
+const tags = inject('tags')
 
+const notify = inject('notify')
+const isEdit = inject('isEdit')
+const onClick = tag => {
+    notify.value = tag //{ event: 'focus', sid: props.tag.sid, tag: props.tag }
+}
+let isAdd = false
 const onAdd = () => {
+    isEdit.value = true
+    isAdd = true
     if (window.kankan) {
         window.kankan.TagManager.editor.then(editor => {
             editor.enter()
             showFiles.value = false
             showToolbar.value = true
         })
+    } else {
+        laserPosition()
+        showFiles.value = false
+        showToolbar.value = true
     }
 }
+const laserPosition = () => {
+    window.laser.then(sdk => {
+        sdk.addMouseDownEvent(e => {
+            if (showToolbar.value == false) {
+                return
+            }
+            if (e.button == 2) {
+                const info3d = sdk.scene.getPointByScreen({ x: e.clientX, y: e.clientY })
+                const info2d = sdk.scene.getScreenByPoint(info3d.position)
+                if (editTag) {
+                    tempTag = {}
+                    tempTag.x = editTag.x
+                    tempTag.y = editTag.y
+                    tempTag.visible = editTag.visible
+                    tempTag.position = new THREE.Vector3(editTag.position.x, editTag.position.y, editTag.position.z)
+
+                    editTag.x = info2d.pos.x
+                    editTag.y = info2d.pos.y
+                    editTag.visible = info2d.trueSide
+                    editTag.position = info3d.position
+                } else {
+                    const tag = {
+                        panoId: null,
+                        createTime: Date.now(),
+                        icon: '',
+                        x: info2d.pos.x,
+                        y: info2d.pos.y,
+                        position: info3d.position,
+                        media: null,
+                        type: '',
+                        title: '',
+                        content: '',
+                        sid: Date.now().toString(),
+                        visible: info2d.trueSide,
+                        __temp: true,
+                    }
+                    let index = tags.value.findIndex(item => item.__temp == true)
+                    if (index != -1) {
+                        tags.value.splice(index, 1)
+                    }
+                    tags.value.push(tag)
+                }
+            }
+        })
+    })
+}
 const onAddCancel = () => {
     showFiles.value = true
     showToolbar.value = false
-    kankan.TagManager.editor.then(editor => editor.exit())
+    isAdd = false
+    if (window.kankan) {
+        kankan.TagManager.editor.then(editor => editor.exit())
+    } else {
+        let index = tags.value.findIndex(item => item.__temp == true)
+        if (index != -1) {
+            tags.value.splice(index, 1)
+        }
+    }
+
+    if (tempTag) {
+        let tag = tags.value.find(item => item.sid == editTag.sid)
+        tag.x = tempTag.x
+        tag.y = tempTag.y
+        tag.visible = tempTag.visible
+        tag.position = tempTag.position
+    }
+    editTag = null
+    tempTag = null
+    isEdit.value = false
 }
 const onAddConfirm = () => {
     showFiles.value = true
     showToolbar.value = false
-    kankan.TagManager.editor.then(editor => {
-        var tag = editor.confirm()
+    if (window.kankan) {
+        kankan.TagManager.editor.then(editor => {
+            var tag = editor.confirm()
+            if (tag) {
+                tag.icon = ''
+                if (isAdd) {
+                    tag.__temp = true
+                    tag._add = true
+                    isAdd = false
+                }
+                // tags.value.push(tag)
+                notify.value = tag
+                isEdit.value = true
+            }
+        })
+    } else {
+        let tag
+        if (isAdd) {
+            tag = tags.value.find(item => item.__temp == true)
+        } else {
+            tag = editTag
+        }
         if (tag) {
-            kankan.TagManager.add([tag])
+            // delete tag.__temp
+
+            if (tag) {
+                if (isAdd) {
+                    tag.__temp = true
+                    // tag._add = true
+                    isAdd = false
+                }
+
+                notify.value = tag
+            }
         }
-    })
+    }
+    editTag = null
+    tempTag = null
 }
+const onOutside = () => {
+    if (showMoreSid.value) {
+        showMoreSid.value = ''
+    }
+}
+const onShowMore = tag => {
+    showMoreSid.value = tag.id
+}
+const onMoreHandler = (type, tag) => {
+    if (type == 'modify') {
+        if (notify.value) {
+            notify.value = null
+        }
+        editTag = tags.value.find(item => item.sid == tag.sid)
+        showFiles.value = false
+        showToolbar.value = true
+        isEdit.value = true
+
+        if (window.kankan) {
+            window.kankan.TagManager.focusTag(editTag.sid)
+            window.kankan.TagManager.editor.then(editor => {
+                editor.enter(editTag)
+            })
+        } else {
+            laserPosition()
+        }
+        // window.kankan.TagManager.focusBeforeModify(editTag.sid)
+    } else if (type == 'delete') {
+        delComfirm.value = tag
+    }
+    showMoreSid.value = ''
+}
+const onTagFocus = tag => {
+    //tid.
+}
+const listStyle = ref('')
 watchEffect(() => {
     showFiles.value = props.show
+    if (showFiles.value && !isEdit.value) {
+        nextTick(() => {
+            listStyle.value = `calc(100vh - ${add$.value.offsetHeight + exit$.value.offsetHeight + 60}px)`
+        })
+    }
 })
+
+onMounted(() => {})
 </script>
 <style lang="scss" scoped>
+button {
+    width: 100px;
+    height: 34px;
+    border: none;
+    outline: none;
+    border-radius: 4px;
+    font-size: 14px;
+    background: transparent;
+    transition: all 0.3s ease;
+    border: solid 1px rgb(0, 118, 246);
+    color: rgb(0, 118, 246);
+    margin: 0;
+    padding: 0;
+    &[type='submit'] {
+        background: rgb(0, 118, 246);
+        color: #fff;
+    }
+}
 .files {
+    display: flex;
+    flex-direction: column;
     position: absolute;
+    padding: 10px 0;
     top: 57px;
     right: 0;
+    bottom: 0;
     width: 240px;
-    height: 100%;
     background: rgba(27, 27, 28, 0.8);
     z-index: 1000;
+    .info {
+        padding: 10px;
+        div {
+            font-size: 16px;
+            font-weight: bold;
+            color: #999999;
+            margin-top: 20px;
+            padding-top: 20px;
+            border-top: solid 1px rgba(255, 255, 255, 0.16);
+            span {
+                color: rgb(0, 118, 246);
+                font-weight: normal;
+            }
+        }
+        button {
+            width: 100%;
+            border: 1px solid rgba(255, 255, 255, 0.4);
+            color: rgba(255, 255, 255, 0.4);
+            &:hover {
+                border-color: #fff;
+                color: #fff;
+            }
+        }
+    }
+    .list {
+        flex: 1;
+        overflow-y: auto;
+    }
+    .exit {
+        padding: 0 10px;
+        button {
+            width: 100%;
+            border: 1px solid rgba(255, 255, 255, 0.4);
+            color: rgba(255, 255, 255, 0.4);
+            &:hover {
+                border-color: #fff;
+                color: #fff;
+            }
+        }
+    }
+
+    .add {
+        padding-bottom: 20px;
+        border-bottom: solid 1px rgba(255, 255, 255, 0.16);
+        button {
+            width: 100%;
+            border: 1px solid rgba(255, 255, 255, 0.4);
+            color: rgba(255, 255, 255, 0.4);
+            &:hover {
+                border-color: #fff;
+                color: #fff;
+            }
+        }
+    }
 }
 .toolbar {
     position: absolute;
@@ -78,41 +358,61 @@ watchEffect(() => {
     transition: all 0.3s ease;
     button {
         width: 160px;
-        height: 34px;
-        border: none;
-        outline: none;
-        border-radius: 4px;
-        font-size: 14px;
-        background: transparent;
-        transition: all 0.3s ease;
-        color: var(--color-main-normal);
-        border: solid 1px rgb(0, 118, 246);
-        color: rgb(0, 118, 246);
         margin: 0 10px;
-        &[type='submit'] {
-            background: rgb(0, 118, 246);
-            color: #fff;
-        }
     }
 }
-ul {
-    margin: 0 10px;
-}
 li {
-    margin-top: 20px;
+    cursor: pointer;
+    height: 44px;
+    padding: 10px;
     list-style: none;
-}
-.add {
-    margin: 0;
-    padding: 0;
-    width: 100%;
-    height: 34px;
-    border: 1px solid rgba(255, 255, 255, 0.4);
-    color: rgba(255, 255, 255, 0.4);
-    border-radius: 4px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    font-size: 14px;
+    &.active {
+        background-color: rgba(0, 118, 246, 0.1);
+    }
+    .title {
+    }
+    .more {
+        position: relative;
+        cursor: pointer;
+        width: 30px;
+        height: 30px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        &.active {
+            > div {
+                display: block;
+            }
+        }
+        i {
+            font-size: 14px;
+        }
+        > div {
+            min-width: 82px;
+            background: #222121;
+            border-radius: 4px;
+            border: 1px solid #000;
+            backdrop-filter: blur(4px);
+            position: absolute;
+            right: 6px;
+            top: 26px;
+            z-index: 100;
+            div {
+                text-align: center;
+                height: 32px;
+                line-height: 32px;
+                &:hover {
+                    background-color: rgb(0, 118, 246, 0.1);
+                }
+            }
+        }
+    }
     &:hover {
-        border-color: #fff;
-        color: #fff;
+        background-color: rgb(0, 118, 246, 0.1);
     }
 }
 

+ 3 - 1
src/components/form/Area.vue

@@ -1,7 +1,7 @@
 <template>
     <div class="control">
         <div class="component area">
-            <textarea  :maxlength="maxlength" :placeholder="placeholder" v-model="modelValue" @input="e => emits('update:modelValue', e.target.value)" ></textarea>
+            <textarea :maxlength="maxlength" :placeholder="placeholder" :value="modelValue" @input="e => emits('update:modelValue', e.target.value)"></textarea>
             <div class="maxlength" v-if="maxlength">
                 <span>{{ modelValue.length }}</span
                 >&nbsp;/&nbsp;{{ maxlength }}
@@ -10,10 +10,12 @@
     </div>
 </template>
 <script setup>
+
 const props = defineProps({
     modelValue: {
         type: String,
         require: true,
+        default: '',
     },
     maxlength: {
         type: Number,

+ 20 - 10
src/components/form/Input.vue

@@ -1,11 +1,11 @@
 <template>
     <div class="control" @click="onInputerClick">
         <div class="component select" v-if="type == 'select'" v-click-outside="onOutside">
-            <div class="place" :class="{placeholder:!selecterText}">{{ selecterText || placeholder }}</div>
-            <div class="icon" :class="{up:selecterShow}">
+            <div class="place" :class="{ placeholder: !selecterText }">{{ selecterText || placeholder }}</div>
+            <div class="icon" :class="{ up: selecterShow }">
                 <i class="iconfont icon-arrows_down"></i>
             </div>
-            <div class="panel" v-show="selecterShow"  >
+            <div class="panel" v-show="selecterShow">
                 <ul>
                     <li v-for="item in data" @click.stop="onselecterChange(item)">
                         <div>{{ item.text }}</div>
@@ -14,7 +14,7 @@
             </div>
         </div>
         <div class="component text" v-if="type == 'text'">
-            <input class="component text" :maxlength="maxlength" :placeholder="placeholder" v-model="modelValue" @input="e => emits('update:modelValue', e.target.value)" />
+            <input class="component text" :maxlength="maxlength" :placeholder="placeholder" :value="modelValue" @input="e => emits('update:modelValue', e.target.value)" />
             <div class="maxlength" v-if="maxlength">
                 <span>{{ modelValue.length }}</span
                 >&nbsp;/&nbsp;20
@@ -23,7 +23,7 @@
     </div>
 </template>
 <script setup>
-import { ref } from 'vue'
+import { ref, watch } from 'vue'
 const props = defineProps({
     type: {
         type: String,
@@ -34,7 +34,7 @@ const props = defineProps({
         default: [],
     },
     modelValue: {
-        type: [String , Number , Boolean],
+        type: [String, Number, Boolean],
         require: true,
     },
     maxlength: {
@@ -48,6 +48,7 @@ const props = defineProps({
         default: '请输入',
     },
 })
+
 const emits = defineEmits(['update:modelValue', 'change'])
 
 const selecterText = ref('')
@@ -66,9 +67,18 @@ const onselecterChange = data => {
     selecterText.value = data.text
     selecterShow.value = false
 }
-const onOutside = ()=>{
+const onOutside = () => {
     selecterShow.value = false
 }
+
+watch(props, () => {
+    if (props.type == 'select' && props.modelValue) {
+        let find = props.data.find(item => item.value == props.modelValue)
+        if (find) {
+            selecterText.value = find.text
+        }
+    }
+})
 </script>
 <style lang="scss" scoped>
 ul,
@@ -108,14 +118,14 @@ li {
 
     .select {
         cursor: pointer;
-        .place{
+        .place {
             width: 100%;
             height: 100%;
             color: #fff;
             line-height: 34px;
             text-indent: 10px;
-            &.placeholder{
-                color:#757575;
+            &.placeholder {
+                color: #757575;
             }
         }
         .panel {

+ 48 - 11
src/components/form/medias/Audio.vue

@@ -1,23 +1,30 @@
 <template>
-    <div class="media" v-show="media">
-
+    <div class="media" v-if="media.length">
+        <i class="iconfont icon-music"></i>
+        <span class="music-name">{{ media[0].name }}</span>
     </div>
-    <div class="placeholder" @click="file.click()" v-show="media == null">
+    <div class="placeholder" @click="file.click()" v-if="!media.length">
         <div class="icon">
             <i class="iconfont icon-add"></i>
-            <span>上传音频</span>
+            <span>{{ $t('components.uploadAudio') }}</span>
         </div>
-        <div class="tips">支持 mp3/wav 文件:≤ 5MB</div>
+        <div class="tips">{{ $t('components.limitFileSize', { file: 'mp3/wav', size: '5' }) }}</div>
         <input ref="file" type="file" style="display: none" accept=".mp3, .wav" @change="onChange" />
     </div>
+    <div class="del-btn" v-if="isEdit && notify.media?.[notify.type]?.length" @click="delMedia">
+        <i class="iconfont icon-del"></i>
+    </div>
 </template>
 <script setup>
-import { ref, onMounted } from 'vue'
+import { ref, onMounted, inject } from 'vue'
 import { checkSizeLimitFree, base64ToDataURL } from '@/utils/file'
-
+import i18n from '@/i18n'
+const { t } = i18n.global
+const notify = inject('notify')
+const isEdit = inject('isEdit')
 const emits = defineEmits(['tips'])
 const file = ref(null)
-const media = ref(null)
+const media = ref([])
 const onChange = e => {
     if (!e.target.files.length) {
         return
@@ -28,20 +35,41 @@ const onChange = e => {
     if (checkSizeLimitFree(file.size, 5)) {
         let reader = new FileReader()
         reader.onload = function () {
-            images.value.push(base64ToDataURL(reader.result))
+            media.value.push({ src: base64ToDataURL(reader.result), file, name: file.name })
+            notify.value.media['audio'] = media.value
         }
         reader.readAsDataURL(file)
     } else {
-        emits('tips','请上传 5MB 以内的 jpg/png 文件')
+        emits('tips', t('components.FileSizeTips', { size: '5', file: 'mp3/wav' }))
     }
     e.target.value = ''
 }
 
+const delMedia = () => {
+    media.value.splice(0, 1)
+}
 onMounted(() => {
-
+    media.value = notify.value.media?.audio || []
 })
 </script>
 <style lang="scss" scoped>
+.del-btn {
+    width: 24px;
+    height: 24px;
+    background: rgba(0, 0, 0, 0.6);
+    border-radius: 50%;
+    position: absolute;
+    cursor: pointer;
+    top: 10px;
+    right: 10px;
+    z-index: 10;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    .iconfont {
+        font-size: 1em;
+    }
+}
 .placeholder {
     cursor: pointer;
     width: 100%;
@@ -72,6 +100,15 @@ onMounted(() => {
 .media {
     width: 100%;
     height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    .music-name {
+        margin-left: 5px;
+    }
+    .iconfont {
+        font-size: 1em;
+    }
     .add {
         cursor: pointer;
         font-size: 12px;

+ 74 - 22
src/components/form/medias/Image.vue

@@ -2,29 +2,38 @@
     <div class="media-image" v-show="images.length">
         <div class="swiper" ref="swiper$">
             <div class="swiper-wrapper">
-                <div class="swiper-slide" v-for="(url, index) in images" :style="`background-image: url(${url})`" :key="index"></div>
+                <div class="swiper-slide" v-for="(item, index) in images" :style="`background-image: url(${item.src})`" :key="index"></div>
             </div>
             <div class="swiper-button-prev"></div>
             <div class="swiper-button-next"></div>
         </div>
-        <div class="add" @click="file.click()" :class="{ disable: images.length >= 9 }">
-            继续添加&nbsp;<span>{{ images.length }}</span
+        <div v-if="isEdit" class="add" @click="file.click()" :class="{ disable: images.length >= 9 }">
+            <span style="color: #fff" v-if="images.length < 9">{{ $t('components.continueAdd') }}</span
+            >&nbsp;<span>{{ images.length }}</span
             >&nbsp;/&nbsp;9
         </div>
     </div>
-    <div class="placeholder" @click="file.click()" v-show="images.length == 0">
+    <div v-if="isEdit" class="placeholder" @click="file.click()" v-show="images.length == 0">
         <div class="icon">
             <i class="iconfont icon-add"></i>
-            <span>上传图片</span>
+            <span>{{ $t('components.uploadImg') }}</span>
         </div>
-        <div class="tips">支持JPG、PNG等图片格式,单张不超过5MB,最多支持上传9张。</div>
-        <input ref="file" type="file" style="display: none" accept="image/jpg,image/jpeg,image/png" @change="onChange" />
+        <div class="tips">{{ $t('components.limitImgLength') }}</div>
+        <input ref="file" multiple type="file" style="display: none" accept="image/jpg,image/jpeg,image/png" @change="onChange" />
+    </div>
+
+    <div class="del-btn" v-if="isEdit" v-show="notify.media?.[notify.type]?.length" @click="delPic">
+        <i class="iconfont icon-del"></i>
     </div>
 </template>
 <script setup>
-import { ref, onMounted } from 'vue'
-import { checkSizeLimitFree, base64ToDataURL } from '@/utils/file'
-
+import { ref, onMounted, inject } from 'vue'
+import { checkSizeLimitFree, base64ToDataURL, convertBlob2File, base64ToBlob } from '@/utils/file'
+import common from '@/utils/common'
+import i18n from '@/i18n'
+const { t } = i18n.global
+const notify = inject('notify')
+const isEdit = inject('isEdit')
 const emits = defineEmits(['tips'])
 
 const file = ref(null)
@@ -32,26 +41,44 @@ const swiper$ = ref(null)
 
 const images = ref([])
 const onChange = e => {
-    if (!e.target.files.length) {
+    let files = e.target.files
+    if (!files.length) {
         return
     }
 
-    let file = e.target.files[0]
-
-    if (checkSizeLimitFree(file.size, 5)) {
-        let reader = new FileReader()
-        reader.onload = function () {
-            images.value.push(base64ToDataURL(reader.result))
+    // let file = e.target.files[0]
+    let frist = false
+    for (let i = 0; i < files.length; i++) {
+        let file = e.target.files[i]
+        if (checkSizeLimitFree(file.size, 5)) {
+            let reader = new FileReader()
+            reader.onload = function () {
+                if (images.value.length >= 9) {
+                    if (!frist) {
+                        frist = true
+                        emits('tips', t('components.TipsImgLength'))
+                    }
+                } else {
+                    images.value.push({ src: base64ToDataURL(reader.result), file })
+                    notify.value.media['image'] = images.value
+                }
+            }
+            reader.readAsDataURL(file)
+        } else {
+            emits('tips', t('components.FileSizeTips', { size: '5', file: 'jpg/png' }))
         }
-        reader.readAsDataURL(file)
-    } else {
-        emits('tips','请上传 5MB 以内的 jpg/png 文件')
+        console.log(images.value.length)
     }
+
     e.target.value = ''
 }
-
+const delPic = () => {
+    let index = swiper.activeIndex
+    images.value.splice(index, 1)
+}
+let swiper
 onMounted(() => {
-    let swiper = new Swiper(swiper$.value, {
+    swiper = new Swiper(swiper$.value, {
         observer: true,
         navigation: {
             prevEl: swiper$.value.querySelector('.swiper-button-prev'),
@@ -63,9 +90,34 @@ onMounted(() => {
             },
         },
     })
+    images.value = notify.value.media?.image || []
+
+    // if (!notify.value.media) {
+    //     notify.value.media = {}
+    // }
+
+    // notify.value.media['image'] = images.value
+    console.log(swiper)
 })
 </script>
 <style lang="scss" scoped>
+.del-btn {
+    width: 24px;
+    height: 24px;
+    background: rgba(0, 0, 0, 0.6);
+    border-radius: 50%;
+    position: absolute;
+    cursor: pointer;
+    top: 10px;
+    right: 10px;
+    z-index: 10;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    .iconfont {
+        font-size: 1em;
+    }
+}
 .placeholder {
     cursor: pointer;
     width: 100%;

+ 13 - 6
src/components/form/medias/Link.vue

@@ -1,12 +1,12 @@
 <template>
     <div class="media" v-show="url">
         <iframe v-if="url" :src="url" frameborder="0"></iframe>
-        <div class="delete" @click.stop="onDelete"><i class="iconfont icon-delete"></i></div>
-        <div class="link">{{ url }}</div>
+        <div v-if="isEdit" class="delete" @click.stop="onDelete"><i class="iconfont icon-delete"></i></div>
+        <div class="link" v-if="isEdit">{{ url }}</div>
     </div>
     <div class="placeholder" v-show="url == null">
         <div class="icon">
-            <span>网页展示区</span>
+            <span>{{$t('components.linkView')}}</span>
         </div>
         <div class="link">
             <input type="text" placeholder="https://" v-model.trim="href" />
@@ -15,21 +15,28 @@
     </div>
 </template>
 <script setup>
-import { ref, onMounted } from 'vue'
+import { ref, onMounted, inject, computed } from 'vue'
 const emits = defineEmits(['tips'])
+const notify = inject('notify')
+const isEdit = inject('isEdit')
 const url = ref(null)
 const href = ref('')
-const onDelete = () =>{
+const onDelete = () => {
     url.value = null
+    notify.value.media['link'] = []
 }
 const onConfirm = () => {
     if (href.value) {
         url.value = 'https://' + href.value.replace(/http(s?):\/\//, '')
         href.value = ''
     }
+
+    notify.value.media['link'] = [{ src: url.value }]
 }
 
-onMounted(() => {})
+onMounted(() => {
+    url.value = notify.value.media?.link && notify.value.media.link[0] ? notify.value.media.link[0].src : null
+})
 </script>
 <style lang="scss" scoped>
 .placeholder {

+ 52 - 13
src/components/form/medias/Video.vue

@@ -1,23 +1,37 @@
 <template>
-    <div class="media" v-show="media">
-
+    <div class="media" v-if="media.length">
+        <video
+            x5-video-player-type="h5-page"
+            controlslist="nodownload"
+            disablepictureinpicture=""
+            webkit-playsinline=""
+            x-webkit-airplay=""
+            playsinline=""
+            controls=""
+            autoplay=""
+            :src="media[0].src"
+        ></video>
     </div>
-    <div class="placeholder" @click="file.click()" v-show="media == null">
+    <div class="placeholder" @click="file.click()" v-if="!media.length">
         <div class="icon">
             <i class="iconfont icon-add"></i>
-            <span>上传视频</span>
+            <span>{{$t('components.uploadVideo')}}</span>
         </div>
-        <div class="tips">支持 mp4/mov 文件:≤ 20MB,≤ 2Mbps</div>
+        <div class="tips">{{$t('components.limitFileSizeBit',{file:"mp4/mov",size:"20",bit:"2"})}}</div>
         <input ref="file" type="file" style="display: none" accept=".mp4, .mov" @change="onChange" />
     </div>
+    <div class="del-btn" v-if="notify.media?.[notify.type]?.length && isEdit" @click="delMedia">
+        <i class="iconfont icon-del"></i>
+    </div>
 </template>
 <script setup>
-import { ref, onMounted } from 'vue'
+import { ref, onMounted, inject } from 'vue'
 import { checkSizeLimitFree, base64ToDataURL } from '@/utils/file'
-
+const notify = inject('notify')
+const isEdit = inject('isEdit')
 const emits = defineEmits(['tips'])
 const file = ref(null)
-const media = ref(null)
+const media = ref([])
 const onChange = e => {
     if (!e.target.files.length) {
         return
@@ -25,23 +39,43 @@ const onChange = e => {
 
     let file = e.target.files[0]
 
-    if (checkSizeLimitFree(file.size, 5)) {
+    if (checkSizeLimitFree(file.size, 20)) {
         let reader = new FileReader()
         reader.onload = function () {
-            images.value.push(base64ToDataURL(reader.result))
+            media.value.push({ src: base64ToDataURL(reader.result), file })
+            notify.value.media['video'] = media.value
         }
         reader.readAsDataURL(file)
     } else {
-        emits('tips','请上传 5MB 以内的 jpg/png 文件')
+      emits('tips', t('components.FileSizeTips', { size: '5', file: 'mp4/mov' }))
     }
     e.target.value = ''
 }
-
+const delMedia = () => {
+    media.value.splice(0, 1)
+}
 onMounted(() => {
-
+    media.value = notify.value.media?.video || []
 })
 </script>
 <style lang="scss" scoped>
+.del-btn {
+    width: 24px;
+    height: 24px;
+    background: rgba(0, 0, 0, 0.6);
+    border-radius: 50%;
+    position: absolute;
+    cursor: pointer;
+    top: 10px;
+    right: 10px;
+    z-index: 10;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    .iconfont {
+        font-size: 1em;
+    }
+}
 .placeholder {
     cursor: pointer;
     width: 100%;
@@ -93,4 +127,9 @@ onMounted(() => {
         }
     }
 }
+video {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+}
 </style>

+ 18 - 3
src/components/form/medias/index.vue

@@ -9,18 +9,24 @@
         <div class="control">
             <component :is="component" @tips="onTips"></component>
             <Toast v-if="tips" type="error" :content="tips" :close="() => (tips = null)" />
+        
         </div>
     </div>
 </template>
 <script setup>
-import { ref, computed } from 'vue'
+import { ref, computed, inject, onMounted } from 'vue'
 import Toast from '@/components/dialog/Toast'
 import Image from './Image.vue'
 import Video from './Video.vue'
 import Audio from './Audio.vue'
 import Link from './Link.vue'
 const tips = ref(null)
-const media = ref('image')
+const notify = inject('notify')
+// const media = ref(notify.value?.type || 'image')
+const media = computed(() => {
+    return notify.value?.type || 'image'
+})
+
 const component = computed(() => {
     switch (media.value) {
         case 'image':
@@ -33,14 +39,23 @@ const component = computed(() => {
             return Link
     }
 })
-const onTips = msg=> {
+const onTips = msg => {
     tips.value = msg
 }
 const onMediaChange = type => {
     media.value = type
+    notify.value.type = type
 }
+onMounted(() => {
+    notify.value.type = notify.value?.type || 'image'
+ if (!notify.value.media) {
+        notify.value.media = {}
+    }
+    
+})
 </script>
 <style lang="scss" scoped>
+
 .medias {
     ul,
     li {

+ 4 - 4
src/components/header/CopyLink.vue

@@ -3,15 +3,15 @@
         <div class="login-layer">
             <div class="login-box">
                 <header>
-                    <h4>分享链接</h4>
+                    <h4>{{ $t('header.shareLink') }}</h4>
                     <span class="close" @click="emits('close')"><i class="iconfont icon-close"></i></span>
                 </header>
                 <main>
                     <input readonly v-model="shareURL" />
                 </main>
                 <footer>
-                    <button @click="emits('close')">取消</button>
-                    <button type="submit" ref="copy" :data-clipboard-text="shareURL">复制链接</button>
+                    <button @click="emits('close')">{{ $t('common.cancel') }}</button>
+                    <button type="submit" ref="copy" :data-clipboard-text="shareURL">{{$t('header.copyLink')}}</button>
                 </footer>
             </div>
         </div>
@@ -21,7 +21,7 @@
 import ClipboardJS from 'clipboard'
 import { ref, onMounted } from 'vue'
 import Toast from '@/components/dialog/Toast'
-const emits = defineEmits(['close','done'])
+const emits = defineEmits(['close', 'done'])
 const shareURL = ref(window.location.href.replace('&split', '').replace('&adjust', ''))
 const showToast = ref(false)
 const copy = ref(null)

+ 13 - 9
src/components/header/Login.vue

@@ -4,7 +4,7 @@
             <div class="login-box">
                 <span class="close" @click="emits('close')"><i class="iconfont icon-close"></i></span>
                 <div class="area">
-                    <h4>用户登录</h4>
+                    <h4>{{ $t('header.userLogin') }}</h4>
                     <div class="input">
                         <span class="icon">
                             <i class="iconfont icon-user"></i>
@@ -25,16 +25,16 @@
                     <div class="remember">
                         <div @click="remember = !remember">
                             <div class="checkbox" :class="{ checked: remember }"></div>
-                            <div class="checkbox-label">记住密码</div>
+                            <div class="checkbox-label">{{ $t('header.rememberPassword') }}</div>
                         </div>
                     </div>
                     <div class="button">
-                        <button type="submit" @click="onLogin">登录</button>
+                        <button type="submit" @click="onLogin">{{ $t('common.login') }}</button>
                         <div class="tips" v-show="errors.message">{{ errors.message }}</div>
                     </div>
                     <div class="links">
-                        <a href="http://test.4dkankan.com/#/login/forget?from=%2F">忘记密码</a>
-                        <a href="http://test.4dkankan.com/#/login/register?from=%2F">官网注册</a>
+                        <a href="http://test.4dkankan.com/#/login/forget?from=%2F">{{ $t('header.forgetPassword') }}</a>
+                        <a href="http://test.4dkankan.com/#/login/register?from=%2F">{{ $t('header.resigter') }}</a>
                     </div>
                 </div>
             </div>
@@ -45,6 +45,8 @@
 import { ref, onMounted } from 'vue'
 import { http } from '@/utils/request'
 import common from '@/utils/common'
+import { useI18n, getLocale } from '@/i18n'
+const { t } = useI18n({ useScope: 'global' })
 const emits = defineEmits(['close', 'user'])
 const showpass = ref(false)
 const remember = ref(false)
@@ -54,15 +56,15 @@ const errors = ref({})
 const onLogin = () => {
     errors.value = {}
     if (!username.value) {
-        errors.value.username = '手机号码不能为空'
+        errors.value.username = t('header.phoneText1')
         return
     }
     if (!/^1[3-9]\d{9}$/.test(username.value)) {
-        errors.value.username = '请输入正确手机号'
+        errors.value.username = t('header.phoneText2')
         return
     }
     if (!password.value) {
-        errors.value.password = '密码不能为空'
+        errors.value.username = t('header.passwordText1')
         return
     }
 
@@ -82,8 +84,10 @@ const onLogin = () => {
                     localStorage.removeItem('remember')
                     localStorage.removeItem('username')
                     localStorage.removeItem('password')
+                    localStorage.removeItem('userId')
                 }
                 localStorage.setItem('token', response.data.token)
+                localStorage.setItem('userId', response.data.user.id)
                 emits('user', {
                     head: response.data.user.head,
                     nickName: response.data.user.nickName,
@@ -94,7 +98,7 @@ const onLogin = () => {
             }
         })
         .catch(() => {
-            errors.value.message = '服务器连接失败'
+            errors.value.message = t('code.failed')
         })
 }
 onMounted(() => {

+ 51 - 29
src/components/header/index.vue

@@ -2,8 +2,8 @@
     <header v-if="props.showAdjust">
         <div v-if="project">{{ project.projectName }}</div>
         <div class="sync">
-            <button @click="onCancel">取消</button>
-            <button type="submit" @click="onSubmit" :class="{ active: points.p1 && points.p2 }">同步</button>
+            <button @click="onCancel">{{ $t('common.cancel') }}</button>
+            <button type="submit" @click="onSubmit" :class="{ active: points.p1 && points.p2 }">{{ $t('common.sync') }}</button>
         </div>
     </header>
     <header v-else>
@@ -11,41 +11,45 @@
         <div class="user">
             <ul>
                 <li>
-                    <i
-                        @click="showLink = true;showCopyDone = false" class="iconfont icon-share"
-                    ></i>
+                    <i @click="openLink" class="iconfont icon-share"></i>
                 </li>
                 <li><em></em></li>
                 <li v-if="user" class="uinfo" @click="showDrop = true">
                     <img :src="user.head + '&x-oss-process=image/resize,m_fill,w_64,h_64/quality,q_70'" alt="" />
                     <div class="menu">
                         <ul>
-                            <li><a href="/smarts/#/personal">个人信息</a></li>
+                            <li>
+                                <a href="/smarts/#/personal">{{ $t('header.userInfo') }}</a>
+                            </li>
                             <li class="split"></li>
-                            <li><a href="javascript:;" @click="onLogout">退出登录</a></li>
+                            <li>
+                                <a href="javascript:;" @click="onLogout">{{ $t('header.loginout') }}</a>
+                            </li>
                         </ul>
                     </div>
                 </li>
-                <li v-else class="login" @click="showLogin = true"><span>登录</span></li>
+                <li v-else class="login" @click="showLogin = true">
+                    <span>{{ $t('common.login') }}</span>
+                </li>
             </ul>
         </div>
     </header>
     <footer v-if="props.showAdjust">
-        <h4>为场景设置关联位置</h4>
-        <div>请选择位置,确认左右视图中的场景在同一位置后,单击右侧按钮将其设为关联位置。</div>
+        <h4>{{ $t('header.adhustText1') }}</h4>
+        <div>{{ $t('header.adhustText2') }}</div>
         <div class="points">
-            <button @click="onSetP1" :class="{ active: points.p1 }">{{ points.p1 ? '重设P1' : '设为P1' }}</button>
-            <button @click="onSetP2" :class="{ active: points.p2 }">{{ points.p2 ? '重设P2' : '设为P2' }}</button>
+            <button @click="onSetP1" :class="{ active: points.p1 }">{{ points.p1 ? `${t('header.reset')}P1` : `${t('header.setting')}P1` }}</button>
+            <button @click="onSetP2" :class="{ active: points.p2 }">{{ points.p2 ? `${t('header.reset')}P2` : `${t('header.setting')}P2` }}</button>
         </div>
     </footer>
-    <Toast v-if="showCopyDone" content="复制成功" />
+    <Toast v-if="showCopyDone" :content="t('common.copySuccess')" />
     <Toast v-if="showTips" :content="showTips" :close="() => (showTips = null)" />
-    <Login v-if="showLogin" @close="showLogin = false" @user="info => (user = info)" />
-    <Loading v-if="showLoading"/>
-    <CopyLink v-if="showLink" @close="showLink = false" @done="showCopyDone = true;showLink = false" />
+    <Login v-if="showLogin" @close="showLogin = false" @user="getUser" />
+    <Loading v-if="showLoading" />
+    <CopyLink v-if="showLink" @close="showLink = false" @done="done" />
 </template>
 <script setup>
-import { ref, defineProps, onMounted, watchEffect } from 'vue'
+import { ref, defineProps, onMounted, watchEffect, inject } from 'vue'
 import { http } from '@/utils/request'
 import browser from '@/utils/browser'
 import Toast from '@/components/dialog/Toast'
@@ -53,13 +57,14 @@ import Loading from '@/components/loading/Loading'
 import Login from './Login'
 import CopyLink from './CopyLink'
 import sync from '@/utils/sync'
-
+import { useI18n, getLocale } from '@/i18n'
+const { t } = useI18n({ useScope: 'global' })
 const props = defineProps({
     project: Object,
     showAdjust: Boolean,
 })
-const emits = defineEmits(['update'])
-
+const emits = defineEmits(['update', 'getUserId'])
+const isLogin = inject('isLogin')
 const user = ref(null)
 const points = ref({ p1: null, p2: null })
 const showLink = ref(false)
@@ -67,7 +72,19 @@ const showLoading = ref(false)
 const showLogin = ref(false)
 const showCopyDone = ref(false)
 const showTips = ref(null)
-
+const done = () => {
+    showCopyDone.value = true
+    showLink.value = false
+}
+const getUser = info => {
+    user.value = info
+    isLogin.value = true
+    // emits('getUserId', localStorage.getItem('userId'))
+}
+const openLink = () => {
+    showLink.value = true
+    showCopyDone.value = false
+}
 const getCurPosInfo = () => {
     let app = sync.sourceInst
     let id
@@ -83,22 +100,22 @@ const getCurPosInfo = () => {
 
 const onSetP1 = () => {
     let p1 = getCurPosInfo()
-    if(points.value.p2 && points.value.p2.id == p1.id){
-        return  showTips.value = '匹配失败,请选择不同点位进行同步'
+    if (points.value.p2 && points.value.p2.id == p1.id) {
+        return (showTips.value = t('header.setPointfaidText'))
     }
     if (points.value.p1) {
-        showTips.value = '关联位置已更新'
+        showTips.value = t('header.pointUpdate')
     }
     points.value.p1 = p1
     emits('update', 'p1', points.value.p1)
 }
 const onSetP2 = () => {
     let p2 = getCurPosInfo()
-    if(points.value.p1 && points.value.p1.id == p2.id){
-        return  showTips.value = '匹配失败,请选择不同点位进行同步'
+    if (points.value.p1 && points.value.p1.id == p2.id) {
+        return (showTips.value = t('header.setPointfaidText'))
     }
     if (points.value.p2) {
-        showTips.value = '关联位置已更新'
+        showTips.value = t('header.pointUpdate')
     }
     points.value.p2 = p2
     emits('update', 'p2', points.value.p2)
@@ -112,6 +129,8 @@ const getUserInfo = () => {
                     head: response.data.head,
                     nickName: response.data.nickName,
                 }
+                localStorage.setItem('userId', response.data.id)
+                isLogin.value = true
             } else {
                 if (response.code == 4008) {
                     // 未登录
@@ -130,7 +149,10 @@ const onLogout = () => [
                 localStorage.removeItem('remember')
                 localStorage.removeItem('username')
                 localStorage.removeItem('password')
+                localStorage.removeItem('userId')
                 user.value = null
+                // emits('getUserId', null)
+                isLogin.value = false
             }
         })
         .catch(() => {}),
@@ -147,7 +169,7 @@ const onSubmit = () => {
         .then(response => {
             showLoading.value = false
             if (response.success) {
-                showTips.value = 'BIM同步成功'
+                showTips.value = 'BIM' + t('common.syncSuccess')
                 setTimeout(() => {
                     window.location.href = window.location.href.replace('&adjust', '&split')
                 }, 4000)
@@ -159,7 +181,7 @@ const onSubmit = () => {
         })
         .catch(() => {
             showLoading.value = false
-            showTips.value = '连接服务器失败'
+            showTips.value = t('code.failed')
         })
 }
 

+ 68 - 0
src/i18n/index.js

@@ -0,0 +1,68 @@
+import { nextTick } from 'vue'
+import { useI18n, createI18n } from 'vue-i18n/index'
+import browser from '@/utils/browser'
+
+export { useI18n }
+export const SUPPORT_LOCALES = ['zh', 'en', 'ja']
+
+export function getLocale() {
+    let lang = browser.getURLParam('lang')
+    if (!lang) {
+        lang = window.navigator.language || window.navigator.userLanguage || null
+        if (lang && !/^zh/.test(lang)) {
+            console.log('自动获取浏览器语言:' + lang)
+            lang = 'en'
+        } else {
+            lang = 'zh'
+        }
+    }
+    return lang
+}
+
+export function setupI18n(options = { locale: 'zh' }) {
+    const i18n = createI18n(options)
+    setI18nLanguage(i18n, options.locale)
+    return i18n
+}
+
+export function setI18nLanguage(i18n, locale) {
+    if (i18n.mode === 'legacy') {
+        i18n.global.locale = locale
+    } else {
+        i18n.global.locale.value = locale
+    }
+    /**
+     * NOTE:
+     * If you need to specify the language setting for headers, such as the `fetch` API, set it here.
+     * The following is an example for axios.
+     *
+     * axios.defaults.headers.common['Accept-Language'] = locale
+     */
+    document.querySelector('html').setAttribute('lang', locale)
+}
+
+export async function loadLocaleMessages(i18n, locale) {
+    // load locale messages with dynamic import
+    // set locale and locale message
+    if (window.location.search.includes('i18n')) {
+        try {
+            const messages = await (await fetch(`https://4dkk.4dage.com/v4/www/locales/${locale}.json?_=${Date.now()}`)).json()
+            i18n.global.setLocaleMessage(locale, messages)
+        } catch (error) {
+            const messages = await import(/* webpackChunkName: "locale-[request]" */ `../locales/zh.json`)
+            i18n.global.setLocaleMessage(locale, messages.default)
+        }
+    } else {
+        const messages = await import(/* webpackChunkName: "locale-[request]" */ `../locales/${locale}.json`)
+        i18n.global.setLocaleMessage(locale, messages.default)
+    }
+
+    return nextTick()
+}
+
+export default setupI18n({
+    globalInjection: true,
+    legacy: false,
+    locale: '',
+    fallbackLocale: 'zh',
+})

+ 105 - 0
src/locales/en.json

@@ -0,0 +1,105 @@
+{
+    "home": {
+        "thickness": "密度选择",
+        "dateScene": "选择日期未上传场景",
+        "notChoosePoint": "您还未选择关联位置",
+        "notFindFile": "未发现BIM文件",
+        "notFindScene": "未发现对比场景",
+        "sceneDelete": "当前场景已被删除,无法进行查看",
+        "tag": "tag",
+        "splitScreen": "分屏",
+        "fullScreen": "全屏"
+    },
+    "home.name": "首页",
+    "header": {
+        "passwordText1": "密码不能为空",
+        "phoneText1": "手机号码不能为空",
+        "phoneText2": "请输入正确手机号",
+        "copyLink": "复制链接",
+        "shareLink": "分享链接",
+        "setting": "设为",
+        "reset": "重设",
+        "userInfo": "个人信息",
+        "loginout": "退出登录",
+        "adhustText1": "为场景设置关联位置",
+        "adhustText2": "请选择位置,确认左右视图中的场景在同一位置后,单击右侧按钮将其设为关联位置。",
+        "userLogin": "用户登录",
+        "rememberPassword": "记住密码",
+        "forgetPassword": "忘记密码",
+        "resigter": "官网注册",
+        "setPointfaidText": "匹配失败,请选择不同点位进行同步",
+        "pointUpdate": "关联位置已更新"
+    },
+    "components": {
+        "uploadVideo": "上传视频",
+        "limitFileSizeBit": "支持 {file} 文件:≤ {size}MB,≤ {bit}Mbps",
+        "linkView": "网页展示区",
+        "continueAdd": "继续添加",
+        "uploadImg": "上传图片",
+        "limitImgLength": "支持JPG、PNG等图片格式,单张不超过5MB,最多支持上传9张。",
+        "TipsImgLength": "图片数量最多支持上传9张",
+        "limitFileSize": "支持 {file} 文件:≤ {size}MB",
+        "FileSizeTips": "请上传 {size}MB 以内的 {file} 文件",
+        "uploadAudio": "上传音频",
+        "year": "年",
+        "month": "月",
+        "day": "日",
+        "chooseTime": "选择时间"
+    },
+    "tag": {
+        "addComment": "发一条评论吧",
+        "addCommentTips": "请输入内容",
+        "deletetCommentTips": "确定要删除吗?",
+        "noComment": "暂无评论",
+        "unkownUser": "未知用户",
+        "reply": "reply",
+        "comment": "comment",
+        "creater": "创建人",
+        "createTime": "创建时间",
+        "statusText1": "待处理",
+        "statusText2": "进行中",
+        "statusText3": "未解决",
+        "statusText4": "已解决",
+        "uploadFile": "上传附件",
+        "desc": "描述",
+        "member": "涉及的成员",
+        "status": "状态",
+        "inputDesc": "请输入描述",
+        "inputMember": "请选择需要通知的项目人员",
+        "inputStatus": "请选择处理状态",
+        "inputTagName": "请输入标注名称",
+        "tagName": "标注名称",
+        "creatTag": "新建标注",
+        "addTag": "添加标注",
+        "isAddTag": "已添加标注",
+        "deleteTagText": "确定要删除资料吗?"
+    },
+    "tag.name": "标注",
+    "components.name": "组件",
+    "header.name": "头部",
+    "common": {
+        "high": "高",
+        "middle": "中",
+        "low": "低",
+        "input": "请输入",
+        "publish": "发布",
+        "submit": "提交",
+        "exit": "退出",
+        "delete": "删除",
+        "edit": "编辑",
+        "confirm": "confirm",
+        "tips": "tips",
+        "login": "login",
+        "cancel": "cancel",
+        "sync": "sync",
+        "deleteSuccess": "删除成功",
+        "copySuccess": "复制成功",
+        "syncSuccess": "同步成功"
+    },
+    "common.name": "通用",
+    "code": {
+        "4008": "用户未登录",
+        "failed": "连接服务器失败"
+    },
+    "code.name": "状态码"
+}

+ 105 - 0
src/locales/ja.json

@@ -0,0 +1,105 @@
+{
+    "home": {
+        "thickness": "密度选择",
+        "dateScene": "选择日期未上传场景",
+        "notChoosePoint": "您还未选择关联位置",
+        "notFindFile": "未发现BIM文件",
+        "notFindScene": "未发现对比场景",
+        "sceneDelete": "当前场景已被删除,无法进行查看",
+        "tag": "标注",
+        "splitScreen": "分屏",
+        "fullScreen": "全屏"
+    },
+    "home.name": "首页",
+    "header": {
+        "passwordText1": "密码不能为空",
+        "phoneText1": "手机号码不能为空",
+        "phoneText2": "请输入正确手机号",
+        "copyLink": "复制链接",
+        "shareLink": "分享链接",
+        "setting": "设为",
+        "reset": "重设",
+        "userInfo": "个人信息",
+        "loginout": "退出登录",
+        "adhustText1": "为场景设置关联位置",
+        "adhustText2": "请选择位置,确认左右视图中的场景在同一位置后,单击右侧按钮将其设为关联位置。",
+        "userLogin": "用户登录",
+        "rememberPassword": "记住密码",
+        "forgetPassword": "忘记密码",
+        "resigter": "官网注册",
+        "setPointfaidText": "匹配失败,请选择不同点位进行同步",
+        "pointUpdate": "关联位置已更新"
+    },
+    "components": {
+        "uploadVideo": "上传视频",
+        "limitFileSizeBit": "支持 {file} 文件:≤ {size}MB,≤ {bit}Mbps",
+        "linkView": "网页展示区",
+        "continueAdd": "继续添加",
+        "uploadImg": "上传图片",
+        "limitImgLength": "支持JPG、PNG等图片格式,单张不超过5MB,最多支持上传9张。",
+        "TipsImgLength": "图片数量最多支持上传9张",
+        "limitFileSize": "支持 {file} 文件:≤ {size}MB",
+        "FileSizeTips": "请上传 {size}MB 以内的 {file} 文件",
+        "uploadAudio": "上传音频",
+        "year": "年",
+        "month": "月",
+        "day": "日",
+        "chooseTime": "选择时间"
+    },
+    "tag": {
+        "addComment": "发一条评论吧",
+        "addCommentTips": "请输入内容",
+        "deletetCommentTips": "确定要删除吗?",
+        "noComment": "暂无评论",
+        "unkownUser": "未知用户",
+        "reply": "回复",
+        "comment": "评论",
+        "creater": "创建人",
+        "createTime": "创建时间",
+        "statusText1": "待处理",
+        "statusText2": "进行中",
+        "statusText3": "未解决",
+        "statusText4": "已解决",
+        "uploadFile": "上传附件",
+        "desc": "描述",
+        "member": "涉及的成员",
+        "status": "状态",
+        "inputDesc": "请输入描述",
+        "inputMember": "请选择需要通知的项目人员",
+        "inputStatus": "请选择处理状态",
+        "inputTagName": "请输入标注名称",
+        "tagName": "标注名称",
+        "creatTag": "新建标注",
+        "addTag": "添加标注",
+        "isAddTag": "已添加标注",
+        "deleteTagText": "确定要删除资料吗?"
+    },
+    "tag.name": "标注",
+    "components.name": "组件",
+    "header.name": "头部",
+    "common": {
+        "high": "高",
+        "middle": "中",
+        "low": "低",
+        "input": "请输入",
+        "publish": "发布",
+        "submit": "提交",
+        "exit": "退出",
+        "delete": "删除",
+        "edit": "编辑",
+        "confirm": "确定",
+        "tips": "提示",
+        "login": "登录",
+        "cancel": "取消",
+        "sync": "同步",
+        "deleteSuccess": "删除成功",
+        "copySuccess": "复制成功",
+        "syncSuccess": "同步成功"
+    },
+    "common.name": "通用",
+    "code": {
+        "4008": "用户未登录",
+        "failed": "连接服务器失败"
+    },
+    "code.name": "状态码"
+}

+ 105 - 0
src/locales/zh.json

@@ -0,0 +1,105 @@
+{
+  "home": {
+    "thickness": "密度选择",
+    "dateScene": "选择日期未上传场景",
+    "notChoosePoint": "您还未选择关联位置",
+    "notFindFile": "未发现BIM文件",
+    "notFindScene": "未发现对比场景",
+    "sceneDelete": "当前场景已被删除,无法进行查看",
+    "tag": "标注",
+    "splitScreen": "分屏",
+    "fullScreen": "全屏"
+  },
+  "home.name": "首页",
+  "header": {
+    "passwordText1": "密码不能为空",
+    "phoneText1": "手机号码不能为空",
+    "phoneText2": "请输入正确手机号",
+    "copyLink": "复制链接",
+    "shareLink": "分享链接",
+    "setting": "设为",
+    "reset": "重设",
+    "userInfo": "个人信息",
+    "loginout": "退出登录",
+    "adhustText1": "为场景设置关联位置",
+    "adhustText2": "请选择位置,确认左右视图中的场景在同一位置后,单击右侧按钮将其设为关联位置。",
+    "userLogin": "用户登录",
+    "rememberPassword": "记住密码",
+    "forgetPassword": "忘记密码",
+    "resigter": "官网注册",
+    "setPointfaidText": "匹配失败,请选择不同点位进行同步",
+    "pointUpdate": "关联位置已更新"
+  },
+  "components": {
+    "uploadVideo": "上传视频",
+    "limitFileSizeBit": "支持 {file} 文件:≤ {size}MB,≤ {bit}Mbps",
+    "linkView": "网页展示区",
+    "continueAdd": "继续添加",
+    "uploadImg": "上传图片",
+    "limitImgLength": "支持JPG、PNG等图片格式,单张不超过5MB,最多支持上传9张。",
+    "TipsImgLength": "图片数量最多支持上传9张",
+    "limitFileSize": "支持 {file} 文件:≤ {size}MB",
+    "FileSizeTips": "请上传 {size}MB 以内的 {file} 文件",
+    "uploadAudio": "上传音频",
+    "year": "年",
+    "month": "月",
+    "day": "日",
+    "chooseTime": "选择时间"
+  },
+  "tag": {
+    "addComment": "发一条评论吧",
+    "addCommentTips": "请输入内容",
+    "deletetCommentTips": "确定要删除吗?",
+    "noComment": "暂无评论",
+    "unkownUser": "未知用户",
+    "reply": "回复",
+    "comment": "评论",
+    "creater": "创建人",
+    "createTime": "创建时间",
+    "statusText1": "待处理",
+    "statusText2": "进行中",
+    "statusText3": "未解决",
+    "statusText4": "已解决",
+    "uploadFile": "上传附件",
+    "desc": "描述",
+    "member": "涉及的成员",
+    "status": "状态",
+    "inputDesc": "请输入描述",
+    "inputMember": "请选择需要通知的项目人员",
+    "inputStatus": "请选择处理状态",
+    "inputTagName": "请输入标注名称",
+    "tagName": "标注名称",
+    "creatTag": "新建标注",
+    "addTag": "添加标注",
+    "isAddTag": "已添加标注",
+    "deleteTagText": "确定要删除资料吗?"
+  },
+  "tag.name": "标注",
+  "components.name": "组件",
+  "header.name": "头部",
+  "common": {
+    "high": "高",
+    "middle": "中",
+    "low": "低",
+    "input": "请输入",
+    "publish": "发布",
+    "submit": "提交",
+    "exit": "退出",
+    "delete": "删除",
+    "edit": "编辑",
+    "confirm": "确定",
+    "tips": "提示",
+    "login": "登录",
+    "cancel": "取消",
+    "sync": "同步",
+    "deleteSuccess": "删除成功",
+    "copySuccess": "复制成功",
+    "syncSuccess": "同步成功"
+  },
+  "common.name": "通用",
+  "code": {
+    "4008": "用户未登录",
+    "failed": "连接服务器失败"
+  },
+  "code.name": "状态码"
+}

+ 12 - 10
src/pages/SViewer.vue

@@ -19,9 +19,9 @@
             </div>
             <div class="item-density" v-if="source.type == 2 && mode == 1" @click="showDensity = true">
                 <span>{{ densityType.text }}</span>
-                <div @click.stop="(showDensity = false)" v-if="showDensity">
+                <div @click.stop="showDensity = false" v-if="showDensity">
                     <ul>
-                        <li class="title">密度选择</li>
+                        <li class="title">{{ $t('home.thickness') }}</li>
                         <li v-for="density in densityTypes" @click="onDensityChange(density)">
                             {{ density.text }}<i class="iconfont" :class="{ 'icon-check': density.type == densityType.type }"></i>
                         </li>
@@ -39,7 +39,9 @@ import { http } from '@/utils/request'
 import Toast from '@/components/dialog/Toast'
 import browser from '@/utils/browser'
 import Calendar from '@/components/calendar/mobile.vue'
-import sync, {  loadSourceScene, loadTargetScene, setPanoWithBim } from '@/utils/sync'
+import sync, { loadSourceScene, loadTargetScene, setPanoWithBim } from '@/utils/sync'
+import i18n from '@/i18n'
+const { t } = i18n.global
 const isDev = process.env.VUE_APP_TEST == 1
 // 点位信息
 let lastFakeApp = null
@@ -58,9 +60,9 @@ const target = ref(null)
 const project = ref(null)
 
 const densityTypes = ref([
-    { type: 'high', text: '高' },
-    { type: 'middle', text: '中' },
-    { type: 'low', text: '低' },
+    { type: 'high', text: t('common.high') },
+    { type: 'middle', text: t('common.middle') },
+    { type: 'low', text: t('common.low') },
 ])
 
 const densityType = ref(densityTypes.value[0])
@@ -229,7 +231,7 @@ const onBimChecked = () => {
     }, 2000)
 
     if (!project.value || !project.value.bimData) {
-        showTips.value = '未发现BIM文件'
+        showTips.value = t('home.notFindFile')
         return
     }
     if (bimChecked.value) {
@@ -257,10 +259,10 @@ onMounted(() => {
                         source.value = project.value.sceneList[project.value.sceneList.length - 1]
                     }
                     if (!source.value) {
-                        return (showTips.value = '当前场景已被删除,无法进行查看')
+                        return (showTips.value = t('home.sceneDelete'))
                     }
                 } else {
-                    return (showTips.value = '当前场景已被删除,无法进行查看')
+                    return (showTips.value = t('home.sceneDelete'))
                 }
                 if (response.data.panos) {
                     response.data.panos = JSON.parse(response.data.panos)
@@ -271,7 +273,7 @@ onMounted(() => {
             }
         })
         .catch(err => {
-            showTips.value = '服务器连接失败'
+            showTips.value = t('code.failed')
         })
 })
 </script>

+ 77 - 38
src/pages/Viewer.vue

@@ -1,11 +1,11 @@
 <template>
-    <AppHeader v-show="!fscChecked" :project="project" :show-adjust="showAdjust" @update="onPointsUpdate" />
+    <AppHeader v-show="!fscChecked" :project="project" @getUserId="getUserId" :show-adjust="showAdjust" @update="onPointsUpdate" />
     <article>
         <main>
             <div class="split">
                 <iframe ref="sourceFrame" v-if="sourceURL" :src="sourceURL" frameborder="0" @load="onLoadSource"></iframe>
-                <div class="tools" v-if="source" v-show="!showAdjust && !fscChecked && (dbsChecked || (!target && !bimChecked))">
-                    <!-- <div class="item-date">
+                <div class="tools" v-if="source" v-show="showWidget && !showAdjust && !fscChecked && (dbsChecked || (!target && !bimChecked))">
+                    <div class="item-date">
                         <calendar
                             name="source"
                             :count="scenes.length"
@@ -17,16 +17,16 @@
                             @prev="onPrevDate"
                             @next="onNextDate"
                         ></calendar>
-                    </div> -->
+                    </div>
                     <div class="item-mode" v-if="source.type == 2">
                         <div class="iconfont icon-show_roaming" :class="{ active: mode == 0 }" @click="onModeChange(0)"></div>
                         <div class="iconfont icon-show_plane" :class="{ active: mode == 1 }" @click="onModeChange(1)"></div>
                     </div>
-                    <div class="item-density" v-if="(source.type == 2 && mode == 1)" :class="{active:showDensity}" @click="showDensity=!showDensity">
-                        <span>{{densityType.text}}</span>
+                    <div class="item-density" v-if="source.type == 2 && mode == 1" :class="{ active: showDensity }" @click="showDensity = !showDensity">
+                        <span>{{ densityType.text }}</span>
                         <i class="iconfont icon-arrows_down"></i>
                         <ul>
-                            <li v-for="density in densityTypes" @click="onDensityChange(density)">{{density.text}}</li>
+                            <li v-for="density in densityTypes" @click="onDensityChange(density)">{{ density.text }}</li>
                         </ul>
                     </div>
                 </div>
@@ -69,11 +69,11 @@
                     </div>
                 </div>
             </div>
-            <div class="model" v-show="!showAdjust">
-                <div class="file" :class="{ active: fileChecked, disable: fileDisable }" v-show="!fscChecked && !showBim">
+            <div class="model" v-show="showWidget && !showAdjust">
+                <div v-if="isLogin" class="file" :class="{ active: fileChecked, disable: fileDisable }" v-show="!fscChecked && !showBim">
                     <div @click="onFileChecked">
                         <i class="iconfont icon-note1"></i>
-                        <span>标注</span>
+                        <span>{{ $t('home.tag') }}</span>
                     </div>
                 </div>
                 <div class="bim" :class="{ active: bimChecked, disable: bimDisable }" v-show="!fscChecked && !showBim">
@@ -85,23 +85,23 @@
                 <div class="dbs" :class="{ active: dbsChecked, disable: dbsDisable }" v-show="!fscChecked && !showBim">
                     <div @click="onDbsChecked">
                         <i class="iconfont icon-split_screen"></i>
-                        <span>分屏</span>
+                        <span>{{ $t('home.splitScreen') }}</span>
                     </div>
                 </div>
                 <div class="fsc" :class="{ active: fscChecked }" @click="onFscChecked">
                     <i class="iconfont" :class="[fscChecked ? 'icon-full_screen_selected' : 'icon-full_screen_normal']"></i>
-                    <span>全屏</span>
+                    <span>{{ $t('home.fullScreen') }}</span>
                 </div>
             </div>
             <TagManager />
         </main>
     </article>
     <Toast v-if="showTips" type="warn" :content="showTips" :close="() => (showTips = null)" />
-    <Files :show="fileChecked" />
+    <Files :show="fileChecked" @exit="() => (fileChecked = false)" />
 </template>
 
 <script setup>
-import { ref, onMounted, computed, nextTick } from 'vue'
+import { ref, onMounted, computed, nextTick, provide, watch } from 'vue'
 import { http } from '@/utils/request'
 
 import browser from '@/utils/browser'
@@ -110,8 +110,25 @@ import AppHeader from '@/components/header'
 import Calendar from '@/components/calendar'
 import Files from '@/components/files'
 import TagManager from '@/components/files/TagManager'
-import sync, {  beforeChangeURL, loadSourceScene, loadTargetScene, setPanoWithBim, flyToP1P2 } from '@/utils/sync'
+import sync, { beforeChangeURL, loadSourceScene, loadTargetScene, setPanoWithBim, flyToP1P2 } from '@/utils/sync'
+import i18n from '@/i18n'
+const { t } = i18n.global
 const isDev = process.env.VUE_APP_TEST == 1
+
+const tags = ref([])
+const notify = ref(null)
+const isEdit = ref(false)
+const isLogin = ref(false)
+provide('tags', tags)
+provide('notify', notify)
+provide('isEdit', isEdit)
+provide('isLogin', isLogin)
+const userId = ref(localStorage.getItem('userId') || null)
+
+const getUserId = id => {
+    userId.value = id
+}
+
 // 是否BIM模式
 const showBim = ref(browser.urlHasValue('bim'))
 // 是否校准模式
@@ -129,15 +146,16 @@ const sourceFrame = ref(null)
 const targetFrame = ref(null)
 
 const mode = ref(0)
+
 const source = ref(null)
 const target = ref(null)
 const project = ref(null)
 const points = ref({ p1: null, p2: null })
 
 const densityTypes = ref([
-    {type:'high',text:'高'},
-    {type:'middle',text:'中'},
-    {type:'low',text:'低'}
+    { type: 'high', text: t('common.high') },
+    { type: 'middle', text: t('common.middle') },
+    { type: 'low', text: t('common.low') },
 ])
 
 const densityType = ref(densityTypes.value[0])
@@ -146,6 +164,13 @@ const showTips = ref(null)
 
 const showDensity = ref(false)
 
+const showWidget = computed(() => {
+    if (fileChecked.value) {
+        return false
+    }
+    return true
+})
+
 const scenes = computed(() => {
     if (!project.value) {
         return []
@@ -198,7 +223,6 @@ const targetURL = computed(() => {
 })
 const sourceDate = computed(() => {
     if (source.value) {
-        console.log(source.value.createTime.toDate())
         return source.value.createTime.toDate()
     }
 })
@@ -241,7 +265,7 @@ const targetDays = computed(() => {
     }
 })
 
-const fileDisable = computed(()=>{
+const fileDisable = computed(() => {
     return false
 })
 
@@ -267,8 +291,9 @@ const onLoadSource = () => {
         // BIM单屏模式
         return
     }
-    if(source.value.type < 2) {
+    if (source.value.type < 2) {
         window['kankan'] = sourceFrame.value.contentWindow.app
+        window['kankan'].TagManager.load(tags.value)
     } else {
         window['laser'] = sourceFrame.value.contentWindow.loaded
     }
@@ -291,7 +316,7 @@ const onModeChange = targetMode => {
     }
 }
 
-const onDensityChange = (density)=>{
+const onDensityChange = density => {
     if (sync.sourceInst) {
         sync.sourceInst.loaded.then(sdk => {
             let data = sdk.scene.changePointDensity(density.type)
@@ -328,7 +353,7 @@ const onSelected = data => {
             }
         }
     } else {
-        showTips.value = '选择日期未上传场景'
+        showTips.value = t('home.dateScene')
     }
 
     datepickName.value = null
@@ -402,16 +427,14 @@ const onNextDate = name => {
     scene.value = scenes.value[index]
 }
 
-
 const onFileChecked = () => {
     fileChecked.value = !fileChecked.value
 }
 
-
 // bim点击
 const onBimChecked = () => {
     if (bimDisable.value) {
-        return (showTips.value = '未发现BIM文件')
+        return (showTips.value = t('home.notFindFile'))
     }
     if (bimChecked.value) {
         bimChecked.value = false
@@ -440,7 +463,7 @@ const onBimChecked = () => {
 // 分屏点击
 const onDbsChecked = () => {
     if (dbsDisable.value && !dbsChecked.value) {
-        return (showTips.value = '未发现对比场景')
+        return (showTips.value = t('home.notFindScene'))
     }
     dbsChecked.value = !dbsChecked.value
     if (dbsChecked.value) {
@@ -499,7 +522,7 @@ const onPointsUpdate = (type, data) => {
 
 const onP1Click = type => {
     if (!points.value.p1) {
-        showTips.value = '您还未选择关联位置'
+        showTips.value = t('home.notChoosePoint')
         return
     }
     console.log(points.value.p1)
@@ -510,7 +533,7 @@ const onP1Click = type => {
 
 const onP2Click = type => {
     if (!points.value.p2) {
-        showTips.value = '您还未选择关联位置'
+        showTips.value = t('home.notChoosePoint')
         return
     }
     // todo 定位
@@ -546,14 +569,14 @@ onMounted(() => {
                             source.value = project.value.sceneList[project.value.sceneList.length - 1]
                         }
                         if (!source.value) {
-                            return (showTips.value = '当前场景已被删除,无法进行查看')
+                            return (showTips.value = t('home.sceneDelete'))
                         }
                         if (showAdjust.value || showSplit.value) {
                             onBimChecked()
                             nextTick(() => onDbsChecked())
                         }
                     } else {
-                        return (showTips.value = '当前场景已被删除,无法进行查看')
+                        return (showTips.value = t('home.sceneDelete'))
                     }
                 }
             } else {
@@ -566,8 +589,24 @@ onMounted(() => {
             }
         })
         .catch(() => {
-            showTips.value = '服务器连接失败'
+            showTips.value = t('code.failed')
         })
+
+    http.post(`smart-site/marking/list`, {
+        projectId: projectId,
+        pageNum: 1,
+        pageSize: 200,
+    }).then(response => {
+        if (response.data && response.data.list) {
+            tags.value = response.data.list.map(item => {
+                item.hotData.visible = false
+                item.hotData.id = item.markingId
+                item.hotData.createTime = item.createTime
+                item.hotData.lastCreateBy = item.lastCreateBy
+                return item.hotData
+            })
+        }
+    })
 })
 </script>
 
@@ -764,12 +803,12 @@ main {
             margin-right: 10px;
             font-size: 16px;
             padding: 0 16px;
-            &.active{
-                ul{
+            &.active {
+                ul {
                     display: block;
                 }
-                i{
-                    transform: scale(0.8) rotate(180deg)
+                i {
+                    transform: scale(0.8) rotate(180deg);
                 }
             }
             span {
@@ -792,9 +831,9 @@ main {
                 box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
                 border: 1px solid #000000;
                 list-style: none;
-                li{
+                li {
                     padding: 5px 0;
-                    &:hover{
+                    &:hover {
                         color: #0076f6;
                     }
                 }

+ 12 - 2
src/pages/sviewer.js

@@ -5,7 +5,8 @@ import { createApp } from 'vue'
 import { DatetimePicker } from 'vant';
 import { setup } from '../utils/request'
 import App from './SViewer.vue'
-
+import i18n, { getLocale, setI18nLanguage, loadLocaleMessages } from '../i18n'
+const local = getLocale()
 Date.prototype.format = function(fmt = 'YYYY-mm-dd HH:MM:SS') {
     var res = ''
     try {
@@ -39,4 +40,13 @@ String.prototype.toDate = function() {
 }
 
 setup()
-createApp(App).use(DatetimePicker).mount('#app')
+// createApp(App).use(DatetimePicker).mount('#app')
+
+
+const app = createApp(App)
+loadLocaleMessages(i18n, local).then(() => {
+  setI18nLanguage(i18n, local)
+  app.use(i18n)
+  app.use(DatetimePicker)
+  app.mount('#app')
+})

+ 18 - 2
src/pages/viewer.js

@@ -4,6 +4,11 @@ import { createApp } from 'vue'
 import { setup } from '../utils/request'
 import ClickOutSide from '../utils/ClickOutSide'
 import App from './Viewer.vue'
+import i18n, { getLocale, setI18nLanguage, loadLocaleMessages } from '../i18n'
+const local = getLocale()
+
+
+
 
 Date.prototype.format = function(fmt = 'YYYY-mm-dd HH:MM:SS') {
     var res = ''
@@ -37,7 +42,18 @@ String.prototype.toDate = function() {
     return new Date(this.replace(/\//g, '-').replace(' ', 'T'))
 }
 
+document.oncontextmenu = function (event){
+    return false
+}
+
 setup()
 const app = createApp(App)
-app.directive('click-outside', ClickOutSide)
-app.mount('#app')
+
+
+
+loadLocaleMessages(i18n, local).then(() => {
+  setI18nLanguage(i18n, local)
+  app.use(i18n)
+  app.directive('click-outside', ClickOutSide)
+  app.mount('#app')
+})

+ 4 - 1
src/utils/ClickOutSide.js

@@ -5,7 +5,10 @@ export const clickOutSide = {
                 binding.value(event, el)
             }
         }
-        document.addEventListener('click', el.clickOutsideEvent)
+        setTimeout(() => {
+            document.addEventListener('click', el.clickOutsideEvent)    
+        }, 200);
+        
     },
     unmounted: function (el) {
         document.removeEventListener('click', el.clickOutsideEvent)

+ 14 - 0
src/utils/browser.js

@@ -11,6 +11,20 @@ var browser = {
             }
         )
     },
+    getURLParam: function (key) {
+      let querys = window.location.search.substring(1).split('&')
+      for (let i = 0; i < querys.length; i++) {
+          let keypair = querys[i].split('=')
+          if (keypair.length === 2 && keypair[0] === key) {
+              try {
+                  return decodeURIComponent(keypair[1])
+              } catch (error) {
+                  return keypair[1]
+              }
+          }
+      }
+      return ''
+  },
     isFullscreen() {
         return document.fullscreenElement || document.mozFullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement
     },

+ 5 - 1
src/utils/request.js

@@ -183,6 +183,7 @@ export function setup(options = {}) {
     )
     fetch.interceptors.response.use(
         response => {
+        
             // 正常的文件流
             if (!/json/gi.test(response.headers['content-type'])) {
                 return response.data
@@ -194,7 +195,10 @@ export function setup(options = {}) {
                 let res = JSON.parse(enc.decode(new Uint8Array(response.data)))
                 return res
             }
-
+            if(response.code==4008){
+              //用户未登录
+              
+            }
             return response.data
         },
         error => {