瀏覽代碼

Merge branch 'dev'

tremble 3 年之前
父節點
當前提交
06ec101d91
共有 53 個文件被更改,包括 2365 次插入1117 次删除
  1. 141 3
      packages/qjkankan-editor/public/static/lib/iconfontQJ1.1.0/demo_index.html
  2. 27 3
      packages/qjkankan-editor/public/static/lib/iconfontQJ1.1.0/iconfont.css
  3. 1 1
      packages/qjkankan-editor/public/static/lib/iconfontQJ1.1.0/iconfont.js
  4. 42 0
      packages/qjkankan-editor/public/static/lib/iconfontQJ1.1.0/iconfont.json
  5. 二進制
      packages/qjkankan-editor/public/static/lib/iconfontQJ1.1.0/iconfont.ttf
  6. 二進制
      packages/qjkankan-editor/public/static/lib/iconfontQJ1.1.0/iconfont.woff
  7. 二進制
      packages/qjkankan-editor/public/static/lib/iconfontQJ1.1.0/iconfont.woff2
  8. 43 10
      packages/qjkankan-editor/src/Store/index.js
  9. 14 0
      packages/qjkankan-editor/src/assets/images/icons/link-dark.svg
  10. 14 0
      packages/qjkankan-editor/src/assets/images/icons/link.svg
  11. 15 0
      packages/qjkankan-editor/src/assets/images/icons/navs/explanation_normal.svg
  12. 15 0
      packages/qjkankan-editor/src/assets/images/icons/navs/explanation_selected.svg
  13. 19 0
      packages/qjkankan-editor/src/assets/images/icons/navs/viewpoint_normal.svg
  14. 19 0
      packages/qjkankan-editor/src/assets/images/icons/navs/viewpoint_selected.svg
  15. 14 0
      packages/qjkankan-editor/src/assets/images/icons/phone-dark.svg
  16. 14 0
      packages/qjkankan-editor/src/assets/images/icons/phone.svg
  17. 二進制
      packages/qjkankan-editor/src/assets/images/pano-image-placeholder.png
  18. 1 1
      packages/qjkankan-editor/src/components/Snapshot.vue
  19. 393 0
      packages/qjkankan-editor/src/components/insertPositionTipInEditor.vue
  20. 130 10
      packages/qjkankan-editor/src/components/materialSelectorForEditor.vue
  21. 503 0
      packages/qjkankan-editor/src/components/materialSelectorFromWorkForEditor.vue
  22. 177 21
      packages/qjkankan-editor/src/components/sceneGroupInEditor.vue
  23. 26 3
      packages/qjkankan-editor/src/components/sceneInGroupInEditor.vue
  24. 1 0
      packages/qjkankan-editor/src/components/select.vue
  25. 20 5
      packages/qjkankan-editor/src/config/menu.js
  26. 20 3
      packages/qjkankan-editor/src/directives/vTitleInEditor.js
  27. 47 1
      packages/qjkankan-editor/src/directives/vTooltipInEditor.js
  28. 7 54
      packages/qjkankan-editor/src/framework/EditorHead.vue
  29. 24 55
      packages/qjkankan-editor/src/framework/EditorMain.vue
  30. 1 1
      packages/qjkankan-editor/src/framework/Main.vue
  31. 1 1
      packages/qjkankan-editor/src/framework/SettingPC.vue
  32. 1 12
      packages/qjkankan-editor/src/framework/Toolbar.vue
  33. 1 0
      packages/qjkankan-editor/src/framework/core/index.vue
  34. 2 1
      packages/qjkankan-editor/src/mixins/index.js
  35. 1 1
      packages/qjkankan-editor/src/pages/edit.js
  36. 101 0
      packages/qjkankan-editor/src/router/editorRouter.js
  37. 10 7
      packages/qjkankan-editor/src/utils/other.js
  38. 27 0
      packages/qjkankan-editor/src/utils/request.js
  39. 16 7
      packages/qjkankan-editor/src/views/base/Toolbar.vue
  40. 7 0
      packages/qjkankan-editor/src/views/base/backgroundMusicSettings.vue
  41. 65 36
      packages/qjkankan-editor/src/views/base/customButtonSettings.vue
  42. 9 1
      packages/qjkankan-editor/src/views/base/customMaskSettings.vue
  43. 6 1
      packages/qjkankan-editor/src/views/base/openingAnimationSettings.vue
  44. 4 0
      packages/qjkankan-editor/src/views/base/passwordSettings.vue
  45. 178 0
      packages/qjkankan-editor/src/views/explanation/explanationSettings.vue
  46. 30 0
      packages/qjkankan-editor/src/views/explanation/index.vue
  47. 25 17
      packages/qjkankan-editor/src/views/hotspot/index.vue
  48. 1 0
      packages/qjkankan-editor/src/views/information/index.vue
  49. 42 409
      packages/qjkankan-editor/src/views/navigation/groupSettings.vue
  50. 11 387
      packages/qjkankan-editor/src/views/navigation/index.vue
  51. 45 39
      packages/qjkankan-editor/src/views/navigation/initialSceneSettings.vue
  52. 34 20
      packages/qjkankan-editor/src/views/screen/Setting.vue
  53. 20 7
      packages/qjkankan-editor/src/views/screen/index.vue

+ 141 - 3
packages/qjkankan-editor/public/static/lib/iconfontQJ1.1.0/demo_index.html

@@ -55,6 +55,42 @@
           <ul class="icon_lists dib-box">
           
             <li class="dib">
+              <span class="icon iconfont">&#xe6f4;</span>
+                <div class="name">eye_on</div>
+                <div class="code-name">&amp;#xe6f4;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe6f5;</span>
+                <div class="name">eye_off</div>
+                <div class="code-name">&amp;#xe6f5;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe67a;</span>
+                <div class="name">show_more_finish</div>
+                <div class="code-name">&amp;#xe67a;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe67c;</span>
+                <div class="name">show_more_music</div>
+                <div class="code-name">&amp;#xe67c;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe67e;</span>
+                <div class="name">show_more</div>
+                <div class="code-name">&amp;#xe67e;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe67f;</span>
+                <div class="name">show_more_vr</div>
+                <div class="code-name">&amp;#xe67f;</div>
+              </li>
+          
+            <li class="dib">
               <span class="icon iconfont">&#xe6f3;</span>
                 <div class="name">top</div>
                 <div class="code-name">&amp;#xe6f3;</div>
@@ -750,9 +786,9 @@
 <pre><code class="language-css"
 >@font-face {
   font-family: 'iconfont';
-  src: url('iconfont.woff2?t=1642757115929') format('woff2'),
-       url('iconfont.woff?t=1642757115929') format('woff'),
-       url('iconfont.ttf?t=1642757115929') format('truetype');
+  src: url('iconfont.woff2?t=1657783922543') format('woff2'),
+       url('iconfont.woff?t=1657783922543') format('woff'),
+       url('iconfont.ttf?t=1657783922543') format('truetype');
 }
 </code></pre>
           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -779,6 +815,60 @@
         <ul class="icon_lists dib-box">
           
           <li class="dib">
+            <span class="icon iconfont icon-eye_on"></span>
+            <div class="name">
+              eye_on
+            </div>
+            <div class="code-name">.icon-eye_on
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-eye_off"></span>
+            <div class="name">
+              eye_off
+            </div>
+            <div class="code-name">.icon-eye_off
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-show_more_finish"></span>
+            <div class="name">
+              show_more_finish
+            </div>
+            <div class="code-name">.icon-show_more_finish
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-show_more_music"></span>
+            <div class="name">
+              show_more_music
+            </div>
+            <div class="code-name">.icon-show_more_music
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-show_more"></span>
+            <div class="name">
+              show_more
+            </div>
+            <div class="code-name">.icon-show_more
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-show_more_vr"></span>
+            <div class="name">
+              show_more_vr
+            </div>
+            <div class="code-name">.icon-show_more_vr
+            </div>
+          </li>
+          
+          <li class="dib">
             <span class="icon iconfont icon-top"></span>
             <div class="name">
               top
@@ -1824,6 +1914,54 @@
           
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-eye_on"></use>
+                </svg>
+                <div class="name">eye_on</div>
+                <div class="code-name">#icon-eye_on</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-eye_off"></use>
+                </svg>
+                <div class="name">eye_off</div>
+                <div class="code-name">#icon-eye_off</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-show_more_finish"></use>
+                </svg>
+                <div class="name">show_more_finish</div>
+                <div class="code-name">#icon-show_more_finish</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-show_more_music"></use>
+                </svg>
+                <div class="name">show_more_music</div>
+                <div class="code-name">#icon-show_more_music</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-show_more"></use>
+                </svg>
+                <div class="name">show_more</div>
+                <div class="code-name">#icon-show_more</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-show_more_vr"></use>
+                </svg>
+                <div class="name">show_more_vr</div>
+                <div class="code-name">#icon-show_more_vr</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
                   <use xlink:href="#icon-top"></use>
                 </svg>
                 <div class="name">top</div>

+ 27 - 3
packages/qjkankan-editor/public/static/lib/iconfontQJ1.1.0/iconfont.css

@@ -1,8 +1,8 @@
 @font-face {
   font-family: "iconfont"; /* Project id 2947721 */
-  src: url('iconfont.woff2?t=1642757115929') format('woff2'),
-       url('iconfont.woff?t=1642757115929') format('woff'),
-       url('iconfont.ttf?t=1642757115929') format('truetype');
+  src: url('iconfont.woff2?t=1657783922543') format('woff2'),
+       url('iconfont.woff?t=1657783922543') format('woff'),
+       url('iconfont.ttf?t=1657783922543') format('truetype');
 }
 
 .iconfont {
@@ -13,6 +13,30 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-eye_on:before {
+  content: "\e6f4";
+}
+
+.icon-eye_off:before {
+  content: "\e6f5";
+}
+
+.icon-show_more_finish:before {
+  content: "\e67a";
+}
+
+.icon-show_more_music:before {
+  content: "\e67c";
+}
+
+.icon-show_more:before {
+  content: "\e67e";
+}
+
+.icon-show_more_vr:before {
+  content: "\e67f";
+}
+
 .icon-top:before {
   content: "\e6f3";
 }

文件差異過大導致無法顯示
+ 1 - 1
packages/qjkankan-editor/public/static/lib/iconfontQJ1.1.0/iconfont.js


+ 42 - 0
packages/qjkankan-editor/public/static/lib/iconfontQJ1.1.0/iconfont.json

@@ -6,6 +6,48 @@
   "description": "全景看看1.1.0",
   "glyphs": [
     {
+      "icon_id": "30581503",
+      "name": "eye_on",
+      "font_class": "eye_on",
+      "unicode": "e6f4",
+      "unicode_decimal": 59124
+    },
+    {
+      "icon_id": "30581504",
+      "name": "eye_off",
+      "font_class": "eye_off",
+      "unicode": "e6f5",
+      "unicode_decimal": 59125
+    },
+    {
+      "icon_id": "19543927",
+      "name": "show_more_finish",
+      "font_class": "show_more_finish",
+      "unicode": "e67a",
+      "unicode_decimal": 59002
+    },
+    {
+      "icon_id": "19543929",
+      "name": "show_more_music",
+      "font_class": "show_more_music",
+      "unicode": "e67c",
+      "unicode_decimal": 59004
+    },
+    {
+      "icon_id": "19543931",
+      "name": "show_more",
+      "font_class": "show_more",
+      "unicode": "e67e",
+      "unicode_decimal": 59006
+    },
+    {
+      "icon_id": "19543932",
+      "name": "show_more_vr",
+      "font_class": "show_more_vr",
+      "unicode": "e67f",
+      "unicode_decimal": 59007
+    },
+    {
       "icon_id": "27373469",
       "name": "top",
       "font_class": "top",

二進制
packages/qjkankan-editor/public/static/lib/iconfontQJ1.1.0/iconfont.ttf


二進制
packages/qjkankan-editor/public/static/lib/iconfontQJ1.1.0/iconfont.woff


二進制
packages/qjkankan-editor/public/static/lib/iconfontQJ1.1.0/iconfont.woff2


+ 43 - 10
packages/qjkankan-editor/src/Store/index.js

@@ -31,6 +31,12 @@ const store = new Vuex.Store({
     uploadStatusListImage: [],
     uploadStatusListPano: [],
     uploadStatusListVideo: [],
+
+    // 编辑器-导航-场景导航中,拖拽时要用到
+    editorNavDragInfo: {
+      type: '', // 'topologyGroupLevel1': 拓扑结构中一级分组;'topologyGroupLevel2': 拓扑结构中二级分组;'scene': 场景(全景图和三维场景)
+      node: {},
+    }
   },
   getters: {
     userAvatar: state => state.userAvatar,
@@ -51,19 +57,31 @@ const store = new Vuex.Store({
         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]
+              const itemLevel2 = deepClone(catalogsItem)
+              itemLevel2.parentId = itemLevel1.id // 看起来,vuex getter中的数据如果存在循环引用,在通过mutation转而再存储到vuex中时,会导致调用栈溢出,原因难道是vuex mutation在深拷贝时没有考虑循环引用的情况?所以这里不进行循环引用,只记录parent的id。
               itemLevel2.children = []
-              // 对于每个三级分类
-              for (const itemLevel3 of state.info.scenes) {
+              itemLevel1.childrenTemp.push(itemLevel2)
+              // 对于每个三级元素
+              for (const sceneItem of state.info.scenes) {
                 // 如果属于上述二级分类
-                if (itemLevel2.id === itemLevel3.category /* 注意拼写!!! */) {
-                  itemLevel2.children.push(deepClone(itemLevel3))
+                if (itemLevel2.id === sceneItem.category /* 注意拼写!!! */) {
+                  const itemLevel3 = deepClone(sceneItem)
+                  itemLevel3.parentId = itemLevel2.id  // 看起来,vuex getter中的数据如果存在循环引用,在通过mutation转而再存储到vuex中时,会导致调用栈溢出,原因难道是vuex mutation在深拷贝时没有考虑循环引用的情况?所以这里不进行循环引用,只记录parent的id。
+                  itemLevel2.children.push(itemLevel3)
                 }
               }
-              
+              // 按weight排序
+              itemLevel2.children.sort((a, b) => {
+                if (a.weight === undefined && b.weight === undefined) {
+                  return 0
+                } else if (a.weight !== undefined && b.weight === undefined) {
+                  return -1
+                } else if (a.weight === undefined && b.weight !== undefined) {
+                  return 1
+                } else {
+                  return a.weight - b.weight
+                }
+              })
               break
             }
           }
@@ -90,6 +108,8 @@ const store = new Vuex.Store({
     uploadStatusListImage: state => state.uploadStatusListImage,
     uploadStatusListPano: state => state.uploadStatusListPano,
     uploadStatusListVideo: state => state.uploadStatusListVideo,
+
+    editorNavDragInfo: state => state.editorNavDragInfo,
   },
   mutations: {
     SetUserAvatar(state, avatar) {
@@ -146,8 +166,21 @@ const store = new Vuex.Store({
     },
     BackupHotSpot(state, data) {
       state.backupHotSpot = data
-    }
+    },
 
+    setEditorNavDragType(state, data) {
+      if (!['topologyGroupLevel1', 'topologyGroupLevel2', 'scene'].includes(data)) {
+        throw("拖拽类型必须是'topologyGroupLevel1', 'topologyGroupLevel2', 'scene'之一!")
+      }
+      state.editorNavDragInfo.type = data
+    },
+    setEditorNavDragNode(state, data) {
+      state.editorNavDragInfo.node = data
+    },
+    clearEditorNavDragInfo(state) {
+      state.editorNavDragInfo.type = ''
+      state.editorNavDragInfo.node = {}
+    }
   },
   actions: {
     refreshUserInfo(context) {

文件差異過大導致無法顯示
+ 14 - 0
packages/qjkankan-editor/src/assets/images/icons/link-dark.svg


文件差異過大導致無法顯示
+ 14 - 0
packages/qjkankan-editor/src/assets/images/icons/link.svg


文件差異過大導致無法顯示
+ 15 - 0
packages/qjkankan-editor/src/assets/images/icons/navs/explanation_normal.svg


文件差異過大導致無法顯示
+ 15 - 0
packages/qjkankan-editor/src/assets/images/icons/navs/explanation_selected.svg


文件差異過大導致無法顯示
+ 19 - 0
packages/qjkankan-editor/src/assets/images/icons/navs/viewpoint_normal.svg


文件差異過大導致無法顯示
+ 19 - 0
packages/qjkankan-editor/src/assets/images/icons/navs/viewpoint_selected.svg


文件差異過大導致無法顯示
+ 14 - 0
packages/qjkankan-editor/src/assets/images/icons/phone-dark.svg


文件差異過大導致無法顯示
+ 14 - 0
packages/qjkankan-editor/src/assets/images/icons/phone.svg


二進制
packages/qjkankan-editor/src/assets/images/pano-image-placeholder.png


+ 1 - 1
packages/qjkankan-editor/src/components/Snapshot.vue

@@ -6,7 +6,7 @@
             enter-active-class="animated fadeIn faster"
             leave-active-class="animated fadeOut faster"
         >
-            <div class="flash" v-if="showFlash"></div>
+        <div class="flash" v-if="showFlash"></div>
         </transition>
         <div class="frame show">
             <div class="corner left-top"></div>

+ 393 - 0
packages/qjkankan-editor/src/components/insertPositionTipInEditor.vue

@@ -0,0 +1,393 @@
+<template>
+  <div
+    class="insert-position-tip"
+    @dragenter="onDragEnter"
+    @dragover="onDragOver"
+    @dragleave="onDragLeave"
+    @drop="onDrop"
+    :style="{
+      marginLeft: marginLeft,
+    }"
+  >
+    {{positionDebug}}
+  </div>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+import { deepClone } from "@/utils/other.js";
+
+export default {
+  props: {
+    index: {
+      type: Number,
+      required: true,
+    },
+    positionDebug: {
+      type: String,
+      default: '',
+    },
+    indentLevel: {
+      type: Number,
+      default: 1,
+    },
+    topologyLevel: {
+      type: Number,
+      default: 1,
+    },
+    parentNode: {
+      type: Object,
+      default: null,
+    },
+  },
+  computed: {
+    ...mapGetters({
+      info: 'info',
+      dragInfo: 'editorNavDragInfo',
+    }),
+    marginLeft() {
+      return (this.indentLevel - 1) * 12 + 'px' 
+    },
+  },
+  methods: {
+    canDrop() {
+      switch (this.dragInfo.type) {
+        case 'scene': // 被拖拽的是场景
+          if (this.topologyLevel === 1) {
+            // console.log('情况1:被拖拽的是场景,拖拽到一级分组列表');
+            return false
+          } else if (this.topologyLevel === 2) {
+            // console.log('情况2:被拖拽的是场景,拖拽到一级分组中');
+            return false
+          } else if (this.topologyLevel === 3) {
+            // console.log('情况3:被拖拽的是场景,拖拽到二级分组中');
+            return true
+          } else {
+            console.error('情况4:不该出现');
+            return false
+          }
+        case 'topologyGroupLevel2': // 被拖拽的是拓扑结构中二级分组
+          if (this.topologyLevel === 1) {
+            // console.log('情况5:被拖拽的是拓扑结构中二级分组,拖拽到一级分组列表');
+            return true
+          } else if (this.topologyLevel === 2) {
+            // console.log('情况6:被拖拽的是拓扑结构中二级分组,拖拽到一级分组中');
+            return true
+          } else if (this.topologyLevel === 3) {
+            if (this.indentLevel === 2) {
+              // console.log('情况7:被拖拽的是拓扑结构中二级分组,拖拽到隐藏的默认二级分组中');
+              return true
+            } else {
+              // console.log('情况8:被拖拽的是拓扑结构中二级分组,拖拽到普通的二级分组中');
+              return false
+            }
+          } else {
+            console.error('情况9:不该出现');
+            return false
+          }
+        case 'topologyGroupLevel1': // 被拖拽的是拓扑结构中一级分组
+          if (this.topologyLevel === 1) {
+            // console.log('情况10:被拖拽的是拓扑结构中一级分组,拖拽到一级分组列表');
+            return true
+          } else if (this.topologyLevel === 2) { // 拖拽到一级分组中
+            if (this.dragInfo.node.children.length === 1 && this.dragInfo.node.children[0].name === '默认二级分组') {
+              // console.log('情况11:被拖拽的一级分组只有一个隐藏的默认二级分组,拖拽到一级分组中');
+              return true
+            } else {
+              // console.log('情况12:被拖拽的一级分组并非只有一个隐藏的默认二级分组,拖拽到一级分组中');
+              return false
+            }
+          } else if (this.topologyLevel === 3) { // 拖拽到二级分组中
+            if (this.indentLevel === 2) { // 拖拽到隐藏的默认二级分组中
+              if (this.dragInfo.node.children.length === 1 && this.dragInfo.node.children[0].name === '默认二级分组') {
+                if (this.dragInfo.node.children[0].id === this.parentNode?.id) {
+                  // console.log('情况13:被拖拽的一级分组只有一个隐藏的默认二级分组,拖拽到自身的默认二级分组中');
+                  return false
+                } else {
+                  // console.log('情况14:被拖拽的一级分组只有一个隐藏的默认二级分组,拖拽到另一个一级分组中隐藏的默认二级分组中');
+                  return true
+                }
+              } else {
+                // console.log('情况15:被拖拽的一级分组并非只有一个隐藏的默认二级分组,拖拽到隐藏的默认二级分组中');
+                return false
+              }
+            } else {
+              // console.log('情况16:被拖拽的是拓扑结构中一级分组,拖拽到普通二级分组中')
+              return false
+            }
+          } else {
+            console.error('情况17:不该出现');
+            return false
+          }
+        default:
+          console.error('情况18:不该出现');
+          return false
+      }
+    },
+    onDragEnter(e) {
+      if (!this.canDrop()) {
+        return
+      } else {
+        e.preventDefault()
+        e.target.style.backgroundColor = '#0076f6'
+        e.dataTransfer.dropEffect = 'move'
+      }
+    },
+    onDragOver(e) {
+      if (!this.canDrop()) {
+        return
+      } else {
+        e.preventDefault()
+        e.dataTransfer.dropEffect = 'move'
+      }
+    },
+    onDragLeave(e) {
+      // e.preventDefault()
+      e.target.style.backgroundColor = ''
+    },
+    onDrop(e) {
+      // e.preventDefault()
+      e.target.style.backgroundColor = ''
+
+      switch (this.dragInfo.type) {
+        case 'scene': // 被拖拽的是场景
+          {
+            // console.log('情况3:被拖拽的是场景,插入到二级分组中')
+
+            /**
+             * 确定要插入的分组中各场景应有的weight(从1开始递增,小的先显示)
+             */
+            const belongGroupCopy = deepClone(this.parentNode.children)
+            const draggedNodeCopy = deepClone(this.dragInfo.node)
+            draggedNodeCopy.isCopy = true
+            belongGroupCopy.splice(this.index, 0, draggedNodeCopy)
+            const toDeleteIndex = belongGroupCopy.findIndex((item) => {
+              return (item.id === this.dragInfo.node.id && !item.isCopy)
+            })
+            if (toDeleteIndex >= 0) {
+              belongGroupCopy.splice(toDeleteIndex, 1)
+            }
+            for (let [index, elem] of belongGroupCopy.entries()) {
+              elem.weight = index + 1
+            }
+
+            /**
+             * 真正修改场景原始数据的weight和所属分组
+             */
+            for (const eachSceneCopy of belongGroupCopy) {
+              for (const eachScene of this.info.scenes) {
+                if (eachSceneCopy.id === eachScene.id) {
+                  this.$set( eachScene, 'weight', eachSceneCopy.weight )
+                }
+                if (this.dragInfo.node.id === eachScene.id) {
+                  eachScene.category = this.parentNode.id // 注意category拼写!
+                }
+              }
+            }
+          }
+          break;
+        case 'topologyGroupLevel2': // 被拖拽的是拓扑结构中二级分组
+          if (this.topologyLevel === 1) {
+            // console.log('情况5:被拖拽的是拓扑结构中二级分组,拖拽到一级分组列表');
+
+            /**
+             * 在拖拽到的位置新增一个一级分组,name是原二级分组的name;唯一child的id赋值为被拖拽的二级分组的id
+             */
+            const newGroupLevel1 = {
+              id: 'r_' + this.$randomWord(true, 8, 8),
+              name: this.dragInfo.node.name,
+              children: [this.dragInfo.node.id],
+            }
+            this.info.catalogRoot.splice(this.index, 0, newGroupLevel1)
+
+            // 被拖拽的二级分组名称改为“默认二级分组”;
+            const draggedGroup = this.info.catalogs.find((item) => {
+              return item.id === this.dragInfo.node.id
+            })
+            draggedGroup.name = '默认二级分组'
+
+            /**
+             * 被拖拽的二级分组原属的一级分组的children中,删除【被拖拽的二级分组对应的元素】
+             */
+            const parentGroup = this.info.catalogRoot.find((item) => {
+              return item.id === this.dragInfo.node.parentId
+            })
+            const idxToDelete = parentGroup.children.findIndex((id) => {
+              return id === this.dragInfo.node.id
+            })
+            parentGroup.children.splice(idxToDelete, 1)
+            
+            // 如果被拖拽的二级分组原属的一级分组中没有任何二级分组了,则新增一个默认二级分组
+            if (parentGroup.children.length === 0) {
+              let newGroupLevel2Id = 'c_' + this.$randomWord(true, 8, 8)
+              parentGroup.children.push(newGroupLevel2Id)
+              this.info.catalogs.push({
+                id: newGroupLevel2Id,
+                name: '默认二级分组',
+              })
+            }
+            break;
+          } else if (this.topologyLevel === 2) {
+            // console.log('情况6:被拖拽的是拓扑结构中二级分组,拖拽到一级分组中');
+
+            // 找到原属一级分组
+            const originalParentGroup = this.info.catalogRoot.find((item) => {
+              return item.id === this.dragInfo.node.parentId
+            })
+            
+            // 找到原属一级分组children中那个二级分组条目,并做上待删除记号
+            const originalGroupIndex = originalParentGroup.children.findIndex((eachLevel2Id) => {
+              return eachLevel2Id === this.dragInfo.node.id
+            })
+            originalParentGroup.children[originalGroupIndex] += '__need__delete__'
+            
+            // 找到要插入的一级分组
+            const targetGroup = this.info.catalogRoot.find((item) => {
+              return item.id === this.parentNode.id
+            })
+            
+            // 把被拖拽的二级分组的id插进去
+            targetGroup.children.splice(this.index, 0, this.dragInfo.node.id)
+
+            // 把原来那个二级分组条目删除
+            this.$nextTick(() => {
+              const toDeleteIndex = originalParentGroup.children.findIndex((eachLevel2Id) => {
+                return eachLevel2Id.endsWith('__need__delete__')
+              })
+              originalParentGroup.children.splice(toDeleteIndex, 1)
+            })
+
+            // 如果被拖拽的二级分组原属的一级分组中没有任何二级分组了,则新增一个默认二级分组
+            if (originalParentGroup.children.length === 0) {
+              let newGroupLevel2Id = 'c_' + this.$randomWord(true, 8, 8)
+              originalParentGroup.children.push(newGroupLevel2Id)
+              this.info.catalogs.push({
+                id: newGroupLevel2Id,
+                name: '默认二级分组',
+              })
+            }
+          } else if (this.topologyLevel === 3) {
+            // console.log('情况7:被拖拽的是拓扑结构中二级分组,拖拽到隐藏的默认二级分组中');
+
+            // 找到拖拽到的二级分组所属的一级分组
+            const targetGroupLevel1 = this.info.catalogRoot.find((item) => {
+              return item.id === this.parentNode.parentId
+            })
+
+            // 新增一个child条目,对应被拖拽的二级分组
+            targetGroupLevel1.children.push(this.dragInfo.node.id)
+            
+            // 找到被拖拽的二级分组原属的一级分组
+            const originalGroupLevel1 = this.info.catalogRoot.find((item) => {
+              return item.id === this.dragInfo.node.parentId
+            })
+            
+            // 删除这个二级分组条目
+            const idxToDelete = originalGroupLevel1.children.findIndex((id) => {
+              return id === this.dragInfo.node.id
+            })
+            originalGroupLevel1.children.splice(idxToDelete, 1)
+
+            // 如果被拖拽的二级分组原属的一级分组中没有任何二级分组了,则新增一个默认二级分组
+            if (originalGroupLevel1.children.length === 0) {
+              let newGroupLevel2Id = 'c_' + this.$randomWord(true, 8, 8)
+              originalGroupLevel1.children.push(newGroupLevel2Id)
+              this.info.catalogs.push({
+                id: newGroupLevel2Id,
+                name: '默认二级分组',
+              })
+            }
+          }
+          break;
+        case 'topologyGroupLevel1': // 被拖拽的是拓扑结构中一级分组
+          if (this.topologyLevel === 1) {
+            // console.log('情况10:被拖拽的是拓扑结构中一级分组,拖拽到一级分组列表');
+
+            // 在一级分组列表中找到这个一级分组条目
+            let originalGroupIndex = this.info.catalogRoot.findIndex((item) => {
+              return item.id === this.dragInfo.node.id
+            })
+            
+            // 复制这个一级分组条目
+            const groupCopy = deepClone(this.info.catalogRoot[originalGroupIndex])
+
+            // 旧的一级分组条目加上待删除标记
+            this.info.catalogRoot[originalGroupIndex].__need__delete__ = true
+
+            // 插入新的一级分组条目
+            this.info.catalogRoot.splice(this.index, 0 , groupCopy)
+
+            // 在一级分组列表中再次找到要删除的一级分组条目
+            originalGroupIndex = this.info.catalogRoot.findIndex((item) => {
+              return item.__need__delete__
+            })
+
+            // 删除旧的一级分组条目
+            this.info.catalogRoot.splice(originalGroupIndex, 1)
+          } else if (this.topologyLevel === 2) {
+            // console.log('情况11:被拖拽的一级分组只有一个隐藏的默认二级分组,拖拽到一级分组中');
+
+            // 默认二级分组改名成原一级分组的名字
+            const groupLevel2 = this.info.catalogs.find((item) => {
+              return item.id === this.dragInfo.node.children[0].id
+            })
+            groupLevel2.name = this.dragInfo.node.name
+
+            // 拖拽到的一级分组中新增一个child,对应那个二级分组
+            const targetGroupLevel1 = this.info.catalogRoot.find((item) => {
+              return item.id === this.parentNode.id
+            })
+            targetGroupLevel1.children.splice(this.index, 0, this.dragInfo.node.children[0].id)
+            
+            // 删除原一级分组
+            const originalGroupLevel1Idx = this.info.catalogRoot.findIndex((item) => {
+              return item.id === this.dragInfo.node.id
+            })
+            this.info.catalogRoot.splice(originalGroupLevel1Idx, 1)
+          } else if (this.topologyLevel === 3) {
+            // console.log('情况14:被拖拽的一级分组只有一个隐藏的默认二级分组,拖拽到另一个一级分组中隐藏的默认二级分组中');
+
+            // 找到被拖拽的一级分组索引
+            const originalGroupLevel1Idx = this.info.catalogRoot.findIndex((item) => {
+              return item.id === this.dragInfo.node.id
+            })
+
+            // 找到被拖拽的一级分组下辖的默认二级分组的完整数据条目
+            const groupLevel2 = this.info.catalogs.find((item) => {
+              return item.id === this.info.catalogRoot[originalGroupLevel1Idx].children[0]
+            })
+
+            // 被拖拽的一级分组下辖默认二级分组的名称改为与父亲同名
+            groupLevel2.name = this.info.catalogRoot[originalGroupLevel1Idx].name
+
+            // 找到拖拽到的二级分组所属的一级分组
+            const targetGroupLevel1 = this.info.catalogRoot.find((item) => {
+              return item.id === this.parentNode.parentId
+            })
+
+            // 新增一个child条目,对应被拖拽的一级分组下辖的默认二级分组
+            targetGroupLevel1.children.push(groupLevel2.id)
+            
+            // 删除被拖拽的一级分组
+            this.info.catalogRoot.splice(originalGroupLevel1Idx, 1)
+          }
+          break;
+        default:
+          break;
+      }
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.insert-position-tip {
+  height: 1px;
+  box-sizing: content-box;
+  padding-top: 3px;
+  padding-bottom: 3px;
+  background-clip: content-box;
+  color: transparent;
+  // background-color: red;
+}
+</style>

+ 130 - 10
packages/qjkankan-editor/src/components/materialSelectorForEditor.vue

@@ -170,6 +170,52 @@
       </div>
     </div>
 
+    <div class="table table-3D" v-show="currentMaterialType === '3D'">
+      <div class="table-head-row">
+        <span class="table-head">1</span>
+        <span class="table-head" v-for="(item,i) in tableHeadersFor3D" :key="i">{{item.name}}</span>
+      </div>
+      <div
+        v-if="scene3DList.length !== 0 || hasMore3DData"
+        class="table-body"
+        v-infinite-scroll="requestMorePanoData"
+        :infinite-scroll-disabled="!hasMore3DData || isRequestingMore3DData"
+      >
+        <div class="table-body-row" v-for="(item,i) in scene3DList" :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 tableHeadersFor3D" :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="scene3DList.length === 0 && !hasMore3DData" 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>
@@ -191,7 +237,7 @@
 </template>
 
 <script>
-import { getMaterialList} from "@/api";
+import { getMaterialList, getSceneList } from "@/api";
 import { changeByteUnit } from '@/utils/file'
 import config from "@/config";
 import { debounce } from "@/utils/other.js"
@@ -220,6 +266,10 @@ export default {
       type: String,
       default: 'image',
     },
+    isMultiSelection: {
+      type: Boolean,
+      default: false,
+    }
   },
   components:{
   },
@@ -238,6 +288,8 @@ export default {
           this.refreshMaterialList('pano')
         } else if (newVal === 'audio' && this.audioList.length === 0) {
           this.refreshMaterialList('audio')
+        } else if (newVal === '3D' && this.scene3DList.length === 0) {
+          this.refreshMaterialList('3D')
         }
       },
       immediate: false,
@@ -255,17 +307,22 @@ export default {
       })
     },
     tableHeadersForAudio() {
-      console.log(this.$MAPTABLEHEADER);
       return this.$MAPTABLEHEADER['audio'].filter(item => {
         return ['ossPath', 'name', 'fileSize'].includes(item.key)
       })
     },
+    tableHeadersFor3D() {
+      return this.$MAPTABLEHEADER['scene'].filter(item => {
+        return ['thumb', 'sceneName', 'createTime'].includes(item.key)
+      })
+    },
   },
   data () {
     return {
       imageList: [],
       panoList: [],
       audioList: [],
+      scene3DList: [],
       
       select: [],
       searchKey:'', // 搜索关键词
@@ -276,22 +333,37 @@ export default {
       isRequestingMoreImageData: false,
       isRequestingMorePanoData: false,
       isRequestingMoreAudioData: false,
+      isRequestingMore3DData: false,
       hasMoreImageData: true,
       hasMorePanoData: true,
       hasMoreAudioData: true,
+      hasMore3DData: true,
     }
   },
 
   methods: {
     selectItem(item, e) {
-      if (item.isUse == '1') {
-        e.target.checked = false
-        this.$alert({content:'选中素材不能超过600kb'})
+      item.materialType = this.currentMaterialType // 三维场景数据没有type字段来表明自己是三维场景。所以统一加一个字段。
+      if (false) {
+        // 对于图片,大于600kb的,压缩?
       } else {
-        if (e.target.checked) {
-          this.select = [item]
+        if (this.isMultiSelection) {
+          if (e.target.checked) {
+            this.select.push(item)
+          } else {
+            const toDeleteIdx = this.select.findIndex((eachSelect) => {
+              return eachSelect.id === item.id
+            })
+            if (toDeleteIdx >= 0) {
+              this.select.splice(toDeleteIdx, 1)
+            }
+          }
         } else {
-          this.select = []
+          if (e.target.checked) {
+            this.select = [item]
+          } else {
+            this.select = []
+          }
         }
       }
     },
@@ -307,7 +379,6 @@ export default {
         },
         (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)
@@ -338,7 +409,6 @@ export default {
         },
         (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)
@@ -387,6 +457,33 @@ export default {
         }
       );
     },
+    requestMore3DData() {
+      this.isRequestingMore3DData = true
+      const latestUsedSearchKey = this.searchKey
+      getSceneList(
+        {
+          pageNum: Math.floor(this.scene3DList.length / config.PAGE_SIZE) + 1,
+          pageSize: config.PAGE_SIZE,
+          searchKey: this.searchKey,
+        },
+        (data) => {
+          const newData = data.data.data.list.map((i) => {
+            return i;
+          });
+          this.scene3DList = this.scene3DList.concat(newData)
+          this.$store.commit("SetSceneList", this.scene3DList); // todo: 为啥别处拿到的场景列表数据受这里的翻页数影响?
+          if (this.scene3DList.length === data.data.data.total) {
+            this.hasMore3DData = false
+          }
+          this.isRequestingMore3DData = false
+          this.latestUsedSearchKey = latestUsedSearchKey
+        },
+        () => {
+          this.isRequestingMore3DData = false
+          this.latestUsedSearchKey = latestUsedSearchKey
+        }
+      )
+    },
     refreshMaterialList: debounce(function(type) {
       if (type === 'image') {
         this.isRequestingMoreImageData = false
@@ -403,6 +500,11 @@ export default {
         this.hasMoreAudioData = true
         this.audioList = []
         this.requestMoreAudioData()
+      } else if (type === '3D') {
+        this.isRequestingMore3DData = false
+        this.hasMore3DData = true
+        this.scene3DList = []
+        this.requestMore3DData()
       }
     }, 700, false),
   },
@@ -646,6 +748,24 @@ export default {
   }
 }
 
+.table-3D .table-head,
+.table-3D .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%;

+ 503 - 0
packages/qjkankan-editor/src/components/materialSelectorFromWorkForEditor.vue

@@ -0,0 +1,503 @@
+<template>
+  <div class="table-select">
+    <span class="title">选择素材</span>
+    <div class="close-btn"><i class="iconfont icon-pop-ups_shut-down" @click="$emit('cancle')"></i></div>
+
+    <div class="material-tab">
+      <a class="material-tab-item" @click.prevent="currentMaterialType = 'pano'">
+        <span class="text">全景图</span>
+        <div v-if="currentMaterialType === 'pano'" class="bottom-line"></div>
+      </a>
+      <a 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-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"
+        class="table-body"
+      >
+        <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.icon + `?x-oss-process=image/resize,p_10&${Math.random()}`" alt="">
+            </div>
+            <span class="ellipsis" v-else v-title="item.sceneTitle">{{item.sceneTitle}}</span>
+          </span>
+        </div>
+      </div>
+      <!-- 无数据时的提示 -->
+      <div v-if="panoList.length === 0" 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-3D" v-show="currentMaterialType === '3D'">
+      <div class="table-head-row">
+        <span class="table-head">1</span>
+        <span class="table-head" v-for="(item,i) in tableHeadersFor3D" :key="i">{{item.name}}</span>
+      </div>
+      <div
+        v-if="scene3DList.length !== 0"
+        class="table-body"
+      >
+        <div class="table-body-row" v-for="(item,i) in scene3DList" :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 tableHeadersFor3D" :key="idx">
+            <div v-if="sub.type=='image'" class="list-img">
+              <img :src="item.icon + `?x-oss-process=image/resize,p_10&${Math.random()}`" alt="">
+            </div>
+            <span class="ellipsis" v-else v-title="item.sceneTitle">{{item.sceneTitle}}</span>
+          </span>
+        </div>
+      </div>
+      <!-- 无数据时的提示 -->
+      <div v-if="scene3DList.length === 0" 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">
+      <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 { mapGetters } from "vuex";
+
+export default {
+  props:{
+    primaryKey: {
+      default: 'id'
+    },
+    isMultiSelection: {
+      type: Boolean,
+      default: false,
+    }
+  },
+  components:{
+  },
+  computed:{
+    ...mapGetters([
+      'info',
+    ]),
+    tableHeadersForPano() {
+      return this.$MAPTABLEHEADER['pano'].filter(item => {
+        return ['icon', 'name'].includes(item.key)
+      })
+    },
+    tableHeadersFor3D() {
+      return this.$MAPTABLEHEADER['scene'].filter(item => {
+        return ['thumb', 'sceneName'].includes(item.key)
+      })
+    },
+    panoList() {
+      return this.info.scenes.filter((item) => {
+        return item.type === 'pano'
+      }).filter((item) => {
+        if (this.searchKey) {
+          return item.sceneTitle.includes(this.searchKey)
+        } else {
+          return item
+        }
+      })
+    },
+    scene3DList() {
+      return this.info.scenes.filter((item) => {
+        return item.type === '4dkk'
+      }).filter((item) => {
+        if (this.searchKey) {
+          return item.sceneTitle.includes(this.searchKey)
+        } else {
+          return item
+        }
+      })
+    }
+  },
+  data () {
+    return {
+      select: [],
+      searchKey:'', // 搜索关键词
+      latestUsedSearchKey: '',
+      currentMaterialType: 'pano',
+    }
+  },
+
+  methods: {
+    selectItem(item, e) {
+      item.materialType = this.currentMaterialType  // 三维场景数据没有type字段来表明自己是三维场景。所以统一加一个字段。
+      if (false) {
+        // 对于图片,大于600kb的,压缩?
+      } else {
+        if (this.isMultiSelection) {
+          if (e.target.checked) {
+            this.select.push(item)
+          } else {
+            const toDeleteIdx = this.select.findIndex((eachSelect) => {
+              return eachSelect.id === item.id
+            })
+            if (toDeleteIdx >= 0) {
+              this.select.splice(toDeleteIdx, 1)
+            }
+          }
+        } else {
+          if (e.target.checked) {
+            this.select = [item]
+          } else {
+            this.select = []
+          }
+        }
+      }
+    },
+  },
+  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-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-3D .table-head,
+.table-3D .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;
+}
+
+.btns {
+  display: flex;
+  justify-content: flex-end;
+  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>

+ 177 - 21
packages/qjkankan-editor/src/components/sceneGroupInEditor.vue

@@ -1,9 +1,16 @@
 <template>
-  <div class="scene-group">
+  <div
+    class="scene-group"
+  >
     <div
       class="top-bar"
       :class="isConfirmingDeletion ? '' : 'show-icons-on-hover'"
       @click="onClickTopBar"
+      @dragstart="onDragStart"
+      @dragenter.self="onDragEnter"
+      @dragend="onDragEnd"
+      @dragleave.self="onDragLeave"
+      draggable="true"
       :style="{
         paddingLeft: topBarPaddingLeft,
       }"
@@ -16,18 +23,31 @@
         <i v-show="level === 1"
           class="iconfont icon-editor_list_add icon-add"
           v-tooltip="'新增二级分组'"
-          @click="onRequestForAddGroup"
+          @click.stop="onRequestForAddGroup"
         >
         </i>
         <i
           class="iconfont icon-editor_list_image icon-image"
-          v-tooltip="'新增全景图或三维场景'"
+          v-tooltip="'新增全景图或三维场景'" 
+          @click.stop="onRequestForAddScene"
+          v-show="
+            level === 2 ||
+            (
+              level === 1 &&
+              (
+                groupNode.children.length === 0 ||
+                (
+                  groupNode.children.length === 1 && groupNode.children[0].name === '默认二级分组'
+                )
+              )
+            )
+          "
         >
         </i>
         <i
           class="iconfont icon-editor_list_edit icon-edit"
           v-tooltip="'重命名'"
-          @click="onClickForRename"
+          @click.stop="onClickForRename"
         >
         </i>
         <i
@@ -45,19 +65,30 @@
           </div>
         </div>
       </template>
-      <input v-if="isRenaming" class="group-title-input" v-model="newName"
+      <input
+        v-if="isRenaming"
+        class="group-title-input"
+        v-model.trim="newName"
         ref="input-for-rename"
         maxlength="50"
         placeholder="输入名字"
         @blur="onInputNewNameComplete"
         @keydown.enter="onInputEnter"
+        @click.stop
       />
     </div>
 
     <div class="group-content" v-if="isExpanded">
       <template v-if="!(groupNode.children.length === 1 && groupNode.children[0].name === '默认二级分组')">
+        <InsertPositionTip
+          position-debug="1"
+          :indentLevel="level + 1"
+          :topologyLevel="level + 1"
+          :parentNode="groupNode"
+          :index="0"
+        ></InsertPositionTip>
         <div
-          v-for="(item) of groupNode.children"
+          v-for="(item, index) of groupNode.children"
           :key=item.id
         >
           <component
@@ -70,7 +101,7 @@
             @renameGroup="onInnerGroupRename"
             @deleteGroup="onInnerGroupConfirmDelete"
           />
-          <SceneInGroupInEditor
+          <SceneInGroup
             v-else
             :style="{
               paddingLeft: sceneItemPaddingLeft,
@@ -79,32 +110,72 @@
             @rename="onRenameScene"
             @delete="onDeleteScene"
           />
+          <InsertPositionTip
+            position-debug="2"
+            :indentLevel="level + 1"
+            :topologyLevel="level + 1"
+            :parentNode="groupNode"
+            :index="index + 1"
+          ></InsertPositionTip>
         </div>
       </template>
       <template v-else>
         <!-- 自动生成的默认二级分组不显示,里边的内容显示成直属于一级分组的效果。 -->
-        <SceneInGroupInEditor
-          v-for="(item) of groupNode.children[0].children"
+        <InsertPositionTip
+          position-debug="3"
+          :indentLevel="level + 1"
+          :topologyLevel="level + 2"
+          :parentNode="groupNode.children[0]"
+          :index="0"
+        ></InsertPositionTip>
+        <div
+          v-for="(item, index) of groupNode.children[0].children"
           :key=item.id
-          :style="{
-            paddingLeft: sceneItemPaddingLeft,
-          }"
-          :sceneInfo="item"
-          @rename="onRenameScene"
-          @delete="onDeleteScene"
-        />
+        >
+          <SceneInGroup
+            :style="{
+              paddingLeft: sceneItemPaddingLeft,
+            }"
+            :sceneInfo="item"
+            @rename="onRenameScene"
+            @delete="onDeleteScene"
+          />
+          <InsertPositionTip
+            position-debug="4"
+            :indentLevel="level + 1"
+            :topologyLevel="level + 2"
+            :parentNode="groupNode.children[0]"
+            :index="index + 1"
+          ></InsertPositionTip>
+        </div>
       </template>
     </div>
+
+    <div class="dialog" style="z-index: 2000" v-if="isShowSelectionWindow">
+      <MaterialSelector
+        title="选择素材"
+        @cancle="isShowSelectionWindow = false"
+        @submit="onSubmitFromMaterialSelector"
+        :selectableType="['pano', '3D']"
+        :initialMaterialType="'pano'"
+        :isMultiSelection="true"
+      />
+    </div>
   </div>
 </template>
 
 <script>
-import SceneInGroupInEditor from "@/components/sceneInGroupInEditor.vue";
+import SceneInGroup from "@/components/sceneInGroupInEditor.vue";
+import MaterialSelector from "@/components/materialSelectorForEditor.vue";
+import InsertPositionTip from "@/components/insertPositionTipInEditor.vue";
+import { mapGetters, mapMutations } from "vuex";
 
 export default {
   name: 'SceneGroup',
   components: {
-    SceneInGroupInEditor,
+    SceneInGroup,
+    MaterialSelector,
+    InsertPositionTip,
   },
   props: {
     groupNode: {
@@ -122,9 +193,14 @@ export default {
       isRenaming: false,
       newName: '',
       isConfirmingDeletion: false,
+      isShowSelectionWindow: false,
+      dragEnterTimerId: null,
     }
   },
   computed: {
+    ...mapGetters({
+      info: "info",
+    }),
     topBarPaddingLeft() {
       return 12 + (this.level - 1) * 12 + 'px' 
     },
@@ -133,6 +209,11 @@ export default {
     },
   },
   methods: {
+    ...mapMutations({
+      recordDragType: 'setEditorNavDragType',
+      recordDragNode: 'setEditorNavDragNode',
+      clearDragInfo: 'clearEditorNavDragInfo',
+    }),
     onClickTopBar() {
       if (this.isConfirmingDeletion) {
         return
@@ -158,7 +239,9 @@ export default {
     onRequestForAddGroup() {
       this.$emit('addGroup', this.groupNode.id)
     },
-
+    onRequestForAddScene() {
+      this.isShowSelectionWindow = true
+    },
     onClickForRename() {
       this.isRenaming = true
       this.newName = this.groupNode.name
@@ -168,7 +251,9 @@ export default {
     },
     onInputNewNameComplete() {
       this.isRenaming = false
-      this.$emit('renameGroup', this.groupNode.id, this.level, this.newName)
+      if (this.newName !== this.groupNode.name) {
+        this.$emit('renameGroup', this.groupNode.id, this.level, this.newName)
+      }
       this.newName = ''
     },
     onInputEnter() {
@@ -196,6 +281,76 @@ export default {
     },
     onInnerGroupConfirmDelete(...params) {
       this.$emit('deleteGroup', ...params)
+    },
+    onSubmitFromMaterialSelector(selected) {
+      let newScenes = []
+      for (const item of selected) {
+        if (item.materialType === 'pano') {
+          newScenes.push({
+            icon: item.icon,
+            sceneCode: item.sceneCode,
+            sceneTitle: item.name,
+            category: this.level === 1 ? this.groupNode.children[0].id : this.groupNode.id,
+            type: "pano",
+            id: 's_' + this.$randomWord(true, 8, 8)
+          })
+        } else if (item.materialType === '3D') {
+          newScenes.push({
+            icon: item.thumb,
+            sceneCode: item.num,
+            sceneTitle: item.sceneName,
+            category: this.level === 1 ? this.groupNode.children[0].id : this.groupNode.id,
+            type: "4dkk",
+            id:'s_'+this.$randomWord(true,8,8)
+          })
+        }
+      }
+
+      let allSuccess = true
+      newScenes.forEach((item, i) => {
+        let temp = this.info.scenes.find(eachScene => {
+          return eachScene.sceneCode === item.sceneCode
+        })
+        if (temp) {
+          setTimeout(() => {
+            this.$msg.message(`${item.type == '4dkk' ? '场景' : '全景图'}${item.sceneTitle}已存在,不可重复添加`)
+          }, i * 100)
+          allSuccess = false
+          return
+        }
+        this.info.scenes.push(item)
+      })
+
+      this.isShowSelectionWindow = false
+      if (allSuccess) {
+        this.$msg.success("操作成功")
+      }
+    },
+    onDragStart(e) {
+      this.recordDragType(`topologyGroupLevel${this.level}`)
+      this.recordDragNode(this.groupNode)
+      // e.dataTransfer.setDragImage(e.target.children[1], -10, -18)
+    },
+    onDragEnter(e) {
+      if (e.target.contains(e.relatedTarget)) {
+        return
+      }
+      this.dragEnterTimerId = setTimeout(() => {
+        if (!this.isExpanded) {
+          this.isExpanded = true
+          this.$bus.emit('scene-group-expanded', this.groupNode.id, this.level)
+        }
+      }, 700)
+    },
+    onDragEnd() {
+      this.clearDragInfo()
+      clearTimeout(this.dragEnterTimerId)
+    },
+    onDragLeave(e) {
+      if (e.target.contains(e.relatedTarget)) {
+        return
+      }
+      clearTimeout(this.dragEnterTimerId)
     }
   },
   mounted() {
@@ -209,7 +364,6 @@ export default {
 
 <style lang="less" scoped>
 .scene-group {
-  margin-top: 6px;
   .top-bar {
     position: relative;
     color: rgba(255, 255, 255, 0.6);
@@ -340,5 +494,7 @@ export default {
       }
     }
   }
+  .group-content {
+  }
 }
 </style>

+ 26 - 3
packages/qjkankan-editor/src/components/sceneInGroupInEditor.vue

@@ -2,11 +2,22 @@
   <div
     class="scene-item"
     :class="isConfirmingDeletion ? '' : 'not-confirming-deletion'"
+    @dragstart="onDragStart"
+    @dragend="clearDragInfo"
+    draggable="true"
   >
-    <img :src="sceneInfo.icon + ossImagePreviewUrlSuffix()" alt="" class="scene-image">
+    <img
+      :src="sceneInfo.icon + ossImagePreviewUrlSuffix()"
+      alt=""
+      class="scene-image"
+      draggable="false"
+    >
     <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"
+      <input
+        v-if="isRenaming"
+        class="scene-title-input"
+        v-model.trim="newName"
         ref="input-for-rename"
         maxlength="50"
         placeholder="输入名字"
@@ -40,6 +51,7 @@
 
 <script>
 import { ossImagePreviewUrlSuffix } from '@/utils/other.js'
+import { mapMutations } from "vuex";
 
 export default {
   name: 'SceneInGroupInEditor', 
@@ -58,7 +70,19 @@ export default {
       isConfirmingDeletion: false,
     }
   },
+  computed: {
+  },
   methods: {
+    ...mapMutations({
+      recordDragType: 'setEditorNavDragType',
+      recordDragNode: 'setEditorNavDragNode',
+      clearDragInfo: 'clearEditorNavDragInfo',
+    }),
+    onDragStart(e) {
+      this.recordDragType('scene')
+      this.recordDragNode(this.sceneInfo)
+      e.dataTransfer.setDragImage(e.target.children[0], -10, -18)
+    },
     ossImagePreviewUrlSuffix,
     translateSceneType(type) {
       if (type === 'pano') {
@@ -99,7 +123,6 @@ export default {
 <style lang="less" scoped>
 .scene-item {
   position: relative;
-  margin-top: 6px;
   margin-left: -12px;
   margin-right: -10px;
   padding-right: 10px;

+ 1 - 0
packages/qjkankan-editor/src/components/select.vue

@@ -1,4 +1,5 @@
 <template>
+  <!-- todo: 旧版的初始场景设置时弹窗,可以删了? -->
   <div class="select-commodity">
     <a class="close" @click="$emit('cancle')">+</a>
     <h3 class="title">{{ title }}</h3>

+ 20 - 5
packages/qjkankan-editor/src/config/menu.js

@@ -1,26 +1,29 @@
-// 前端路由配置
+// 编辑器主菜单
 const PCMenu = [
   {
     text: "基础",
     icon: "settings",
     link: "/base",
     name: "base",
+    hasPreviewArea: false,
     hidden: false,
-    hidescene:true
   },
   {
     text: "导航",
     icon: "navigation",
     link: "/navigation",
     name: "navigation",
+    hasPreviewArea: true,
+    previewAreaExtraLeft: 300,
     hidden: false,
-    hidescene:true
   },
   {
-    text: "初始",
-    icon: "start",
+    text: "视角",
+    icon: "viewpoint",
     link: "/screen",
     name: "screen",
+    hasPreviewArea: true,
+    previewAreaExtraLeft: 0,
     hidden: false
   },
   {
@@ -28,10 +31,22 @@ const PCMenu = [
     icon: "hot",
     link: "/hotspot",
     name: "hotspot",
+    hasPreviewArea: true,
+    previewAreaExtraLeft: 0,
+    hidden: false
+  },
+  {
+    text: "讲解",
+    icon: "explanation",
+    link: "/explanation",
+    name: "explanation",
+    hasPreviewArea: true,
+    previewAreaExtraLeft: 0,
     hidden: false
   }
 ];
 
+// 管理平台主菜单
 const MATERIALMenu = [
   {
     text: "我的作品",

+ 20 - 3
packages/qjkankan-editor/src/directives/vTitleInEditor.js

@@ -1,13 +1,14 @@
 import Vue from 'vue'
 
+let timerId = null
+let isShowTitle = false
+let titleNode = null
+
 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)
@@ -73,5 +74,21 @@ Vue.directive('title', {
         document.body.removeChild(titleNode)
       }
     })
+    el.addEventListener('dragover', function () {
+      if (!isShowTitle) {
+        clearTimeout(timerId)
+      } else {
+        isShowTitle = false
+        document.body.removeChild(titleNode)
+      }
+    })
+    el.addEventListener('dragleave', function () {
+      if (!isShowTitle) {
+        clearTimeout(timerId)
+      } else {
+        isShowTitle = false
+        document.body.removeChild(titleNode)
+      }
+    })
   },
 })

+ 47 - 1
packages/qjkankan-editor/src/directives/vTooltipInEditor.js

@@ -75,7 +75,53 @@ Vue.directive('tooltip', {
       passive: false,
     })
     el.addEventListener('mouseleave', function () {
-      document.body.removeChild(tooltipNode)
+      try {
+        document.body.removeChild(tooltipNode)
+      } catch(e) {
+        console.log('尝试从DOM上移除tooltip元素失败,通常是因为已经在其他回调中被移除了,不需处理:', e);
+      }
+    })
+    el.addEventListener('mousedown', function () {
+      try {
+        document.body.removeChild(tooltipNode)
+      } catch(e) {
+        console.log('尝试从DOM上移除tooltip元素失败,通常是因为已经在其他回调中被移除了,不需处理:', e);
+      }
+    })
+    el.addEventListener('keydown', function () {
+      try {
+        document.body.removeChild(tooltipNode)
+      } catch(e) {
+        console.log('尝试从DOM上移除tooltip元素失败,通常是因为已经在其他回调中被移除了,不需处理:', e);
+      }
+    })
+    el.addEventListener('scroll', function () {
+      try {
+        document.body.removeChild(tooltipNode)
+      } catch(e) {
+        console.log('尝试从DOM上移除tooltip元素失败,通常是因为已经在其他回调中被移除了,不需处理:', e);
+      }
+    })
+    el.addEventListener('dragstart', function () {
+      try {
+        document.body.removeChild(tooltipNode)
+      } catch(e) {
+        console.log('尝试从DOM上移除tooltip元素失败,通常是因为已经在其他回调中被移除了,不需处理:', e);
+      }
+    })
+    el.addEventListener('dragstart', function () {
+      try {
+        document.body.removeChild(tooltipNode)
+      } catch(e) {
+        console.log('尝试从DOM上移除tooltip元素失败,通常是因为已经在其他回调中被移除了,不需处理:', e);
+      }
+    })
+    el.addEventListener('dragleave', function () {
+      try {
+        document.body.removeChild(tooltipNode)
+      } catch(e) {
+        console.log('尝试从DOM上移除tooltip元素失败,通常是因为已经在其他回调中被移除了,不需处理:', e);
+      }
     })
   },
 })

+ 7 - 54
packages/qjkankan-editor/src/framework/EditorHead.vue

@@ -60,6 +60,7 @@ export default {
     ...mapGetters({
       info: "info",
       isShow: "isShow",
+      catalogTopology: 'catalogTopology',
     }),
   },
   methods: {
@@ -120,65 +121,17 @@ export default {
     },
 
     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.icon) {
+        this.info.icon = this.catalogTopology[0].children[0].children[0].icon
+      }
 
+      // todo: 干啥呢?
       if (this.info.firstScene) {
         this.info.firstScene = this.info.scenes.find(item=>item.sceneCode==this.info.firstScene.sceneCode)
       }
 
+      // todo: 干啥呢?
       this.$store.commit("SetInfo", this.info);
     },
 

+ 24 - 55
packages/qjkankan-editor/src/framework/EditorMain.vue

@@ -3,90 +3,59 @@
   <main class="app-main">
     <!-- 左侧菜单栏 -->
     <app-menu class="app-menu"></app-menu>
+    <!-- 作品预览区域。无论切换至哪个菜单页面,始终存在。-->
+    <div
+      class="app-player"
+      ref="layer"
+      v-show="$route.meta.hasPreviewArea"
+      :style="{
+        left: $route.meta.previewAreaExtraLeft + 58 + 'px'
+      }"
+    />
     <!-- 其余 -->
-    <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 class="app-view">
+      <keep-alive>
+        <router-view />
+      </keep-alive>
     </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%;
+  position: relative;
   > .app-menu {
     flex: 0 0 auto;
   }
-  > .app-content {
+  > .app-player {
+    position: absolute;
+    top: 0;
+    height: 100%;
+    left: 58px;
+    right: 274px;
+    background-color: yellow;
+  }
+  > .app-view {
     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 - 1
packages/qjkankan-editor/src/framework/Main.vue

@@ -18,12 +18,12 @@
     </div>
   </main>
 </template>
+
 <script>
 import AppMenu from "./MenuPC";
 import Core from "./core";
 import Toolbar from "./Toolbar";
 
-
 export default {
   name: "app-main",
   components: {

+ 1 - 1
packages/qjkankan-editor/src/framework/SettingPC.vue

@@ -34,7 +34,7 @@ export default {
 </script>
 <style lang="less" scoped>
 .pc-setting {
-  width: 100%;
+  // width: 100%;
   .view-setting {
     padding: 10px;
     > li {

+ 1 - 12
packages/qjkankan-editor/src/framework/Toolbar.vue

@@ -176,7 +176,7 @@ export default {
 </script>
 <style lang="less" scoped>
 .app-view-toolbar {
-  overflow: visible;
+  background: green;
   a {
     color: #fff;
     text-decoration: none;
@@ -226,15 +226,4 @@ export default {
     justify-content: center;
   }
 }
-.unable {
-  &::before {
-    content: "";
-    z-index: 22;
-    position: absolute;
-    width: 100%;
-    top: -10px;
-    height: calc(100% + 10px);
-    background: rgba(0, 0, 0, 0.4);
-  }
-}
 </style>

+ 1 - 0
packages/qjkankan-editor/src/framework/core/index.vue

@@ -153,6 +153,7 @@ export default {
 
 <style lang="less" scoped>
 .pano-body {
+  background: blue;
   width: 100%;
   position: relative;
   height: 100%;

+ 2 - 1
packages/qjkankan-editor/src/mixins/index.js

@@ -1,7 +1,7 @@
 import Vue from "vue";
 
 import config from "../config";
-import bus from "../utils/eventbus";
+import bus from "../utils/eventbus.js";
 import { Alert, Confirm, Tips } from "../components/shared/message";
 
 import clickoutside from "./v-clickoutside";
@@ -114,6 +114,7 @@ Vue.prototype.$nameSort = {
 
 Vue.prototype.$scrollbars = [];
 
+// todo: 无用了
 let SettingPanel = require("@/framework/SettingPC").default;
 
 Vue.mixin({

+ 1 - 1
packages/qjkankan-editor/src/pages/edit.js

@@ -1,7 +1,7 @@
 import Vue from 'vue'
 import '../mixins'
 import App from './Edit.vue'
-import router from '../router'
+import router from '../router/editorRouter'
 import store from '../Store'
 import 'viewerjs/dist/viewer.css'
 import Viewer from 'v-viewer'

+ 101 - 0
packages/qjkankan-editor/src/router/editorRouter.js

@@ -0,0 +1,101 @@
+import Vue from 'vue'
+import Router from 'vue-router'
+import { PCMenu } from "../config/menu.js";
+import { checkWork, checkLogin, getPanoInfo } from '@/api'
+import store from '../Store'
+
+let vue = new Vue()
+
+import { LoginDetector,OnlineDetector } from "@/utils/starter";
+Vue.use(Router)
+
+const originalPush = Router.prototype.push
+Router.prototype.push = function push (location) {
+  return originalPush.call(this, location).catch(err => err)
+}
+
+let routes = [];
+PCMenu.forEach(item => {
+  routes.push({
+    name: item.name,
+    path: `${item.link}`,
+    meta: {
+      loading: null,
+      hasPreviewArea: item.hasPreviewArea,
+      previewAreaExtraLeft: item.previewAreaExtraLeft,
+    },
+    component: () => import(`../views/${item.name}/index.vue`)
+  });
+});
+
+
+LoginDetector.register(
+  detector => new Promise(resolve => detector.resolve(resolve))
+);
+
+OnlineDetector.register(
+  detector => new Promise(resolve => detector.resolve(resolve))
+);
+
+//检验是不是该用户作品
+
+
+checkWork().then(res=>{
+  if (res.data) {
+    checkLogin().then(response => {
+      if (response.code == 3005) {
+        store.commit('UpdateIsShowState', false)
+        vue.$bus.emit('canLoad',false) 
+        return vue.$alert({content: '当前无操作权限'});
+      } else{
+        vue.$bus.emit('canLoad',true) 
+        LoginDetector.valid();
+      }
+    });
+  } 
+  else{
+    return vue.$alert({content: '该作品已被删除'});
+  }
+  
+})
+
+getPanoInfo().then(() => {
+  store.commit('UpdateIsShowState', true)
+  // if(response&&response.status == 1){
+  // }
+  OnlineDetector.valid();
+});
+
+
+const router = new Router({
+  routes: routes
+})
+
+
+router.beforeEach(async (to, from, next) => {
+  
+    await LoginDetector.listener();
+    await OnlineDetector.listener();
+    if (from.name == 'hotspot') {
+      if (store.getters.isEditing) {
+        vue.$confirm({
+            content: "热点内容未编辑完,确定要关闭吗",
+            ok: () => {
+              vue.$bus.emit('delhotspot') 
+             return next()
+            }
+        });
+        return
+      }
+    }
+
+
+    if (to.path == '/') {
+        return next({path: "/base" })
+    }
+    
+    next()
+})
+
+
+export default router;

+ 10 - 7
packages/qjkankan-editor/src/utils/other.js

@@ -80,29 +80,32 @@ export function isValidPhoneNumber(value) {
 }
 
 // 深拷贝
-export function deepClone(target) {
+export function deepClone(target, hash = new WeakMap()) {
   // 定义一个变量
-  let result
+  let result = null
   // 如果当前需要深拷贝的是一个对象的话
   if (typeof target === 'object') {
-    // 如果是一个数组的话
-    if (Array.isArray(target)) {
+    if (hash.has(target)) { // 如果是循环引用
+      result = hash.get(target)
+    } else if (Array.isArray(target)) { // 如果是一个数组的话
       result = [] // 将result赋值为一个数组,并且执行遍历
+      hash.set(target, result)
       for (let i in target) {
         // 递归克隆数组中的每一项
-        result.push(deepClone(target[i]))
+        result.push(deepClone(target[i], hash))
       }
       // 判断如果当前的值是null的话;直接赋值为null
     } else if (target === null) {
       result = null
-      // 判断如果当前的值是一个RegExp对象的话,直接赋值    
+      // 判断如果当前的值是一个RegExp对象的话,直接赋值
     } else if (target.constructor === RegExp) {
       result = target
     } else {
       // 否则是普通对象,直接for in循环,递归赋值对象的所有值
       result = {}
+      hash.set(target, result)
       for (let i in target) {
-        result[i] = deepClone(target[i])
+        result[i] = deepClone(target[i], hash)
       }
     }
     // 如果不是对象的话,就是基本数据类型,那么直接赋值

+ 27 - 0
packages/qjkankan-editor/src/utils/request.js

@@ -21,6 +21,7 @@ const noop = function() {};
 // 请求回调队列
 let postQueue = [];
 
+// 这个看起来过时了
 export const statusCode = {
   NEXT: -999, //继续执行
   SUCCESS: 0, //成功
@@ -37,6 +38,32 @@ export const statusCode = {
   FAILURE_CODE_5004: 5004, // 密码错误
 };
 
+/* 伟浩给的新版本错误码
+FAILURE_SYS_3001(3001, "对象不存在"),
+
+// 301X -> 权限问题类
+FAILURE_SYS_3010(3010, "Token为空"),
+FAILURE_SYS_3011(3011, "没有操作权限"),
+
+// 302X -> 文件类
+FAILURE_CODE_3020(3020, "空文件"),
+FAILURE_CODE_3021(3021, "非法文件"),
+FAILURE_CODE_3022(3022, "上传文件超过最大值"),
+FAILURE_CODE_3023(3023, "非法格式"),
+FAILURE_CODE_3024(3024, "存储空间已满"),
+FAILURE_CODE_3025(3025, "上传失败"),
+
+
+// 31XX -> 自定义异常
+FAILURE_CODE_3101(3101, "素材已经被引用, 不能删除"),
+FAILURE_CODE_3102(3102, "计算中的场景不能删除"),
+FAILURE_CODE_3103(3103, "服务器someData.json文件不存在"),
+
+// 32XX -> 请求第三方API错误
+FAILURE_API_3200(3200, "请求第三方API失败"),
+FAILURE_API_3201(3201, "请求第三方API返回失败"),
+*/
+
 /**
  * 已知错误提示集合
  */

+ 16 - 7
packages/qjkankan-editor/src/views/base/Toolbar.vue

@@ -17,8 +17,8 @@
           </div>
           <div class="title-input-wrapper">
             <input
-              v-model="info.name"
-              @blur="$store.commit('SetInfo',info)"
+              v-model.trim="info.name"
+              @keydown.enter="onTitleInputEnter"
               type="text"
               maxlength="50"
               placeholder="请输入作品标题"
@@ -28,8 +28,7 @@
           <div class="ui-title jianjie"><span>简介</span></div>
           <div class="jianjie-textarea-wrapper">
             <textarea
-              v-model="info.description"
-              @blur="$store.commit('SetInfo',info)"
+              v-model.trim="info.description"
               maxlength="500"
               placeholder="请输入作品简介"
               type="text"
@@ -62,9 +61,10 @@
 
     <div class="dialog" style="z-index: 2000" v-if="isShowSettingCoverWindow">
       <MaterialSelectorForEditor
+        :selectableType="['image', 'pano', '3D']"
         title="选择素材"
         @cancle="isShowSettingCoverWindow = false"
-        @submit="handleSubmitFromTable2"
+        @submit="onCoverSelected"
       />
     </div>
   </div>
@@ -129,10 +129,19 @@ export default {
     onClickSettingCover() {
       this.isShowSettingCoverWindow = true
     },
-    handleSubmitFromTable2(selected) {
-      this.info.icon = selected[0].icon
+    onCoverSelected(selected) {
+      if (selected[0].materialType === 'image') {
+        this.info.icon = selected[0].icon
+      } else if (selected[0].materialType === 'pano') {
+        this.info.icon = selected[0].icon
+      } else if (selected[0].materialType === '3D') {
+        this.info.icon = selected[0].thumb
+      }
       this.isShowSettingCoverWindow = false
     },
+    onTitleInputEnter(e) {
+      e.target.blur()
+    },
   },
 }
 </script>

+ 7 - 0
packages/qjkankan-editor/src/views/base/backgroundMusicSettings.vue

@@ -72,6 +72,13 @@ export default {
     }
   },
   mounted() {
+    if (!this.info.backgroundMusic) {
+      this.info.backgroundMusic = {
+        id: '',
+        name: '',
+        ossPath: '',
+      }
+    }
   }
 }
 </script>

+ 65 - 36
packages/qjkankan-editor/src/views/base/customButtonSettings.vue

@@ -8,14 +8,19 @@
     <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="title-bar"
+        :class="info.customButton[index].isShow ? 'bright' : 'dark'"
+        @click="onRequestForChangeExpandStatus(index)"
+      >
         <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="">
+          <img v-if="info.customButton[index].type === '电话' && info.customButton[index].isShow" :src="require('@/assets/images/icons/phone.svg')" class="button-icon" alt="">
+          <img v-if="info.customButton[index].type === '电话' && !info.customButton[index].isShow" :src="require('@/assets/images/icons/phone-dark.svg')" class="button-icon" alt="">
+          <img v-if="info.customButton[index].type === '链接' && info.customButton[index].isShow" :src="require('@/assets/images/icons/link.svg')" class="button-icon" alt="">
+          <img v-if="info.customButton[index].type === '链接' && !info.customButton[index].isShow" :src="require('@/assets/images/icons/link-dark.svg')" class="button-icon" alt="">
           <span class="button-name">
             {{info.customButton[index].name}}
           </span>
@@ -23,30 +28,22 @@
         <div class="right">
           <i
             class="iconfont icon-editor_list_edit btn-edit"
-            @click="onRequestForEdit(index)"
+            @click.stop="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"
+          <i
+            v-show="info.customButton[index].isShow"
+            class="iconfont icon-eye_on btn-show"
             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>
+            @click.stop="info.customButton[index].isShow = false"
+          ></i>
+          <i
+            v-show="!info.customButton[index].isShow"
+            class="iconfont icon-eye_off btn-hide"
+            v-tooltip="'显示'"
+            @click.stop="onRequestForShow(index)"
+          ></i>
         </div>
       </div>
       <div class="edit-content">
@@ -68,7 +65,6 @@
           <span class="item-name">{{buttonValueTips[index]}}</span>
           <input
             class="value-input"
-            :placeholder="`请输入${buttonValueTips[index]}`"
             v-model="info.customButton[index].value"
           >
         </div>
@@ -193,6 +189,25 @@ export default {
       }
     },
   },
+  beforeMount() {
+    if (!this.info.customButton) {
+      // 这是在v1.2版之前创建的作品,还没设置过自定义按钮,所以还没有customButton字段
+      this.info.customButton = [
+        {
+          "type": "电话",
+          "name": "电话",
+          "value": "",
+          "isShow": false
+        },
+        {
+          "type": "链接",
+          "name": "链接",
+          "value": "",
+          "isShow": false
+        }
+      ]
+    }
+  },
   methods: {
     onRequestForChangeExpandStatus(index) {
       this.$set(this.expandStatus, index, !this.expandStatus[index])
@@ -238,6 +253,15 @@ export default {
       this.info.customButton[this.editingButtonIdx].value = this.editingInfo.value 
       this.$msg.success('操作成功')
       this.isEditing = false
+    },
+    onRequestForShow(index) {
+      if (!this.checkButtonName(this.info.customButton[index].name)) {
+        return
+      }
+      if (!this.checkButtonValue(this.info.customButton[index].value, this.info.customButton[index].type)) {
+        return
+      }
+      this.info.customButton[index].isShow = true
     }
   }
 }
@@ -274,6 +298,13 @@ export default {
       justify-content: space-between;
       align-items: center;
       padding: 0 16px;
+      cursor: pointer;
+      &.bright {
+        color: #fff;
+      }
+      &.dark {
+        color: #808080;
+      }
       > .left {
         display: flex;
         align-items: center;
@@ -284,12 +315,13 @@ export default {
           cursor: pointer;
         }
         > .button-icon {
-          width: 36px;
-          height: 36px;
+          width: 18px;
+          height: 18px;
+          margin-left: 6px;
         }
         > .button-name {
           font-size: 16px;
-          color: #fff;
+          margin-left: 6px;
         }
       }
       > .right {
@@ -305,19 +337,16 @@ export default {
         > .btn-show {
           margin-left: 16px;
           cursor: pointer;
-          display: flex;
-          flex-direction: column;
-          justify-content: center;
-          img {
-            height: 100%;
+          &:hover {
+            color: #0076F6;
           }
         }
         > .btn-hide {
-          display: flex;
-          flex-direction: column;
-          justify-content: center;
           margin-left: 16px;
           cursor: pointer;
+          &:hover {
+            color: #0076F6;
+          }
         }
       }
     }

+ 9 - 1
packages/qjkankan-editor/src/views/base/customMaskSettings.vue

@@ -97,7 +97,15 @@ export default {
     onCancelSelection(cancelFor) {
       this.info.customMask[cancelFor] = ''
     },
-  }
+  },
+  mounted() {
+    if (!this.info.customMask) {
+      this.info.customMask = {
+        earth: '',
+        sky: '',
+      }
+    }
+  },
 }
 </script>
 

+ 6 - 1
packages/qjkankan-editor/src/views/base/openingAnimationSettings.vue

@@ -49,7 +49,12 @@ export default {
     })
   },
   methods: {
-  }
+  },
+  mounted() {
+    if (!this.info.openingAnimationType) {
+      this.$set(this.info, 'openingAnimationType', this.openingTypeList[0])
+    }
+  },
 }
 </script>
 

+ 4 - 0
packages/qjkankan-editor/src/views/base/passwordSettings.vue

@@ -9,6 +9,7 @@
         :maxlength="20"
         oninput="value=value.replace(/\s+/g,'')"
         v-model="info.password"
+        @keydown.enter="onInputEnter"
       >
       <i class="iconfont" @click="canSee = !canSee" :class="canSee ? ' icon-editor_on' : 'icon-editor_off'"></i>
     </div>
@@ -33,6 +34,9 @@ export default {
     })
   },
   methods: {
+    onInputEnter(e) {
+      e.target.blur()
+    }
   }
 }
 </script>

+ 178 - 0
packages/qjkankan-editor/src/views/explanation/explanationSettings.vue

@@ -0,0 +1,178 @@
+<template>
+  <div class="explanation-settings"  app-border dir-left>
+    <div class="title">
+      语音讲解
+      <i class="iconfont icon-material_prompt tool-tip-for-editor" v-tooltip="'您可以为当前全景图添加语音讲解音频。'"/>
+    </div>
+    <button v-if="!info.explanation.audioId" class="ui-button submit" @click="isShowSelectionWindow = true">
+      <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.explanation.audioUrl"></Audio>
+        <div class="name" v-title="info.explanation.audioName" @click="onClickCurrentMusic">{{info.explanation.audioName}}</div>
+        <i class="iconfont icon-editor_list_delete" @click.stop="onClickDeleteMusicBtn"></i>
+      </div>
+      <button class="ui-button" @click="isShowSelectionWindow = true">
+        <i class="iconfont icon-editor_update"></i>
+        更换音频
+      </button>
+    </template>
+
+    <div class="switch-wrapper">
+      <span class="label">默认开启</span>
+      <Switcher :value="info.explanation.openByDefault" @change="info.explanation.openByDefault = !info.explanation.openByDefault"></Switcher>
+    </div>
+    <div class="switch-wrapper">
+      <span class="label">循环播放</span>
+      <Switcher :value="info.explanation.repeat" @change="info.explanation.repeat = !info.explanation.repeat"></Switcher>
+    </div>
+
+    <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 Switcher from "@/components/shared/Switcher";
+import MaterialSelectorForEditor from "@/components/materialSelectorForEditor.vue";
+import Audio from "@/components/audio/audioForEditor.vue";
+
+export default {
+  components: {
+    Switcher,
+    MaterialSelectorForEditor,
+    Audio,
+  },
+  computed: {
+    ...mapGetters({
+        info: "info",
+    }),
+  },
+  data() {
+    return {
+      isShowSelectionWindow: false,
+    }
+  },
+  methods: {
+    onClickCurrentMusic() {
+      if (this.$refs['my-audio']) {
+        this.$refs['my-audio'].switchPlayPause()
+      }
+    },
+    onClickDeleteMusicBtn() {
+      this.info.explanation.audioId = ''
+      this.info.explanation.audioUrl = ''
+      this.info.explanation.audioName = ''
+    },
+    handleSubmitFromMaterialSelector(selected) {
+      this.isShowSelectionWindow = false
+      this.info.explanation.audioId = selected[0].id
+      this.info.explanation.audioName = selected[0].name
+      this.info.explanation.audioOssPath = selected[0].ossPath
+    },
+  },
+  created() {
+    if (!this.info.explanation) {
+      this.$set(this.info, 'explanation', {
+        audioId: '',
+        audioName: '',
+        audioUrl: '',
+        openByDefault: true,
+        repeat: true,
+      })
+    }
+  },
+}
+</script>
+
+<style lang="less" scoped>
+.explanation-settings {
+  padding: 20px;
+  > .title {
+    font-size: 18px;
+    color: #fff;
+    > i {
+      font-size: 12px;
+      position: relative;
+      top: -2px;
+    }
+  }
+  > button {
+    width: 100%;
+    margin-top: 16px;
+    i {
+      font-size: 14px;
+    }
+  }
+  > .music-display {
+    cursor: pointer;
+    margin-top: 16px;
+    width: 234px;
+    height: 36px;
+    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;
+      }
+    }
+  }
+
+  .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>

+ 30 - 0
packages/qjkankan-editor/src/views/explanation/index.vue

@@ -0,0 +1,30 @@
+<template>
+  <div class="editor-explanation">
+    <ExplanationSettings class="explanation-settings"></ExplanationSettings>
+  </div>
+</template>
+
+<script>
+import ExplanationSettings from "./explanationSettings.vue";
+
+export default {
+  name: "EditorExplanation",
+  components: {
+    ExplanationSettings,
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.editor-explanation {
+  height: 100%;
+  position: relative;
+  .explanation-settings {
+    position: absolute;
+    right: 0;
+    width: 274px;
+    top: 0;
+    height: 100%;
+  }
+}
+</style>

+ 25 - 17
packages/qjkankan-editor/src/views/hotspot/index.vue

@@ -1,9 +1,9 @@
 <template>
-  <div>
-    <setting @select="handleSelectScene"></setting>
-    <!-- <toolbar></toolbar> -->
+  <!-- 编辑器-热点 -->
+  <div class="editor-hotspot">
+    <setting class="hotspot-settings" @select="handleSelectScene"></setting>
     <div class="dialog" v-if="show">
-        <Select
+      <Select
         @updateList="undatePano"
         :panos="list"
         :current="activeItem"
@@ -40,14 +40,14 @@
   </div>
 </template>
 <script>
-import Setting from "./Setting";
+import Setting from "./Setting.vue";
 import Select from "@/components/select";
 
 export default {
   name: "home",
   components: {
     Setting,
-    Select
+    Select,
   },
   mounted(){
     
@@ -104,8 +104,17 @@ export default {
 
 
 <style lang="less" scoped>
-
-.dialog {
+.editor-hotspot {
+  height: 100%;
+  position: relative;
+  .hotspot-settings {
+    position: absolute;
+    right: 0;
+    width: 274px;
+    top: 0;
+    height: 100%;
+  }
+  .dialog {
     position: fixed;
     z-index: 30;
     left: 0;
@@ -113,15 +122,14 @@ export default {
     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;
+    .pano-con{
+      height: auto;
+      background: none;
+      padding: 10px 0;
+      .ui-remark{
+        padding-left: 10px;
+      }
+    }
   }
 }
-
 </style>

+ 1 - 0
packages/qjkankan-editor/src/views/information/index.vue

@@ -1,4 +1,5 @@
 <template>
+<!-- todo: 用不到了? -->
   <div>
     <setting @select="handleInitScene"></setting>
     <toolbar

+ 42 - 409
packages/qjkankan-editor/src/views/navigation/groupSettings.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="group-settings">
+  <div class="group-settings" app-border dir-right>
     <div class="ui-title-big">场景导航
       <i class="iconfont icon-material_prompt tool-tip-for-editor" v-tooltip="'场景素材包括全景图和三维场景,您可自定义分组及场景的排列顺序。'">
       </i>
@@ -11,133 +11,24 @@
       新增分组
     </button>
 
-    <div class="obstructor"></div>
-
     <div class="scene-group-wrap">
-      <SceneGroupInEditor
-        v-for="(item) of catalogTopology"
+      <InsertPositionTip position-debug="-1" :index="0"></InsertPositionTip>
+      <div
+        v-for="(item, index) 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>
+      >
+        <SceneGroupInEditor
+          :groupNode="item"
+          :level="1"
+          @addGroup="onRequestForAddGroupLevel2"
+          @renameScene="onRenameScene"
+          @deleteScene="onDeleteScene"
+          @renameGroup="onRenameGroup"
+          @deleteGroup="onDeleteGroup"
+        />
+        <InsertPositionTip position-debug="0" :index="index + 1"></InsertPositionTip>
       </div>
     </div>
-
     <popup v-if="addGroupLevel" :canClose="false">
       <div class="ui-message ui-message-confirm dark add-group-window">
         <div class="ui-message-header">
@@ -151,8 +42,9 @@
           <input
             class="name-input"
             placeholder="请输入分组名称,限15个字"
-            v-model="newGroupName"
+            v-model.trim="newGroupName"
             maxlength="15"
+            @keydown.enter="newGroupName && onConfirmAddingGroup()"
           >
         </div>
 
@@ -162,6 +54,7 @@
           </button>
           <button
             class="ui-button submit"
+            :class="{disable: !newGroupName}"
             @click="onConfirmAddingGroup"
           >
             确定
@@ -173,124 +66,39 @@
 </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";
+import InsertPositionTip from "@/components/insertPositionTipInEditor.vue";
 
 export default {
   components: {
-    draggable,
-    tabList,
     SceneGroupInEditor,
     Popup,
+    InsertPositionTip,
   },
   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() {
+    onConfirmAddingGroup() { 
       if (this.addGroupLevel === 1) {
         let newGroupLevel2Id = 'c_' + this.$randomWord(true, 8, 8)
         const newGroupLevel1 = {
@@ -300,7 +108,7 @@ export default {
         }
         this.info.catalogRoot.push(newGroupLevel1)
         this.info.catalogs.push({
-          newGroupLevel2Id,
+          id: newGroupLevel2Id,
           name: '默认二级分组',
         })
       } else if (this.addGroupLevel === 2) {
@@ -357,10 +165,11 @@ export default {
     },
     onDeleteGroup(groupId, groupLevel) {
       const deleteGroupLevel2 = (groupId) => {
-        // 删除所属一级分组中的children中元素
-        // 因为用户无法看到、操作默认二级分组,所以这里要删除的group不可能是默认二级分组
+        // 要删除的二级分组在catalogRoot[x].children中的索引
         let targetGroupIdxLevel2 = null
+        // 要删除的二级分组所属的一级分组在catalogRoot中的索引
         let belongGroupIdxLevel1 = null
+        // 确定上边两个变量的取值
         for (const [groupIdxLevel1, groupLevel1] of this.info.catalogRoot.entries()) {
           for (const [groupIdxLevel2, childId] of groupLevel1.children.entries()) {
             if (childId === groupId) {
@@ -377,9 +186,11 @@ export default {
           console.log(targetGroupIdxLevel2, belongGroupIdxLevel1);
           throw('一级分组列表中没有找到要删除的二级分组!')
         }
+
+        // 删除catalogRoot[x].children中那个二级分组条目
         this.info.catalogRoot[belongGroupIdxLevel1].children.splice(targetGroupIdxLevel2, 1)
         
-        // 删除二级分组列表中元素
+        // 删除catalogs中那个二级分组条目
         const targetIdx = this.info.catalogs.findIndex((item) => {
           return item.id === groupId
         })
@@ -392,6 +203,17 @@ export default {
         this.info.scenes = this.info.scenes.filter((item) => {
           return item.category !== groupId
         })
+
+        // 如果所属一级分组中没有任何二级分组了,则新增一个默认二级分组
+        if (this.info.catalogRoot[belongGroupIdxLevel1].children.length === 0) {
+          let newGroupLevel2Id = 'c_' + this.$randomWord(true, 8, 8)
+          this.info.catalogRoot[belongGroupIdxLevel1].children.push(newGroupLevel2Id)
+          this.info.catalogs.push({
+            id: newGroupLevel2Id,
+            name: '默认二级分组',
+          })
+        }
+
         return
       }
 
@@ -433,70 +255,6 @@ export default {
         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=>{
@@ -505,42 +263,7 @@ export default {
           !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) {
@@ -552,6 +275,7 @@ export default {
   },
 };
 </script>
+
 <style lang="less" scoped>
 .group-settings {
   display: flex;
@@ -581,106 +305,15 @@ export default {
       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;
+    margin-top: 24px;
     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 {

+ 11 - 387
packages/qjkankan-editor/src/views/navigation/index.vue

@@ -1,365 +1,19 @@
 <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>
+    <GroupSettings class="group-settings-area"/>
+    <InitialSceneSettings class="initial-scene-settings-area"></InitialSceneSettings>
   </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'
+import GroupSettings from "./groupSettings.vue";
 
 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>
@@ -367,50 +21,20 @@ export default {
 <style lang="less" scoped>
 .editor-navigation {
   height: 100%;
-  display: flex;
+  position: relative;
   .group-settings-area {
+    position: absolute;
+    left: 0;
     width: 300px;
-    flex: 0 0 auto;
-  }
-  .preview-area {
-    background: red;
-    flex: 1 0 auto;
+    top: 0;
+    height: 100%; 
   }
   .initial-scene-settings-area {
+    position: absolute;
+    right: 0;
     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>

+ 45 - 39
packages/qjkankan-editor/src/views/navigation/initialSceneSettings.vue

@@ -2,32 +2,32 @@
   <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>
+      <i class="iconfont icon-material_prompt tool-tip-for-editor" v-tooltip="'初始场景为查看链接时进入的第一个场景,未设\n置时,不固定从某一场景打开。'"/>
     </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}">
+    
+    <img class="preview" v-if="info.firstScene" :src="info.firstScene.icon" alt="">
+    <img class="placeholder" v-else src="@/assets/images/pano-image-placeholder.png" alt="">
+
+    <div class="change-init" v-if="info.firstScene">
+      <button class="ui-button deepcancel" @click="deleteIndexInfo">删除场景</button>
+      <button @click="showInitScene=true" class="ui-button submit">
         修改场景
       </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="set-init" v-else>
+      <button @click="showInitScene=true" class="ui-button submit">
+        设置初始场景
+      </button>
+    </div>
 
     <div class="dialog" style="z-index: 2000" v-if="showInitScene">
+      <Selector
+        @cancle="showInitScene = false"
+        @submit="handleSelect"
+      />
+    </div>
+
+    <!-- <div class="dialog" style="z-index: 2000" v-if="showInitScene">
       <Select
         @cancle="showInitScene = false"
         :selected='info.firstScene'
@@ -35,17 +35,17 @@
         @submit="handleSelect"
       >
       </Select>
-    </div>
+    </div> -->
   </div>
 </template>
 
 <script>
 import { mapGetters } from "vuex";
-import Select from "@/components/select";
+import Selector from "@/components/materialSelectorFromWorkForEditor.vue";
 
 export default {
   components:{
-    Select,
+    Selector,
   },
   data(){
     return {
@@ -64,21 +64,17 @@ export default {
       });
       
     },
-    handleSelect(data){
-      this.info.firstScene = data
-      this.$store.commit("SetInfo", this.info);
-      console.log(this.info.firstScene);
-      this.showInitScene=false
+    handleSelect(data) {
+      this.info.firstScene = data[0] // 注意此处是浅拷贝
+      this.showInitScene = false
     }
   },
   computed: {
     ...mapGetters({
       info: "info",
-      backupInfo: "backupInfo"
     })
   },
   mounted(){
- 
   }
 }
 </script>
@@ -89,6 +85,7 @@ export default {
   > .initial-scene-settings__title {
     font-size: 18px;
     color: #fff;
+    margin-bottom: 16px;
     > i {
       font-size: 12px;
       position: relative;
@@ -97,20 +94,29 @@ export default {
   }
   .preview {
     width: 100%;
-    height: 102px;
-    overflow: hidden;
-    >img{
-      width: 100%;
-      height: 100%;
-    }
+    height: 132px;
+    border-radius: 4px;
+    margin-bottom: 16px;
+    object-fit: cover;
+    image-rendering: smooth;
   }
-  .setinit {
+  .placeholder {
+    width: 100%;
+    height: 132px;
+    margin-bottom: 16px;
+  }
+  .change-init {
     width: 100%;
-    margin: 15px 0;
     display: flex;
     justify-content: space-between;
     .ui-button {
-      width: 48%;
+      width: calc((100% - 14px) / 2)
+    }
+  }
+  .set-init {
+    width: 100%;
+    .ui-button {
+      width: 100%;
     }
   }
   .dialog {

+ 34 - 20
packages/qjkankan-editor/src/views/screen/Setting.vue

@@ -1,16 +1,12 @@
 <template>
-  <setting-panel>
-    <div class="view-setting" app-border dir-left>
-      <div class="ui-title">初始画面</div>
-      <div class="ui-remark">初始画面为进入场景时第一画面,请拖动全景图选择合适的画面设置。</div>
-      <div class="preview">
-        <img :src="initImg+`?${Math.random()}`" v-if="initImg" alt="">
-        <div class="tips" v-else>
-          <i class="iconfont iconphotoview" style="font-size: 40px"></i>
-        </div>
-      </div>
+  <div class="view-setting" app-border dir-left>
+    <div class="title">
+      初始画面
+      <i class="iconfont icon-material_prompt tool-tip-for-editor" v-tooltip="'初始画面为进入场景时第一画面,请拖动全景图选择合适的画面设置。'"/>
     </div>
-  </setting-panel>
+    <img class="preview" v-if="initImg" :src="`${initImg}?${Math.random()}`" alt="">
+    <img class="placeholder" v-else src="@/assets/images/pano-image-placeholder.png" alt="">
+  </div>
 </template>
 
 <script>
@@ -24,20 +20,38 @@ export default {
     }
   },
   mounted(){
-      this.$bus.on('initView',data=>{
-        this.initImg = data
-      })
+    this.$bus.on('initView', data => {
+      this.initImg = data
+    })
   }
 }
 </script>
 
 <style lang="less" scoped>
-.preview{
-    height: 122px;
-    margin-top: 10px;
-    >img{
-      width: 100%;
-      height: 100%;
+.view-setting {
+  padding: 20px;
+  > .title {
+    font-size: 18px;
+    color: #fff;
+    margin-bottom: 16px;
+    > i {
+      font-size: 12px;
+      position: relative;
+      top: -2px;
     }
+  }
+  .preview {
+    width: 100%;
+    height: 132px;
+    border-radius: 4px;
+    margin-bottom: 16px;
+    object-fit: cover;
+    image-rendering: smooth;
+  }
+  .placeholder {
+    width: 100%;
+    height: 132px;
+    margin-bottom: 16px;
+  }
 }
 </style>

+ 20 - 7
packages/qjkankan-editor/src/views/screen/index.vue

@@ -1,17 +1,30 @@
 <template>
-  <div>
-    <setting></setting>
-    <!-- <toolbar></toolbar> -->
+  <!-- 编辑器-初始 -->
+  <div class="view-screen">
+    <setting class="setting-panel"></setting>
   </div>
 </template>
 <script>
-import Setting from "./Setting";
-// import Toolbar from "../hotspot/Toolbar";
+import Setting from "./Setting.vue";
+
 export default {
-  name: "home",
+  name: "editor-initial-setting",
   components: {
     Setting,
-    // Toolbar
   }
 };
 </script>
+
+<style lang="less" scoped>
+.view-screen {
+  height: 100%;
+  position: relative;
+  .setting-panel {
+    position: absolute;
+    top: 0;
+    height: 100%;
+    right: 0;
+    width: 274px;
+  }
+}
+</style>