Browse Source

update master

tremble 3 years ago
parent
commit
375386a3b1
72 changed files with 7993 additions and 1746 deletions
  1. 3 3
      packages/code/.env
  2. 0 7
      packages/code/.env.bendi
  3. 0 2
      packages/code/.env.prod
  4. 72 20
      packages/code/README.md
  5. 458 567
      packages/code/package-lock.json
  6. 1 3
      packages/code/package.json
  7. 0 4
      packages/code/public/showMobile.html
  8. 70 33
      packages/code/src/Store/index.js
  9. 7 5
      packages/code/src/api/index.js
  10. BIN
      packages/code/src/assets/images/default/empty_04.png
  11. BIN
      packages/code/src/assets/images/default/empty_04_search.png
  12. BIN
      packages/code/src/assets/images/default/img_cover_default_2.png
  13. BIN
      packages/code/src/assets/images/default/mask_bg.png
  14. BIN
      packages/code/src/assets/images/icons/eye_off.png
  15. BIN
      packages/code/src/assets/images/icons/eye_on.png
  16. BIN
      packages/code/src/assets/images/icons/link.png
  17. 18 0
      packages/code/src/assets/images/icons/pause_for_editor.svg
  18. BIN
      packages/code/src/assets/images/icons/phone.png
  19. 17 0
      packages/code/src/assets/images/icons/play_for_editor.svg
  20. BIN
      packages/code/src/assets/images/select_pic_btn.png
  21. 14 15
      packages/code/src/assets/style/component.less
  22. 1747 0
      packages/code/src/assets/style/style.pc.edit.less
  23. 4 4
      packages/code/src/assets/style/style.pc.less
  24. BIN
      packages/code/src/assets/videos/小行星巡游开场.mp4
  25. BIN
      packages/code/src/assets/videos/小行星开场.mp4
  26. BIN
      packages/code/src/assets/videos/小行星缩放开场.mp4
  27. BIN
      packages/code/src/assets/videos/水平巡游开场.mp4
  28. BIN
      packages/code/src/assets/videos/水晶球开场.mp4
  29. 166 0
      packages/code/src/components/audio/audioForEditor.vue
  30. 726 0
      packages/code/src/components/materialSelectorForEditor.vue
  31. 6 5
      packages/code/src/components/preview/index.vue
  32. 93 0
      packages/code/src/components/pulldownMenuInEditor.vue
  33. 24 11
      packages/code/src/components/range/index.vue
  34. 37 36
      packages/code/src/components/rangeItem/index.vue
  35. 344 0
      packages/code/src/components/sceneGroupInEditor.vue
  36. 220 0
      packages/code/src/components/sceneInGroupInEditor.vue
  37. 62 0
      packages/code/src/components/selectedImageInEditor.vue
  38. 8 11
      packages/code/src/components/shared/Switcher.vue
  39. 1 1
      packages/code/src/components/shared/popup/index.vue
  40. 1 1
      packages/code/src/components/userInfo.vue
  41. 1 2
      packages/code/src/config/index.js
  42. 3 2
      packages/code/src/config/menu.js
  43. 77 0
      packages/code/src/directives/vTitleInEditor.js
  44. 81 0
      packages/code/src/directives/vTooltipInEditor.js
  45. 29 0
      packages/code/src/framework/EditorAppLayout.vue
  46. 277 0
      packages/code/src/framework/EditorHead.vue
  47. 92 0
      packages/code/src/framework/EditorMain.vue
  48. 1 0
      packages/code/src/framework/Toolbar.vue
  49. 124 191
      packages/code/src/framework/show/index.vue
  50. 1 1
      packages/code/src/framework/show/popup/index.vue
  51. 64 87
      packages/code/src/framework/showMobile/iframe.vue
  52. 9 121
      packages/code/src/framework/showMobile/index.vue
  53. 268 315
      packages/code/src/framework/showMobile/list.vue
  54. 2 0
      packages/code/src/mixins/index.js
  55. 8 4
      packages/code/src/pages/Edit.vue
  56. 2 0
      packages/code/src/pages/edit.js
  57. 1 1
      packages/code/src/router/index.js
  58. 59 0
      packages/code/src/utils/other.js
  59. 198 267
      packages/code/src/views/base/Toolbar.vue
  60. 59 0
      packages/code/src/views/base/autoCruiseSettings.vue
  61. 153 0
      packages/code/src/views/base/backgroundMusicSettings.vue
  62. 437 0
      packages/code/src/views/base/customButtonSettings.vue
  63. 175 0
      packages/code/src/views/base/customLogoSettings.vue
  64. 147 0
      packages/code/src/views/base/customMaskSettings.vue
  65. 11 5
      packages/code/src/views/base/index.vue
  66. 105 0
      packages/code/src/views/base/openingAnimationSettings.vue
  67. 169 0
      packages/code/src/views/base/openingTipSettings.vue
  68. 84 0
      packages/code/src/views/base/passwordSettings.vue
  69. 703 0
      packages/code/src/views/navigation/groupSettings.vue
  70. 416 0
      packages/code/src/views/navigation/index.vue
  71. 126 0
      packages/code/src/views/navigation/initialSceneSettings.vue
  72. 12 22
      packages/code/vue.config.js

+ 3 - 3
packages/code/.env

@@ -2,6 +2,6 @@ VUE_APP_MAIN_COLOR=''
 VUE_APP_STATIC_DIR=static
 VUE_APP_INNERNET=https://fcb.intranet.4dkankan.com
 VUE_APP_CDN=https://oss-xiaoan.oss-cn-shenzhen.aliyuncs.com
-VUE_APP_PROXY_URL='http://test.4dkankan.com/qjkankan/'
-VUE_APP_PROXY_4DKANKAN_URL=''
-VUE_APP_URL_FILL=/qjkankan
+VUE_APP_PROXY_URL_ROOT='https://test.4dkankan.com'
+VUE_APP_PROXY_URL='https://test.4dkankan.com/qjkankan/'
+VUE_APP_URL_FILL=

+ 0 - 7
packages/code/.env.bendi

@@ -2,12 +2,5 @@ VUE_APP_MAIN_COLOR=''
 VUE_APP_STATIC_DIR=static
 VUE_APP_INNERNET=https://fcb.intranet.4dkankan.com
 VUE_APP_CDN=https://oss-xiaoan.oss-cn-shenzhen.aliyuncs.com
-VUE_APP_PROXY_URL='http://test.4dkankan.com/qjkankan/'
-VUE_APP_URL_FILL=/qjkankan
-
-VUE_APP_MAIN_COLOR=''
-VUE_APP_STATIC_DIR=static
-VUE_APP_INNERNET=https://fcb.intranet.4dkankan.com
-VUE_APP_CDN=https://oss-xiaoan.oss-cn-shenzhen.aliyuncs.com
 VUE_APP_PROXY_URL='http://192.168.0.135:8001/'
 VUE_APP_URL_FILL='/'

+ 0 - 2
packages/code/.env.prod

@@ -1,8 +1,6 @@
-NODE_ENV=production
 VUE_APP_MAIN_COLOR=''
 VUE_APP_STATIC_DIR=static
 VUE_APP_INNERNET=https://fcb.intranet.4dkankan.com
 VUE_APP_CDN=https://4dkk.4dage.com
 VUE_APP_PROXY_URL='http://www.4dkankan.com/qjkankan/'
-VUE_APP_PROXY_4DKANKAN_URL=''
 VUE_APP_URL_FILL=/qjkankan

+ 72 - 20
packages/code/README.md

@@ -1,24 +1,76 @@
-# code2.6
+# someData模板
 
-## Project setup
-```
-npm install
-```
-
-### Compiles and hot-reloads for development
-```
-npm run serve
-```
+## 位置
+/阿里云-四维时代-官网web测试服务器-120.25.146.52/mnt/720yun_fd_manage_data/baseData
 
-### Compiles and minifies for production
-```
-npm run build
-```
-
-### Lints and fixes files
-```
-npm run lint
+## 含义
+```js
+{
+  "isPassword": 0,
+  "password": "",
+  "ossSomeData": "",
+  "icon": "",
+  "description": "",
+  "updateTime": "",
+  "sceneIndex": "",
+  "catalogRoot": [
+    {
+      "id": 100,
+      "name": "一级分组",
+      "children": [
+        1
+      ]
+    }
+  ],
+  "firstScene": "",
+  "userId": "",
+  "isLogo": 1,
+  "appIcon": "",
+  "qrCode": "",
+  "createTime": "",
+  "catalogs": [
+    {
+      "id": 1,
+      "name": "默认二级分组"
+    }
+  ],
+  "isRemind": 1,
+  "name": "",
+  "pcIcon": "",
+  "scenes": [],
+  "isAuto": 1,
+  "logo": "",
+  "logoChange": false,
+  "share": "",
+  "id": "",
+  "remindTime": 1,
+  "status": 0,
+  "openingAnimationType": "小行星开场", // 开场动画类型
+  "backgroundMusic": { // 背景音乐
+    "id": "",
+    "name": "",
+    "ossPath": ""
+  },
+  "customMask": { // 自定义遮罩
+    "sky": "",
+    "earth": ""
+  },
+  "customButton": [ // 自定义按钮
+    {
+      "type": "电话",
+      "name": "电话",
+      "value": "",
+      "isShow": false
+    },
+    {
+      "type": "链接",
+      "name": "链接",
+      "value": "",
+      "isShow": false
+    }
+  ]
+}
 ```
 
-### Customize configuration
-See [Configuration Reference](https://cli.vuejs.org/config/).
+# 会员测试账号
+15915816041  4DAge123456

File diff suppressed because it is too large
+ 458 - 567
packages/code/package-lock.json


+ 1 - 3
packages/code/package.json

@@ -3,17 +3,15 @@
   "version": "0.1.0",
   "private": true,
   "scripts": {
-    "clean": "rimraf dist/**/*",
     "serve": "vue-cli-service serve",
     "serve-prod": "vue-cli-service serve --mode prod",
-    "serve-testprod": "vue-cli-service serve --mode testprod",
     "serve-local": "vue-cli-service serve --mode bendi",
     "build": "vue-cli-service build",
     "build-prod": "vue-cli-service build --mode prod",
-    "build-testprod": "vue-cli-service build --mode testprod",
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
+    "@floating-ui/dom": "^0.5.4",
     "core-js": "^3.8.2",
     "element-ui": "^2.15.1",
     "html2canvas": "^1.4.1",

+ 0 - 4
packages/code/public/showMobile.html

@@ -33,10 +33,6 @@
     <!-- built files will be auto injected -->
     <script src="<%= VUE_APP_STATIC_DIR %>/lib/jquery-2.1.1.min.js"></script>
     <script src="<%= VUE_APP_STATIC_DIR %>/lib/krpano/js/tour.js"></script>
-    
-    <script src="https://4dkk.4dage.com/v4/www/sdk/kankan-sdk-deps.js?v=4.0.0-alpha.44"></script>
-    <script src="https://4dkk.4dage.com/v4/www/sdk/kankan-sdk.js?v=4.0.0-alpha.44"></script>
-
     <script src="https://res2.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
 
 <!-- 

+ 70 - 33
packages/code/src/Store/index.js

@@ -1,7 +1,8 @@
 import Vue from 'vue'
 import Vuex from 'vuex'
 
-import browser from "@/utils/browser";
+import browser from "@/utils/browser"
+import { deepClone } from "@/utils/other.js";
 
 Vue.use(Vuex)
 
@@ -9,20 +10,21 @@ const store = new Vuex.Store({
   state: {
     userAvatar: '',
     userNickName: '',
-    
-    info:'',
-    backupInfo:'',
-    showInfo:'',
-    hotspot:'',
-    backupHotSpot:'',
-    initScene:'',
-    tablist:[],
-    temptablist:[],
-    vrlist:[],
-    allVrlist:[],
-    sceneList:[],
-    activeItem:'',
-    isEditing:false,
+
+    info: '',
+    // todo: 新版本开发完毕后就不需要这个了
+    backupInfo: '',
+    showInfo: '',
+    hotspot: '',
+    backupHotSpot: '',
+    initScene: '',
+    tablist: [],
+    temptablist: [],
+    vrlist: [],
+    allVrlist: [],
+    sceneList: [],
+    activeItem: '',
+    isEditing: false,
     isShow: false,
 
     uploadStatusListAudio: [],
@@ -34,20 +36,55 @@ const store = new Vuex.Store({
     userAvatar: state => state.userAvatar,
     userNickName: state => state.userNickName,
 
-    isEditing:state=>state.isEditing,
-    info:state=>state.info,
-    showInfo:state=>state.showInfo,
-    backupInfo:state=>state.backupInfo,
-    hotspot:state=>state.hotspot,
-    backupHotSpot:state=>state.backupHotSpot,
-    initScene:state=>state.initScene,
-    activeItem:state=>state.activeItem,
-    vrlist:state=>state.vrlist,
-    tablist:state=>state.tablist,
-    temptablist:state=>state.temptablist,
-    sceneList:state=>state.sceneList,
-    isShow:state=>state.isShow,
-    allVrlist:state=>state.allVrlist,
+    isEditing: state => state.isEditing,
+    info: state => state.info,
+    catalogTopology: (state) => {
+      if (!state.info) {
+        return
+      }
+      //四层:root,level1(一级分类),level2(二级分类或直属于一级分类的场景), level3(场景)
+      let root = deepClone(state.info.catalogRoot)
+      // 对于每个一级分类
+      for (const itemLevel1 of root) {
+        // 指定每个一级分类的下级
+        itemLevel1.childrenTemp = []
+        for (const itemLevel2Id of itemLevel1.children) {
+          for (const catalogsItem of state.info.catalogs) {
+            if (itemLevel2Id === catalogsItem.id) {
+              itemLevel1.childrenTemp.push(deepClone(catalogsItem))
+              
+              // 对于该二级分类
+              const itemLevel2 = itemLevel1.childrenTemp[itemLevel1.childrenTemp.length - 1]
+              itemLevel2.children = []
+              // 对于每个三级分类
+              for (const itemLevel3 of state.info.scenes) {
+                // 如果属于上述二级分类
+                if (itemLevel2.id === itemLevel3.category /* 注意拼写!!! */) {
+                  itemLevel2.children.push(deepClone(itemLevel3))
+                }
+              }
+              
+              break
+            }
+          }
+        }
+        itemLevel1.children = itemLevel1.childrenTemp
+        delete itemLevel1.childrenTemp
+      }
+      return root
+    },
+    showInfo: state => state.showInfo,
+    backupInfo: state => state.backupInfo,
+    hotspot: state => state.hotspot,
+    backupHotSpot: state => state.backupHotSpot,
+    initScene: state => state.initScene,
+    activeItem: state => state.activeItem,
+    vrlist: state => state.vrlist,
+    tablist: state => state.tablist,
+    temptablist: state => state.temptablist,
+    sceneList: state => state.sceneList,
+    isShow: state => state.isShow,
+    allVrlist: state => state.allVrlist,
 
     uploadStatusListAudio: state => state.uploadStatusListAudio,
     uploadStatusListImage: state => state.uploadStatusListImage,
@@ -65,7 +102,7 @@ const store = new Vuex.Store({
         state.userNickName = nickName
       }
     },
-    
+
     SetTabList(state, list) {
       state.tablist = list
     },
@@ -98,19 +135,19 @@ const store = new Vuex.Store({
     },
     SetInfo(state, data) {
       state.info = data
-      this.commit("BackupInfo", browser.CloneObject(data));
+      this.commit("BackupInfo", browser.CloneObject(data))
     },
     BackupInfo(state, data) {
       state.backupInfo = data
     },
     SetHotspot(state, data) {
       state.hotspot = data
-      this.commit("BackupHotSpot", browser.CloneObject(data));
+      this.commit("BackupHotSpot", browser.CloneObject(data))
     },
     BackupHotSpot(state, data) {
       state.backupHotSpot = data
     }
-    
+
   },
   actions: {
     refreshUserInfo(context) {

+ 7 - 5
packages/code/src/api/index.js

@@ -20,12 +20,14 @@ const URL_FILL =  config.urlFill
 
 let ossUrl = config.CDN
 
-let f4KKUrl = config.f4KKUrl
-
-
-
 // https://ossxiaoan.4dage.com/720yun_fd_manage
 
+/**
+ * 获取用户账号信息
+ */
+export function getUserInfo(ok, no) {
+  return http.postJson(`/api/user/getUserInfo`, {}, ok, no)
+}
 
 /**
  * 获取全景作品基本信息
@@ -46,7 +48,7 @@ let f4KKUrl = config.f4KKUrl
  * @param {*} no 
  */
  export function getSceneInfomation(data, ok, no) {
-    return http.get(`${f4KKUrl}/api/scene/getInfo?num=${data.id}&_=${Math.random()}`, {}, ok, no)
+    return http.get(`/api/scene/getInfo?num=${data.id}&_=${Math.random()}`, {}, ok, no)
 }
 
 /**

BIN
packages/code/src/assets/images/default/empty_04.png


BIN
packages/code/src/assets/images/default/empty_04_search.png


BIN
packages/code/src/assets/images/default/img_cover_default_2.png


BIN
packages/code/src/assets/images/default/mask_bg.png


BIN
packages/code/src/assets/images/icons/eye_off.png


BIN
packages/code/src/assets/images/icons/eye_on.png


BIN
packages/code/src/assets/images/icons/link.png


+ 18 - 0
packages/code/src/assets/images/icons/pause_for_editor.svg

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="8px" height="8px" viewBox="0 0 8 8" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>pause</title>
+    <g id="管理中心" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="管理中心-音频" transform="translate(-617.000000, -833.000000)" fill="#0076F6">
+            <g id="编组-7备份-7" transform="translate(580.000000, 796.000000)">
+                <g id="编组-8" transform="translate(16.000000, 10.000000)">
+                    <g id="编组-9" transform="translate(16.000000, 22.000000)">
+                        <g id="pause" transform="translate(5.000000, 5.000000)">
+                            <rect id="矩形" x="1" y="1" width="2" height="6" rx="1"></rect>
+                            <rect id="矩形" x="5" y="1" width="2" height="6" rx="1"></rect>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

BIN
packages/code/src/assets/images/icons/phone.png


File diff suppressed because it is too large
+ 17 - 0
packages/code/src/assets/images/icons/play_for_editor.svg


BIN
packages/code/src/assets/images/select_pic_btn.png


+ 14 - 15
packages/code/src/assets/style/component.less

@@ -32,25 +32,25 @@
 
 input::-webkit-input-placeholder,
 textarea::-webkit-input-placeholder {
-  color: #888888 !important;
+  color: rgba(255, 255, 255, 0.2) !important;
   font-weight: normal !important;
 }
 
 input:-moz-placeholder,
 textarea:-moz-placeholder {
-  color: #888888 !important;
+  color: rgba(255, 255, 255, 0.2) !important;
   font-weight: normal !important;
 }
 
 input::-moz-placeholder,
 textarea::-moz-placeholder {
-  color: #888888 !important;
+  color: rgba(255, 255, 255, 0.2) !important;
   font-weight: normal !important;
 }
 
 input:-ms-input-placeholder,
 textarea:-ms-input-placeholder {
-  color: #888888 !important;
+  color: rgba(255, 255, 255, 0.2) !important;
   font-weight: normal !important;
 }
 
@@ -300,10 +300,9 @@ textarea:-ms-input-placeholder {
 .preview {
   position: relative;
   background-color: #161a1a;
-  border: 1px solid #555a5a;
-  width: 216px;
-  height: 216px;
-  margin-right: 20px;
+  width: 240px;
+  height: 240px;
+  margin-right: 30px;
   color: #a0a0a0;
   display: flex;
   align-items: center;
@@ -553,7 +552,7 @@ textarea:-ms-input-placeholder {
   white-space: normal;
   font-size: 14px;
   line-height: 1.5;
-  color: #fff;
+  color: rgba(255, 255, 255, 0.6);
   margin-bottom: 10px;
 }
 
@@ -562,7 +561,7 @@ textarea:-ms-input-placeholder {
   white-space: normal;
   font-size: 12px;
   line-height: 1.5;
-  color: #ababab;
+  color: rgba(255, 255, 255, 0.3);
 }
 
 .require {
@@ -680,15 +679,15 @@ textarea:-ms-input-placeholder {
 
 .ui-input {
   outline: none;
-  border: 1px solid #555a5a;
-  background-color: #161a1a;
-  padding: 0 10px;
+  border: 1px solid rgba(151, 151, 151, 0.2);
+  background: #252526;
+  padding: 0 16px;
   color: #fff;
   letter-spacing: 1px;
   border-radius: 2px;
-  height: 32px;
+  height: 36px;
   width: 100%;
-  font-size: 12px;
+  font-size: 14px;
 }
 
 .ui-textarea {

File diff suppressed because it is too large
+ 1747 - 0
packages/code/src/assets/style/style.pc.edit.less


+ 4 - 4
packages/code/src/assets/style/style.pc.less

@@ -16,7 +16,7 @@
     }
 
     &[dir-top] {
-        border-top: solid 1px rgb(93, 93, 93);
+        border-top: solid 1px #47484a;
 
         &::after {
             top: 0;
@@ -27,7 +27,7 @@
     }
 
     &[dir-left] {
-        border-left: solid 1px rgb(93, 93, 93);
+        border-left: solid 1px #47484a;
 
         &::after {
             top: 0;
@@ -38,7 +38,7 @@
     }
 
     &[dir-right] {
-        border-right: solid 1px rgb(93, 93, 93);
+        border-right: solid 1px #47484a;
 
         &::after {
             top: 0;
@@ -49,7 +49,7 @@
     }
 
     &[dir-bottom] {
-        border-bottom: solid 1px rgb(93, 93, 93);
+        border-bottom: solid 1px #47484a;
 
         &::after {
             left: 0;

BIN
packages/code/src/assets/videos/小行星巡游开场.mp4


BIN
packages/code/src/assets/videos/小行星开场.mp4


BIN
packages/code/src/assets/videos/小行星缩放开场.mp4


BIN
packages/code/src/assets/videos/水平巡游开场.mp4


BIN
packages/code/src/assets/videos/水晶球开场.mp4


+ 166 - 0
packages/code/src/components/audio/audioForEditor.vue

@@ -0,0 +1,166 @@
+<template>
+  <div class="audio-wrapper">
+    <audio ref="audio">
+      <source :src="myAudioUrl" type="audio/mp3" />
+    </audio>
+    <div class="UI" @click="switchPlayPause">
+      <div ref="circle-left" class="left"></div>
+      <div ref="circle-right" class="right"></div>
+      <div class="circle" :style="{background: backgroundColor}"></div>
+      <img v-if="notPlaying" :src="require('@/assets/images/icons/play_for_editor.svg')" />
+      <img v-if="!notPlaying" :src="require('@/assets/images/icons/pause_for_editor.svg')" />
+    </div>
+  </div>
+</template>
+ 
+<script>
+export default {
+  name: "myAudioForEditor",
+  props: [
+    "myAudioUrl",
+    "backgroundColor",
+  ],
+  data() {
+    return {
+      audio: "",
+      notPlaying: true,
+      progress: 0,
+    };
+  },
+  mounted() {
+    this.init()
+  },
+  methods: {
+    init() {
+      this.$nextTick(() => { // props的变化并不会立刻反映到html element的attribute上
+        if (this.audio) {
+          this.audio.pause()
+        }
+        if (this.$refs.audio) {
+          this.audio = this.$refs.audio;
+          this.audio.load();
+          this.audio.pause();
+          this.updateProgress();
+          this.notPlaying = true;
+          let self = this;
+          this.audio.addEventListener(
+            "timeupdate",
+            function () {
+              self.updateProgress();
+            },
+            false
+          );
+          this.audio.addEventListener(
+            "ended",
+            function () {
+              self.audioEnded();
+            },
+            false
+          );
+        }
+      })
+    },
+    loadCircle() {
+      let leftEl = this.$refs['circle-left']
+      let rightEl = this.$refs['circle-right']
+      if (!rightEl && !leftEl) {
+        return
+      }
+      if (this.progress) {
+        if (this.progress > 0 && this.progress < 50) {
+          rightEl.style.transform = "rotate(" + 3.6 * this.progress + "deg)";
+        } else {
+          rightEl.style.transform = "rotate(0)";
+          rightEl.style.background = "#033d7c";
+          leftEl.style.transform =
+            "rotate(" + 3.6 * (this.progress - 50) + "deg)";
+        }
+      } else {
+        rightEl.style.transform = "rotate(0deg)";
+        leftEl.style.transform = "rotate(0deg)";
+        rightEl.style.background = "#0076F6";
+      }
+    },
+    switchPlayPause() {
+      setTimeout(() => {
+        if (this.audio.paused) {
+          // 开始播放当前点击的音频
+          this.audio.play();
+          this.notPlaying = false;
+        } else {
+          this.audio.pause();
+          this.notPlaying = true;
+        }
+        this.updateProgress();
+      });
+    },
+    // 更新进度条与当前播放时间
+    updateProgress() {
+      let value = this.audio.currentTime / this.audio.duration;
+      this.progress = value * 100;
+      this.loadCircle();
+    },
+    // 播放结束时
+    audioEnded() {
+      this.progress = 0;
+      this.notPlaying = true;
+      this.$emit('audioEnded')
+      this.loadCircle();
+    },
+  },
+  watch: {
+    myAudioUrl: {
+      handler: function () {
+        this.init()
+      },
+    },
+  },
+};
+</script>
+ 
+<style lang="less" scoped>
+@radiusOuter: 16px;
+@pcolor: #0076F6;
+@radiusInner: 12px;
+
+.audio-wrapper {
+  width: @radiusOuter;
+  height: @radiusOuter;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.UI {
+  position: relative;
+  width: @radiusOuter;
+  height: @radiusOuter;
+  min-width: @radiusOuter;
+  background: #033d7c;
+
+  border-radius: 50%;
+  .left {
+    border-radius: 50%;
+    background: @pcolor;
+    clip: rect(0, @radiusOuter / 2, @radiusOuter, 0);
+  }
+  .right {
+    border-radius: 50%;
+    background: @pcolor;
+    clip: rect(0, @radiusOuter, @radiusOuter, @radiusOuter / 2);
+  }
+  .circle {
+    width: @radiusInner;
+    height: @radiusInner;
+    border-radius: 50%;
+  }
+  * {
+    position: absolute;
+    left: 0;
+    top: 0;
+    bottom: 0;
+    right: 0;
+    margin: auto;
+  }
+}
+</style>

+ 726 - 0
packages/code/src/components/materialSelectorForEditor.vue

@@ -0,0 +1,726 @@
+<template>
+  <div class="table-select">
+    <span class="title">{{title}}</span>
+    <div class="close-btn"><i class="iconfont icon-pop-ups_shut-down" @click="$emit('cancle')"></i></div>
+
+    <div class="material-tab">
+      <a v-if="selectableType.includes('image')" class="material-tab-item" @click.prevent="currentMaterialType = 'image'">
+        <span class="text">图片</span>
+        <div v-if="currentMaterialType === 'image'" class="bottom-line"></div>
+      </a>
+      <a v-if="selectableType.includes('pano')" class="material-tab-item" @click.prevent="currentMaterialType = 'pano'">
+        <span class="text">全景图</span>
+        <div v-if="currentMaterialType === 'pano'" class="bottom-line"></div>
+      </a>
+      <a v-if="selectableType.includes('audio')" class="material-tab-item" @click.prevent="currentMaterialType = 'audio'">
+        <span class="text">音频</span>
+        <div v-if="currentMaterialType === 'pano'" class="bottom-line"></div>
+      </a>
+      <a v-if="selectableType.includes('3D')" class="material-tab-item" @click.prevent="currentMaterialType = '3D'">
+        <span class="text">三维场景</span>
+        <div v-if="currentMaterialType === '3D'" class="bottom-line"></div>
+      </a>
+    </div>
+    
+    <div class="filter">
+      <input type="text" placeholder="输入关键词" v-model="searchKey"/>
+      <i v-if="!searchKey" class="iconfont icon-editor_search search-icon"/>
+      <i v-if="searchKey" @click="searchKey=''" class="iconfont icontoast_red clear-icon"></i>
+    </div>
+
+    <div class="table table-image" v-show="currentMaterialType === 'image'">
+      <div class="table-head-row">
+        <span class="table-head">1</span>
+        <span class="table-head" v-for="(item,i) in tableHeadersForImage" :key="i">{{item.name}}</span>
+      </div>
+      <div
+        v-if="imageList.length !== 0 || hasMoreImageData"
+        class="table-body"
+        v-infinite-scroll="requestMoreImageData"
+        :infinite-scroll-disabled="!hasMoreImageData || isRequestingMoreImageData"
+      >
+        <div class="table-body-row" v-for="(item,i) in imageList" :key="i">
+          <span class="table-data">
+            <div class="checkbox">
+              <!-- 负责功能 -->
+              <input
+                type="checkbox"
+                @change="e => selectItem(item, e)"
+                :checked="select.some(i => i[primaryKey] === item[primaryKey])"
+              >
+              <!-- 负责外观 -->
+              <span class="for-outer-circle"></span>
+              <span class="for-inner-circle"></span>
+            </div>
+          </span>
+          <span class="table-data" v-for="(sub,idx) in tableHeadersForImage" :key="idx">
+            <div v-if="sub.type=='image'" class="list-img">
+              <img :src="item[sub.key] + `?x-oss-process=image/resize,p_10&${Math.random()}`" alt="">
+            </div>
+            <span class="ellipsis" v-else v-title="sub.key === 'name' ? item[sub.key] : ''">{{ item[sub.key] }}</span>
+          </span>
+        </div>
+      </div>
+      <!-- 无数据时的提示 -->
+      <div v-if="imageList.length === 0 && !hasMoreImageData" class="no-data">
+        <div v-if="latestUsedSearchKey">
+          <img :src="require('@/assets/images/default/empty_04_search.png')" alt="">
+          <span>{{'未搜索到结果~'}}</span>
+        </div>
+        <div v-if="!latestUsedSearchKey">
+          <img :src="require('@/assets/images/default/empty_04.png')" alt="">
+          <span>{{'暂无素材~'}}</span>
+        </div>
+      </div>
+    </div>
+
+    <div class="table table-pano" v-show="currentMaterialType === 'pano'">
+      <div class="table-head-row">
+        <span class="table-head">1</span>
+        <span class="table-head" v-for="(item,i) in tableHeadersForPano" :key="i">{{item.name}}</span>
+      </div>
+      <div
+        v-if="panoList.length !== 0 || hasMorePanoData"
+        class="table-body"
+        v-infinite-scroll="requestMorePanoData"
+        :infinite-scroll-disabled="!hasMorePanoData || isRequestingMorePanoData"
+      >
+        <div class="table-body-row" v-for="(item,i) in panoList" :key="i">
+          <span class="table-data">
+            <div class="checkbox">
+              <!-- 负责功能 -->
+              <input
+                type="checkbox"
+                @change="e => selectItem(item, e)"
+                :checked="select.some(i => i[primaryKey] === item[primaryKey])"
+              >
+              <!-- 负责外观 -->
+              <span class="for-outer-circle"></span>
+              <span class="for-inner-circle"></span>
+            </div>
+          </span>
+          <span class="table-data" v-for="(sub,idx) in tableHeadersForPano" :key="idx">
+            <div v-if="sub.type=='image'" class="list-img">
+              <img :src="item[sub.key] + `?x-oss-process=image/resize,p_10&${Math.random()}`" alt="">
+            </div>
+            <span class="ellipsis" v-else v-title="sub.key === 'name' ? item[sub.key] : ''">{{item[sub.key]}}</span>
+          </span>
+        </div>
+      </div>
+      <!-- 无数据时的提示 -->
+      <div v-if="panoList.length === 0 && !hasMorePanoData" class="no-data">
+        <div v-if="latestUsedSearchKey">
+          <img :src="require('@/assets/images/default/empty_04_search.png')" alt="">
+          <span>{{'未搜索到结果~'}}</span>
+        </div>
+        <div v-if="!latestUsedSearchKey">
+          <img :src="require('@/assets/images/default/empty_04.png')" alt="">
+          <span>{{'暂无素材~'}}</span>
+        </div>
+      </div>
+    </div>
+
+    <div class="table table-audio" v-show="currentMaterialType === 'audio'">
+      <div class="table-head-row">
+        <span class="table-head">1</span>
+        <span class="table-head" v-for="(item,i) in tableHeadersForAudio" :key="i">{{item.name}}</span>
+      </div>
+      <div
+        v-if="audioList.length !== 0 || hasMoreAudioData"
+        class="table-body"
+        v-infinite-scroll="requestMoreAudioData"
+        :infinite-scroll-disabled="!hasMoreAudioData || isRequestingMoreAudioData"
+      >
+        <div class="table-body-row" v-for="(item,i) in audioList" :key="i">
+          <span class="table-data">
+            <div class="checkbox">
+              <!-- 负责功能 -->
+              <input
+                type="checkbox"
+                @change="e => selectItem(item, e)"
+                :checked="select.some(i => i[primaryKey] === item[primaryKey])"
+              >
+              <!-- 负责外观 -->
+              <span class="for-outer-circle"></span>
+              <span class="for-inner-circle"></span>
+            </div>
+          </span>
+          <span class="table-data" v-for="(sub,idx) in tableHeadersForAudio" :key="idx">
+            <div v-if="sub.type=='audio'" class="list-img">
+              <img
+                :src="require('@/assets/images/icons/icon-music@2x.png')"
+                style="object-fit: contain;"
+                alt=""
+              >
+            </div>
+            <span class="ellipsis" v-else v-title="sub.key === 'name' ? item[sub.key] : ''">{{item[sub.key]}}</span>
+          </span>
+        </div>
+      </div>
+      <!-- 无数据时的提示 -->
+      <div v-if="audioList.length === 0 && !hasMoreAudioData" class="no-data">
+        <div v-if="latestUsedSearchKey">
+          <img :src="require('@/assets/images/default/empty_04_search.png')" alt="">
+          <span>{{'未搜索到结果~'}}</span>
+        </div>
+        <div v-if="!latestUsedSearchKey">
+          <img :src="require('@/assets/images/default/empty_04.png')" alt="">
+          <span>{{'暂无素材~'}}</span>
+        </div>
+      </div>
+    </div>
+
+    <div class="btns">
+      <button class="ui-button upload-btn">
+        <span>上传素材</span>
+        <i class="iconfont icon-material_prompt tool-tip-for-editor"
+          v-tooltip="
+            currentMaterialType === 'image' ? '请上传10MB以内、jpg/png格式的图片' :
+            currentMaterialType === 'pano' ? '请上传2:1、120MB以内、jpg格式的图片' : ''
+          ">
+        </i>
+      </button>
+      <div>
+        <button class="ui-button deepcancel" @click="$emit('cancle')">取消</button>
+        <button class="ui-button submit" :class="{disable: !select.length}" @click="$emit('submit', select)">
+          确定
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getMaterialList} from "@/api";
+import { changeByteUnit } from '@/utils/file'
+import config from "@/config";
+import { debounce } from "@/utils/other.js"
+
+export default {
+  props:{
+    title:{
+      default:'',
+      type:String
+    },
+    primaryKey: {
+      default:'id'
+    },
+    selectableType: {
+      type: Array,
+      default: function() {
+        return [
+          'image',
+          'pano',
+          'audio',
+          '3D',
+        ]
+      },
+    },
+    initialMaterialType: {
+      type: String,
+      default: 'image',
+    },
+  },
+  components:{
+  },
+  watch:{
+    searchKey: {
+      handler: function () {
+        this.refreshMaterialList(this.currentMaterialType)
+      },
+      immediate: false,
+    },
+    currentMaterialType: {
+      handler: function (newVal) {
+        if (newVal === 'image' && this.imageList.length === 0) {
+          this.refreshMaterialList('image')
+        } else if (newVal === 'pano' && this.panoList.length === 0) {
+          this.refreshMaterialList('pano')
+        } else if (newVal === 'audio' && this.audioList.length === 0) {
+          this.refreshMaterialList('audio')
+        }
+      },
+      immediate: false,
+    }
+  },
+  computed:{
+    tableHeadersForImage() {
+      return this.$MAPTABLEHEADER['image'].filter(item => {
+        return ['icon', 'name', 'fileSize', 'dpi'].includes(item.key)
+      })
+    },
+    tableHeadersForPano() {
+      return this.$MAPTABLEHEADER['pano'].filter(item => {
+        return ['icon', 'name', 'fileSize'].includes(item.key)
+      })
+    },
+    tableHeadersForAudio() {
+      console.log(this.$MAPTABLEHEADER);
+      return this.$MAPTABLEHEADER['audio'].filter(item => {
+        return ['ossPath', 'name', 'fileSize'].includes(item.key)
+      })
+    },
+  },
+  data () {
+    return {
+      imageList: [],
+      panoList: [],
+      audioList: [],
+      
+      select: [],
+      searchKey:'', // 搜索关键词
+      latestUsedSearchKey: '',
+
+      currentMaterialType: this.initialMaterialType,
+      
+      isRequestingMoreImageData: false,
+      isRequestingMorePanoData: false,
+      isRequestingMoreAudioData: false,
+      hasMoreImageData: true,
+      hasMorePanoData: true,
+      hasMoreAudioData: true,
+    }
+  },
+
+  methods: {
+    selectItem(item, e) {
+      if (item.isUse == '1') {
+        e.target.checked = false
+        this.$alert({content:'选中素材不能超过600kb'})
+      } else {
+        if (e.target.checked) {
+          this.select = [item]
+        } else {
+          this.select = []
+        }
+      }
+    },
+    requestMoreImageData() {
+      this.isRequestingMoreImageData = true
+      const latestUsedSearchKey = this.searchKey
+      getMaterialList(
+        {
+          pageNum: Math.floor(this.imageList.length / config.PAGE_SIZE) + 1,
+          pageSize: config.PAGE_SIZE,
+          searchKey: this.searchKey,
+          type: 'image',
+        },
+        (data) => {
+          const newData = data.data.list.map((i) => {
+            i.isUse = i.fileSize > 600 ? '1' : '0'
+            i.fileSize = changeByteUnit(Number(i.fileSize));
+            i.createTime = i.createTime.substring(0, i.createTime.length - 3)
+            i.updateTime = i.updateTime.substring(0, i.updateTime.length - 3)
+            return i;
+          });
+          this.imageList = this.imageList.concat(newData)
+          if (this.imageList.length === data.data.total) {
+            this.hasMoreImageData = false
+          }
+          this.isRequestingMoreImageData = false
+          this.latestUsedSearchKey = latestUsedSearchKey
+        },
+        () => {
+          this.isRequestingMoreImageData = false
+          this.latestUsedSearchKey = latestUsedSearchKey
+        }
+      );
+    },
+    requestMorePanoData() {
+      this.isRequestingMorePanoData = true
+      const latestUsedSearchKey = this.searchKey
+      getMaterialList(
+        {
+          pageNum: Math.floor(this.panoList.length / config.PAGE_SIZE) + 1,
+          pageSize: config.PAGE_SIZE,
+          searchKey: this.searchKey,
+          type: 'pano',
+        },
+        (data) => {
+          const newData = data.data.list.map((i) => {
+            i.isUse = i.fileSize > 600 ? '1' : '0'
+            i.fileSize = changeByteUnit(Number(i.fileSize));
+            i.createTime = i.createTime.substring(0, i.createTime.length - 3)
+            i.updateTime = i.updateTime.substring(0, i.updateTime.length - 3)
+            return i;
+          });
+          this.panoList = this.panoList.concat(newData)
+          if (this.panoList.length === data.data.total) {
+            this.hasMorePanoData = false
+          }
+          this.isRequestingMorePanoData = false
+          this.latestUsedSearchKey = latestUsedSearchKey
+        },
+        () => {
+          this.isRequestingMorePanoData = false
+          this.latestUsedSearchKey = latestUsedSearchKey
+        }
+      );
+    },
+    requestMoreAudioData() {
+      this.isRequestingMoreAudioData = true
+      const latestUsedSearchKey = this.searchKey
+      getMaterialList(
+        {
+          pageNum: Math.floor(this.audioList.length / config.PAGE_SIZE) + 1,
+          pageSize: config.PAGE_SIZE,
+          searchKey: this.searchKey,
+          type: 'audio',
+        },
+        (data) => {
+          const newData = data.data.list.map((i) => {
+            i.fileSize = changeByteUnit(Number(i.fileSize));
+            i.createTime = i.createTime.substring(0, i.createTime.length - 3)
+            i.updateTime = i.updateTime.substring(0, i.updateTime.length - 3)
+            return i;
+          });
+          this.audioList = this.audioList.concat(newData)
+          if (this.audioList.length === data.data.total) {
+            this.hasMoreAudioData = false
+          }
+          this.isRequestingMoreAudioData = false
+          this.latestUsedSearchKey = latestUsedSearchKey
+        },
+        () => {
+          this.isRequestingMoreAudioData = false
+          this.latestUsedSearchKey = latestUsedSearchKey
+        }
+      );
+    },
+    refreshMaterialList: debounce(function(type) {
+      if (type === 'image') {
+        this.isRequestingMoreImageData = false
+        this.hasMoreImageData = true
+        this.imageList = []
+        this.requestMoreImageData()
+      } else if (type === 'pano') {
+        this.isRequestingMorePanoData = false
+        this.hasMorePanoData = true
+        this.panoList = []
+        this.requestMorePanoData()
+      } else if (type === 'audio') {
+        this.isRequestingMoreAudioData = false
+        this.hasMoreAudioData = true
+        this.audioList = []
+        this.requestMoreAudioData()
+      }
+    }, 700, false),
+  },
+  mounted() {
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.ellipsis{
+  text-overflow: ellipsis;
+  overflow: hidden;
+  white-space: nowrap;
+  width: 100%;
+  display: inline-block;
+}
+
+.table-select {
+  position: absolute;
+  z-index: 3;
+  left: 50%;
+  top: 50%;
+  transform: translateX(-50%) translateY(-50%);
+  width: 600px;
+  height: 730px;
+  background: #1A1B1D;
+  border-radius: 4px;
+  border: 1px solid #404040;
+  padding: 26px;
+}
+
+.title {
+  font-size: 18px;
+  color: rgba(255, 255, 255, 0.6);
+}
+
+.close-btn {
+  display: inline-block;
+  position: absolute;
+  top: 26px;
+  right: 20px;
+  font-size: 12px;
+  color: #969799;
+  cursor: pointer;
+  padding: 6px;
+}
+
+.material-tab {
+  margin-top: 35px;
+  > .material-tab-item {
+    display: inline-block;
+    margin-right: 20px;
+    position: relative;
+    cursor: pointer;
+    > .text {
+      font-size: 14px;
+      font-family: MicrosoftYaHei;
+      color: rgba(255, 255, 255, 0.6);
+    }
+    > .bottom-line {
+      position: absolute;
+      left: 50%;
+      transform: translateX(-50%);
+      bottom: -4px;
+      width: 16px;
+      height: 2px;
+      background: #0076F6;
+      border-radius: 1px;
+    }
+  }
+}
+
+.filter {
+  margin-top: 28px;
+  width: 100%;
+  height: 36px;
+  background: #252526;
+  border-radius: 2px;
+  border: 1px solid #404040;
+  position: relative;
+  > input {
+    box-sizing: border-box;
+    width: calc(100% - 42px);
+    height: 100%;
+    border: none;
+    padding-left: 16px;
+    background: transparent;
+    color: #fff;
+    outline: none;
+  }
+  > .search-icon {
+    position: absolute;
+    top: 50%;
+    transform: translateY(-50%);
+    right: 18px;
+    color: #404040;
+    font-size: 20px;
+  }
+  > .clear-icon {
+    position: absolute;
+    top: 50%;
+    transform: translateY(-50%);
+    right: 18px;
+    color: #404040;
+    font-size: 20px;
+    cursor: pointer;
+  }
+}
+
+@table-height: 440px;
+@table-head-row-height: 40px;
+@table-border-size: 1px;
+
+.table {
+  margin-top: 20px;
+  border: @table-border-size solid #404040;
+  background: #1A1B1D;
+  width: 100%;
+  height: @table-height;
+  > .table-head-row {
+    width: 100%;
+    height: @table-head-row-height;
+    background: #252526;
+    color: rgba(255, 255, 255, 0.6);
+    .table-head {
+      font-size: 16px;
+      line-height: @table-head-row-height;
+      height: 100%;
+      display: inline-block;
+    }
+  }
+  > .table-body {
+    height: calc(@table-height - @table-head-row-height - @table-border-size - @table-border-size);
+    overflow: auto;
+    display: inline-block;
+    width: 100%;
+    > .table-body-row {
+      height: 50px;
+      border-bottom: 1px solid #404040;
+      display: flex;
+      align-items: center;
+      > .table-data {
+        font-size:14px;
+        line-height:50px;
+        height: 100%;
+        color: #fff;
+        > .list-img {
+          position: relative;
+          height: 100%;
+          display: inline-block;
+          width: 100%;
+          > img {
+            position: absolute;
+            top: 50%;
+            transform: translateY(-50%);
+            width: 40px;
+            height: 40px;
+            object-fit: cover;
+          }
+        }
+      }
+    }
+  }
+  > .no-data {
+    height: calc(@table-height - @table-head-row-height - @table-border-size - @table-border-size);
+    width: 100%;
+    position: relative;
+    > div {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      text-align: center;
+      > img {
+        width: 116px;
+      }
+      > span {
+        margin-top: 20px;
+        display: block;
+        font-size: 14px;
+        color: rgba(255, 255, 255, 0.6);
+      }
+    }
+  }
+}
+
+.table-image .table-head,
+.table-image .table-data {
+  &:nth-of-type(1) {
+    width: 50px;
+    color: transparent;
+  }
+  &:nth-of-type(2) {
+    width: calc(116px - 50px);
+  }
+  &:nth-of-type(3) {
+    width: calc(316px - 116px);
+    padding-right: 30px;
+  }
+  &:nth-of-type(4) {
+    width: calc(416px - 316px);
+  }
+  &:nth-of-type(5) {
+    width: calc(100% - 416px);
+  }
+}
+
+.table-pano .table-head,
+.table-pano .table-data {
+  &:nth-of-type(1) {
+    width: 50px;
+    color: transparent;
+  }
+  &:nth-of-type(2) {
+    width: calc(116px - 50px);
+  }
+  &:nth-of-type(3) {
+    width: calc(416px - 116px);
+    padding-right: 30px;
+  }
+  &:nth-of-type(4) {
+    width: calc(100% - 416px);
+  }
+}
+
+.table-audio .table-head,
+.table-audio .table-data {
+  &:nth-of-type(1) {
+    width: 50px;
+    color: transparent;
+  }
+  &:nth-of-type(2) {
+    width: calc(116px - 50px);
+  }
+  &:nth-of-type(3) {
+    width: calc(416px - 116px);
+    padding-right: 30px;
+  }
+  &:nth-of-type(4) {
+    width: calc(100% - 416px);
+  }
+}
+
+.checkbox {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  input {
+    width: 20px;
+    height: 20px;
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    cursor: pointer;
+    opacity: 0;
+  }
+  .for-outer-circle {
+    width: 16px;
+    height: 16px;
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    border-radius: 50%;
+    border: 1px solid #404040;
+    pointer-events: none;
+  }
+  .for-inner-circle {
+    width: 8px;
+    height: 8px;
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    border-radius: 50%;
+    background: #0076F6;
+    pointer-events: none;
+    opacity: 0;
+  }
+}
+
+.checkbox > input:checked ~ .for-outer-circle {
+  border: 1px solid #0076F6;
+}
+
+.checkbox > input:checked ~ .for-inner-circle {
+  opacity: 1;
+}
+
+.checkbox > input:disabled {
+  cursor: not-allowed;
+}
+
+// .checkbox > input:disabled + span {
+// }
+
+.btns {
+  display: flex;
+  justify-content: space-between;
+  margin-top: 40px;
+  .upload-btn {
+    display: flex;
+    align-items: center;
+    > span {
+      display: inline-block;
+      margin-right: 4px;
+    }
+    i.tool-tip-for-editor {
+      font-size: 12px;
+      transform: scale(0.923) translateY(1px);
+      cursor: default;
+    }
+  }
+  > div {
+    .deepcancel {
+      margin-right: 16px;
+    }
+  }
+}
+</style>

+ 6 - 5
packages/code/src/components/preview/index.vue

@@ -1,6 +1,6 @@
 <template>
     <popup v-if="show" :zIdx="'1100'">
-      <div class="ui-message ui-message-confirm">
+      <div class="ui-message ui-message-confirm dark">
         <div class="ui-message-header">
           <span>全景作品预览</span>
           <span @click="$emit('close')">
@@ -11,7 +11,8 @@
           <iframe :src="ifr" frameborder="0"></iframe>
         </div>
         <div class="ui-message-footer">
-          <button class="ui-button ui-button-rect" @click="copy">复制链接</button>
+          <button class="ui-button deepcancel ui-button-rect" @click="$emit('close')">取消</button>
+          <button class="ui-button submit ui-button-rect" @click="copy">复制链接</button>
           <button class="ui-button submit ui-button-rect" @click="openBlank">新窗口打开</button>
         </div>
       </div>
@@ -22,13 +23,13 @@
 import Popup from "@/components/shared/popup";
 
 export default {
-  // TODO: name不需要了?
   props:['show','ifr','name'],
   components:{
-    Popup
+    Popup,
   },
   data(){
-    return {}
+    return {
+    }
   },
   methods:{
     copy() {

+ 93 - 0
packages/code/src/components/pulldownMenuInEditor.vue

@@ -0,0 +1,93 @@
+<template>
+  <div class="pull-down-menu-in-editor" v-clickoutside="onClickOutside">
+    <button class="menu-cover" @click="isExpand = !isExpand">
+      {{placeholder ? placeholder : value}}
+      <i class="iconfont icon-material_preview_upload_collect" :class="{flip: isExpand}"></i>
+    </button>
+    <div class="menu" v-show="isExpand">
+      <button v-for="(item, index) of valueList" :key="index"
+        @click="onSelect(item)"
+      >
+        {{item}}
+      </button>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    valueList: {
+      type: Array,
+      default: function() {
+        return [
+        '111',
+        '222',
+        ]
+      },
+    },
+    placeholder: {
+      type: String,
+      default: '',
+    },
+    value: {
+      type: String,
+      required: true,
+    }
+  },
+  data() {
+    return {
+      isExpand: false,
+    }
+  },
+  methods: {
+    onClickOutside() {
+      if (this.isExpand) {
+        this.isExpand = false
+      }
+    },
+    onSelect(item) {
+      this.isExpand = false
+      this.$emit('input', item)
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  button {
+    background: #252526;
+    border: 1px solid #404040;
+    height: 36px;
+    color: #fff;
+    width: 100%;
+  }
+.pull-down-menu-in-editor {
+  width: 140px;
+  position: relative;
+  > button.menu-cover {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 16px;
+    cursor: pointer;
+    > .icon-material_preview_upload_collect {
+      font-size: 11px;
+      color: rgba(255, 255, 255, 0.6);
+      border-radius: 2px;
+      &.flip {
+        transform: translateY(-2px) rotate(180deg);
+      }
+    }
+  }
+  > .menu {
+    position: absolute;
+    width: 100%;
+    > button {
+      display: block;
+      border-top: none;
+      cursor: pointer;
+    }
+  }
+}
+</style>

+ 24 - 11
packages/code/src/components/range/index.vue

@@ -52,10 +52,9 @@ export default {
   methods: {
 
     toChangeNum(tmp){
-      
-        let max = [0,0]
+        let fallInRange = [0,0]
         if (this.isLegal) {
-          max = this.gradientArr.find(item=>{
+          fallInRange = this.gradientArr.find(item=>{
             return tmp >= item[0] && tmp < item[1]
           })
         }
@@ -65,8 +64,19 @@ export default {
           return 
         }
 
-        return Number(this.isLegal ? (max ? max[1] : tmp) : tmp)
-
+        if (this.isLegal) {
+          if (fallInRange) {
+            if (tmp >= ((fallInRange[1] + fallInRange[0]) / 2)) {
+              return Number(fallInRange[1])
+            } else {
+              return Number(fallInRange[0])
+            }
+          } else {
+            return Number(tmp)
+          }
+        } else {
+          return Number(tmp)
+        }
     },
     clickHandle(ev) {
       let tmp = this.min + this.len * (ev.offsetX / this.width)
@@ -134,22 +144,25 @@ export default {
   margin: 10px 0;
   position: relative;
   width: 100%;
-  height: 4px;
+  height: 6px;
   cursor: pointer;
-  border: 1px solid #5d5d5d;
+  border-radius: 3px;
+  background: #1A1B1D;
+  border: 1px solid #404040;
 
   >.percentage {
-    height: calc(100% + 2px);
+    height: 100%;
     background-color: @color;
     position: absolute;
-    top: -1px;
+    top: 0;
     left: 0;
+    border-radius: 3px;
   }
 
   >.bar {
     position: absolute;
-    height: 15px;
-    width: 15px;
+    height: 20px;
+    width: 20px;
     background-color: #ffffff;
     position: absolute;
     border-radius: 50%;

+ 37 - 36
packages/code/src/components/rangeItem/index.vue

@@ -10,8 +10,11 @@
       </div>
       <Range :isGradient='current.gradient' :min="current.min" :max="current.max" v-model="current.value" />
       <div class="minmax">
-        <span class="title">{{current.min}}{{current.unit}}<span class="ui-remark" v-if="current.tip"> (不显示)</span></span>
-        <span class="title">{{current.max}}{{current.unit}}</span>
+        <span>
+          {{current.min}}{{current.unit}}
+          <span v-if="current.tip"> (不显示)</span>
+        </span>
+        <span>{{current.max}}{{current.unit}}</span>
       </div>
     </template>
   </div>
@@ -52,45 +55,43 @@ export default {
 </script>
 
 <style lang="less" scoped>
-.item {
-  display: flex;
-  justify-content: space-between;
-  padding-bottom: 4px;
-
-  >span {
-    font-size: 14px;
-  }
-
-  >span:last-child {
-    font-size: 10px;
+.item-layer {
+  color: rgba(255, 255, 255, 0.6);
+  font-size: 14px;
+  .item {
     display: flex;
-    align-items: center;
-    justify-content: center;
+    justify-content: space-between;
+    margin-bottom: 24px;
+    
+    >span:last-child {
+      display: flex;
+      align-items: center;
+      justify-content: center;
 
-    >input {
-      width: 40px;
-      height: 24px;
-      padding: 4px 0;
-      text-align: center;
-      color: #fff;
-      letter-spacing: 0;
-      outline: none;
-      border: 1px solid #737373;
-      background-color: #000;
-      border-radius: 2px;
-      margin-right: 4px;
+      >input {
+        width: 40px;
+        height: 24px;
+        padding: 4px 0;
+        text-align: center;
+        color: #fff;
+        letter-spacing: 0;
+        outline: none;
+        border: 1px solid #737373;
+        background-color: #000;
+        border-radius: 2px;
+        margin-right: 4px;
+      }
     }
   }
-}
 
-.minmax{
-  display: flex;
-  justify-content: space-between;
-  padding-bottom: 4px;
-  .title{
-    font-size: 12px;
-    .ui-remark{
-      display: inline-block;
+  .minmax{
+    margin-top: 24px;
+    display: flex;
+    justify-content: space-between;
+    .title{
+      .ui-remark{
+        display: inline-block;
+      }
     }
   }
 }

+ 344 - 0
packages/code/src/components/sceneGroupInEditor.vue

@@ -0,0 +1,344 @@
+<template>
+  <div class="scene-group">
+    <div
+      class="top-bar"
+      :class="isConfirmingDeletion ? '' : 'show-icons-on-hover'"
+      @click="onClickTopBar"
+      :style="{
+        paddingLeft: topBarPaddingLeft,
+      }"
+    >
+      <i class="iconfont icon-edit_input_arrow icon-expand" :class="isExpanded ? '' : 'collapsed'"></i>
+      <i v-show="isExpanded" class="iconfont icon-editor_folder_on folder_expalded"></i>
+      <i v-show="!isExpanded" class="iconfont icon-editor_folder_off folder_collapsed"></i>
+      <template v-if="!isRenaming">
+        <span class="group-name" v-title="groupNode.name">{{groupNode.name}}</span>
+        <i v-show="level === 1"
+          class="iconfont icon-editor_list_add icon-add"
+          v-tooltip="'新增二级分组'"
+          @click="onRequestForAddGroup"
+        >
+        </i>
+        <i
+          class="iconfont icon-editor_list_image icon-image"
+          v-tooltip="'新增全景图或三维场景'"
+        >
+        </i>
+        <i
+          class="iconfont icon-editor_list_edit icon-edit"
+          v-tooltip="'重命名'"
+          @click="onClickForRename"
+        >
+        </i>
+        <i
+          class="iconfont icon-editor_list_delete icon-delete"
+          v-tooltip="'删除'"
+          @click.stop="onRequestForDelete"
+        >
+        </i>
+        <div class="deletion-confirm-wrap">
+          <div class="deletion-confirm" :class="isConfirmingDeletion ? 'show' : 'hide'"
+            v-clickoutside="onRequestForCancelDelete"
+            @click.stop="onConfirmDelete"
+          >
+            删除
+          </div>
+        </div>
+      </template>
+      <input v-if="isRenaming" class="group-title-input" v-model="newName"
+        ref="input-for-rename"
+        maxlength="50"
+        placeholder="输入名字"
+        @blur="onInputNewNameComplete"
+        @keydown.enter="onInputEnter"
+      />
+    </div>
+
+    <div class="group-content" v-if="isExpanded">
+      <template v-if="!(groupNode.children.length === 1 && groupNode.children[0].name === '默认二级分组')">
+        <div
+          v-for="(item) of groupNode.children"
+          :key=item.id
+        >
+          <component
+            :is="'SceneGroup'"
+            v-if="!item.type"
+            :groupNode="item"
+            :level="level + 1"
+            @renameScene="onRenameScene"
+            @deleteScene="onDeleteScene"
+            @renameGroup="onInnerGroupRename"
+            @deleteGroup="onInnerGroupConfirmDelete"
+          />
+          <SceneInGroupInEditor
+            v-else
+            :style="{
+              paddingLeft: sceneItemPaddingLeft,
+            }"
+            :sceneInfo="item"
+            @rename="onRenameScene"
+            @delete="onDeleteScene"
+          />
+        </div>
+      </template>
+      <template v-else>
+        <!-- 自动生成的默认二级分组不显示,里边的内容显示成直属于一级分组的效果。 -->
+        <SceneInGroupInEditor
+          v-for="(item) of groupNode.children[0].children"
+          :key=item.id
+          :style="{
+            paddingLeft: sceneItemPaddingLeft,
+          }"
+          :sceneInfo="item"
+          @rename="onRenameScene"
+          @delete="onDeleteScene"
+        />
+      </template>
+    </div>
+  </div>
+</template>
+
+<script>
+import SceneInGroupInEditor from "@/components/sceneInGroupInEditor.vue";
+
+export default {
+  name: 'SceneGroup',
+  components: {
+    SceneInGroupInEditor,
+  },
+  props: {
+    groupNode: {
+      type: Object,
+      required: true,
+    },
+    level: {
+      type: Number,
+      default: 1,
+    }
+  },
+  data() {
+    return {
+      isExpanded: false,
+      isRenaming: false,
+      newName: '',
+      isConfirmingDeletion: false,
+    }
+  },
+  computed: {
+    topBarPaddingLeft() {
+      return 12 + (this.level - 1) * 12 + 'px' 
+    },
+    sceneItemPaddingLeft() {
+      return 18 + this.level * 12 + 'px' 
+    },
+  },
+  methods: {
+    onClickTopBar() {
+      if (this.isConfirmingDeletion) {
+        return
+      }
+      this.isExpanded = !this.isExpanded
+      if (this.isExpanded) {
+        this.$bus.emit('scene-group-expanded', this.groupNode.id, this.level)
+      }
+    },
+    onOtherSceneGroupExpanded(id, level) {
+      if (id !== this.groupNode.id && level === this.level) {
+        this.isExpanded = false
+      }
+    },
+
+    onRenameScene(...params) {
+      this.$emit('renameScene', ...params)
+    },
+    onDeleteScene(...params) {
+      this.$emit('deleteScene', ...params)
+    },
+
+    onRequestForAddGroup() {
+      this.$emit('addGroup', this.groupNode.id)
+    },
+
+    onClickForRename() {
+      this.isRenaming = true
+      this.newName = this.groupNode.name
+      this.$nextTick(() => {
+        this.$refs['input-for-rename'].focus()
+      })
+    },
+    onInputNewNameComplete() {
+      this.isRenaming = false
+      this.$emit('renameGroup', this.groupNode.id, this.level, this.newName)
+      this.newName = ''
+    },
+    onInputEnter() {
+      this.isRenaming = false // 会导致input blur,进而触发onInputNewNameComplete
+    },
+    onInnerGroupRename(...params) {
+      this.$emit('renameGroup', ...params)
+    },
+
+    onRequestForDelete() {
+      this.isConfirmingDeletion = true
+    },
+    onRequestForCancelDelete() {
+      if (!this.isConfirmingDeletion) {
+        return
+      }
+      // 先保持isConfirmingDeletion不变,因为onClickTopBar可能要用到
+      setTimeout(() => {
+        this.isConfirmingDeletion = false
+      }, 0);
+    },
+    onConfirmDelete() {
+      this.$emit('deleteGroup', this.groupNode.id, this.level)
+      this.isConfirmingDeletion = false
+    },
+    onInnerGroupConfirmDelete(...params) {
+      this.$emit('deleteGroup', ...params)
+    }
+  },
+  mounted() {
+    this.$bus.on('scene-group-expanded', this.onOtherSceneGroupExpanded)
+  },
+  destroyed() {
+    this.$bus.removeListener('scene-group-expanded', this.onOtherSceneGroupExpanded)
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.scene-group {
+  margin-top: 6px;
+  .top-bar {
+    position: relative;
+    color: rgba(255, 255, 255, 0.6);
+    height: 40px;
+    border-radius: 4px;
+    display: flex;
+    align-items: center;
+    margin-left: -12px;
+    margin-right: -10px;
+    padding-right: 10px;
+    &:hover {
+      background: #313131;
+      > .group-name {
+        width: 120px;
+      }
+    }
+    &.show-icons-on-hover:hover {
+      > .icon-add, .icon-image, .icon-edit, .icon-delete {
+        display: block;
+      }
+    }
+    > .icon-expand {
+      display: inline-block;
+      font-size: 12px;
+      transform: scale(0.7);
+      &.collapsed {
+        display: inline-block;
+        transform: scale(0.7) rotate(-90deg);
+      }
+    }
+    > .folder_expalded {
+      font-size: 16px;
+      margin-left: 7px;
+    }
+    > .folder_collapsed {
+      font-size: 16px;
+      margin-left: 7px;
+    }
+    > .group-name {
+      margin-left: 6px;
+      display: inline-block;
+      text-overflow: ellipsis;
+      overflow: hidden;
+      white-space: nowrap;
+      flex: 1 1 auto;
+    }
+    > .icon-add {
+      margin-left: 12px;
+      display: none;
+      cursor: pointer;
+      &:hover {
+        color: #0076f6;
+      }
+    }
+    > .icon-image {
+      margin-left: 12px;
+      display: none;
+      cursor: pointer;
+      &:hover {
+        color: #0076f6;
+      }
+    }
+    > .icon-edit {
+      margin-left: 12px;
+      display: none;
+      cursor: pointer;
+      &:hover {
+        color: #0076f6;
+      }
+    }
+    > .icon-delete {
+      margin-left: 12px;
+      display: none;
+      cursor: pointer;
+      &:hover {
+        color: #fa5555;
+      }
+    }
+    > .deletion-confirm-wrap {
+      position: absolute;
+      top: 0;
+      bottom: 0;
+      right: 0;
+      width: 44px;
+      overflow: hidden;
+      pointer-events: none;
+      border-top-right-radius: 4px;
+      border-bottom-right-radius: 4px;
+      > .deletion-confirm {
+        position: absolute;
+        top: 0;
+        bottom: 0;
+        width: 100%;
+        background: #FA5555;
+        transition: right 0.3s;
+        cursor: pointer;
+        text-align: center;
+        font-size: 12px;
+        color: #fff;
+        pointer-events: auto;
+        &::after {
+          content: '';
+          height: 100%;
+          vertical-align: middle;
+          display: inline-block;
+        }
+        &.show {
+          right: 0;
+        }
+        &.hide {
+          right: -44px;
+        }
+      }
+    }
+    > .group-title-input {
+      margin-left: 6px;
+      flex: 1 1 auto;
+      width: 1px;
+      height: 30px;
+      background: #1A1B1D;
+      border-radius: 2px;
+      border: 1px solid #404040;
+      color: #fff;
+      font-size: 14px;
+      padding: 0 10px 0 16px;
+      &:focus {
+        border-color: #0076F6;
+      }
+    }
+  }
+}
+</style>

+ 220 - 0
packages/code/src/components/sceneInGroupInEditor.vue

@@ -0,0 +1,220 @@
+<template>
+  <div
+    class="scene-item"
+    :class="isConfirmingDeletion ? '' : 'not-confirming-deletion'"
+  >
+    <img :src="sceneInfo.icon + ossImagePreviewUrlSuffix()" alt="" class="scene-image">
+    <div class="right">
+      <span v-if="!isRenaming" class="scene-title" v-title="sceneInfo.sceneTitle">{{sceneInfo.sceneTitle}}</span>
+      <input v-if="isRenaming" class="scene-title-input" v-model="newName"
+        ref="input-for-rename"
+        maxlength="50"
+        placeholder="输入名字"
+        @blur="onInputNewNameComplete"
+        @keydown.enter="onInputEnter"
+      />
+      <div class="right-bottom">
+        <span class="scene-type">{{translateSceneType(sceneInfo.type)}}</span>
+        <div class="icons">
+          <i class="iconfont icon-editor_list_edit icon-edit" v-tooltip="'重命名'"
+            @click="onRequestForRename"
+          >
+          </i>
+          <i class="iconfont icon-editor_list_delete icon-delete" v-tooltip="'删除'"
+            @click="onRequestForDelete"
+          >
+          </i>
+        </div>
+      </div>
+    </div>
+    <div class="deletion-confirm-wrap">
+      <div class="deletion-confirm" :class="isConfirmingDeletion ? 'show' : 'hide'"
+        v-clickoutside="onRequestForCancelDelete"
+        @click="onConfirmDelete"
+      >
+        删除
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { ossImagePreviewUrlSuffix } from '@/utils/other.js'
+
+export default {
+  name: 'SceneInGroupInEditor', 
+  components: {
+  },
+  props: {
+    sceneInfo: {
+      type: Object,
+      require: true,
+    },
+  },
+  data() {
+    return {
+      isRenaming: false,
+      newName: '',
+      isConfirmingDeletion: false,
+    }
+  },
+  methods: {
+    ossImagePreviewUrlSuffix,
+    translateSceneType(type) {
+      if (type === 'pano') {
+        return '全景'
+      } else {
+        return '三维'
+      }
+    },
+    onRequestForRename() {
+      this.isRenaming = true
+      this.newName = this.sceneInfo.sceneTitle
+      this.$nextTick(() => {
+        this.$refs['input-for-rename'].focus()
+      })
+    },
+    onInputNewNameComplete() {
+      this.isRenaming = false
+      this.$emit('rename', this.sceneInfo.id, this.newName)
+      this.newName = ''
+    },
+    onInputEnter() {
+      this.isRenaming = false // 会导致input blur,进而触发onInputNewNameComplete
+    },
+    onRequestForDelete() {
+      this.isConfirmingDeletion = true
+    },
+    onRequestForCancelDelete() {
+      this.isConfirmingDeletion = false
+    },
+    onConfirmDelete() {
+      this.$emit('delete', this.sceneInfo.id)
+      this.isConfirmingDeletion = false
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.scene-item {
+  position: relative;
+  margin-top: 6px;
+  margin-left: -12px;
+  margin-right: -10px;
+  padding-right: 10px;
+  padding-top: 10px;
+  padding-bottom: 10px;
+  display: flex;
+  border-radius: 4px;
+  &:hover {
+    background: #313131;
+  }
+  &.not-confirming-deletion:hover {
+    .icons {
+      display: block !important;
+    }
+  }
+  .scene-image {
+    flex: 0 0 auto;
+    width: 64px;
+    height: 64px;
+    background: #B0B0B0;
+    border-radius: 2px;
+    object-fit: cover;
+  }
+  .right {
+    margin-left: 10px;
+    width: 0px;
+    flex: 1 1 auto;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    .scene-title {
+      word-break: break-all;
+      display: -webkit-box;
+      -webkit-box-orient: vertical;
+      -webkit-line-clamp: 2;
+      overflow: hidden;
+      font-size: 14px;
+      color: rgba(255, 255, 255, 0.6);
+    }
+    .scene-title-input {
+      height: 30px;
+      background: #1A1B1D;
+      border-radius: 2px;
+      border: 1px solid #404040;
+      color: #fff;
+      font-size: 14px;
+      padding: 0 10px 0 16px;
+      &:focus {
+        border-color: #0076F6;
+      }
+    }
+    .right-bottom {
+      display: flex;
+      justify-content: space-between;
+      align-items: flex-end;
+      .scene-type {
+        color: #0076F6;
+        line-height: 16px;
+      }
+      .icons {
+        display: none;
+        i {
+          font-size: 16px;
+          cursor: pointer;
+          color: rgba(255, 255, 255, 0.6);
+        }
+        .icon-edit {
+          &:hover {
+            color: #0076F6;
+          }
+        }
+        .icon-delete {
+          margin-left: 11px;
+          &:hover {
+            color: #fa5555;
+          }
+        }
+      }
+    }
+  }
+  .deletion-confirm-wrap {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    right: 0;
+    width: 44px;
+    overflow: hidden;
+    pointer-events: none;
+    border-top-right-radius: 4px;
+    border-bottom-right-radius: 4px;
+    > .deletion-confirm {
+      position: absolute;
+      top: 0;
+      bottom: 0;
+      width: 100%;
+      background: #FA5555;
+      transition: right 0.3s;
+      cursor: pointer;
+      text-align: center;
+      font-size: 12px;
+      color: #fff;
+      pointer-events: auto;
+      &::after {
+        content: '';
+        height: 100%;
+        vertical-align: middle;
+        display: inline-block;
+      }
+      &.show {
+        right: 0;
+      }
+      &.hide {
+        right: -44px;
+      }
+    }
+  }
+}
+</style>

+ 62 - 0
packages/code/src/components/selectedImageInEditor.vue

@@ -0,0 +1,62 @@
+<template>
+  <div class="img-wrapper">
+    <img :src="imgSrc || defaultImgSrc" alt="">
+    <div v-if="imgSrc" class="cancel-btn-background" @click="$emit('cancel')">
+      <i class="iconfont icon-pop-ups_shut-down"></i>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    imgSrc: {
+      type: String,
+      default: '',
+    },
+    defaultImgSrc: {
+      type: String,
+      require: true,
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.img-wrapper {
+  flex: 0 0 auto;
+  position: relative;
+  width: 136px;
+  height: 136px;
+  margin-right: 16px;
+  background: #1A1B1D;
+  border-radius: 4px;
+  border: 1px solid #404040;
+  overflow: hidden;
+  > img {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+  }
+  > .cancel-btn-background {
+    width: 52px;
+    height: 52px;
+    position: absolute;
+    top: -28px;
+    right: -28px;
+    background: rgba(0, 0, 0, 0.5);
+    border-radius: 50%;
+    cursor: pointer;
+    &:hover {
+      color: #FA5555;
+    }
+    > i {
+      font-size: 12px;
+      transform: scale(0.8);
+      position: absolute;
+      left: 9px;
+      bottom: 9px;
+    }
+  }
+}
+</style>

+ 8 - 11
packages/code/src/components/shared/Switcher.vue

@@ -26,23 +26,20 @@ export default {
 <style lang="less" scoped>
 .com-switcher {
     position: relative;
-    width: 54px;
+    width: 50px;
     height: 24px;
-    background-color: rgb(0, 0, 0);
-    border: 2px solid #737373;
-    border-radius: 13px;
-    transition: background-color 0.2s;
-    transition-timing-function: linear;
+    background: #1A1B1D;
+    border: 1px solid #404040;
+    border-radius: 12px;
     cursor: pointer;
     > div {
-        top: 1px;
-        width: 18px;
-        height: 18px;
+        width: 22px;
+        height: 22px;
         background-color: #737373;
         border-radius: 11px;
         position: absolute;
-        transition: right 0.2s, color 0.2s;
-        right: 31px;
+        transition: right 0.2s, background-color 0.2s;
+        right: calc(50px - 22px - 2px);
         transition-timing-function: ease-in-out;
     }
     &.active {

+ 1 - 1
packages/code/src/components/shared/popup/index.vue

@@ -9,7 +9,7 @@ export default {
     name: "v-popup-layer",
     props: {
         isPass: Boolean,
-        canClose:Boolean,
+        canClose: Boolean,
         bgColor: String,
         zIdx: String,
     },

+ 1 - 1
packages/code/src/components/userInfo.vue

@@ -34,7 +34,7 @@ export default {
   },
   methods: {
     onClickPersonalCenter() {
-      window.location.href = '/#/information'
+      window.location.href = '/#/navigation'
     },
     onClickLogout() {
       localStorage.setItem('token', '')

+ 1 - 2
packages/code/src/config/index.js

@@ -7,7 +7,7 @@ const config = {
      * 静态资源路径
      */
     staticURL: process.env.VUE_APP_STATIC_DIR,
-    urlFill:  process.env.NODE_ENV === "production" ? process.env.VUE_APP_URL_FILL : "", 
+    urlFill:  process.env.VUE_APP_URL_FILL, 
     thumb:require('@/assets/images/default/img_cover_default.jpg'),
     noresult:require('@/assets/images/icons/img_noresults@2x.png'),
     empty: require('@/assets/images/icons/img_empty@2x.png'),
@@ -16,7 +16,6 @@ const config = {
     hengdaNum: browser.urlQueryValue('h') || '6017118343179540233',
     intranet: process.env.VUE_APP_INNERNET,
     CDN: process.env.VUE_APP_CDN,
-    f4KKUrl: process.env.VUE_APP_PROXY_4DKANKAN_URL,
     panoSetting:{
       angle_of_view:{
         viewSettings:[

+ 3 - 2
packages/code/src/config/menu.js

@@ -1,3 +1,4 @@
+// 前端路由配置
 const PCMenu = [
   {
     text: "基础",
@@ -10,8 +11,8 @@ const PCMenu = [
   {
     text: "导航",
     icon: "navigation",
-    link: "/information",
-    name: "information",
+    link: "/navigation",
+    name: "navigation",
     hidden: false,
     hidescene:true
   },

+ 77 - 0
packages/code/src/directives/vTitleInEditor.js

@@ -0,0 +1,77 @@
+import Vue from 'vue'
+
+Vue.directive('title', {
+  bind: function (el, binding) {
+    if (!binding.value) {
+      return
+    }
+    let timerId = null
+    let isShowTitle = false
+    let titleNode = null
+    el.addEventListener('mousemove', function(e) {
+      if (!isShowTitle) {
+        clearTimeout(timerId)
+        timerId = setTimeout(() => {
+          isShowTitle = true
+
+          titleNode = document.createElement('div')
+
+          titleNode.style.position = 'fixed',
+          titleNode.style.zIndex = 100,
+          titleNode.style.backgroundColor = '#191A1C'
+          titleNode.style.borderRadius = '2px'
+          titleNode.style.border = '1px solid rgba(151, 151, 151, 0.2)'
+          titleNode.style.padding = '2px 6px'
+          titleNode.style.fontSize = '12px'
+          titleNode.style.color = 'rgba(255, 255, 255, 0.6)'
+          titleNode.innerText = binding.value
+          titleNode.style.left = e.clientX + 11 + 'px'
+          titleNode.style.top = e.clientY + 18 + 'px'
+
+          document.body.appendChild(titleNode)
+
+          if (e.clientX + 11 + titleNode.offsetWidth > document.documentElement.clientWidth) {
+            titleNode.style.left = document.documentElement.clientWidth - titleNode.offsetWidth + 'px'
+          }
+          if (e.clientY + 18 + titleNode.offsetHeight > document.documentElement.clientHeight) {
+            titleNode.style.top = document.documentElement.clientHeight - titleNode.offsetHeight + 'px'
+          }
+        }, 500);
+      }
+    }, {
+      passive: false,
+    })
+    el.addEventListener('mouseleave', function () {
+      if (!isShowTitle) {
+        clearTimeout(timerId)
+      } else {
+        isShowTitle = false
+        document.body.removeChild(titleNode)
+      }
+    })
+    el.addEventListener('mousedown', function () {
+      if (!isShowTitle) {
+        clearTimeout(timerId)
+      } else {
+        isShowTitle = false
+        document.body.removeChild(titleNode)
+      }
+    })
+    el.addEventListener('keydown', function () {
+      if (!isShowTitle) {
+        clearTimeout(timerId)
+      } else {
+        isShowTitle = false
+        document.body.removeChild(titleNode)
+      }
+    })
+    el.addEventListener('scroll', function () {
+      if (!isShowTitle) {
+        clearTimeout(timerId)
+      } else {
+        isShowTitle = false
+        document.body.removeChild(titleNode)
+      }
+    })
+  },
+})

+ 81 - 0
packages/code/src/directives/vTooltipInEditor.js

@@ -0,0 +1,81 @@
+import Vue from 'vue'
+import {computePosition, offset, flip, shift, arrow} from '@floating-ui/dom';
+ 
+Vue.directive('tooltip', {
+  bind: function (el, binding) {
+    if (!binding.value) {
+      return
+    }
+    let tooltipNode = null
+    el.addEventListener('mouseenter', function(e) {
+      tooltipNode = document.createElement('div')
+      tooltipNode.style.position = 'fixed'
+      tooltipNode.style.zIndex = 100
+      tooltipNode.style.backgroundColor = '#191A1C'
+      tooltipNode.style.border = '1px solid rgba(151, 151, 151, 0.2)'
+      tooltipNode.style.borderRadius = '3px'
+      tooltipNode.style.border = '1px solid rgba(151, 151, 151, 0.2)'
+      tooltipNode.style.boxShadow = '0px 2px 12px 0px rgba(0, 0, 0, 0.06)'
+      tooltipNode.style.padding = '8px 8px'
+      tooltipNode.style.fontSize = '12px'
+      tooltipNode.style.cursor = 'default'
+      tooltipNode.style.pointerEvents = 'none'
+      tooltipNode.style.wordBreak = 'keep-all'
+      tooltipNode.style.whiteSpace = 'pre'
+      tooltipNode.style.fontSize = '12px'
+      tooltipNode.style.lineHeight = '17px'
+      tooltipNode.style.color = 'rgba(255, 255, 255, 0.6)'
+      tooltipNode.innerText = binding.value
+
+      const arrowNode = document.createElement('div')
+      arrowNode.style.position = 'absolute'
+      arrowNode.style.backgroundColor = 'inherit'
+      arrowNode.style.boxSizing = 'border-box'
+      arrowNode.style.width = '12px'
+      arrowNode.style.height = '12px'
+      arrowNode.style.border = '1px solid transparent'
+      arrowNode.style.borderRight = 'inherit'
+      arrowNode.style.borderBottom = 'inherit'
+      arrowNode.style.transform = 'rotate(45deg)'
+      
+      tooltipNode.appendChild(arrowNode)
+      document.body.appendChild(tooltipNode)
+
+      computePosition(el, tooltipNode, {
+        placement: 'top',
+        middleware: [
+          offset(13),
+          flip(),
+          shift({padding: 12}),
+          arrow({
+            element: arrowNode,
+            padding: 3,
+          }),
+        ],
+      }).then(({x, y, placement, middlewareData}) => {
+        Object.assign(tooltipNode.style, {
+          left: `${x}px`,
+          top: `${y}px`,
+        });
+
+        const {x: arrowX, y: arrowY} = middlewareData.arrow;
+        const staticSide = {
+          top: 'bottom',
+          right: 'left',
+          bottom: 'top',
+          left: 'right',
+        }[placement.split('-')[0]];
+       
+        Object.assign(arrowNode.style, {
+          left: arrowX != null ? `${arrowX}px` : '',
+          [staticSide]: '-6px',
+        });
+      });
+    }, {
+      passive: false,
+    })
+    el.addEventListener('mouseleave', function () {
+      document.body.removeChild(tooltipNode)
+    })
+  },
+})

+ 29 - 0
packages/code/src/framework/EditorAppLayout.vue

@@ -0,0 +1,29 @@
+<template>
+  <div class="app-layout">
+    <app-head class="app-head"></app-head>
+    <app-main></app-main>
+  </div>
+</template>
+<script>
+import AppHead from "./EditorHead.vue";
+import AppMain from "./EditorMain.vue";
+export default {
+  name: "app-layout",
+  components: {
+    AppHead,
+    AppMain
+  }
+};
+</script>
+<style lang="less">
+.app-layout {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  .app-head {
+    flex: 0 0 auto;
+  }
+}
+</style>

+ 277 - 0
packages/code/src/framework/EditorHead.vue

@@ -0,0 +1,277 @@
+<template>
+  <header class="app-head" app-border dir-bottom>
+    <a class="app-head-back" href="./material.html#/works">
+      <i class="iconfont icon-editor_return" ></i>
+      返回我的作品
+    </a>
+    <span class="app-head-title">{{ info.name }}</span>
+    <div class="app-head-save ui-button deepcancel app-head-view" @click="onView" :class="{ disable: !canLoad }">
+      <i class="iconfont iconeditor_preview"></i>
+      预览
+    </div>
+
+    <div
+      class="ui-button submit app-head-save"
+      @click="onSave"
+      :class="{ disable: !canLoad }"
+    >
+      <i class="iconfont iconeditor_save"></i>
+      保存
+    </div>
+    <preview
+      v-if="info"
+      :name="info.name"
+      :show="showPreview"
+      :ifr="`./show.html?id=${info.id}`"
+      @close="showPreview = false"
+    />
+  </header>
+</template>
+<script>
+import { saveWorks, getPanoInfo, checkLogin } from "@/api";
+import { mapGetters } from "vuex";
+// import config from '@/config'
+import preview from "@/components/preview";
+let hhhreg = /\\\\\\\\/g
+
+export default {
+  name: "app-head",
+  data() {
+    return {
+      showPreview: false,
+      canLoad: false,
+    };
+  },
+
+  components: { preview },
+
+  mounted() {
+    this.$bus.on('canLoad',(data)=>{
+      this.canLoad = data
+      if (data) {
+        this.getInfo()
+
+      }
+    }) 
+
+
+  },
+  computed: {
+    ...mapGetters({
+      info: "info",
+      isShow: "isShow",
+    }),
+  },
+  methods: {
+    checkParams() {
+      if (!this.info.name && !this.info.icon && !this.info.description && this.info.scenes.length<=0) {
+        this.$alert({
+          content: "您还未创建任何内容哦",
+          ok: () => {
+            this.$router.push({ path: "/base" });
+          },
+        });
+        return false;
+      }
+      // if (this.info.scenes.length <= 0 && this.isShow) {
+      //   this.$alert({
+      //     content: "至少添加一个场景才保存/预览,请前往“场景导航”添加",
+      //     forceOK: true,
+      //     ok: () => {
+      //       this.$router.push({ path: "/navigation" });
+      //     },
+      //   });
+      //   return false;
+      // }
+      return true;
+    },
+    onView() {
+      if (!this.checkParams()) {
+        return;
+      }
+      this.fixData()
+      this.info.scenes = this.info.scenes.map(item=>{
+        if (typeof item.someData == 'string') {
+          item.someData =  item.someData.replace(hhhreg,'')
+        }
+        return item
+      })
+
+      saveWorks(
+        {
+          password: this.info.password,
+          someData: { ...this.info, status: 1 },
+        },
+        () => {
+          this.$msg.success("保存成功");
+          document.title = this.info.name
+          this.getInfo();
+          this.$store.commit("UpdateIsShowState", true);
+          setTimeout(() => {
+            if (this.info.scenes.length <= 0 && this.isShow) {
+              return this.$alert({
+                content: "至少添加一个场景才可预览,请前往“场景导航”添加",
+              });
+            }
+            this.showPreview = true;
+          }, 500);
+        }
+      );
+    },
+
+    fixData() {
+    //   let tmp = [];
+    //   this.info.scenes.forEach((item) => {
+    //     this.info.catalogs.forEach((sub) => {
+    //       if (item.category == sub.id) {
+    //         tmp.push(sub);
+    //       }
+    //     });
+    //   });
+    //   tmp = this.$unique(tmp)
+    //   this.info.catalogs = tmp;
+    //   let rootmp = [];
+    //   tmp.forEach((item) => {
+    //     this.info.catalogRoot.forEach((sub) => {
+    //       sub.children = this.$unique(sub.children)
+    //       if (sub.children.indexOf(item.id) > -1) {
+    //         rootmp.push(sub);
+    //       }
+    //     });
+    //   });
+    //   rootmp = this.$unique(rootmp)
+
+    //   this.info.catalogRoot = rootmp.map((item) => {
+    //     let temp = [];
+    //     item.children = this.$unique(item.children)
+    //     item.children.forEach((sub) => {
+    //       tmp.forEach((jj) => {
+    //         if (jj.id == sub) {
+    //           temp.push(sub);
+    //         }
+    //       });
+    //     });
+    //     return {
+    //       ...item,
+    //       children: temp,
+    //     };
+    //   });
+    //   this.info.catalogs = tmp
+
+    //   let cid = 'c_'+this.$randomWord(true,8,8)
+
+    //   if (this.info.catalogRoot.length <= 0) {
+    //     this.info.catalogRoot.push({
+    //       id: 'r_'+this.$randomWord(true,8,8),
+    //       name: "全部场景",
+    //       children:[cid]
+    //     });
+    //   }
+
+    //   if (this.info.catalogs.length <= 0) {
+    //     this.info.catalogs.push({
+    //       id: cid,
+    //       name: "默认二级分组",
+    //     });
+    //   }
+
+      if (this.info.firstScene) {
+        this.info.firstScene = this.info.scenes.find(item=>item.sceneCode==this.info.firstScene.sceneCode)
+      }
+
+      this.$store.commit("SetInfo", this.info);
+    },
+
+    onSave() {
+      if (!this.checkParams()) {
+        return;
+      }
+      this.fixData();
+
+
+      this.info.scenes = this.info.scenes.map(item=>{
+        if (typeof item.someData == 'string') {
+          item.someData =  item.someData.replace(hhhreg,'')
+        }
+        return item
+      })
+
+      saveWorks(
+        {
+          password: this.info.password,
+          someData: { ...this.info, status: 1 },
+        },
+        () => {
+          this.$msg.success("保存成功")
+          document.title = this.info.name
+          this.getInfo();
+          this.$store.commit("UpdateIsShowState", true);
+        },
+        () => {}
+      );
+    },
+    getInfo() {
+      checkLogin().then((res) => {
+        if (res.code == 0) {
+          getPanoInfo("", (data) => {
+            this.$store.commit("SetInfo", data);
+            document.title = this.info.name || '无标题'
+          });
+
+        }
+      });
+    },
+  },
+};
+</script>
+<style lang="less">
+.app-head {
+  width: 100%;
+  min-width: 50px;
+  height: 60px;
+  position: relative;
+}
+.app-head-back {
+  color: white;
+  display: flex;
+  align-items: baseline;
+  font-size: 16px;
+  position: absolute;
+  left: 24px;
+  top: 50%;
+  transform: translateY(-50%);
+  > i {
+    margin-right: 15px;
+  }
+}
+.app-head-title {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  transform: translate(-50%, -50%);
+  font-size: 16px;
+  color: white;
+}
+.app-head-save {
+  cursor: pointer;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  position: absolute;
+  top: 50%;
+  transform: translateY(-50%);
+  right: 10px;
+  i {
+    font-size: 14px;
+    margin-right: 4px;
+  }
+}
+
+.ui-preview{
+  background: #313131;
+}
+
+.app-head-view {
+  right: 120px;
+}
+</style>

+ 92 - 0
packages/code/src/framework/EditorMain.vue

@@ -0,0 +1,92 @@
+<template>
+  <!-- 编辑器顶部栏以下部分 -->
+  <main class="app-main">
+    <!-- 左侧菜单栏 -->
+    <app-menu class="app-menu"></app-menu>
+    <!-- 其余 -->
+    <div class="app-content">
+      <!-- 内嵌的全景场景预览区域 -->
+      <div
+        class="app-player"
+        ref="layer"
+        v-show="$route.meta.loadScene"
+      >
+        <Core/>
+      </div>
+      <!-- 初始和热点 底部的 全景场景列表 -->
+      <toolbar v-show="$route.meta.loadScene"></toolbar>
+      <div class="app-view">
+        <keep-alive>
+          <router-view />
+        </keep-alive>
+      </div>
+    </div>
+  </main>
+</template>
+<script>
+import AppMenu from "./MenuPC";
+import Core from "./core";
+import Toolbar from "./Toolbar";
+
+
+export default {
+  name: "editor-main",
+  components: {
+    AppMenu,
+    Core,
+    Toolbar
+  },
+  created() {},
+  async mounted() {},
+  computed: {}
+};
+</script>
+<style lang="less">
+.app-main {
+  display: flex;
+  flex: 1 1 auto;
+  height: 1px;
+  width: 100%;
+  > .app-menu {
+    flex: 0 0 auto;
+  }
+  > .app-content {
+    position: relative;
+    flex: 1 0 auto;
+    height: 100%;
+    display: flex;
+    .app-player {
+      padding: 10px;
+      position: relative;
+      display: flex;
+      flex: 0 0 auto;
+      height: calc(100% - 260px); // 260: 底部toolbar的高度
+      flex-direction: column;
+      &.page-guide {
+        height: calc(100% - 210px);
+      }
+    }
+    .app-view {
+      width: 100%;
+      height: 100%;
+    }
+  }
+}
+
+
+.app-view-toolbar {
+  display: flex;
+  position: fixed !important;
+  left: 58px;
+  right: 236px;
+  bottom: 0;
+  height: 260px;
+  overflow: hidden;
+}
+
+.app-view-full-toolbar {
+  height: 100%;
+  padding: 10px;
+  display: block;
+}
+</style>

+ 1 - 0
packages/code/src/framework/Toolbar.vue

@@ -47,6 +47,7 @@ import tabList from "@/components/tablist";
 let frame;
 let $scroll = null;
 export default {
+  name: 'whatisthis',
   components: {tabList},
   data() {
     return {

+ 124 - 191
packages/code/src/framework/show/index.vue

@@ -1,29 +1,50 @@
 <template>
   <div class="panocon">
     <template v-if="showInfo">
-      <iframe allowfullscreen="true" v-if="activeItem.type == '4dkk'" :src="`/spc.html?m=${activeItem.sceneCode}&lang=zh`" frameborder="0"></iframe>
-      <div v-show="activeItem.type != '4dkk'" id="pano"></div>
+      <iframe allowfullscreen="true" v-if="activeItem.type=='4dkk'" 
+      :src="`/spc.html?m=${activeItem.sceneCode}&lang=zh`" frameborder="0"></iframe>
+      <div v-show="activeItem.type!='4dkk'" id="pano"></div>
 
-      <div class="pano-logo" v-if="showInfo.isLogo && activeItem.type != '4dkk'">
-        <img :src="showInfo.logo || require('@/assets/images/default/img_logoshow@2x.png')" alt="" />
+      <div class="pano-logo" v-if="showInfo.isLogo&&activeItem.type!='4dkk'">
+        <img :src="showInfo.logo||require('@/assets/images/default/img_logoshow@2x.png')" alt="" />
       </div>
 
-      <div class="oper-tips" :class="{ hidetips: !showTips }" v-if="localRemind">
-        <img :src="showInfo.pcIcon || require('@/assets/images/default/show/img_tipspc_default.png')" alt="" />
+
+      <div
+        class="oper-tips"
+        :class="{ hidetips: !showTips }"
+        v-if="localRemind"
+      >
+        <img
+          :src="
+            showInfo.pcIcon ||
+            require('@/assets/images/default/show/img_tipspc_default.png')
+          "
+          alt=""
+        />
       </div>
 
       <list v-if="canLoad" @select="handleSelect" :select="activeItem"></list>
-      <password :bg="showInfo.icon" :show="showPassword" @submit="handlePassword" @close="showPassword = false" />
+      <password
+        :bg="showInfo.icon"
+        :show="showPassword"
+        @submit="handlePassword"
+        @close="showPassword = false"
+      />
       <popup :title="'简介'" :show="showIntro" @close="showIntro = false">
         <div slot="content" class="introcon">
           {{ showInfo.description || "暂无简介" }}
         </div>
       </popup>
 
-      <preview :item="currentHotspot" :show="showPreview" @close="(showPreview = false), (currentHotspot = '')" />
+      <preview
+        :item="currentHotspot"
+        :show="showPreview"
+        @close="showPreview = false"
+      />
 
-      <ul class="aside" v-if="activeItem.type != '4dkk'">
-        <li :class="{ ismuted: item.id == 'bgm' && !isMusic }" v-for="(item, i) in aside" :key="i">
+      <ul class="aside" v-if="activeItem.type!='4dkk'">
+        <li v-for="(item, i) in aside" :key="i">
           <span @click="handleItem(item)">
             <i class="iconfont" :class="item.icon"></i>
           </span>
@@ -32,21 +53,23 @@
     </template>
     <template v-else>
       <div class="hasDel" v-if="loadFinish">
-        <div>
-          <img :src="$noresult" alt="" />
-          <p>作品已被删除</p>
-        </div>
+      <div>
+        <img :src="$noresult" alt="">
+        <p>作品已被删除</p>
       </div>
+    </div>
+
     </template>
   </div>
 </template>
 
 <script>
 import * as krfn from "@/core/index.js";
-import { getPanoInfo, checkPassword, checkWork } from "@/api";
+import { getPanoInfo, checkPassword,checkWork } from "@/api";
 import password from "./popup/password";
 import preview from "./popup/preview";
-import { $smallWaiting } from "@/components/shared/loading";
+import { $smallWaiting } from '@/components/shared/loading'
+
 
 import popup from "./popup/";
 import { mapGetters } from "vuex";
@@ -69,13 +92,11 @@ export default {
   },
   data() {
     return {
-      bgmAudio: "",
-      isMusic: false,
       showPreview: false,
-      isFullscreen: false,
+      isFullscreen:false,
       showTips: false,
       canLoad: false,
-      loadFinish: false,
+      loadFinish:false,
       showPassword: false,
       showIntro: false,
       localRemind: false,
@@ -102,41 +123,37 @@ export default {
   },
   methods: {
     onFullScreen() {
-      let element = document.documentElement;
-      if (this.isFullscreen) {
-        if (document.exitFullscreen) {
-          document.exitFullscreen();
-        } else if (document.webkitCancelFullScreen) {
-          document.webkitCancelFullScreen();
-        } else if (document.mozCancelFullScreen) {
-          document.mozCancelFullScreen();
-        } else if (document.msExitFullscreen) {
-          document.msExitFullscreen();
-        }
-      } else {
-        if (element.requestFullscreen) {
-          element.requestFullscreen();
-        } else if (element.webkitRequestFullScreen) {
-          element.webkitRequestFullScreen();
-        } else if (element.mozRequestFullScreen) {
-          element.mozRequestFullScreen();
-        } else if (element.msRequestFullscreen) {
-          element.msRequestFullscreen();
+        let element = document.documentElement;
+        if (this.isFullscreen) {
+            if (document.exitFullscreen) {
+                document.exitFullscreen();
+            } else if (document.webkitCancelFullScreen) {
+                document.webkitCancelFullScreen();
+            } else if (document.mozCancelFullScreen) {
+                document.mozCancelFullScreen();
+            } else if (document.msExitFullscreen) {
+                document.msExitFullscreen();
+            }
+        } else {
+            if (element.requestFullscreen) {
+                element.requestFullscreen();
+            } else if (element.webkitRequestFullScreen) {
+                element.webkitRequestFullScreen();
+            } else if (element.mozRequestFullScreen) {
+                element.mozRequestFullScreen();
+            } else if (element.msRequestFullscreen) {
+                element.msRequestFullscreen();
+            }
         }
-      }
-      // 改变当前全屏状态
-      this.isFullscreen = !this.isFullscreen;
-    },
+          // 改变当前全屏状态
+        this.isFullscreen = !this.isFullscreen;
+      },
     handleItem(data) {
-      if (data.id == "bgm") {
-        this.isMusic = !this.isMusic;
-        localStorage.setItem("g_bgmstatus", this.isMusic ? "open" : "close");
-      }
       if (data.id == "about") {
         this.showIntro = true;
       }
       if (data.id == "full") {
-        this.onFullScreen();
+        this.onFullScreen()
       }
       if (data.id == "vr") {
         var krpano = document.getElementById("krpanoSWFObject");
@@ -154,7 +171,7 @@ export default {
           if (res.code == 0) {
             this.showPassword = false;
             this.canLoad = true;
-          }
+          } 
         }
       );
     },
@@ -176,13 +193,14 @@ export default {
           }
         });
       });
-      tmp = this.$unique(tmp);
+      tmp = this.$unique(tmp)
 
       this.showInfo.catalogs = tmp;
       let rootmp = [];
       tmp.forEach((item) => {
         this.showInfo.catalogRoot.forEach((sub) => {
-          sub.children = this.$unique(sub.children);
+
+          sub.children = this.$unique(sub.children)
 
           if (sub.children.indexOf(item.id) > -1) {
             rootmp.push(sub);
@@ -190,16 +208,18 @@ export default {
         });
       });
 
-      rootmp = this.$unique(rootmp);
 
-      let sortArr = this.showInfo.catalogRoot.map((item) => item.name);
-      rootmp.sort((a, b) => {
-        return sortArr.indexOf(a.name) - sortArr.indexOf(b.name);
-      });
+      rootmp = this.$unique(rootmp)
+
+      let sortArr = this.showInfo.catalogRoot.map(item=>item.name)
+      rootmp.sort((a,b)=>{
+        return sortArr.indexOf(a.name) - sortArr.indexOf(b.name)
+      })
+
 
       this.showInfo.catalogRoot = rootmp.map((item) => {
         let temp = [];
-        item.children = this.$unique(item.children);
+        item.children = this.$unique(item.children)
         item.children.forEach((sub) => {
           tmp.forEach((jj) => {
             if (jj.id == sub) {
@@ -213,15 +233,15 @@ export default {
         };
       });
 
-      this.showInfo.catalogs = tmp;
+      this.showInfo.catalogs = tmp
 
-      let cid = "c_" + this.$randomWord(true, 8, 8);
+      let cid = 'c_'+this.$randomWord(true,8,8)
 
       if (this.showInfo.catalogRoot.length <= 0) {
         this.showInfo.catalogRoot.push({
-          id: "r_" + this.$randomWord(true, 8, 8),
+          id: 'r_'+this.$randomWord(true,8,8),
           name: "全部场景",
-          children: [cid],
+          children:[cid]
         });
       }
 
@@ -233,39 +253,36 @@ export default {
       }
 
       if (this.showInfo.firstScene) {
-        this.showInfo.firstScene = this.showInfo.scenes.find((item) => item.sceneCode == this.showInfo.firstScene.sceneCode);
+        this.showInfo.firstScene = this.showInfo.scenes.find(item=>item.sceneCode==this.showInfo.firstScene.sceneCode)
       }
 
       this.$store.commit("SetShowInfo", this.showInfo);
     },
 
     getSceneInfo() {
-      checkWork("", (data) => {
+      checkWork("",data=>{
         if (data.data) {
-          getPanoInfo("", (res) => {
-            this.$store.commit("SetShowInfo", res);
-            this.fixData();
-            this.loadFinish = true;
-          });
-        } else {
-          this.loadFinish = true;
+          getPanoInfo(
+            "",
+            (res) => {
+              this.$store.commit("SetShowInfo", res);
+              this.fixData()
+              this.loadFinish = true
+            }
+          );
+        } else{
+          this.loadFinish = true
         }
-      });
-    },
+      })
+    }
   },
   watch: {
-    isMusic: {
-      handler: function(newVal) {
-        newVal ? this.bgmAudio.play() : this.bgmAudio.pause();
-      },
-    },
     currentHotspot: {
       deep: true,
-      handler: function(newVal) {
+      handler: function (newVal) {
         if (newVal) {
           if (newVal.hotspotType == "link") {
             window.open(newVal.hyperlink, "_blank");
-            this.currentHotspot = "";
             return;
           }
           if (newVal.hotspotType == "scene") {
@@ -279,7 +296,7 @@ export default {
     canLoad(newVal) {
       if (newVal) {
         setTimeout(() => {
-          this.showTips = this.localRemind;
+          this.showTips = this.localRemind
           setTimeout(() => {
             this.showTips = false;
           }, this.showInfo.remindTime * 1000);
@@ -289,90 +306,23 @@ export default {
     showInfo: {
       deep: true,
       immediate: true,
-      handler: function(newVal) {
+      handler: function (newVal) {
         if (newVal) {
-          document.title = newVal.name || "无标题";
-          let locoR = "localRemind" + newVal.id;
+          document.title = newVal.name || '无标题'
+          let locoR = "localRemind"+newVal.id
           if (!newVal.description) {
-            this.aside.shift();
-          }
-          if (newVal.bgMusic) {
-            this.aside.push({
-              id: "bgm",
-              icon: "iconbs_nav_sound",
-            });
-            this.bgmAudio = new Audio();
-            this.bgmAudio.src = newVal.bgMusic;
-            this.bgmAudio.loop = true;
-            this.bgmAudio.autoplay = true;
-
-            let playingfn = () => {
-              this.isMusic = true;
-            };
-
-            let pausedingfn = () => {
-              this.isMusic = false;
-            };
-
-            this.$nextTick(() => {
-              setTimeout(() => {
-                if (this.bgmAudio.paused) {
-                  this.isMusic = false;
-                  localStorage.setItem("g_bgmstatus", this.isMusic ? "open" : "close");
-                }
-
-                document.addEventListener(
-                  "WeixinJSBridgeReady",
-                  () => {
-                    this.bgmAudio.play();
-                  },
-                  false
-                );
-              });
-            });
-
-            let onclick = () => {
-              document.removeEventListener("click", onclick);
-              document.removeEventListener("touchstart", onclick);
-              if (this.bgmAudio.paused) {
-                this.bgmAudio.play();
-              }
-            };
-            document.addEventListener("click", onclick);
-            document.addEventListener("touchstart", onclick);
-
-            this.bgmAudio.removeEventListener("playing", playingfn);
-            this.bgmAudio.removeEventListener("paused", pausedingfn);
-
-            this.bgmAudio.addEventListener("paused", pausedingfn);
-            this.bgmAudio.addEventListener("playing", playingfn);
-
-            this.bgmAudio.addEventListener("canplay", () => {
-              this.bgmAudio.play();
-              localStorage.setItem("g_bgmstatus", this.isMusic ? "open" : "close");
-            });
-
-            document.addEventListener("visibilitychange", () => {
-              let data = document.hidden;
-              if (data) {
-                this.isMusic = false;
-              } else {
-                let status = localStorage.getItem("g_bgmstatus");
-                if (status === "open") {
-                  this.isMusic = true;
-                }
-              }
-            });
+            this.aside.shift()
           }
-          if (newVal.isRemind == 1) {
-            this.localRemind = localStorage.getItem(locoR) == 1 ? false : true;
+          if (newVal.isRemind==1) {
+            this.localRemind = localStorage.getItem(locoR) == 1 ? false : true
             localStorage.setItem(locoR, 1);
-          } else {
+          }
+          else{
             this.localRemind = true;
             localStorage.setItem(locoR, 0);
           }
           if (this.showInfo.firstScene) {
-            if (this.showInfo.firstScene.type == "4dkk") {
+            if (this.showInfo.firstScene.type == '4dkk') {
               this.localRemind = false;
             }
           }
@@ -382,19 +332,19 @@ export default {
     },
     activeItem: {
       handler(newVal) {
-        this.currentHotspot = "";
+        this.currentHotspot = ''
         $("#pano").empty();
         window.vrInitFn = () => {
-          $smallWaiting.hide();
+          $smallWaiting.hide()
           var krpano = document.getElementById("krpanoSWFObject");
           __krfn.utils.initHotspot(krpano, newVal && newVal.someData, false);
         };
         window.vrViewFn = () => {
           try {
-            let tmp = newVal.initVisual || {};
+            let tmp = newVal.initVisual||{};
             var krpano = document.getElementById("krpanoSWFObject");
-            krpano.set("view.vlookat", tmp.vlookat || 0);
-            krpano.set("view.hlookat", tmp.hlookat || 0);
+            krpano.set("view.vlookat", tmp.vlookat||0);
+            krpano.set("view.hlookat", tmp.hlookat||0);
             krpano.set("autorotate.enabled", Boolean(this.showInfo.isAuto));
           } catch (error) {
             error;
@@ -405,13 +355,13 @@ export default {
           "events[skin_events].onloadcomplete": "js(window.vrInitFn());",
         };
         if (newVal) {
-          if (newVal.type == "4dkk") {
-            removepano("#pano");
-            $("#pano").empty();
-            return;
+          if (newVal.type=='4dkk') {
+              removepano("#pano");
+              $("#pano").empty();
+              return
           }
           removepano("#pano");
-          $smallWaiting.show();
+          $smallWaiting.show()
 
           embedpano({
             // xml: "%HTMLPATH%/static/template/tour.xml",
@@ -431,8 +381,8 @@ export default {
     window.__krfn = __krfn;
 
     this.$bus.on("clickHotspot", (data) => {
-      let someData = this.activeItem.someData;
-      if (typeof someData == "string") {
+      let someData = this.activeItem.someData
+      if (typeof someData == 'string') {
         someData = JSON.parse(this.activeItem.someData);
       }
       let idx = someData.hotspots.findIndex((item) => item.name == data);
@@ -448,20 +398,20 @@ export default {
 .panocon {
   width: 100%;
   height: 100%;
-  .hasDel {
+  .hasDel{
     background: #fff;
     width: 100%;
     height: 100%;
     position: relative;
-    > div {
+    >div{
       position: absolute;
       top: 50%;
       left: 50%;
-      transform: translate(-50%, -50%);
+      transform: translate(-50%,-50%);
       color: #909090;
       text-align: center;
       font-size: 18px;
-      > p {
+      >p{
         margin-top: 20px;
       }
     }
@@ -470,7 +420,7 @@ export default {
     width: 100%;
     height: 100%;
   }
-  > iframe {
+  >iframe{
     width: 100%;
     height: 100%;
   }
@@ -526,23 +476,6 @@ export default {
           transform: translate(-50%, -50%);
         }
       }
-      &.ismuted {
-        position: relative;
-        &::before {
-          content: "";
-          pointer-events: none;
-          display: inline-block;
-          width: 1px;
-          height: 46%;
-          position: absolute;
-          left: 30%;
-          top: 30%;
-          transform: rotate(-45deg);
-          transform-origin: top;
-          z-index: 99;
-          background: #fff;
-        }
-      }
     }
   }
   .introcon {

+ 1 - 1
packages/code/src/framework/show/popup/index.vue

@@ -15,7 +15,7 @@
 </template>
 
 <script>
-import Popup from "@/components/shared/popup";
+import Popup from "@/components/shared/popup/index.vue";
 
 export default {
   props:['show','title'],

+ 64 - 87
packages/code/src/framework/showMobile/iframe.vue

@@ -1,128 +1,105 @@
 <template>
   <div class="ifrcon">
     <Header :bgmUrl="tempBgm" @toggleBGM="handleBGM" :somedatainfo="somedatainfo" :bgmstatus="bgmstatus" :params="params" />
-    <iframe v-if="!isUpGrade" allowfullscreen="true" ref="iframe" id="showifr" :src="url" frameborder="0" @load="onIframLoad"></iframe>
-    <iframe
-      v-else
-      allowfullscreen="true"
-      ref="iframe"
-      id="showifr"
-      :src="`smg.html?m=${num}`"
-      frameborder="0"
-      @load="onIframLoad"
-    ></iframe>
+    <iframe allowfullscreen="true" ref="iframe" id="showifr" :src="url" frameborder="0" @load="onIframLoad"></iframe>
   </div>
 </template>
 
 <script>
 import Header from "./ui/Show.Header";
-import browser from "@/utils/browser";
+import browser from "@/utils/browser"
 
 export default {
-  props: ["url", "bgmUrl", "somedatainfo","num", "isUpGrade"],
-  components: { Header },
-  data() {
+  props:['url','bgmUrl','somedatainfo'],
+  components:{Header},
+  data(){
     return {
-      params: "",
-      bgmstatus: false,
-    };
+      params:'',
+      bgmstatus: false
+    }
   },
-  computed: {
-    tempBgm() {
-      return this.bgmUrl;
-    },
+  computed:{
+    tempBgm(){
+      return this.bgmUrl
+    }
   },
-  methods: {
-    onIframLoad() {
-      window.onmessage = (e) => {
-
+  methods:{
+    onIframLoad(){
+      window.onmessage = e =>{
         if (e.data.source != "4dage") {
-          return;
-        }
-
-        if (e.data.event == "showTours") {
-          if (e.data.data) {
-            this.$bus.emit('toggleShowList',!e.data.data)
-          }
+            return
         }
 
         if (e.data.event == "guide-rooms") {
-          this.$bus.emit("guideRoomsData", e.data.params);
+          this.$bus.emit('guideRoomsData',e.data.params)
         }
 
-        if (e.data.event == "link-click") {
-          if (
-            e.data.params.url.indexOf("https://www.4dkankan.com/spc.html?") > -1 ||
-            e.data.params.url.indexOf("https://www.4dkankan.com/smobile.html?") > -1 ||
-            e.data.params.url.indexOf("https://test.4dkankan.com/spc.html?") > -1 ||
-            e.data.params.url.indexOf("https://test.4dkankan.com/smobile.html?") > -1
-          ) {
-            let m = browser.urlHasValueFromUrl("m", e.data.params.url);
-            this.$emit("changeUrl", m);
-          } else {
-            this.$emit("otherUrl", e.data.params.url);
-          }
+          if (e.data.event == "link-click") {
+            if (e.data.params.url.indexOf('https://www.4dkankan.com/spc.html?')>-1
+            ||e.data.params.url.indexOf('https://www.4dkankan.com/smobile.html?')>-1
+            ||e.data.params.url.indexOf('https://test.4dkankan.com/spc.html?')>-1
+            ||e.data.params.url.indexOf('https://test.4dkankan.com/smobile.html?')>-1) {
+              let m = browser.urlHasValueFromUrl('m',e.data.params.url)
+              this.$emit('changeUrl',m)
+            } else{
+              this.$emit('otherUrl',e.data.params.url)
+            }
+
         }
 
-        if (e.data.event == "action") {
-          this.$bus.emit("currentMode", e.data.params);
 
+
+
+        if (e.data.event == "action") {
+          this.$bus.emit('currentMode',e.data.params)
+          
           if (e.data.params.type == "playMusic") {
-            console.log(e.data.params.data.status, "e.data");
-            this.bgmstatus = e.data.params.data.status;
+            console.log(e.data.params.data.status,'e.data');
+            this.bgmstatus = e.data.params.data.status
+            
           }
         }
 
         if (e.data.event == "loaded") {
           console.log(e.data.params);
-          this.params = e.data.params;
-          this.$refs.iframe.contentWindow.postMessage(
-            {
+          this.params = e.data.params
+          this.$refs.iframe.contentWindow.postMessage({
               source: "mingyuan",
-              event: "guide-rooms",
-            },
-            "*"
-          );
+              event: 'guide-rooms'
+          },"*")
 
-          this.$refs.iframe.contentWindow.postMessage(
-            {
-              source: "mingyuan",
-              event: "settings",
-              params: {
-                playMusic: true,
-              },
-            },
-            "*"
-          );
+          this.$refs.iframe.contentWindow.postMessage({
+              source : "mingyuan",
+              event:"settings",
+              params:{
+                  playMusic:true
+              }
+          },"*")
         }
-      };
-    },
-    handleBGM(status) {
-      this.$refs.iframe.contentWindow.postMessage(
-        {
-          source: "mingyuan",
-          event: "settings",
-          params: {
-            playMusic: status,
-          },
-        },
-        "*"
-      );
+
+      }
     },
-  },
-  mounted() {
-    console.log(this.somedatainfo);
-  },
-};
+    handleBGM(status){
+
+       this.$refs.iframe.contentWindow.postMessage({
+            source : "mingyuan",
+            event:"settings",
+            params:{
+                playMusic:status
+            }
+        },"*")
+    }
+  }
+}
 </script>
 
 <style lang="less" scoped>
-.ifrcon {
+.ifrcon{
   width: 100%;
   height: 100%;
-  > iframe {
+  >iframe{
     width: 100%;
     height: 100%;
   }
 }
-</style>
+</style>

+ 9 - 121
packages/code/src/framework/showMobile/index.vue

@@ -5,10 +5,8 @@
         v-if="activeItem.type == '4dkk'"
         @changeUrl="handleChange"
         @otherUrl="handleOther"
-        :isUpGrade="isUpGrade"
         :bgmUrl="bgmUrl"
         :somedatainfo="somedatainfo"
-        :num="activeItem.sceneCode"
         :key="embeM || activeItem.sceneCode"
         :url="otherLink ? otherLink : `/embed.html?from=mingyuan&m=${embeM || activeItem.sceneCode}&lang=zh&scene-link=1&rnd=${rnd}`"
       />
@@ -23,7 +21,7 @@
       <list v-if="canLoad && !isVR" @select="handleSelect" :firstScene="firstScene" :select="activeItem" :mapvisit="mapvisit"></list>
 
       <ul class="aside" v-show="activeItem.type != '4dkk'">
-        <li :class="{ ismuted: item.id == 'bgm' && !isMusic }" v-for="(item, i) in aside" :key="i">
+        <li v-for="(item, i) in aside" :key="i">
           <span @click="handleItem(item)">
             <i class="iconfont" :class="item.icon"></i>
           </span>
@@ -33,16 +31,16 @@
         <img :src="showInfo.appIcon || require('@/assets/images/default/show/img_tipsmb_default.png')" alt="" />
       </div>
       <password :bg="showInfo.icon" :show="showPassword" @submit="handlePassword" @close="showPassword = false" />
-      <popup :title="'简介'" :show="showIntro" @close="(showIntro = false), (currentHotspot = '')">
+      <popup :title="'简介'" :show="showIntro" @close="showIntro = false">
         <div slot="content" class="introcon">
           <span>{{ showInfo.description || "暂无简介" }}</span>
         </div>
       </popup>
-      <imgview @close="(showImage = false), (currentHotspot = '')" v-if="showImage" :image="currentHotspot.image" />
+      <imgview @close="showImage = false" v-if="showImage" :image="currentHotspot.image" />
 
-      <preview :item="currentHotspot" :show="showPreview" @close="(showPreview = false), (currentHotspot = '')" />
+      <preview :item="currentHotspot" :show="showPreview" @close="showPreview = false" />
 
-      <popup :title="currentHotspot.hotspotTitle" :show="showTextarea" @close="(showTextarea = false), (currentHotspot = '')">
+      <popup :title="currentHotspot.hotspotTitle" :show="showTextarea" @close="showTextarea = false">
         <div slot="content" class="introcon">
           <span>{{ currentHotspot.textarea }}</span>
         </div>
@@ -122,13 +120,10 @@ export default {
   },
   data() {
     return {
-      bgmAudio: "",
-      isMusic: false,
       mapvisit: 0,
       bgmUrl: "",
-      somedatainfo: "",
+      somedatainfo:'',
       isVR: false,
-      isUpGrade:0,
       localRemind: false,
       showPreview: false,
       audioUrl: "",
@@ -171,14 +166,9 @@ export default {
       this.rnd = Math.random();
     },
     getSceneInfomation() {
-      this.isUpGrade = 0
       getSceneInfomation({ id: this.activeItem.sceneCode }, (data) => {
-        if (data.data.isUpgrade) {
-          this.isUpGrade = data.data.isUpgrade
-          return
-        }
         this.mapvisit = data.data.mapVisi;
-        this.somedatainfo = data.data;
+        this.somedatainfo = data.data
         if (data.data.bgMusic) {
           if (bgmMap[data.data.bgMusic]) {
             this.bgmUrl =
@@ -187,9 +177,10 @@ export default {
                 : `https://4dkk.4dage.com/v3-test/audio/${bgmMap[data.data.bgMusic]}`;
           }
 
-          if (data.data.bgMusic == "user") {
+          if (data.data.bgMusic == 'user') {
             this.bgmUrl = data.data.bgMusic ? `https://4dkk.4dage.com/images/images${data.data.num}/${data.data.bgMusicName}` : "";
           }
+
         }
       });
     },
@@ -197,10 +188,6 @@ export default {
       this.audioUrl = "";
     },
     handleItem(data) {
-      if (data.id == "bgm") {
-        this.isMusic=!this.isMusic
-        localStorage.setItem("g_bgmstatus", this.isMusic ? "open" : "close");
-      }
       if (data.id == "about") {
         this.showIntro = true;
       }
@@ -320,11 +307,6 @@ export default {
     },
   },
   watch: {
-     isMusic:{
-      handler: function(newVal) {
-        newVal?this.bgmAudio.play():this.bgmAudio.pause()
-      }
-    },
     currentHotspot: {
       deep: true,
       handler: function(newVal) {
@@ -332,7 +314,6 @@ export default {
           this.audioUrl = "";
           if (newVal.hotspotType == "link") {
             window.open(newVal.hyperlink, "_blank");
-            this.currentHotspot = "";
             return;
           }
           if (newVal.hotspotType == "scene") {
@@ -349,7 +330,6 @@ export default {
           }
           if (newVal.hotspotType == "audio") {
             this.audioUrl = newVal;
-            this.currentHotspot = "";
             return;
           }
 
@@ -378,79 +358,6 @@ export default {
           if (!newVal.description) {
             this.aside.shift();
           }
-          if (newVal.bgMusic) {
-            this.aside.push({
-              id: "bgm",
-              icon: "iconbs_nav_sound",
-            });
-            this.bgmAudio = new Audio();
-            this.bgmAudio.src = newVal.bgMusic;
-            this.bgmAudio.loop = true;
-            this.bgmAudio.autoplay = true;
-
-            let playingfn = () => {
-              this.isMusic = true;
-            };
-
-            let pausedingfn = () => {
-              this.isMusic = false;
-            };
-
-            this.$nextTick(() => {
-              setTimeout(() => {
-                if (this.bgmAudio.paused) {
-                  this.isMusic = false;
-                  localStorage.setItem("g_bgmstatus", this.isMusic ? "open" : "close");
-                }
-
-                document.addEventListener(
-                  "WeixinJSBridgeReady",
-                  () => {
-                    this.bgmAudio.play();
-                  },
-                  false
-                );
-              });
-            });
-
-            let onclick = () => {
-                
-              document.removeEventListener("click", onclick);
-              document.removeEventListener("touchstart", onclick);
-              if (this.bgmAudio.paused) {
-                this.bgmAudio.play();
-              }
-            };
-            document.addEventListener("click", onclick);
-            document.addEventListener("touchstart", onclick);
-
-            this.bgmAudio.removeEventListener("playing", playingfn);
-            this.bgmAudio.removeEventListener("paused", pausedingfn);
-
-            this.bgmAudio.addEventListener("paused", pausedingfn);
-            this.bgmAudio.addEventListener("playing", playingfn);
-
-            this.bgmAudio.addEventListener("canplay", () => {
-              this.bgmAudio.play();
-              localStorage.setItem("g_bgmstatus", this.isMusic ? "open" : "close");
-            });
-
-            document.addEventListener("visibilitychange", () => {
-              let data = document.hidden;
-              if (data) {
-                this.isMusic = false;
-              } else {
-                let status = localStorage.getItem("g_bgmstatus");
-                if (status === "open") {
-                  this.isMusic = true;
-                }
-              }
-            });
-
-            document.addEventListener("WeixinJSBridgeReady", () => {
-              this.bgmAudio.play();
-            },false);
-          }
           if (newVal.isRemind == 1) {
             this.localRemind = localStorage.getItem(locoR) == 1 ? false : true;
             localStorage.setItem(locoR, 1);
@@ -476,13 +383,11 @@ export default {
     activeItem: {
       handler(newVal) {
         this.$nextTick(() => {
-          this.currentHotspot = "";
           if (newVal.type == "4dkk") {
             this.embeM = null;
             this.otherLink = null;
             removepano("#pano");
             $("#pano").empty();
-            console.log('執行------------------------');
             this.getSceneInfomation();
             return;
           } else {
@@ -669,23 +574,6 @@ export default {
           transform: translate(-50%, -50%);
         }
       }
-      &.ismuted {
-        position: relative;
-        &::before {
-          content: "";
-          display: inline-block;
-          width: 1px;
-          height: 46%;
-          position: absolute;
-          left: 30%;
-          top: 30%;
-          transform: rotate(-45deg);
-          transform-origin: top;
-          z-index: 99;
-          pointer-events: none;
-          background: #fff;
-        }
-      }
     }
   }
   .introcon {

+ 268 - 315
packages/code/src/framework/showMobile/list.vue

@@ -1,73 +1,51 @@
 <template>
   <div class="list">
-    <div class="l-con" :class="showList ? 'active' : ''" v-if="!(showInfo.catalogRoot.length == 1 && scenes.length == 1 && showInfo.catalogs.length == 1)">
+     <div class="l-con" :class="showList?'active':''"  v-if="!(showInfo.catalogRoot.length == 1 && scenes.length == 1 && showInfo.catalogs.length == 1)">
       <div class="pic-con">
-        <div class="clip-scroller" ref="sw" v-swiper:mySwiper="swiperOptions" v-if="scenes.length > 0">
+
+        <div class="clip-scroller" ref="sw" v-swiper:mySwiper="swiperOptions"  v-if="scenes.length>0">
           <ul class="pic-list swiper-wrapper">
-            <li class="swiper-slide" @click="handleVR(item, i)" v-for="(item, i) in scenes" :key="i">
-              <div :class="{ active: selected.sceneCode == item.sceneCode }">
-                <img :src="item.icon + `?${Math.random()}`" alt="" />
-                <i class="iconfont" :class="item.type != '4dkk' ? 'icon-edit_type_panorama' : 'icon-edit_type_3d'"></i>
-                <rollName :offset="20" :active="selected.sceneCode == item.sceneCode" class="pic-name" :myref="'subw' + item.id" :name="item.sceneTitle" />
+            <li class="swiper-slide" @click="handleVR(item,i)" v-for="(item,i) in scenes" :key="i">
+              <div :class="{active:selected.sceneCode==item.sceneCode}">
+                <img :src="item.icon+`?${Math.random()}`" alt="" />
+                <i class="iconfont" :class="item.type!='4dkk'?'icon-edit_type_panorama':'icon-edit_type_3d'"></i>
+                <rollName :offset="20" :active="selected.sceneCode==item.sceneCode" class="pic-name" :myref="'subw'+item.id" :name="item.sceneTitle"/>
               </div>
             </li>
           </ul>
         </div>
 
         <div ref="sw1" class="clip-scroller" v-swiper:mySwipera="swiperOptions" v-if="childTab.length > 1">
-          <ul class="tap pp-tap swiper-wrapper">
-            <li class="swiper-slide" @click="handleTabtow(item, i)" v-for="(item, i) in childTab" :key="i">
-              <rollName
-                :offset="30"
-                :mgin="20"
-                :active="item.id == tabtowActive.id"
-                class="btn pic-rect"
-                :class="{ active: item.id == tabtowActive.id }"
-                :myref="'ww' + item.id"
-                :name="item.name"
-              />
-            </li>
-          </ul>
+          
+        <ul class="tap pp-tap swiper-wrapper"  >
+          <li class="swiper-slide" @click="handleTabtow(item,i)" v-for="(item,i) in childTab" :key="i">
+              <rollName :offset="30" :mgin="20" :active="item.id == tabtowActive.id" class="btn pic-rect" :class="{active:item.id == tabtowActive.id}" :myref="'ww'+item.id" :name="item.name"/>
+          </li>
+        </ul>
         </div>
       </div>
-
-      <div class="clip-scroller" ref="sw2" v-swiper:mySwiperb="swiperOptions" v-if="showInfo.catalogRoot.length > 0 && showInfo.catalogs.length > 1">
-        <ul class="tap swiper-wrapper" v-if="showInfo.catalogRoot.length > 1">
-          <li
-            class="swiper-slide"
-            @click="handleTabone(item, i)"
-            :class="{ active: item.id == taboneActive.id }"
-            v-for="(item, i) in showInfo.catalogRoot"
-            :key="i"
-          >
-            <rollName
-              :offset="30"
-              :mgin="20"
-              :active="item.id == taboneActive.id"
-              class="btn rect"
-              :class="{ active: item.id == taboneActive.id }"
-              :myref="'zw' + item.id"
-              :name="item.name"
-            />
+      
+      <div class="clip-scroller" ref="sw2"  v-swiper:mySwiperb="swiperOptions" v-if="showInfo.catalogRoot.length > 0 && showInfo.catalogs.length > 1">
+         <ul class="tap swiper-wrapper" v-if="showInfo.catalogRoot.length > 1">
+          <li class="swiper-slide" @click="handleTabone(item,i)" :class="{ active: item.id == taboneActive.id }" v-for="(item,i) in showInfo.catalogRoot" :key="i">
+              <rollName :offset="30" :mgin="20" :active="item.id == taboneActive.id" class="btn rect" :class="{active:item.id == taboneActive.id}" :myref="'zw'+item.id" :name="item.name"/>
           </li>
         </ul>
       </div>
     </div>
 
-    <div v-if="selected.type == '4dkk' && isShowDL" class="menu">
-      <div @click="handleDL" v-if="isShowDL">
-        <i class="iconfont" :class="isDaolan ? 'icon-show_suspension' : 'icon-show_playback'"></i>
-      </div>
+
+    <div v-if="selected.type=='4dkk' && isShowDL" class="menu">
+        <div  @click="handleDL" v-if="isShowDL">
+            <i class="iconfont" :class="isDaolan?'icon-show_suspension':'icon-show_playback'"></i>
+        </div>
     </div>
 
-    <div
-      class="btn dl"
-      :class="{ deepbg: showList }"
-      v-if="!(showInfo.catalogRoot.length == 1 && scenes.length == 1 && showInfo.catalogs.length == 1)"
-      @click="handleShowList"
-    >
-      <i class="iconfont " :class="showList ? 'icon-nav_scene_down' : 'icon-nav_scene_up'"></i>场景导航
+    <div class="btn dl" :class="{deepbg:showList}" v-if="!(showInfo.catalogRoot.length == 1 && scenes.length == 1 && showInfo.catalogs.length == 1)" @click="showList = !showList">
+      <i class="iconfont " :class="showList?'icon-nav_scene_down':'icon-nav_scene_up'"></i>场景导航
     </div>
+
+  
   </div>
 </template>
 
@@ -80,237 +58,214 @@ import { directive } from "vue-awesome-swiper";
 import "swiper/css/swiper.css";
 
 export default {
-  props: ["select", "mapvisit"],
-  components: { rollName },
+  props:['select','mapvisit'],
+  components:{rollName},
   directives: {
     swiper: directive,
   },
-  data() {
-    let menu = [
-      {
-        id: "pano",
-        name: "漫游",
-        icon: "icon-show_roaming_normal",
-        active: "icon-show_roaming_selected",
-        params: "pano",
-      },
-      {
-        id: "2d",
-        name: "平面",
-        icon: "icon-show_plane_normal",
-        active: "icon-show_plane_selected",
-        params: "2d",
-      },
-      {
-        id: "3d",
-        name: "三维",
-        icon: "icon-show_3d_normal",
-        active: "icon-show_3d_selected",
-        params: "3d",
-      },
-    ];
-    return {
-      isDaolan: false,
-      isShowDL: "",
-      currentActive: "pano",
-      taboneActive: { children: [] },
-      tabtowActive: "",
-      childTab: [],
+  data(){
+    let menu = [{
+      id:'pano',
+      name:'漫游',
+      icon:'icon-show_roaming_normal',
+      active:'icon-show_roaming_selected',
+      params:'pano'
+    },{
+      id:'2d',
+      name:'平面',
+      icon:'icon-show_plane_normal',
+      active:'icon-show_plane_selected',
+      params:'2d'
+    },{
+      id:'3d',
+      name:'三维',
+      icon:'icon-show_3d_normal',
+      active:'icon-show_3d_selected',
+      params:'3d'
+    }]
+    return{
+      isDaolan:false,
+      isShowDL:'',
+      currentActive:'pano',
+      taboneActive:{children:[]},
+      tabtowActive:'',
+      childTab:[],
       sceneNum: config.sceneNum,
-      scenes: [],
+      scenes:[],
       menu,
-      showList: true,
+      showList:true,
       loadFirst: true,
-      shouqi: false,
-      canClick: true,
-    };
+      shouqi:false,
+      canClick:true
+    }
   },
   computed: {
-    ...mapGetters({
-      showInfo: "showInfo",
-    }),
-    swiperOptions() {
-      return {
-        slidesPerView: "auto",
-        // observer:true,
-        // observeParents: true,
-        centeredSlides: true,
-        centerInsufficientSlides: true,
-        centeredSlidesBounds: true,
-        freeMode: true,
-      };
-    },
-    selected() {
-      return { ...this.select };
-    },
-  },
-
-  mounted() {
-    this.$bus.on("guideRoomsData", (data) => {
-      if (data.length > 0) {
-        this.isShowDL = true;
+      ...mapGetters({
+        showInfo: "showInfo"
+      }),
+      swiperOptions() {
+        return {
+          slidesPerView: "auto",
+          // observer:true,
+          // observeParents: true,
+          centeredSlides : true,
+          centerInsufficientSlides: true,
+          centeredSlidesBounds: true,
+          freeMode:true
+        };
+      },
+      selected(){
+        return {...this.select}
       }
-    });
+  },
 
-    this.$bus.on("toggleShowList", (data) => {
-      this.showList = data;
-    });
+  mounted(){
+      this.$bus.on('guideRoomsData',data=>{
+        if (data.length>0) {
+          this.isShowDL = true
+        }
+      })
 
-    this.$bus.on("currentMode", (data) => {
-      this.delHandle();
-      this.currentActive = data.data.mode || "pano";
-      // this.menu.findItem(item=>item.params == data.data.mode)
-    });
+      this.$bus.on('currentMode',data=>{
+        this.delHandle()
+        this.currentActive = data.data.mode || 'pano'
+        // this.menu.findItem(item=>item.params == data.data.mode)
+      })
+      
   },
-  methods: {
-    handleShowList() {
-      this.showList = !this.showList;
-      let ele = document.getElementById("showifr");
-      ele.contentWindow.postMessage(
-        {
-          source: "qjkankan",
-          event: "showList",
-          data: this.showList,
-        },
-        "*"
-      );
-    },
-    handleDL() {
-      this.isDaolan = !this.isDaolan;
-      let ele = document.getElementById("showifr");
-      ele.contentWindow.postMessage(
-        {
+  methods:{
+    handleDL(){
+      this.isDaolan = !this.isDaolan
+      let ele = document.getElementById('showifr');
+      ele.contentWindow.postMessage({
           source: "mingyuan",
-          event: this.isDaolan ? "guide-start" : "guide-pause",
-        },
-        "*"
-      );
+          event: this.isDaolan?'guide-start':'guide-pause',
+      },"*")
     },
-    delHandle() {
-      this.canClick = false;
+    delHandle(){
+      this.canClick = false
       setTimeout(() => {
-        this.canClick = true;
+        this.canClick = true
       }, 1800);
     },
-    handleMenu(data) {
+    handleMenu(data){
       if (this.canClick) {
-        this.currentActive = data.id;
-        let ele = document.getElementById("showifr");
-        ele.contentWindow.postMessage(
-          {
+        this.currentActive = data.id
+        let ele = document.getElementById('showifr');
+        ele.contentWindow.postMessage({
             source: "mingyuan",
-            event: "mode",
+            event: 'mode',
             params: data.params,
-          },
-          "*"
-        );
-        this.delHandle();
+        },"*")
+        this.delHandle()
       }
     },
 
-    fixPosit(ref, i, prenum, nexnum) {
-      if (i !== null) {
-        let acidx = this.$refs[ref].swiper.activeIndex;
-        if (i - acidx > nexnum) {
-          this.$refs[ref].swiper.slideNext();
+     fixPosit(ref,i,prenum,nexnum){
+         if (i!==null) {
+        let acidx = this.$refs[ref].swiper.activeIndex
+        if (i - acidx > nexnum ) {
+          this.$refs[ref].swiper.slideNext()
         }
 
-        if (acidx - i > prenum) {
-          this.$refs[ref].swiper.slidePrev();
+        if (acidx - i > prenum ) {
+          this.$refs[ref].swiper.slidePrev()
         }
       }
     },
 
-    handleTabtow(item, i = null) {
-      this.tabtowActive = item;
-      this.fixPosit("sw1", i, 1, 1);
+    handleTabtow(item,i=null){
+      this.tabtowActive = item
+      this.fixPosit('sw1',i,1,1)
     },
 
-    handleTabone(item, i = null) {
-      this.taboneActive = item;
-      this.fixPosit("sw2", i, 1, 1);
+    handleTabone(item,i=null){
+      this.taboneActive = item
+      this.fixPosit('sw2',i,1,1)
     },
 
-    handleVR(item, i = null) {
+    handleVR(item,i=null){
       history.replaceState(null, null, "".concat(window.location.pathname, "?").concat(`id=${this.showInfo.id}&vr=${item.sceneCode}`));
-      this.sceneNum = item.sceneCode;
+      this.sceneNum = item.sceneCode
 
-      this.fixPosit("sw", i, 1, 1);
-    },
+      this.fixPosit('sw',i,1,1)
+
+    }
   },
-  watch: {
+  watch:{
     taboneActive: {
-      handler: function(newVal) {
-        let temp = [];
-        newVal.children &&
-          newVal.children.forEach((item) => {
-            this.showInfo.catalogs.forEach((sub) => {
-              if (item == sub.id) {
-                temp.push(sub);
-              }
-            });
-          });
-        this.childTab = temp;
+      handler: function (newVal) {
+        let temp = []
+        newVal.children && newVal.children.forEach(item=>{
+          this.showInfo.catalogs.forEach(sub=>{
+            if (item==sub.id) {
+              temp.push(sub)
+            }
+          })
+        })
+        this.childTab = temp
         if (!this.loadFirst) {
-          this.tabtowActive = "";
+            this.tabtowActive = ''
         }
       },
     },
     tabtowActive: {
-      handler: function(newVal) {
+      handler: function (newVal) {
         if (!newVal) {
-          this.tabtowActive = this.childTab[0];
-        } else {
-          let arr = this.showInfo.scenes.filter((item) => {
-            return newVal.id == item.category;
-          });
-          this.scenes = arr.sort((a, b) => a.weight - b.weight);
+          this.tabtowActive = this.childTab[0]
+        } else{
+          let arr = this.showInfo.scenes.filter(item=>{
+            return newVal.id == item.category
+          })
+          this.scenes = arr.sort((a,b)=>a.weight-b.weight)
         }
-      },
+      }
     },
 
-    sceneNum: {
-      deep: true,
-      immediate: true,
-      handler: function(newVal) {
-        if (!newVal) {
-          let tmp = this.showInfo.firstScene || this.showInfo.scenes[0];
-          this.handleVR(tmp);
-          return;
-        }
-        let val = this.showInfo.scenes.find((item) => item.sceneCode == newVal);
-        let tmp = this.showInfo.catalogs.find((item) => item.id == val.category);
-        let rootTmp = this.showInfo.catalogRoot.find((item) => {
-          return item.children.indexOf(tmp.id) > -1;
-        });
-        this.taboneActive = rootTmp;
-        this.tabtowActive = tmp;
+    sceneNum:{
+      deep:true,
+      immediate:true,
+      handler:function (newVal) {
+          if (!newVal) {
+            let tmp = this.showInfo.firstScene||this.showInfo.scenes[0]
+            this.handleVR(tmp)
+            return
+          }
+          let val = this.showInfo.scenes.find(item=>item.sceneCode==newVal)
+          let tmp = this.showInfo.catalogs.find(item=>item.id==val.category)
+          let rootTmp = this.showInfo.catalogRoot.find(item => {
+            return item.children.indexOf(tmp.id)>-1
+          });
+          this.taboneActive = rootTmp
+          this.tabtowActive = tmp
 
         setTimeout(() => {
-          this.$emit("select", val);
-          this.loadFirst = false;
+          this.$emit('select',val)
+          this.loadFirst = false 
         });
-      },
+      }
     },
 
-    selected: {
-      handler: function(newVal) {
-        this.handleVR(newVal);
-        if (newVal.type == "4dkk") {
-          this.showList = true;
+   
+    selected:{
+      handler:function (newVal) {
+        this.handleVR(newVal)
+        if (newVal.type == '4dkk') {
+          this.showList = true
           setTimeout(() => {
-            this.showList = false;
+            this.showList = false
           }, 800);
         }
-      },
-    },
-  },
-};
+      }
+    }
+  }
+}
 </script>
 
 <style lang="less" scoped>
-.btn {
-  display: inline-block;
+
+.btn{
+ display: inline-block;
   margin: 0 auto;
   padding: 0 20px;
   height: 26px;
@@ -324,7 +279,6 @@ export default {
   text-overflow: ellipsis;
   overflow: hidden;
   white-space: nowrap;
-  pointer-events: auto;
   &.active {
     border: 1px solid rgba(255, 255, 255, 1);
     color: rgba(255, 255, 255, 1);
@@ -358,25 +312,25 @@ export default {
   }
 }
 
-.line {
-  background: #0076f6;
+.line{
+  background:#0076F6;
   width: 100%;
   height: 0.1px;
   opacity: 0.5;
 }
 
-.list {
+.list{
   position: fixed;
   bottom: 44px;
   left: 0;
   text-align: right;
   width: 100%;
-  pointer-events: none;
-  .clip-scroller {
+
+  .clip-scroller{
     padding: 0 15px;
   }
 
-  .dl {
+  .dl{
     background: rgba(0, 0, 0, 0.3);
     border-radius: 18px;
     border: 1px solid rgba(255, 255, 255, 0.2);
@@ -391,67 +345,66 @@ export default {
     align-items: center;
     justify-content: center;
     width: 120px;
-    > i {
+    >i{
       margin-right: 8px;
       vertical-align: bottom;
       font-size: 18px;
     }
   }
 
-  .deepbg {
+  .deepbg{
     background: rgba(0, 0, 0, 0.5);
   }
-  .menu {
-    position: fixed;
-    left: 15px;
-    bottom: 40px;
-    pointer-events: auto;
-    > div {
-      width: 36px;
-      height: 36px;
-      display: inline-block;
-      background: rgba(0, 0, 0, 0.5);
-      border: 1px solid rgba(255, 255, 255, 0.2);
-      opacity: 1;
-      border-radius: 18px;
-      position: relative;
-      cursor: pointer;
-      > i {
-        position: absolute;
-        top: 50%;
-        left: 50%;
-        transform: translate(-50%, -50%);
+  .menu{
+      position: fixed;
+      left: 15px;
+      bottom: 40px;
+      >div{
+        width: 36px;
+        height: 36px;
+        display: inline-block;
+        background: rgba(0, 0, 0, 0.5);
+        border: 1px solid rgba(255, 255, 255, 0.2);
+        opacity: 1;
+        border-radius: 18px;
+        position: relative;
+        cursor: pointer;
+        >i{
+          position: absolute;
+          top: 50%;
+          left: 50%;
+          transform: translate(-50%, -50%);
+        }
       }
-    }
   }
-  .l-con {
+  .l-con{
     width: 100%;
     transition: all ease 0.3s;
     max-height: 0px;
-    pointer-events: auto;
     overflow: hidden;
-    .pic-con {
+    .pic-con{
       background: rgba(0, 0, 0, 0.5);
     }
-    ul {
+    ul{
       align-items: center;
       display: flex;
-      li {
+      li{
         margin: 0 5px;
         text-align: center;
+        
       }
     }
-    .tap {
+    .tap{
       padding: 12px 0 0;
-      &::-webkit-scrollbar {
+      &::-webkit-scrollbar{
         display: none;
       }
-      > li {
-        font-size: 0;
+       > li {
+         font-size: 0;
         position: relative;
         width: 104px;
 
-        .btn {
+        .btn{
           font-size: 14px;
           width: 100%;
           padding: 0;
@@ -475,56 +428,56 @@ export default {
       }
     }
 
-    .pp-tap {
+    .pp-tap{
       padding: 8px 0 18px;
-      > li {
-        width: 88px;
+       > li {
+         width: 88px;
       }
     }
-    .pic-list {
+    .pic-list{
       padding: 10px 0;
       &::-webkit-scrollbar {
         display: none;
       }
-      > li {
+      >li{
         cursor: pointer;
         width: 70px;
         height: 70px;
-        > div {
+        >div{
           width: 100%;
           height: 100%;
-          border: 2px solid #ffffff;
+          border: 2px solid #FFFFFF;
           opacity: 1;
           border-radius: 4px;
           overflow: hidden;
           position: relative;
           cursor: pointer;
-          > img {
+          >img{
             position: absolute;
             top: 50%;
             left: 50%;
-            transform: translate(-50%, -50%);
+            transform: translate(-50%,-50%);
           }
-          .iconfont {
+          .iconfont{
             position: absolute;
             left: 4px;
             top: 4px;
             z-index: 99;
-            &::after {
+            &::after{
               background: rgba(0, 0, 0, 0.3);
-              content: "";
+              content: '';
               width: 14px;
               height: 14px;
               display: inline-block;
               position: absolute;
               top: 50%;
               left: 50%;
-              transform: translate(-50%, -50%);
+              transform: translate(-50%,-50%);
               z-index: -1;
               filter: blur(4px);
             }
           }
-          > span {
+          >span{
             display: inline-block;
             background: rgba(0, 0, 0, 0.3);
             position: absolute;
@@ -535,83 +488,83 @@ export default {
             bottom: 0;
             width: 100%;
           }
-          &.active {
-            border: 2px solid #0076f6;
+          &.active{
+              border: 2px solid #0076F6;
           }
         }
-        .pic-name {
+        .pic-name{
           background: rgba(0, 0, 0, 0.3);
           position: absolute;
           left: 0;
           bottom: 0;
           font-size: 0;
-          /deep/ span {
+          /deep/ span{
             font-size: 12px;
             word-break: keep-all;
-            white-space: nowrap;
+            white-space:nowrap;
             padding: 3px 0;
           }
         }
       }
     }
-    &.active {
+    &.active{
       max-height: 200px;
     }
   }
 }
 
 @media screen and (max-width: 350px) {
-  .list {
-    .menu {
-      .menucon {
-        padding-right: 10px;
-        > ul {
-          > li {
+  .list{
+  .menu{
+    .menucon{
+      padding-right: 10px;
+      >ul{
+        >li{
             color: #fff;
             margin: 0;
             height: 36px;
             line-height: 36px;
             padding: 0 4px;
             background: none;
-            > span {
+            >span{
               display: inline-block;
               margin-left: 6px;
             }
-            &:last-of-type {
+            &:last-of-type{
               padding-right: 14px;
             }
           }
-        }
-        > i {
+      }
+      >i{
           font-size: 12px;
           right: 8px;
           top: 50%;
           transform: translateY(-50%);
         }
-      }
+    }
 
-      .shouqi {
-        padding-right: 30px;
-        > ul {
-          max-width: 0;
-          width: 0;
-        }
-        > i {
-          transform: translateY(-50%) rotate(180deg);
-        }
+    .shouqi{
+      padding-right: 30px;
+      >ul{
+        max-width: 0;
+        width: 0;
       }
-      .scenedl {
-        width: 36px;
-        height: 36px;
-        > i {
-          top: 50%;
-          left: 50%;
-          position: absolute;
-          transform: translate(-50%, -50%);
-          font-size: 14px;
-        }
+      >i{
+        transform: translateY(-50%) rotate(180deg);
+      }
+    }
+    .scenedl{
+      width: 36px;
+      height: 36px;
+      >i{
+        top: 50%;
+        left: 50%;
+        position: absolute;
+        transform: translate(-50%,-50%);
+        font-size: 14px;
       }
     }
   }
 }
-</style>
+}
+</style>

+ 2 - 0
packages/code/src/mixins/index.js

@@ -17,6 +17,8 @@ MARERIAL.forEach((item) => {
   MAPTABLEHEADER[item] = require(`@/views/material/${item}/${item}`).data;
 });
 
+console.log('$MAPTABLEHEADER: ', MAPTABLEHEADER);
+
 const MARERIALSTR = {
   image: "图片",
   pano: "全景图",

+ 8 - 4
packages/code/src/pages/Edit.vue

@@ -4,13 +4,14 @@
   </div>
 </template>
 <script>
-import AppLayout from "@/framework";
+import AppLayout from "@/framework/EditorAppLayout.vue";
 
 export default {
   components: {
     AppLayout
-  }
+  },
 };
+
 </script>
 <style lang="less">
 html,
@@ -19,6 +20,10 @@ body,
   width: 100%;
   height: 100%;
   overflow: hidden;
+
+  // 形成层叠上下文
+  position: relative;
+  z-index: 0;
 }
 body {
   margin: 0;
@@ -39,6 +44,5 @@ img {
   outline: 0;
 }
 </style>
-<style lang="less" src="@/assets/style/style.pc.less"></style>
-<style lang="less" src="@/assets/style/common.less"></style>
+<style lang="less" src="@/assets/style/style.pc.edit.less"></style>
 <style src="@/assets/fonts/iconfont.css"></style>

+ 2 - 0
packages/code/src/pages/edit.js

@@ -5,6 +5,8 @@ import router from '../router'
 import store from '../Store'
 import 'viewerjs/dist/viewer.css'
 import Viewer from 'v-viewer'
+import '@/directives/vTitleInEditor.js'
+import '@/directives/vTooltipInEditor.js'
 
 Vue.use(Viewer,{
   defaultOptions: {

+ 1 - 1
packages/code/src/router/index.js

@@ -1,6 +1,6 @@
 import Vue from 'vue'
 import Router from 'vue-router'
-import { PCMenu } from "../config/menu";
+import { PCMenu } from "../config/menu.js";
 import { checkWork, checkLogin,getPanoInfo } from '@/api'
 import { $alert } from '@/components/shared/message'
 import store from '../Store'

+ 59 - 0
packages/code/src/utils/other.js

@@ -56,4 +56,63 @@ export function debounce(fn, delay, isImmediateCall = false) {
       }, delay)
     }
   }
+}
+
+/**
+同时验证⼿机号码和固定电话号码(带区号或不带区号或带分机号)
+
+规则说明:
+1、可以是1开头的11位数字(⼿机号)
+2、可以是“区号-电话号-分机号”或者是“(区号)电话号-分机号”格式
+3、区号是0开头的3~4位数字,可以没有区号
+4、电话号是5~8位数字,不能以0开头
+5、分机号是1~8位数字,可以没有分机号
+
+合法数据⽰例:
+①13812341234
+②010-12345678
+③(0432)1234567-1234
+④12345678
+ */
+export function isValidPhoneNumber(value) {
+  const reg = /^1\d{10}$|^(0\d{2,3}-?|\(0\d{2,3}\))?[1-9]\d{4,7}(-\d{1,8})?$/
+  return reg.test(value)
+}
+
+// 深拷贝
+export function deepClone(target) {
+  // 定义一个变量
+  let result
+  // 如果当前需要深拷贝的是一个对象的话
+  if (typeof target === 'object') {
+    // 如果是一个数组的话
+    if (Array.isArray(target)) {
+      result = [] // 将result赋值为一个数组,并且执行遍历
+      for (let i in target) {
+        // 递归克隆数组中的每一项
+        result.push(deepClone(target[i]))
+      }
+      // 判断如果当前的值是null的话;直接赋值为null
+    } else if (target === null) {
+      result = null
+      // 判断如果当前的值是一个RegExp对象的话,直接赋值    
+    } else if (target.constructor === RegExp) {
+      result = target
+    } else {
+      // 否则是普通对象,直接for in循环,递归赋值对象的所有值
+      result = {}
+      for (let i in target) {
+        result[i] = deepClone(target[i])
+      }
+    }
+    // 如果不是对象的话,就是基本数据类型,那么直接赋值
+  } else {
+    result = target
+  }
+  // 返回最终结果
+  return result
+}
+
+export function ossImagePreviewUrlSuffix(downScaleRate = 10) {
+  return `?x-oss-process=image/resize,p_${downScaleRate}&${Math.random()}`
 }

+ 198 - 267
packages/code/src/views/base/Toolbar.vue

@@ -1,22 +1,13 @@
 <template>
+  <!-- 编辑器-基础-中间部分 -->
   <div class="app-view-toolbar app-view-full-toolbar">
     <div class="main">
       <div class="ui-title-big">基础设置</div>
-      <div class="ui-title"><span class="">封面</span></div>
       <div class="upload-con">
         <div class="uc-l">
           <div class="preview">
-            <img :src="info.icon||$thumb" alt="" />
-          </div>
-          <div class="upload-btn">
-            <button class="ui-button submit" @click="selectHandle('image')">选择图片</button>
-            <button class="ui-button submit" @click="addScene">全景封面
-              <div>
-                <div class="remark">
-                  选择全景图封面做为作品封面
-                </div>
-              </div>
-            </button>
+            <img :src="info.icon || require('@/assets/images/default/img_cover_default_2.png')" alt="" />
+            <button class="ui-button submit setting-cover-btn" @click="onClickSettingCover">设置封面</button>
           </div>
           <div class="ui-remark">512*512px,支持jpg/png格式</div>
         </div>
@@ -24,328 +15,268 @@
           <div class="ui-title">
             <span class="">标题</span>
           </div>
-          <div :class="{ 'ui-warning': false }">
+          <div class="title-input-wrapper">
             <input
               v-model="info.name"
               @blur="$store.commit('SetInfo',info)"
               type="text"
-              class="ui-input"
               maxlength="50"
-              placeholder="作品标题,限50字"
+              placeholder="请输入作品标题"
             />
+            <span class="count">{{titleLength}}/50</span>
           </div>
-          <div class="ui-title" style="margin-top:10px"><span>简介</span></div>
-          <div :class="{ 'ui-warning': false }">
+          <div class="ui-title jianjie"><span>简介</span></div>
+          <div class="jianjie-textarea-wrapper">
             <textarea
               v-model="info.description"
               @blur="$store.commit('SetInfo',info)"
-              style="height:110px;"
               maxlength="500"
-              placeholder="作品简介,限500字"
+              placeholder="请输入作品简介"
               type="text"
-              class="ui-input ui-textarea"
             />
+            <span class="count">{{jianjieLength}}/500</span>
           </div>
         </div>
       </div>
-      <div class="ui-title-big">全局设置</div>
-      <ul class="setting-con">
-        <li @click="activeSetting=item" v-for="(item,i) in settings" :key="i">
-         <button class="ui-button" :class="{submit:activeSetting.id==item.id}">{{item.name}}</button>
+      <menu>
+        <li
+          v-for="(item) in tabs"
+          :key="item"
+          :class="{active: activeTab === item}"
+          @click="activeTab = item"
+        >
+          {{item}}
         </li>
-
-      </ul>
-    </div>
-    <eidt-panel :select="select" @openMaterial="handleOpen" v-if="activeSetting" @close="onCloseEdit" :show="activeSetting"></eidt-panel>
-
-    <div class="dialog" style="z-index: 2000" v-if="isShowSelect">
-      <Table
-        :list="list"
-        :tabHeader="$MAPTABLEHEADER[type]"
-        @updateList="update"
-        @cancle="isShowSelect = false"
-        :title="`选择${$MARERIALSTR[type]}`"
-        @changeCurrent="changeCurrent"
-        :paging="paging"
-        :hideAll="true"
-        @submit="handleSelect"
-      >
-    </Table>
+      </menu>
+      <div class="settings-view-wrapper">
+        <OpeningTipSettings v-show="activeTab === '开场提示'"></OpeningTipSettings>
+        <OpeningAnimationSettings v-show="activeTab === '开场动画'"></OpeningAnimationSettings>
+        <PasswordSettings v-show="activeTab === '访问密码'"></PasswordSettings>
+        <AutoCruiseSettings v-show="activeTab === '自动巡游'"></AutoCruiseSettings>
+        <BackgroundMusicSettings v-show="activeTab === '背景音乐'"></BackgroundMusicSettings>
+        <CustomLogoSettings v-show="activeTab === '自定义LOGO'"></CustomLogoSettings>
+        <CustomMaskSettings v-show="activeTab === '自定义遮罩'"></CustomMaskSettings>
+        <CustomButtonSettings v-show="activeTab === '自定义按钮'"></CustomButtonSettings>
+      </div>
     </div>
 
-    <div class="dialog" style="z-index: 2000" v-if="showScene">
-      <Select
-        @cancle="showScene = false"
-        :title="'选择素材'"
-        @submit="handleSelect"
-      >
-      </Select>
+    <div class="dialog" style="z-index: 2000" v-if="isShowSettingCoverWindow">
+      <MaterialSelectorForEditor
+        title="选择素材"
+        @cancle="isShowSettingCoverWindow = false"
+        @submit="handleSubmitFromTable2"
+      />
     </div>
   </div>
 </template>
 
 <script>
-import { getMaterialList} from "@/api";
-import EidtPanel from "./EditPanel";
-import Table from "@/components/tableSelect";
-import { changeByteUnit } from '@/utils/file'
-
 import { mapGetters } from "vuex";
-import Select from "@/components/select";
+import MaterialSelectorForEditor from "@/components/materialSelectorForEditor.vue";
+import OpeningTipSettings from '@/views/base/openingTipSettings.vue'
+import OpeningAnimationSettings from '@/views/base/openingAnimationSettings.vue'
+import PasswordSettings from "@/views/base/passwordSettings.vue";
+import AutoCruiseSettings from '@/views/base/autoCruiseSettings.vue'
+import BackgroundMusicSettings from "@/views/base/backgroundMusicSettings.vue";
+import CustomLogoSettings from "@/views/base/customLogoSettings.vue";
+import CustomMaskSettings from "@/views/base/customMaskSettings.vue";
+import CustomButtonSettings from "@/views/base/customButtonSettings.vue";
 
 export default {
   components: {
-    EidtPanel,
-    Table,
-    Select
+    MaterialSelectorForEditor,
+    OpeningTipSettings,
+    OpeningAnimationSettings,
+    PasswordSettings,
+    AutoCruiseSettings,
+    BackgroundMusicSettings,
+    CustomLogoSettings,
+    CustomMaskSettings,
+    CustomButtonSettings,
   },
   data() {
     return {
-      type:'',
-      settings:[{
-        name:"开场提示",
-        id:"opening"
-      },{
-        name:"自定义LOGO",
-        id:"logo"
-      },{
-        name:"访问密码",
-        id:"password"
-      },{
-        name:"自动巡游",
-        id:"xy"
-      }],
-      showScene:false,
-      activeSetting:'',
-      dataURL: "",
-      isShowSelect:false,
-      list:[],
-      key:'',
-      clickFrom:'',
-      paging: {
-        pageSize: 8,
-        pageNum: 1,
-        total: 0,
-        showSize: 4,
-        current: 1
-      },
-      select:''
-    };
+      isShowSettingCoverWindow: false,
+      tabs: [
+        '开场提示',
+        '开场动画',
+        '访问密码',
+        '自动巡游',
+        '背景音乐',
+        '自定义LOGO',
+        '自定义遮罩',
+        '自定义按钮',
+      ],
+      activeTab: '开场提示',
+    }
   },
   computed: {
     ...mapGetters({
-      info:'info'
-    })
+      info: 'info'
+    }),
+    titleLength() {
+      return this.info?.name?.length || '0'
+    },
+    jianjieLength() {
+      return this.info?.description?.length || '0'
+    },
   },
-  mounted(){
-
+  mounted() {
   },
-  watch:{
-    "paging.pageNum": function () {
-      this.isShowSelect && this.getMaterialList();
-    },
-    isShowSelect(newVal){
-      if (!newVal) {
-        this.paging.pageNum = 1
-        this.key = ''
-      }
-    }
+  watch: {
   },
   methods: {
-    handleOpen(){
-      this.selectHandle('image','editpanel')
-    },
-    addScene(){
-      this.clickFrom = 'scene'
-      this.showScene = true
-    },
-    changeCurrent(data){
-      this.paging.pageNum = data;
+    onClickSettingCover() {
+      this.isShowSettingCoverWindow = true
     },
-    update(data) {
-      this.key = data;
-      this.getMaterialList();
+    handleSubmitFromTable2(selected) {
+      this.info.icon = selected[0].icon
+      this.isShowSettingCoverWindow = false
     },
-    handleSelect(data){
-      if (this.clickFrom == 'scene') {
-        this.info.icon = data.icon
-        this.showScene = false
-      }
-      else{
-        this.clickFrom == 'editpanel'?  this.select = data[0].icon :this.info.icon = data[0].icon
-        this.isShowSelect = false
-      }
-      setTimeout(() => {
-        this.select = ''
-      });
-    },
-    getMaterialList() {
-      getMaterialList(
-        {
-          pageNum: this.paging.pageNum,
-          pageSize: this.paging.pageSize,
-          searchKey: this.key,
-          type:this.type
-        },
-        (data) => {
-          this.paging.pageNum = data.data.pageNum;
-          this.paging.pageSize = data.data.pageSize;
-          this.paging.total = data.data.total;
-
-          this.list = data.data.list.map(i=>{
-            i.isUse = i.fileSize>600 ?'1':'0'
-            i.fileSize = changeByteUnit(Number(i.fileSize))
-            i.createTime = i.createTime.substring(0,i.createTime.length-3)
-            i.updateTime = i.updateTime.substring(0,i.updateTime.length-3)
-            return i
-          })
-
-        }
-      );
-    },
-    selectHandle(type, from='base'){
-      this.clickFrom = from
-      this.type = type
-      this.isShowSelect = true
-      this.getMaterialList()
-    },
-    onCloseEdit(){
-      this.activeSetting = ''
-    }
   },
-};
+}
 </script>
 
 <style lang="less" scoped>
-
-.ui-title-big{
-  margin-top: 20px;
-}
 .main {
   position: fixed;
-  width: 800px;
-  top: 15%;
-  left: 50%;
-  transform: translateX(-50%);
+  width: 930px;
+  top: 90px;
+  left: calc(50% - (930px) / 2);
 }
 
 .upload-con {
   display: flex;
-}
-.uc-l {
-  .ui-remark{
-    margin-top: 10px;
-  }
-
-}
-
-.uc-r {
-  width: 100%;
-}
-
-.upload-btn {
-  display: flex;
-  width: 216px;
-  justify-content: space-between;
-  align-items: center;
-  margin-top: 15px;
-  .ui-button {
-    width: 48%;
-    min-width: 64px;
-    position: relative;
-    &:hover {
-    > div {
-        display: block;
+  margin-bottom: 30px;
+  .uc-l {
+    .preview {
+      overflow: hidden;
+      position: relative;
+      img {
+        height: 100%;
       }
-    }
-    > div {
-        cursor: default;
-        padding: 0 10px;
-        display: none;
-        z-index: 10000;
+      .setting-cover-btn {
         position: absolute;
         left: 50%;
         transform: translateX(-50%);
-        top: -46px;
-        background: rgba(0, 0, 0, 0.8); 
-        border-radius: 5px;
-        color: #fff;
-        pointer-events: none;
-        &::before{
-          border: 10px solid transparent;
-          border-top: 10px solid rgba(0, 0, 0, 0.8);
-          width: 0;
-          height: 0px;
-          content: "";
-          display: inline-block;
-          position: absolute;
-          bottom: -20px;
-          left: 50%;
-          transform: translateX(-50%);
-        }
-      .remark {
-          line-height: 2.5;
+        bottom: 16px;
       }
     }
-  }
-}
-
-.guide {
-  cursor: pointer;
-  position: relative;
-  &:hover {
-    > div {
-      display: block;
+    .ui-remark{
+      margin-top: 16px;
     }
   }
-  i {
-    color: #ababab;
-  }
-  > div {
-    cursor: default;
-    padding: 10px 18px;
-    display: none;
-    z-index: 10000;
-    position: fixed;
-    right: 0;
-    top: -90px;
-    width: 500px;
-    background: #161a1a;
-    box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.16);
-    border-radius: 5px;
-    overflow: hidden;
-    color: rgba(255, 255, 255, 0.5);
-    .remark {
-      line-height: 2.5;
+  .uc-r {
+    width: 100%;
+    > .title-input-wrapper {
+      position: relative;
+      border: 1px solid rgba(151, 151, 151, 0.2);
+      padding: 0 16px;
+      background: #252526;
+      border-radius: 2px;
+      height: 36px;
+      width: 100%;
+      &:focus-within {
+        border-color: #0076F6;
+      }
+      > input {
+        border: none;
+        background: transparent;
+        outline: none;
+        height: 100%;
+        width: calc(100% - 50px);
+        padding: 0;
+        color: #fff;
+        letter-spacing: 1px;
+        font-size: 14px;
+      }
+      > .count {
+        position: absolute;
+        top: 50%;
+        transform: translateY(-50%);
+        right: 16px;
+        font-size: 14px;
+        color: rgba(255, 255, 255, 0.2);
+      }
     }
-
-    .strong {
-      color: #fff;
+    > .jianjie {
+      margin-top: 20px;
     }
-
-    .line {
+    > .jianjie-textarea-wrapper {
+      position: relative;
+      border: 1px solid rgba(151, 151, 151, 0.2);
+      background: #252526;
+      border-radius: 2px;
+      height: 123px;
       width: 100%;
-      height: 1px;
-      background: rgba(255, 255, 255, 0.16);
-      margin: 10px 0;
+      &:focus-within {
+        border-color: #0076F6;
+      }
+      > textarea {
+        padding: 9px 16px 30px 16px;
+        resize: none;
+        border: none;
+        background: transparent;
+        outline: none;
+        width: 100%;
+        height: calc(100% - 30px);
+        color: #fff;
+        letter-spacing: 1px;
+        font-size: 14px;
+      }
+      > .count {
+        position: absolute;
+        right: 16px;
+        bottom: 9px;
+        font-size: 14px;
+        color: rgba(255, 255, 255, 0.2);
+      }
     }
   }
 }
 
-.preview {
-  overflow: hidden;
-  img {
-    height: 100%;
+menu {
+  display: inline-block;
+  width: 133px;
+  font-size: 14px;
+  color: rgba(255, 255, 255, 0.5);
+  padding-left: 0;
+  margin: 0;
+  vertical-align: top;
+  li {
+    display: block;
+    height: 47px;
+    padding-left: 16px;
+    cursor: pointer;
+    &::after {
+      content: "";
+      display: inline-block;
+      height: 100%;
+      vertical-align: middle;
+    }
+    &.active {
+      font-size: 16px;
+      font-weight: bold;
+      color: #FFFFFF;
+      background: #252526;
+      &::before {
+        content: "";
+        display: inline-block;
+        width: 2px;
+        height: 10px;
+        border-radius: 1px;
+        background: #0076F6;
+        margin-right: 4px;
+        vertical-align: middle;
+      }
+    }
   }
 }
 
-.setting-con{
-  >li{
-    display: inline-block;
-    .ui-button{
-      padding: 0 20px;
-      margin-right: 10px;
-    }
-  }
+.settings-view-wrapper {
+  vertical-align: top;
+  display: inline-block;
+  width: 797px;
+
 }
 </style>

+ 59 - 0
packages/code/src/views/base/autoCruiseSettings.vue

@@ -0,0 +1,59 @@
+<template>
+  <div class="auto-cruise-settings">
+    <span class="title">自动巡游</span>
+    <br>
+    <div class="switch-wrapper">
+      <span class="label">进入全景图自动巡游(3分钟完整巡游一次)</span>
+      <Switcher :value="info.isAuto" @change="onSwitcherChange"></Switcher>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+import Switcher from "@/components/shared/Switcher";
+
+export default {
+  components: {
+    Switcher,
+  },
+  data() {
+    return {
+      password:'',
+      canSee: false,
+    }
+  },
+  computed: {
+    ...mapGetters({
+      info:'info'
+    })
+  },
+  methods: {
+    onSwitcherChange(data) {
+      this.info.isAuto = data
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.auto-cruise-settings {
+  padding: 24px 30px;
+  background: #252526;
+  height: 546px;
+  .title {
+    font-size: 18px;
+    color: #FFFFFF;
+  }
+  .switch-wrapper {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-top: 18px;
+    .label {
+      color: rgba(255, 255, 255, 0.6);
+      font-size: 14px;
+    }
+  }
+}
+</style>

+ 153 - 0
packages/code/src/views/base/backgroundMusicSettings.vue

@@ -0,0 +1,153 @@
+<template>
+  <div class="background-music-settings">
+    <span class="title">设置背景音乐</span>
+    <br>
+    <button v-if="!(info && info.backgroundMusic && info.backgroundMusic.name)" @click="onClickMusicSelectionBtn">
+      <i class="iconfont icon-editor_add"></i>
+      添加音频
+    </button>
+    <template v-else>
+      <div class="music-display" @click.self="onClickCurrentMusic">
+        <Audio ref="my-audio" class="audio-control" :backgroundColor="'#1A1B1D'" :myAudioUrl="info.backgroundMusic.ossPath"></Audio>
+        <div class="name" v-title="info.backgroundMusic.name" @click="onClickCurrentMusic">{{info.backgroundMusic.name}}</div>
+        <i class="iconfont icon-editor_list_delete" @click.stop="onClickDeleteMusicBtn"></i>
+      </div>
+      <button @click="onClickMusicSelectionBtn">
+        <i class="iconfont icon-editor_update"></i>
+        更换音频
+      </button>
+    </template>
+
+    <div class="dialog" style="z-index: 2000" v-if="isShowSelectionWindow">
+      <MaterialSelectorForEditor
+        title="选择素材"
+        @cancle="isShowSelectionWindow = false"
+        @submit="handleSubmitFromMaterialSelector"
+        :selectableType="['audio']"
+        initialMaterialType="audio"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+import MaterialSelectorForEditor from "@/components/materialSelectorForEditor.vue";
+import Audio from "@/components/audio/audioForEditor.vue";
+
+export default {
+  components: {
+    MaterialSelectorForEditor,
+    Audio,
+  },
+  data() {
+    return {
+      isShowSelectionWindow: false,
+    }
+  },
+  computed: {
+    ...mapGetters({
+      info:'info'
+    }),
+  },
+  methods: {
+    onClickMusicSelectionBtn() {
+      this.isShowSelectionWindow = true
+    },
+    onClickDeleteMusicBtn() {
+      this.info.backgroundMusic.id = null
+      this.info.backgroundMusic.name = null
+      this.info.backgroundMusic.ossPath = null
+    },
+    handleSubmitFromMaterialSelector(selected) {
+      this.isShowSelectionWindow = false
+      this.info.backgroundMusic.id = selected[0].id
+      this.info.backgroundMusic.name = selected[0].name
+      this.info.backgroundMusic.ossPath = selected[0].ossPath
+    },
+    onClickCurrentMusic() {
+      if (this.$refs['my-audio']) {
+        this.$refs['my-audio'].switchPlayPause()
+      }
+    }
+  },
+  mounted() {
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.background-music-settings {
+  padding: 24px 30px;
+  background: #252526;
+  height: 546px;
+  > .title {
+    font-size: 18px;
+    color: #FFFFFF;
+  }
+  > button {
+    margin-top: 16px;
+    width: 234px;
+    height: 40px;
+    background: #1A1B1D;
+    border-radius: 2px;
+    border: 1px solid #404040;
+    display: block;
+    color: #0076F6;
+    font-size: 14px;
+    cursor: pointer;
+    i {
+      font-size: 14px;
+    }
+  }
+  > .music-display {
+    cursor: pointer;
+    margin-top: 16px;
+    width: 234px;
+    height: 40px;
+    background: #1A1B1D;
+    border-radius: 2px;
+    border: 1px solid #404040;
+    color: #fff;
+    font-size: 14px;
+    position: relative;
+    &:hover {
+      color: #0076F6;
+      > .audio-control {
+        display: inline-block;
+      }
+      > i {
+        display: inline-block;
+      }
+    }
+    > .audio-control {
+      position: absolute;
+      top: 50%;
+      transform: translateY(-50%);
+      left: 18px;
+      display: none;
+    }
+    > .name {
+      position: absolute;
+      left: 50%;
+      top: 50%;
+      transform: translate(-50%, -50%);
+      width: 65%;
+      text-overflow: ellipsis;
+      overflow: hidden;
+      white-space: nowrap;
+      display: inline-block;
+    }
+    > i {
+      display: none;
+      position: absolute;
+      top: 50%;
+      transform: translateY(-50%);
+      right: 18px;
+      &:hover {
+        color: #FA5555;
+      }
+    }
+  }
+}
+</style>

+ 437 - 0
packages/code/src/views/base/customButtonSettings.vue

@@ -0,0 +1,437 @@
+<template>
+  <div class="custom-button-settings">
+    <span class="title">自定义按钮</span>
+
+
+    <i class="iconfont icon-material_prompt tool-tip-for-editor" v-tooltip="'自定义按钮可为作品添加联系方式或网站链接等,设置可见后即可在作品显示。'">
+    </i>
+    <br/>
+    
+    <div v-for="(item, index) of info.customButton" :key="index" class="button-setting-item" :class="{expand: expandStatus[index]}">
+      <div class="title-bar">
+        <div class="left">
+          <i
+            class="iconfont icon-edit_input_arrow icon-expand"
+            @click="onRequestForChangeExpandStatus(index)"
+          ></i>
+          <img v-if="info.customButton[index].type === '电话'" :src="require('@/assets/images/icons/phone.png')" class="button-icon" alt="">
+          <img v-if="info.customButton[index].type === '链接'" :src="require('@/assets/images/icons/link.png')" class="button-icon" alt="">
+          <span class="button-name">
+            {{info.customButton[index].name}}
+          </span>
+        </div>
+        <div class="right">
+          <i
+            class="iconfont icon-editor_list_edit btn-edit"
+            @click="onRequestForEdit(index)"
+            v-tooltip="'编辑'"
+          >
+          </i>
+          <div
+            class="btn-show"
+            v-show="info.customButton[index].value && info.customButton[index].isShow"
+            v-tooltip="'显示'"
+          >
+            <img
+              class="eye-on" :src="require('@/assets/images/icons/eye_on.png')" alt=""
+              @click="info.customButton[index].isShow = !info.customButton[index].isShow"
+            >
+          </div>
+          <div
+            class="btn-hide"
+            v-show="info.customButton[index].value && !info.customButton[index].isShow"
+            v-tooltip="'隐藏'"
+          >
+            <img
+              class="eye-off" :src="require('@/assets/images/icons/eye_off.png')" alt=""
+              @click="info.customButton[index].isShow = !info.customButton[index].isShow"
+            >
+          </div>
+        </div>
+      </div>
+      <div class="edit-content">
+        <div class="edit-content-item">
+          <span class="item-name">按钮名称</span>
+          <PulldownMenuInEditor
+            class="selector"
+            :valueList="buttonTypeList"
+            v-model="info.customButton[index].type"
+          ></PulldownMenuInEditor>
+          <input
+            class="name-input"
+            placeholder="请输入按钮名称"
+            v-model="info.customButton[index].name"
+            maxlength="15"
+          >
+        </div>
+        <div class="edit-content-item">
+          <span class="item-name">{{buttonValueTips[index]}}</span>
+          <input
+            class="value-input"
+            :placeholder="`请输入${buttonValueTips[index]}`"
+            v-model="info.customButton[index].value"
+          >
+        </div>
+      </div>
+    </div>
+
+    <popup v-if="isEditing" :canClose="false">
+      <div class="ui-message ui-message-confirm dark edit-window">
+        <div class="ui-message-header">
+          <span>自定义按钮</span>
+          <span @click="isEditing = false">
+            <i class="iconfont icon_close"></i>
+          </span>
+        </div>
+
+        <div class="ui-message-main">
+          <div class="edit-content-item">
+            <span class="item-name">按钮名称</span>
+            <PulldownMenuInEditor
+              class="selector"
+              :valueList="buttonTypeList"
+              v-model="editingInfo.type"
+            ></PulldownMenuInEditor>
+            <input
+              class="name-input"
+              placeholder="请输入按钮名称"
+              v-model="editingInfo.name"
+              maxlength="15"
+            >
+          </div>
+          <div class="edit-content-item">
+            <span class="item-name">{{editingButtonValueTip}}</span>
+            <input
+              class="value-input"
+              :placeholder="`请输入${editingButtonValueTip}`"
+              v-model="editingInfo.value"
+            >
+          </div>
+        </div>
+
+        <div class="ui-message-footer">
+          <button class="ui-button deepcancel" @click="isEditing = false">
+            取消
+          </button>
+          <button
+            class="ui-button submit"
+            @click="onConfirmEditing"
+          >
+            确定
+          </button>
+        </div>
+      </div>
+    </popup>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+import PulldownMenuInEditor from "@/components/pulldownMenuInEditor.vue";
+import { isValidPhoneNumber } from "@/utils/other.js";
+import Popup from "@/components/shared/popup/index.vue";
+
+export default {
+  components: {
+    PulldownMenuInEditor,
+    Popup,
+  },
+  data() {
+    return {
+      expandStatus: [],
+      buttonTypeList: [
+        '电话',
+        '链接',
+      ],
+      isEditing: false,
+      editingButtonIdx: -1,
+      isIgnoreTypeChangeWhenEditing: false,
+      editingInfo: {
+        type: '',
+        name: '',
+        value: '',
+      }
+    }
+  },
+  computed: {
+    ...mapGetters({
+      info:'info'
+    }),
+    buttonValueTips() {
+      if (this?.info?.customButton) {
+        return this.info.customButton.map((item) => {
+          if (item.type === '电话') {
+            return '电话号码'
+          } else if (item.type === '链接') {
+            return '链接地址'
+          } else {
+            return ''
+          }
+        })
+      } else {
+        return null
+      }
+    },
+    editingButtonValueTip() {
+      if (this.editingInfo.type === '电话') {
+        return '电话号码'
+      } else if (this.editingInfo.type === '链接') {
+        return '链接地址'
+      } else {
+        return ''
+      }
+    },
+  },
+  watch: {
+    'editingInfo.type': {
+      handler(vNew) {
+        if (!this.isIgnoreTypeChangeWhenEditing) {
+          this.editingInfo.name = vNew
+          this.editingInfo.value = ''
+        }
+        this.isIgnoreTypeChangeWhenEditing = false
+      }
+    },
+  },
+  methods: {
+    onRequestForChangeExpandStatus(index) {
+      this.$set(this.expandStatus, index, !this.expandStatus[index])
+    },
+    onRequestForEdit(index) {
+      this.editingButtonIdx = index
+      this.isIgnoreTypeChangeWhenEditing = true,
+      this.editingInfo.type = this.info.customButton[index].type
+      this.editingInfo.name = this.info.customButton[index].name
+      this.editingInfo.value = this.info.customButton[index].value
+      this.isEditing = true
+    },
+    checkButtonName(name) {
+      if (!name) {
+        this.$msg.warning('请填写完整信息')
+        return false
+      }
+      return true
+    },
+    checkButtonValue(value, type) {
+      if (type === '电话') {
+        if (!isValidPhoneNumber(value)) {
+          this.$msg.warning('请正确填写电话号码')
+          return false
+        }
+      } else if (type === '链接') {
+        if (!value) {
+          this.$msg.warning('请填写完整信息')
+          return false
+        }
+      }
+      return true
+    },
+    onConfirmEditing() {
+      if (!this.checkButtonName(this.editingInfo.name)) {
+        return
+      }
+      if (!this.checkButtonValue(this.editingInfo.value, this.editingInfo.type)) {
+        return
+      }
+      this.info.customButton[this.editingButtonIdx].type = this.editingInfo.type 
+      this.info.customButton[this.editingButtonIdx].name = this.editingInfo.name 
+      this.info.customButton[this.editingButtonIdx].value = this.editingInfo.value 
+      this.$msg.success('操作成功')
+      this.isEditing = false
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.custom-button-settings {
+  padding: 24px 30px;
+  background: #252526;
+  height: 546px;
+  .title {
+    font-size: 18px;
+    color: #FFFFFF;
+  }
+  .tool-tip-for-editor {
+    margin-left: 4px;
+    font-size: 12px;
+    cursor: default;
+    position: relative;
+    top: -2px;
+  }
+  > .button-setting-item {
+    margin-top: 16px;
+    position: relative;
+    min-height: 50px;
+    > .title-bar {
+      position: absolute;
+      width: 100%;
+      height: 50px;
+      background: #1A1B1D;
+      border-radius: 2px;
+      border: 1px solid #404040;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 0 16px;
+      > .left {
+        display: flex;
+        align-items: center;
+        > .icon-expand {
+          font-size: 10px;
+          color: rgba(255, 255, 255, 0.6);
+          transform: rotate(-90deg);
+          cursor: pointer;
+        }
+        > .button-icon {
+          width: 36px;
+          height: 36px;
+        }
+        > .button-name {
+          font-size: 16px;
+          color: #fff;
+        }
+      }
+      > .right {
+        display: flex;
+        align-items: center;
+        i.btn-edit {
+          margin-left: 16px;
+          cursor: pointer;
+          &:hover {
+            color: #0076F6;
+          }
+        }
+        > .btn-show {
+          margin-left: 16px;
+          cursor: pointer;
+          display: flex;
+          flex-direction: column;
+          justify-content: center;
+          img {
+            height: 100%;
+          }
+        }
+        > .btn-hide {
+          display: flex;
+          flex-direction: column;
+          justify-content: center;
+          margin-left: 16px;
+          cursor: pointer;
+        }
+      }
+    }
+    > .edit-content {
+      border-radius: 2px;
+      border: 1px solid #404040;
+      padding-top: 58px;
+      padding-bottom: 26px;
+      display: none;
+      pointer-events: none;
+      > .edit-content-item {
+        margin-top: 16px;
+        display: flex;
+        align-items: center;
+        > .item-name {
+          margin-left: 16px;
+          font-size: 14px;
+          color: rgba(255, 255, 255, 0.5)
+        }
+        > .selector {
+          margin-left: 16px;
+        }
+        > .name-input {
+          height: 36px;
+          background: transparent;
+          border-radius: 2px;
+          border: 1px solid #404040;
+          color: #fff;
+          font-size: 14px;
+          padding: 0 16px;
+          letter-spacing: 1px;
+          width: 470px;
+          &:focus {
+            border-color: #0076F6;
+          }
+        }
+        > .value-input {
+          margin-left: 16px;
+          height: 36px;
+          background: transparent;
+          border-radius: 2px;
+          border: 1px solid #404040;
+          color: #fff;
+          font-size: 14px;
+          padding: 0 16px;
+          letter-spacing: 1px;
+          width: 610px;
+          &:focus {
+            border-color: #0076F6;
+          }
+        }
+      }
+    }
+  }
+  > .button-setting-item.expand {
+    > .title-bar {
+      > .left {
+        > .icon-expand {
+          transform: rotate(0deg);
+        }
+      }
+    }
+    > .edit-content {
+      display: block;
+    }
+  }
+
+  .edit-window {
+    width: 574px;
+    > .ui-message-main {
+      margin-bottom: 40px;
+      > .edit-content-item {
+        margin-top: 16px;
+        display: flex;
+        align-items: center;
+        > .item-name {
+          flex: 0 0 auto;
+          font-size: 14px;
+          color: rgba(255, 255, 255, 0.5)
+        }
+        > .selector {
+          margin-left: 16px;
+        }
+        > .name-input {
+          height: 36px;
+          background: #252526;
+          border-radius: 2px;
+          border: 1px solid #404040;
+          color: #fff;
+          font-size: 14px;
+          padding: 0 16px;
+          letter-spacing: 1px;
+          width: 470px;
+          &:focus {
+            border-color: #0076F6;
+          }
+        }
+        > .value-input {
+          margin-left: 16px;
+          height: 36px;
+          background: #252526;
+          border-radius: 2px;
+          border: 1px solid #404040;
+          color: #fff;
+          font-size: 14px;
+          padding: 0 16px;
+          letter-spacing: 1px;
+          width: 610px;
+          &:focus {
+            border-color: #0076F6;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 175 - 0
packages/code/src/views/base/customLogoSettings.vue

@@ -0,0 +1,175 @@
+<template>
+  <div class="custom-logo-settings">
+    <span class="title">自定义LOGO</span>
+    <br>
+    <div class="switch-wrapper">
+      <span class="label">显示LOGO</span>
+      <Switcher :value="info.isLogo" @change="onSwitcherChange"></Switcher>
+    </div>
+    <div class="bottom" :class="{disabled: !info.isLogo}">
+        <SelectedImage
+          :imgSrc="info.logo"
+          :defaultImgSrc="require('@/assets/images/default/img_logo_default.png')"
+          @cancel="onClickCancelCustomLogo"
+        ></SelectedImage>
+      <div class="bottom-right">
+        <img 
+          class="select-pic-btn"
+          :src="require('@/assets/images/select_pic_btn.png')" alt=""
+          @click="onClickSelectingPicBtn"
+        >
+        <!-- <button
+          class="ui-button submit"
+          @click="onClickSelectingPicBtn"
+        >
+          选择图片
+        </button> -->
+        <div class="remark">
+          300*300px,600kb以内,支持<br>
+          jpg/png格式
+        </div>
+      </div>
+    </div>
+
+    <div class="dialog" style="z-index: 2000" v-if="isShowSelectionWindow">
+      <MaterialSelectorForEditor
+        title="选择素材"
+        @cancle="isShowSelectionWindow = false"
+        @submit="handleSubmitFromMaterialSelector"
+        :selectableType="['image']"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+import Switcher from "@/components/shared/Switcher";
+import MaterialSelectorForEditor from "@/components/materialSelectorForEditor.vue";
+import { getUserInfo } from "@/api";
+import SelectedImage from "@/components/selectedImageInEditor.vue";
+
+export default {
+  components: {
+    Switcher,
+    MaterialSelectorForEditor,
+    SelectedImage,
+  },
+  data() {
+    return {
+      password:'',
+      canSee: false,
+      isShowSelectionWindow: false,
+    }
+  },
+  computed: {
+    ...mapGetters({
+      info:'info'
+    })
+  },
+  methods: {
+    onSwitcherChange(data) {
+      this.info.isLogo = data
+    },
+    onClickSelectingPicBtn() {
+      getUserInfo((res) => {
+        console.log(res);
+        try {
+          if (res.data.incrementNum > 0) {
+            this.isShowSelectionWindow = true
+          } else {
+            // 非会员,点击跳转四维看看会员权益页。
+            window.open('/#/mall/member')
+          }
+        } catch(e) {
+          console.error('解析会员身份失败:', e)
+        }
+      })
+    },
+    handleSubmitFromMaterialSelector(selected) {
+      this.info.logo = selected[0].icon
+      this.info.logoChange = true
+      this.isShowSelectionWindow = false
+    },
+    onClickCancelCustomLogo() {
+      this.info.logo = ''
+      this.info.logoChange = false
+    }
+  },
+  mounted() {
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.custom-logo-settings {
+  padding: 24px 30px;
+  background: #252526;
+  height: 546px;
+  .title {
+    font-size: 18px;
+    color: #FFFFFF;
+  }
+  .switch-wrapper {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-top: 18px;
+    .label {
+      color: rgba(255, 255, 255, 0.6);
+      font-size: 14px;
+    }
+  }
+  .bottom {
+    margin-top: 19px;
+    display: flex;
+    align-items: flex-start;
+    &.disabled {
+      opacity: 0.5;
+    }
+    .img-wrapper {
+      position: relative;
+      width: 136px;
+      height: 136px;
+      border-radius: 4px;
+      background: #1a1b1d;
+      overflow: hidden;
+      .logo-img {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+      }
+      .cancel-btn-background {
+        width: 52px;
+        height: 52px;
+        position: absolute;
+        top: -28px;
+        right: -28px;
+        background: rgba(0, 0, 0, 0.5);
+        border-radius: 50%;
+        cursor: pointer;
+        i {
+          font-size: 12px;
+          transform: scale(0.8);
+          position: absolute;
+          left: 9px;
+          bottom: 9px;
+        }
+      }
+    }
+    .bottom-right {
+      margin-left: 16px;
+      .select-pic-btn {
+        cursor: pointer;
+      }
+      .remark {
+        margin-top: 20px;
+        font-size: 14px;
+        font-family: MicrosoftYaHei;
+        color: rgba(255, 255, 255, 0.3);
+        line-height: 19px;
+      }
+    }
+  }
+}
+</style>

+ 147 - 0
packages/code/src/views/base/customMaskSettings.vue

@@ -0,0 +1,147 @@
+<template>
+  <div class="custom-mask-settings">
+    <span class="title">遮罩设置</span>
+    <i class="iconfont icon-material_prompt tool-tip-for-editor" v-tooltip="'天空遮罩显示在场景的顶部,地面遮罩显示在场景的底部。'">
+    </i>
+    <br>
+    <div class="image-selection">
+      <div class="title">天空遮罩</div>
+      <div class="bottom">
+        <SelectedImage
+          :imgSrc="info && info.customMask && info.customMask.sky"
+          :defaultImgSrc="require('@/assets/images/default/mask_bg.png')"
+          @cancel="onCancelSelection('sky')"
+        ></SelectedImage>
+        <div class="bottom-right">
+          <img 
+            class="select-pic-btn"
+            :src="require('@/assets/images/select_pic_btn.png')" alt=""
+            @click="onSelectPic('sky')"
+          >
+          <div class="ui-remark">建议500*500px,<br/>支持jpg/png格式</div>
+        </div>
+      </div>
+    </div>
+    <div class="image-selection">
+      <div class="title">地面遮罩</div>
+      <div class="bottom">
+        <SelectedImage
+          :imgSrc="info && info.customMask && info.customMask.earth"
+          :defaultImgSrc="require('@/assets/images/default/mask_bg.png')"
+          @cancel="onCancelSelection('earth')"
+        ></SelectedImage>
+        <div class="bottom-right">
+          <img 
+            class="select-pic-btn"
+            :src="require('@/assets/images/select_pic_btn.png')" alt=""
+            @click="onSelectPic('earth')"
+          >
+          <div class="ui-remark">建议500*500px,<br/>支持jpg/png格式</div>
+        </div>
+      </div>
+    </div>
+
+    <div class="dialog" style="z-index: 2000" v-if="isShowSelectionWindow">
+      <MaterialSelectorForEditor
+        title="选择素材"
+        @cancle="isShowSelectionWindow = false"
+        @submit="handleSubmitFromMaterialSelector"
+        :selectableType="['image']"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import MaterialSelectorForEditor from "@/components/materialSelectorForEditor.vue";
+import { mapGetters } from "vuex";
+import SelectedImage from "@/components/selectedImageInEditor.vue";
+import { getUserInfo } from "@/api";
+
+export default {
+  components: {
+    MaterialSelectorForEditor,
+    SelectedImage,
+  },
+  data() {
+    return {
+      isShowSelectionWindow: false,
+      selectingFor: '', // 'sky', 'earth'
+    }
+  },
+  computed: {
+    ...mapGetters({
+      info:'info'
+    })
+  },
+  methods: {
+    onSelectPic(selectingFor) {
+      getUserInfo((res) => {
+        try {
+          if (res.data.incrementNum > 0) {
+            this.selectingFor = selectingFor
+            this.isShowSelectionWindow = true
+          } else {
+            // 非会员,点击跳转四维看看会员权益页。
+            window.open('/#/mall/member')
+          }
+        } catch(e) {
+          console.error('解析会员身份失败:', e)
+        }
+      })
+    },
+    handleSubmitFromMaterialSelector(selected) {
+      this.info.customMask[this.selectingFor] = selected[0].icon
+      this.isShowSelectionWindow = false
+    },
+    onCancelSelection(cancelFor) {
+      this.info.customMask[cancelFor] = ''
+    },
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.custom-mask-settings {
+  padding: 24px 30px;
+  background: #252526;
+  height: 546px;
+  .title {
+    font-size: 18px;
+    color: #FFFFFF;
+  }
+  .tool-tip-for-editor {
+    margin-left: 4px;
+    font-size: 12px;
+    cursor: default;
+    position: relative;
+    top: -2px;
+  }
+  .image-selection {
+    width: 50%;
+    display: inline-block;
+    margin-top: 16px;
+    margin-bottom: 24px;
+    .title {
+      color: rgba(255, 255, 255, 0.6);
+      font-size: 14px;
+      margin-bottom: 16px;
+    }
+    .bottom {
+      display: flex;
+      align-items: flex-start;
+      .bottom-right {
+        display: inline-block;
+        .select-pic-btn {
+          cursor: pointer;
+        }
+
+        .ui-remark {
+          margin-top: 20px;
+          font-size: 14px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 11 - 5
packages/code/src/views/base/index.vue

@@ -1,17 +1,23 @@
 <template>
+  <!-- 编辑器-基础 -->
   <div>
-    <setting></setting>
-    <toolbar></toolbar>
+    <toolbar v-if="info"></toolbar>
   </div>
 </template>
+
 <script>
-import Setting from "./Setting";
 import Toolbar from "./Toolbar";
+import { mapGetters } from "vuex";
+
 export default {
-  name: "home",
+  name: "EditorBase",
   components: {
-    Setting,
     Toolbar
+  },
+  computed: {
+    ...mapGetters({
+      info: 'info'
+    }),
   }
 };
 </script>

+ 105 - 0
packages/code/src/views/base/openingAnimationSettings.vue

@@ -0,0 +1,105 @@
+<template>
+  <div class="opening-animation-settings">
+    <span class="title">设置开场动画</span>
+    <br>
+    <div class="btns-and-video">
+      <div class="btn-wrapper">
+        <button
+          v-for="item of openingTypeList" :key="item"
+          class="opening-selection-btn"
+          :class="{'active-opening-type': info.openingAnimationType === item}"
+          @click="info.openingAnimationType = item"
+        >
+          {{item}}
+        </button>
+      </div>
+      <div class="video-wrapper">
+        <video
+          v-for="item of openingTypeList" :key="item"
+          v-show="item === info.openingAnimationType"
+          :src="require(`@/assets/videos/${item}.mp4`)"
+          autoplay
+          loop
+        ></video>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+
+export default {
+  components: {
+  },
+  data() {
+    return {
+      openingTypeList: [
+        '小行星开场',
+        '小行星巡游开场',
+        '小行星缩放开场',
+        '水平巡游开场',
+        '水晶球开场',
+      ],
+    }
+  },
+  computed: {
+    ...mapGetters({
+      info:'info'
+    })
+  },
+  methods: {
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.opening-animation-settings {
+  padding: 24px 30px;
+  background: #252526;
+  height: 546px;
+  .title {
+    font-size: 18px;
+    color: #FFFFFF;
+  }
+  .btns-and-video {
+    margin-top: 16px;
+    display: flex;
+    align-items: flex-start;
+    .btn-wrapper {
+      .opening-selection-btn {
+        width: 180px;
+        height: 40px;
+        background: #1A1B1D;
+        border-radius: 2px;
+        border: 1px solid #404040;
+        margin-bottom: 16px;
+        text-align: center;
+        font-size: 14px;
+        color: #fff;
+        cursor: pointer;
+        display: block;
+        &::after {
+          content: '';
+          height: 100%;
+          vertical-align: middle;
+          display: inline-block;
+        }
+      }
+      .active-opening-type {
+        color: #0076F6;
+      }
+    }
+    .video-wrapper {
+      width: 469px;
+      height: 264px;
+      margin-left: 20px;
+      video {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+      }
+    }
+  }
+}
+</style>

+ 169 - 0
packages/code/src/views/base/openingTipSettings.vue

@@ -0,0 +1,169 @@
+<template>
+  <div class="opening-tip-settings">
+    <span class="title">提示设置</span>
+    <i class="iconfont icon-material_prompt tool-tip-for-editor" v-tooltip="'开场提示仅适用于全景图。若初始场景为三维模\n型,以下开场提示不适用。'">
+    </i>
+    <br>
+    <div class="image-selection">
+      <div class="title">PC端</div>
+      <div class="bottom">
+        <SelectedImage
+          :imgSrc="info.pcIcon"
+          :defaultImgSrc="require('@/assets/images/default/img_tipspc_default.png')"
+          @cancel="onCancelPcTip"
+        ></SelectedImage>
+        <div class="bottom-right">
+          <button class="ui-button submit" @click="isShowSelectionWindow = true, selectingFor = 'pc'">选择图片</button>
+          <div class="ui-remark">建议300*300px,600kb以内,<br/>支持jpg/png格式</div>
+        </div>
+      </div>
+    </div>
+    <div class="image-selection">
+      <div class="title">移动端</div>
+      <div class="bottom">
+        <SelectedImage
+          :imgSrc="info.appIcon"
+          :defaultImgSrc="require('@/assets/images/default/img_tipsmb_default.png')"
+          @cancel="onCancelAppTip"
+        ></SelectedImage>
+        <div class="bottom-right">
+          <button class="ui-button submit" @click="isShowSelectionWindow = true, selectingFor = 'mobile'">选择图片</button>
+          <div class="ui-remark">建议300*300px,600kb以内,<br/>支持jpg/png格式</div>
+        </div>
+      </div>
+    </div>
+
+    <div class="title">显示设置</div>
+    <div class="switch-wrapper">
+        <span class="label">仅首次打开链接时,提示</span>
+        <switcher :value="info.isRemind" @change="onSwitcherChange"></switcher>
+    </div>
+    <div class="range-wrapper">
+      <RangeItem :value="rang" @input="onRangeChange" />
+    </div>
+
+    <div class="dialog" style="z-index: 2000" v-if="isShowSelectionWindow">
+      <MaterialSelectorForEditor
+        title="选择素材"
+        @cancle="isShowSelectionWindow = false"
+        @submit="handleSubmitFromMaterialSelector"
+        :selectableType="['image']"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import MaterialSelectorForEditor from "@/components/materialSelectorForEditor.vue";
+import { mapGetters } from "vuex";
+import Switcher from "@/components/shared/Switcher";
+import RangeItem from "@/components/rangeItem/index.vue";
+import SelectedImage from "@/components/selectedImageInEditor.vue";
+
+export default {
+  components: {
+    MaterialSelectorForEditor,
+    Switcher,
+    RangeItem,
+    SelectedImage,
+  },
+  data() {
+    return {
+      isShowSelectionWindow: false,
+      selectingFor: '', // 'pc', 'mobile'
+      rang: {
+        label: '显示时间',
+        unit: "秒",
+        value: 1,
+        min: 0,
+        max: 3,
+        gradient: 1,
+        tip:true
+      }
+    }
+  },
+  computed: {
+    ...mapGetters({
+      info:'info'
+    })
+  },
+  methods: {
+    handleSubmitFromMaterialSelector(selected) {
+      if (this.selectingFor === 'pc') {
+        this.info.pcIcon = selected[0].icon
+      } else if (this.selectingFor === 'mobile') {
+        this.info.appIcon = selected[0].icon
+      }
+      this.isShowSelectionWindow = false
+    },
+    onCancelPcTip() {
+      this.info.pcIcon = ''
+    },
+    onCancelAppTip() {
+      this.info.appIcon = ''
+    },
+    onSwitcherChange(data){
+      this.info.isRemind = data
+    },
+    onRangeChange(data) {
+      this.info.remindTime = parseInt(data.value)
+    },
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.opening-tip-settings {
+  padding: 24px 30px;
+  background: #252526;
+  height: 546px;
+  .title {
+    font-size: 18px;
+    color: #FFFFFF;
+  }
+  .tool-tip-for-editor {
+    margin-left: 4px;
+    font-size: 12px;
+    cursor: default;
+    position: relative;
+    top: -2px;
+  }
+  .image-selection {
+    width: 50%;
+    display: inline-block;
+    margin-top: 16px;
+    margin-bottom: 24px;
+    .title {
+      color: rgba(255, 255, 255, 0.6);
+      font-size: 14px;
+      margin-bottom: 16px;
+    }
+    .bottom {
+      display: flex;
+      align-items: flex-start;
+      .bottom-right {
+        display: inline-block;
+        button {
+          margin-bottom: 20px;
+        }
+        .ui-remark {
+          font-size: 14px;
+        }
+      }
+    }
+  }
+  .switch-wrapper {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-top: 18px;
+    .label {
+      color: rgba(255, 255, 255, 0.6);
+      font-size: 14px;
+    }
+  }
+  .range-wrapper {
+    margin-top: 18px;
+  }
+}
+</style>

+ 84 - 0
packages/code/src/views/base/passwordSettings.vue

@@ -0,0 +1,84 @@
+<template>
+  <div class="password-settings">
+    <span class="title">设置访问密码</span>
+    <br>
+    <div class="input-wrapper">
+      <input
+        :type="canSee ? 'text' : 'password'"
+        placeholder="请输入访问密码,限20位"
+        :maxlength="20"
+        oninput="value=value.replace(/\s+/g,'')"
+        v-model="info.password"
+      >
+      <i class="iconfont" @click="canSee = !canSee" :class="canSee ? ' icon-editor_on' : 'icon-editor_off'"></i>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+
+export default {
+  components: {
+  },
+  data() {
+    return {
+      password:'',
+      canSee: false,
+    }
+  },
+  computed: {
+    ...mapGetters({
+      info:'info'
+    })
+  },
+  methods: {
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.password-settings {
+  padding: 24px 30px;
+  background: #252526;
+  height: 546px;
+  .title {
+    font-size: 18px;
+    color: #FFFFFF;
+  }
+  > .input-wrapper {
+    margin-top: 16px;
+    position: relative;
+    border: 1px solid #404040;
+    padding: 0 16px;
+    background: #1A1B1D;
+    border-radius: 2px;
+    height: 36px;
+    width: 500px;
+    &:focus-within {
+      border-color: #0076F6;
+    }
+    > input {
+      border: none;
+      background: transparent;
+      outline: none;
+      height: 100%;
+      width: calc(100% - 22px);
+      padding: 0;
+      color: #fff;
+      letter-spacing: 1px;
+      font-size: 14px;
+      background: redv;
+    }
+    > i {
+      padding: 5px;
+      position: absolute;
+      top: 50%;
+      transform: translateY(-50%);
+      right: 11px;
+      font-size: 14px;
+      color: rgba(255, 255, 255);
+    }
+  }
+}
+</style>

+ 703 - 0
packages/code/src/views/navigation/groupSettings.vue

@@ -0,0 +1,703 @@
+<template>
+  <div class="group-settings">
+    <div class="ui-title-big">场景导航
+      <i class="iconfont icon-material_prompt tool-tip-for-editor" v-tooltip="'场景素材包括全景图和三维场景,您可自定义分组及场景的排列顺序。'">
+      </i>
+    </div>
+    <button class="ui-button create-group-btn"
+      @click="onRequestForAddLevel1Group"
+    >
+      <i class="iconfont icon-editor_add"></i>
+      新增分组
+    </button>
+
+    <div class="obstructor"></div>
+
+    <div class="scene-group-wrap">
+      <SceneGroupInEditor
+        v-for="(item) of catalogTopology"
+        :key=item.id
+        :groupNode="item"
+        :level="1"
+        @addGroup="onRequestForAddGroupLevel2"
+        @renameScene="onRenameScene"
+        @deleteScene="onDeleteScene"
+        @renameGroup="onRenameGroup"
+        @deleteGroup="onDeleteGroup"
+      />
+
+      <div class="pano-con">
+        <tabList
+          :deviation="-35"
+          :list="info.catalogRoot"
+          @clickItem="
+            (item) => {
+              taboneActive = item;
+            }
+          "
+          :active="taboneActive"
+          :id="'rand'"
+          @addGroup="hadnleAddGroup"
+          :subId="'rand1'"
+        >
+          <template slot="hover" slot-scope="{ item }">
+            <ul>
+              <li @click="$emit('addGroup', { type: 1, oper: 'edit', item })">
+                重命名
+              </li>
+              <li
+                @click="
+                  $emit('addGroup', {
+                    type: 2,
+                    oper: 'add',
+                    item: { parentId: item.id },
+                  })
+                "
+              >
+                创建二级分组
+              </li>
+              <li @click="del(item, 'one')">删除</li>
+            </ul>
+          </template>
+        </tabList>
+
+        <tabList
+          :deviation="-35"
+          v-if="childTab.length > 1"
+          :list="childTab"
+          @clickItem="
+            (item) => {
+              tabtowActive = item;
+            }
+          "
+          :active="tabtowActive"
+          :id="'subrand'"
+          @addGroup="
+            $emit('addGroup', {
+              type: 2,
+              oper: 'add',
+              item: { parentId: taboneActive.id },
+            })
+          "
+          :subId="'subrand1'"
+        >
+          <template slot="hover" slot-scope="{ item }">
+            <ul @mouseover.prevent @mouseleave.prevent>
+              <li @click="$emit('addGroup', { type: 2, oper: 'edit', item })">
+                重命名
+              </li>
+              <li @click="del(item, 'two')">删除</li>
+            </ul>
+          </template>
+        </tabList>
+
+        <template v-if="scenes.length > 0">
+          <draggable
+            tag="ul"
+            v-model="scenes"
+            animation="300"
+            @sort="uploadListSort"
+          >
+            <li v-for="(item, i) in scenes" :key="i">
+              <div class="typeli">
+                <i
+                  class="iconfont iconedit_type_3d"
+                  :class="{iconedit_type_panorama: item.type !== '4dkk' }"
+                ></i>
+              </div>
+              <div class="img">
+                <img :src="item.icon+`?${Math.random()}`" alt="" />
+              </div>
+              <div class="oper">
+                <i class="iconfont iconmore"></i>
+                <ul>
+                  <li @click="$emit('rename', item)">重命名</li>
+                  <li @click="delPano(item)">删除</li>
+                </ul>
+              </div>
+              <div class="ui-title">
+                <span>{{
+                  item.type == "house" ? item.roomName : item.sceneTitle
+                }}</span>
+              </div>
+            </li>
+          </draggable>
+        </template>
+        <div class="no-record" v-else>
+          <i class="iconfont iconedit_list_default"></i>
+          <p>暂无全景图或三维场景,可点击下方按钮进行添加</p>
+        </div>
+        <div class="add-btn">
+          <button class="ui-button submit" @click="$emit('addPano')">
+            选择全景图
+          </button>
+          <button class="ui-button submit" @click="$emit('addScene')">
+            选择三维场景
+          </button>
+        </div>
+      </div>
+    </div>
+
+    <popup v-if="addGroupLevel" :canClose="false">
+      <div class="ui-message ui-message-confirm dark add-group-window">
+        <div class="ui-message-header">
+          <span>新增{{addGroupLevel === 1 ? '一' : addGroupLevel === 2 ? '二' : ''}}级分组</span>
+          <span @click="addGroupLevel = 0">
+            <i class="iconfont icon_close"></i>
+          </span>
+        </div>
+
+        <div class="ui-message-main">
+          <input
+            class="name-input"
+            placeholder="请输入分组名称,限15个字"
+            v-model="newGroupName"
+            maxlength="15"
+          >
+        </div>
+
+        <div class="ui-message-footer">
+          <button class="ui-button deepcancel" @click="addGroupLevel = 0">
+            取消
+          </button>
+          <button
+            class="ui-button submit"
+            @click="onConfirmAddingGroup"
+          >
+            确定
+          </button>
+        </div>
+      </div>
+    </popup>
+  </div>
+</template>
+
+<script>
+import tabList from "@/components/tablist/index.vue";
+import browser from "@/utils/browser";
+import draggable from "vuedraggable";
+import SceneGroupInEditor from "@/components/sceneGroupInEditor.vue";
+import { mapGetters } from "vuex";
+import { deepClone } from "@/utils/other.js";
+import Popup from "@/components/shared/popup/index.vue";
+
+export default {
+  components: {
+    draggable,
+    tabList,
+    SceneGroupInEditor,
+    Popup,
+  },
+  computed: {
+    ...mapGetters({
+      vrlist: "vrlist",
+      info: "info",
+      catalogTopology: 'catalogTopology',
+    }),
+    oneWidth() {
+      let tmp = $("#tablist").width();
+      return tmp;
+    },
+    menuWidth() {
+      let tmp = $("#menucon").width();
+      return tmp;
+    },
+  },
+  data() {
+    return {
+      taboneActive: { children: [] },
+      tabtowActive: "",
+      childTab: [],
+      scenes: [],
+      list: [],
+
+      addGroupLevel: 0, // 0: 没有在新增分组;1:在新增一级分组;2:在新增二级分组
+      newGroupName: '',
+      parentGroupId: '',
+    };
+  },
+  watch: {
+    "info.scenes": {
+      deep: true,
+      handler: function (newVal) {
+        let arr = newVal.filter((item) => {
+          return this.tabtowActive.id == item.category;
+        });
+        this.scenes = arr.sort((a,b)=>a.weight-b.weight)
+      },
+    },
+    "info.catalogs": {
+      deep: true,
+      handler: function (newVal) {
+        let temp = [];
+        this.childTab = [];
+        let id = this.taboneActive.id;
+        let oneActive = this.info.catalogRoot.find((item) => item.id == id);
+        if (!oneActive) {
+          oneActive = this.info.catalogRoot[0]
+          this.taboneActive = this.info.catalogRoot[0]
+        }
+        oneActive.children &&
+          oneActive.children.forEach((item) => {
+            newVal.forEach((sub) => {
+              if (item == sub.id) {
+                temp.push(sub);
+              }
+            });
+          });
+        this.childTab = temp;
+      },
+    },
+    taboneActive: {
+      immediate: true,
+      deep: true,
+      handler: function (newVal, oldVal) {
+        if (!newVal.id) {
+          this.taboneActive = this.info.catalogRoot[0];
+        }
+        let temp = [];
+        newVal.children &&
+          newVal.children.forEach((item) => {
+            this.info.catalogs.forEach((sub) => {
+              if (item == sub.id) {
+                temp.push(sub);
+              }
+            });
+          });
+        this.childTab = temp;
+        if (this.childTab.length == 1 || newVal != oldVal) {
+          this.tabtowActive = this.childTab[0];
+        }
+      },
+    },
+    tabtowActive: {
+      immediate: true,
+      handler: function (newVal) {
+        if (!newVal) {
+          this.tabtowActive = this.childTab[0];
+        } else {
+          this.$emit("catalog", newVal);
+          let arr = this.info.scenes.filter((item) => {
+            return newVal.id == item.category;
+          });
+          this.scenes = arr.sort((a,b)=>a.weight-b.weight)
+        }
+      },
+    },
+  },
+  methods: {
+    onRequestForAddLevel1Group() {
+      this.newGroupName = ''
+      this.addGroupLevel = 1
+    },
+    onConfirmAddingGroup() {
+      if (this.addGroupLevel === 1) {
+        let newGroupLevel2Id = 'c_' + this.$randomWord(true, 8, 8)
+        const newGroupLevel1 = {
+          id: 'r_' + this.$randomWord(true, 8, 8),
+          name: this.newGroupName,
+          children: [newGroupLevel2Id],
+        }
+        this.info.catalogRoot.push(newGroupLevel1)
+        this.info.catalogs.push({
+          newGroupLevel2Id,
+          name: '默认二级分组',
+        })
+      } else if (this.addGroupLevel === 2) {
+        let id = 'c_' + this.$randomWord(true, 8, 8)
+        let parent = this.info.catalogRoot.find((item) => {
+          return item.id === this.parentGroupId
+        })
+        parent.children.push(id);
+        const newGroupLevel2 = {
+          id,
+          name: this.newGroupName,
+        }
+        this.info.catalogs.push(newGroupLevel2);
+      }
+      this.$msg.success("操作成功")
+      this.newGroupName = ''
+      this.parentGroupId = ''
+      this.addGroupLevel = 0
+    },
+    onRequestForAddGroupLevel2(parentGroupId) {
+      this.addGroupLevel = 2
+      this.parentGroupId = parentGroupId
+    },
+    onRenameScene(sceneId, newName) {
+      const renameTarget = this.info.scenes.find((item) => {
+        return item.id === sceneId
+      })
+      renameTarget.sceneTitle = newName
+    },
+    onDeleteScene(sceneId) {
+      const deleteTargetIdx = this.info.scenes.findIndex((item) => {
+        return item.id === sceneId
+      })
+      this.info.scenes.splice(deleteTargetIdx, 1)
+      this.delFirstScene()
+      this.$msg.success("删除成功")
+    },
+    onRenameGroup(groupId, level, newName) {
+      if (level === 1) {
+        const target = this.info.catalogRoot.find((item) => {
+          return item.id === groupId
+        })
+        target.name = newName
+        this.$msg.success('操作成功')
+      } else if (level === 2) {
+        const target = this.info.catalogs.find((item) => {
+          return item.id === groupId
+        })
+        target.name = newName
+        this.$msg.success('操作成功')
+      } else {
+        console.error('invalid level!');
+      }
+    },
+    onDeleteGroup(groupId, groupLevel) {
+      const deleteGroupLevel2 = (groupId) => {
+        // 删除所属一级分组中的children中元素
+        // 因为用户无法看到、操作默认二级分组,所以这里要删除的group不可能是默认二级分组
+        let targetGroupIdxLevel2 = null
+        let belongGroupIdxLevel1 = null
+        for (const [groupIdxLevel1, groupLevel1] of this.info.catalogRoot.entries()) {
+          for (const [groupIdxLevel2, childId] of groupLevel1.children.entries()) {
+            if (childId === groupId) {
+              targetGroupIdxLevel2 = groupIdxLevel2
+              break
+            }
+          }
+          if (targetGroupIdxLevel2 !== null) {
+            belongGroupIdxLevel1 = groupIdxLevel1
+            break
+          }
+        }
+        if (targetGroupIdxLevel2 === null || belongGroupIdxLevel1 === null) {
+          console.log(targetGroupIdxLevel2, belongGroupIdxLevel1);
+          throw('一级分组列表中没有找到要删除的二级分组!')
+        }
+        this.info.catalogRoot[belongGroupIdxLevel1].children.splice(targetGroupIdxLevel2, 1)
+        
+        // 删除二级分组列表中元素
+        const targetIdx = this.info.catalogs.findIndex((item) => {
+          return item.id === groupId
+        })
+        if (targetIdx < 0) {
+          throw('二级分组列表中没有找到要删除的二级分组!')
+        }
+        this.info.catalogs.splice(targetIdx, 1)
+
+        // 删除场景列表中属于要删除的二级分组的那些场景
+        this.info.scenes = this.info.scenes.filter((item) => {
+          return item.category !== groupId
+        })
+        return
+      }
+
+      const backup = deepClone(this.info)
+      try {
+        if (groupLevel === 1) {
+          if (this.info.catalogRoot.length === 1) {
+            return this.$alert({
+              content: "请至少保留一个分组",
+              ok: () => {
+                return
+              },
+            })
+          }
+
+          const targetGroupIdx = this.info.catalogRoot.findIndex((groupLevel1) => {
+            return groupLevel1.id === groupId
+          })
+          if (targetGroupIdx < 0) {
+            throw('没有找到要删除的一级分组!')
+          }
+          // 删除该一级分组下的所有二级分组及属于这些二级分组的场景
+          for (const childId of this.info.catalogRoot[targetGroupIdx].children) {
+            deleteGroupLevel2(childId)
+          }
+          // 删除该一级分组
+          this.info.catalogRoot.splice(targetGroupIdx, 1)
+        } else if (groupLevel === 2) {
+          deleteGroupLevel2(groupId)
+        } else {
+          throw('group level invalid!');
+        }
+        // 酌情清空初始场景设置
+        this.delFirstScene()
+        this.$msg.success("删除成功")
+      } catch(e) {
+        console.error(e);
+        this.$msg.error('删除失败')
+        this.$store.commit("SetInfo", backup)
+      }
+    },
+
+    hadnleAddGroup() {
+      this.$emit("addGroup", { type: 1, oper: "add", item: {} });
+    },
+    
+    uploadListSort() {
+      this.scenes.forEach((item, i) => {
+        item.weight = i+1
+      });
+
+      this.info.scenes.forEach((item)=>{
+        this.scenes.forEach((sub, idx) => {
+          sub.weight = idx+1
+          if (item.sceneCode == sub.sceneCode) {
+            item = sub
+          }
+        });
+      })
+
+      this.$store.commit("SetInfo", this.info);
+    },
+
+    delTree(data, type) {
+      let fn = (ele) => {
+        let tmp = [];
+        this.info.catalogs.forEach((sub, i) => {
+          if (ele == sub.id) {
+            this.info.scenes.forEach((item) => {
+              if (sub.id != item.category) {
+                tmp.push(item);
+              }
+            });
+            this.info.scenes = tmp;
+            this.info.catalogs.splice(i, 1);
+          }
+        });
+      };
+      if (type == "one") {
+        data.children.forEach((ele) => {
+          fn(ele);
+        });
+        let idx = this.info.catalogRoot.findIndex((item) => item.id == data.id);
+        this.info.catalogRoot.splice(idx, 1);
+        this.taboneActive = this.info.catalogRoot[0];
+      }
+
+      if (type == "two") {
+        let id = this.taboneActive.id;
+        let oneActive = this.info.catalogRoot.find((item) => item.id == id);
+        let idx = oneActive.children.findIndex((item) => item == data.id);
+        oneActive.children.splice(idx, 1);
+        fn(data.id);
+
+        let temp = browser.CloneObject(this.taboneActive);
+        this.taboneActive = "";
+        this.taboneActive = temp;
+      }
+
+
+      this.delFirstScene()
+      this.$bus.emit('scenesChange')
+      this.$store.commit("SetInfo", this.info);
+    },
+
+    delFirstScene(){
+        if (this.info.firstScene) {
+          let firIdx = this.info.scenes.find(item=>{
+            return item.sceneCode == this.info.firstScene.sceneCode
+          });
+          !firIdx&&(this.info.firstScene='')
+        }
+    },
+
+    del(data, type) {
+      if (this.info.catalogRoot.length <= 1 && type == "one") {
+        return this.$alert({
+          content: "请至少保留一个分组",
+          ok: () => {
+            return;
+          },
+        });
+      }
+
+      this.$confirm({
+        content: `分组“${data.name}”下所有${
+          type == "one" ? "二级分组和" : ""
+        }(场景/全景图)也都将会被删除,是否删除?`,
+        ok: () => {
+          this.delTree(data, type);
+          this.$msg.success("删除成功")
+        },
+      });
+    },
+    delPano(item) {
+      this.$confirm({
+        content: `${item.type=='4dkk'?'场景':'全景图'}“${item.sceneTitle}”下所有设置也都将会被删除,是否删除?`,
+        ok: () => {
+          let idx = this.info.scenes.findIndex(ele=>ele.sceneCode==item.sceneCode)
+          this.info.scenes.splice(idx,1)
+          this.delFirstScene()
+          this.$store.commit("SetInfo", this.info);
+          this.$bus.emit('scenesChange')
+          this.$msg.success("删除成功")
+        },
+      });
+    },
+  },
+
+  mounted() {
+    this.$bus.on('getActive',data=>{
+      if (data.type == 1) {
+        this.taboneActive = data.willActive
+      } else{
+        this.tabtowActive = data.willActive
+      }
+    })
+  },
+};
+</script>
+<style lang="less" scoped>
+.group-settings {
+  display: flex;
+  flex-direction: column;
+  background: #252526;
+  position: relative;
+  > .ui-title-big {
+    margin-top: 24px;
+    margin-left: 20px;
+    margin-right: 20px;
+    margin-bottom: 0;
+    flex: 0 0 auto;
+    > .tool-tip-for-editor {
+      font-size: 12px;
+      cursor: default;
+      position: relative;
+      top: -1px;
+      font-weight: normal;
+    }
+  }
+  > .create-group-btn {
+    margin-top: 24px;
+    margin-left: 20px;
+    margin-right: 20px;
+    flex: 0 0 auto;
+    > i {
+      font-size: 14px;
+    }
+  }
+  > .obstructor {
+    position: absolute;
+    top: 108px;
+    left: 0px;
+    right: 0px;
+    height: 37px;
+    // background: red;
+    background: #252526;
+    z-index: 1;
+  }
+  > .scene-group-wrap {
+    flex: 1 1 auto;
+    height: 1px;
+    overflow: auto;
+    padding-top: 32px;
+    padding-bottom: 38px;
+    padding-left: 20px;
+    padding-right: 20px;
+    &::-webkit-scrollbar-button {
+      background-color: transparent; height: 38px;
+    }
+    .pano-con {
+      padding: 20px 30px;
+      height: calc(100vh - 250px);
+      .menu-con {
+        position: relative;
+        > .iconfont {
+          left: -20px;
+          top: 8px;
+          position: absolute;
+          cursor: pointer;
+        }
+        > .icon_forward {
+          right: 40px;
+          left: auto;
+        }
+        .sub-menu {
+          max-width: calc(100% - 70px);
+          position: relative;
+          overflow: hidden;
+          height: 40px;
+
+          > ul {
+            max-width: unset;
+            overflow: unset;
+            position: absolute;
+            left: 0;
+            transition: 0.3s ease all;
+            .fixed {
+              position: absolute;
+              right: 0;
+            }
+            // &::before{
+            //   width: calc(100% - 70px);
+            //   height: calc(100% - 10px);
+            //   pointer-events: none;
+            //   content: '';
+            //   background: linear-gradient(to right,rgba(#fff,0) 0%,rgba(#fff,0) 98%,rgba(#000,1) 100%);
+            //   position: absolute;
+            //   top: 0;
+            //   left: 0;
+            //   z-index: 999;
+            //   display: inline-block;
+            // }
+          }
+        }
+      }
+
+      > ul {
+        max-height: calc(100% - 82px);
+        overflow-y: auto;
+        position: relative;
+        left: -2px;
+        > li {
+          margin: 10px 20px 30px 0;
+        }
+      }
+      .add-btn {
+        z-index: 20;
+        .ui-button {
+          margin: 0 5px;
+        }
+      }
+      .no-record {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        text-align: center;
+        color: rgba(255, 255, 255, 0.5);
+        > i {
+          font-size: 40px;
+        }
+        > p {
+          margin-top: 10px;
+        }
+      }
+    }
+  }
+  
+  .ui-message {
+    > .ui-message-main {
+      > .name-input {
+        height: 36px;
+        background: #252526;
+        border-radius: 2px;
+        border: 1px solid #404040;
+        color: #fff;
+        font-size: 14px;
+        padding: 0 16px;
+        letter-spacing: 1px;
+        width: 100%;
+        &:focus {
+          border-color: #0076F6;
+        }
+      }
+    }
+  }
+}
+</style>

+ 416 - 0
packages/code/src/views/navigation/index.vue

@@ -0,0 +1,416 @@
+<template>
+  <div class="editor-navigation">
+    <GroupSettings
+      class="group-settings-area"
+      @catalog="data=>activeCataLog = data"
+      @addPano="onAddPano"
+      @addGroup="onAddGroup"
+      @addScene="onAddScene"
+      @rename="onRename"
+    />
+    <div class="preview-area"></div>
+    <InitialSceneSettings class="initial-scene-settings-area" @select="handleInitScene"></InitialSceneSettings>
+    <popup v-show="showAddGroup" :can-close="false">
+      <div class="ui-message ui-message-confirm dark" style="width: 400px">
+        <div class="ui-message-header">
+          <span>{{
+            currentTabAtri.oper == "add"
+              ? `新增${currentTabAtri.type == 1 ? "一" : "二"}级分组`
+              : `重命名${currentTabAtri.type == 1 ? "一" : "二"}级分组`
+          }}</span>
+          <span @click="showAddGroup = false">
+            <i class="iconfont icon_close"></i>
+          </span>
+        </div>
+        <div class="ui-message-main re-name">
+          <div>
+            <input
+              class="ui-input"
+              type="text"
+              maxlength="15"
+              placeholder="请输入分组名,限15个字"
+              v-model="currentTabAtri.name"
+            />
+          </div>
+        </div>
+
+        <div class="ui-message-footer">
+          <button class="ui-button cancel" @click="showAddGroup = false">
+            取消
+          </button>
+          <button
+            class="ui-button submit"
+            :class="{ disable: !currentTabAtri.name }"
+            @click="handleAddGroup()"
+          >
+            确定
+          </button>
+        </div>
+      </div>
+    </popup>
+
+    <popup v-show="showRename" :can-close="false">
+      <div class="ui-message ui-message-confirm dark" style="width: 400px">
+        <div class="ui-message-header">
+          <span>重命名</span>
+          <span @click="handleRenameClose">
+            <i class="iconfont icon_close"></i>
+          </span>
+        </div>
+        <div class="ui-message-main re-name">
+          <div>
+            <input
+              class="ui-input"
+              type="text"
+              maxlength="50"
+              placeholder="输入名字"
+              v-model="reNameItem.sceneTitle"
+            />
+          </div>
+        </div>
+
+        <div class="ui-message-footer">
+          <button class="ui-button cancel" @click="handleRenameClose">
+            取消
+          </button>
+          <button
+            class="ui-button submit"
+            :class="{ disable: !reNameItem.sceneTitle }"
+            @click="handleRename()"
+          >
+            确定
+          </button>
+        </div>
+      </div>
+    </popup>
+
+    <div class="dialog" style="z-index: 2000" v-if="showList">
+      <Table
+        :list="type == 'scene' ? sceneList : panoList"
+        :tabHeader="$MAPTABLEHEADER[type]"
+        @updateList="update"
+        @cancle="showList = false"
+        :title="type == 'scene' ? '选择三维场景' : '选择全景图'"
+        :primaryKey="type == 'scene' ? 'num' : 'id'"
+        @changeCurrent="changeCurrent"
+        :paging="paging"
+        @submit="handleSelect"
+      >
+      </Table>
+    </div>
+  </div>
+</template>
+<script>
+import InitialSceneSettings from "./initialSceneSettings.vue";
+import GroupSettings from "./groupSettings";
+import Popup from "@/components/shared/popup/index.vue";
+import {
+  getSceneList,
+  getMaterialList,
+} from "@/api";
+import Table from "@/components/tableSelect.vue";
+import { mapGetters } from "vuex";
+import { savePanoToWorks } from "@/api";
+import { changeByteUnit } from '@/utils/file'
+
+export default {
+  name: "EditorNavigation",
+  components: {
+    InitialSceneSettings,
+    GroupSettings,
+    Popup,
+    Table,
+  },
+  computed: {
+    ...mapGetters({
+      sceneList: "sceneList",
+      info: "info",
+      backupInfo: "backupInfo"
+    }),
+  },
+  data() {
+    return {
+      activeCataLog: "",
+      type: "scene",
+      currentTabAtri: "",
+      showAddGroup: false,
+      showRename: false,
+      showList: false,
+      showInitScene: true,
+      reNameItem: {
+        id: "",
+        sceneTitle: "",
+      },
+      key: "",
+      paging: {
+        pageSize: 8,
+        pageNum: 1,
+        total: 0,
+        showSize: 4,
+        current: 1,
+      },
+      panoList: [],
+    };
+  },
+  mounted() {},
+
+  watch: {
+    "paging.pageNum": function () {
+      this.type == "scene" ? this.getSceneList() : this.getMaterialList();
+    },
+    showList(newVal) {
+      if (!newVal) {
+        this.paging = {
+          pageSize: 8,
+          pageNum: 1,
+          total: 0,
+          showSize: 4,
+          current: 1,
+        };
+      }
+    },
+  },
+
+  methods: {
+    handleInitScene() {
+      this.showInitScene = true;
+    },
+    update(data) {
+      this.key = data;
+      this.type == "scene" ? this.getSceneList() : this.getMaterialList();
+    },
+    handleSelect(data) {
+     let params = ''
+      if (this.type == "scene") {
+        params = data.map((item) => {
+          return {
+            icon: item.thumb,
+            sceneCode: item.num,
+            sceneTitle: item.sceneName,
+            type: "4dkk",
+            category:this.activeCataLog.id,
+            id:'s_'+this.$randomWord(true,8,8)
+          };
+        });
+      } else {
+        params = data.map((item) => {
+          return {
+            icon: item.icon,
+            sceneCode: item.sceneCode,
+            sceneTitle: item.name,
+            category:this.activeCataLog.id,
+            type: "pano",
+            id:'s_'+this.$randomWord(true,8,8)
+          };
+        });
+      }
+
+      params.forEach((item,i) => {
+        let temp = this.info.scenes.find(sub=>sub.sceneCode == item.sceneCode)
+        if (temp) {
+          console.log(this.$msg);
+            setTimeout(() => {
+              this.$msg.message(`${item.type=='4dkk'?'场景':'全景图'}${item.sceneTitle}已存在,不可重复添加`);
+            }, i*100);
+            return
+        }
+        !temp&&this.info.scenes.push(item)
+      });
+
+      this.$bus.emit('scenesChange')
+      this.$store.commit("SetInfo", this.info);
+      this.showList = false;
+    },
+
+    changeCurrent(data) {
+      this.paging.pageNum = data;
+    },
+
+    savePanoToWorks(data) {
+      savePanoToWorks(data, () => {
+        this.$bus.emit("refresh");
+      });
+    },
+
+    onRename(data) {
+      this.reNameItem = data;
+      this.showRename = true;
+    },
+
+    handleRenameClose(){
+      this.showRename = false
+      this.$bus.emit('scenesChange')
+      this.$store.commit("SetInfo", this.backupInfo);
+    },
+
+    handleRename() {
+      if (!this.reNameItem.sceneTitle.trim()) {
+        return this.$alert({ content: "请输入名字" });
+      }
+      this.$msg.success("重命名成功")
+      this.$store.commit("SetInfo", this.info);
+      this.showRename = false;
+    },
+
+    onAddGroup(data) {
+      this.showAddGroup = true;
+      this.currentTabAtri = { ...data, name: data.item.name || "" };
+    },
+
+    handleAddGroup() {
+      if (!this.currentTabAtri.name.trim()) {
+        return this.$alert({ content: "请输入名字" });
+      }
+      let willActive = ''
+      let tmp = this.currentTabAtri.item;
+      if (this.currentTabAtri.oper == "edit") {
+        tmp.name = this.currentTabAtri.name;
+      } else {
+        if (this.currentTabAtri.type == 1) {
+          let id = 'c_'+this.$randomWord(true,8,8)
+          willActive = {
+            id: 'r_'+this.$randomWord(true,8,8),
+            name: this.currentTabAtri.name,
+            children: [id],
+          }
+          this.info.catalogRoot.push(willActive);
+          this.info.catalogs.push({
+            id,
+            name: '默认二级分组',
+          });
+          console.log(this.info.catalogs);
+        }
+        if (this.currentTabAtri.type == 2) {
+          let id = 'c_'+this.$randomWord(true,8,8)
+          let item = this.info.catalogRoot.find(
+            (item) => item.id == tmp.parentId
+          );
+          item.children.push(id);
+          willActive = {
+            id,
+            name: this.currentTabAtri.name,
+          }
+
+          this.info.catalogs.push(willActive);
+        }
+      }
+
+      this.$bus.emit('scenesChange')
+      this.$store.commit("SetInfo", this.info);
+      this.$msg.success("操作成功")
+      this.showAddGroup = false;
+
+      if (this.currentTabAtri.oper != "edit") {
+        this.$bus.emit('getActive',{
+          type:this.currentTabAtri.type,
+          willActive
+        })
+      }
+    },
+
+    onAddScene() {
+      this.type = "scene";
+      this.getSceneList();
+      this.showList = true;
+    },
+
+    onAddPano() {
+      this.type = "pano";
+      this.getMaterialList();
+      this.showList = true;
+    },
+
+    getSceneList() {
+      getSceneList(
+        {
+          pageNum: this.paging.pageNum,
+          pageSize: this.paging.pageSize,
+          searchKey: this.key,
+        },
+        (data) => {
+          let { list, total } = data.data.data;
+          this.paging.total = total;
+
+          
+
+          this.$store.commit("SetSceneList", list);
+        }
+      );
+    },
+
+    getMaterialList() {
+      getMaterialList(
+        {
+          pageNum: this.paging.pageNum,
+          pageSize: this.paging.pageSize,
+          searchKey: this.key,
+          type: this.type,
+          urlSelect: true,
+        },
+        (data) => {
+          this.paging.pageNum = data.data.pageNum;
+          this.paging.pageSize = data.data.pageSize;
+          this.paging.total = data.data.total;
+          this.panoList = data.data.list.map(i=>{
+            i.fileSize = changeByteUnit(Number(i.fileSize))
+            i.createTime = i.createTime.substring(0,i.createTime.length-3)
+            i.updateTime = i.updateTime.substring(0,i.updateTime.length-3)
+            return i
+          })
+        }
+      );
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.editor-navigation {
+  height: 100%;
+  display: flex;
+  .group-settings-area {
+    width: 300px;
+    flex: 0 0 auto;
+  }
+  .preview-area {
+    background: red;
+    flex: 1 0 auto;
+  }
+  .initial-scene-settings-area {
+    width: 274px;
+    flex: 0 0 auto;
+  }
+  .dialog {
+    position: fixed;
+    z-index: 30;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.5);
+  }
+  .pano-con {
+    height: auto;
+    background: none;
+    padding: 10px 0;
+    .ui-remark {
+      padding-left: 10px;
+    }
+    > ul {
+      > li {
+        cursor: pointer;
+      }
+    }
+  }
+  .re-name {
+    width: 80%;
+    margin: 40px auto;
+  }
+  .add-vr {
+    text-align: left;
+    .ui-remark {
+      margin: 10px 0;
+    }
+  }
+}
+</style>

+ 126 - 0
packages/code/src/views/navigation/initialSceneSettings.vue

@@ -0,0 +1,126 @@
+<template>
+  <div class="initial-scene-settings" app-border dir-left>
+    <div class="initial-scene-settings__title">
+      初始场景
+      <i class="iconfont icon-material_prompt tool-tip-for-editor" v-tooltip="'初始场景为查看链接时进入的第一个场景,未设\n置时,不固定从某一场景打开。'">
+      </i>
+    </div>
+    <div class="preview">
+      <img v-if="info.firstScene" :src="info.firstScene.icon" alt="">
+      <div class="tips" v-else>
+        <i class="iconfont iconphotoview" style="font-size: 40px"></i>
+      </div>
+    </div>
+    <div class="setinit" v-if="info.firstScene">
+      <button class="ui-button" @click="deleteIndexInfo">删除场景</button>
+      <button @click="showInitScene=true" class="ui-button submit" :class="{disable:false}">
+        修改场景
+      </button>
+    </div>
+
+    <template v-else>
+      <div class="setinit">
+        <button style="width:100%" @click="showInitScene=true" class="ui-button submit" :class="{disable:false}">
+          设置初始场景
+        </button>
+      </div>
+      <div class="ui-remark">初始场景为查看链接时进入的第一个场景,未设置时,不固定从某一场景打开</div>
+    </template>
+
+    <div class="dialog" style="z-index: 2000" v-if="showInitScene">
+      <Select
+        @cancle="showInitScene = false"
+        :selected='info.firstScene'
+        :title="'选择素材'"
+        @submit="handleSelect"
+      >
+      </Select>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+import Select from "@/components/select";
+
+export default {
+  components:{
+    Select,
+  },
+  data(){
+    return {
+      showInitScene:false
+    }
+  },
+  methods:{
+    deleteIndexInfo(){
+      this.$confirm({
+          content: "是否删除?",
+          ok: () => {
+              this.info.firstScene = ''
+              this.$store.commit("SetInfo", this.info);
+              this.$msg.success('删除成功')
+          }
+      });
+      
+    },
+    handleSelect(data){
+      this.info.firstScene = data
+      this.$store.commit("SetInfo", this.info);
+      console.log(this.info.firstScene);
+      this.showInitScene=false
+    }
+  },
+  computed: {
+    ...mapGetters({
+      info: "info",
+      backupInfo: "backupInfo"
+    })
+  },
+  mounted(){
+ 
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.initial-scene-settings {
+  padding: 20px;
+  > .initial-scene-settings__title {
+    font-size: 18px;
+    color: #fff;
+    > i {
+      font-size: 12px;
+      position: relative;
+      top: -2px;
+    }
+  }
+  .preview {
+    width: 100%;
+    height: 102px;
+    overflow: hidden;
+    >img{
+      width: 100%;
+      height: 100%;
+    }
+  }
+  .setinit {
+    width: 100%;
+    margin: 15px 0;
+    display: flex;
+    justify-content: space-between;
+    .ui-button {
+      width: 48%;
+    }
+  }
+  .dialog {
+    position: fixed;
+    z-index: 30;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.5);
+  }
+}
+</style>

+ 12 - 22
packages/code/vue.config.js

@@ -1,9 +1,5 @@
-// const proxy_url = process.env.VUE_APP_PROXY_URL || 'http://8.135.98.231:8084'
-const proxy_url = process.env.VUE_APP_PROXY_URL || 'https://www.4dkankan.com'
-const proxy_4dkankan_url = process.env.VUE_APP_PROXY_4DKANKAN_URL || 'https://www.4dkankan.com'
-const path = require('path')
-
-const isDev = process.env.NODE_ENV === 'development'
+const proxy_url = process.env.VUE_APP_PROXY_URL || 'https://fcb.test.4dkankan.com'
+// https://vr-web01-uat.fcb.com.cn/
 let pages = {
   edit: 'src/pages/edit.js',
   show: 'src/pages/show.js',
@@ -16,7 +12,6 @@ module.exports = {
   pages: pages,
   assetsDir: process.env.VUE_APP_STATIC_DIR,
   publicPath: process.env.NODE_ENV === "production" ? "/panorama/" : "",
-  outputDir: isDev ? 'dist' : path.resolve('../../dist'),
   productionSourceMap: process.env.NODE_ENV !== "production",
   lintOnSave: false,
   css: {
@@ -34,23 +29,18 @@ module.exports = {
     hot: true,
     liveReload: false,
     proxy: {
-        '/manage': {
-            target: proxy_url,
-            changeOrigin: true,
-        },
-        '/web': {
+      '/manage': {
           target: proxy_url,
           changeOrigin: true,
-        },
-        '/api': {
-          target: proxy_4dkankan_url,
-          changeOrigin: true,
-        },
-        "/service": {
-          // target: "https://v4-test.4dkankan.com/",
-          target: proxy_4dkankan_url,
-          changeOrigin: true,
-        },
+      },
+      '/web': {
+        target: proxy_url,
+        changeOrigin: true,
+      },
+      '/api': {
+        target: process.env.VUE_APP_PROXY_URL_ROOT,
+        changeOrigin: true,
+      }
     },
   }
 };