소스 검색

Merge branch 'master' of http://192.168.0.115:3000/bill/fuse-code

xzw 3 년 전
부모
커밋
0a277b3329
75개의 변경된 파일1095개의 추가작업 그리고 382개의 파일을 삭제
  1. 1 1
      src/api/fuse-model.ts
  2. 23 30
      src/api/view.ts
  3. 1 1
      src/components/bill-ui/assets/scss/editor/_toolbox.scss
  4. 1 1
      src/components/bill-ui/components/audio/index.vue
  5. 1 1
      src/components/bill-ui/components/button/index.vue
  6. 1 1
      src/components/bill-ui/components/cropper/cropper.vue
  7. 1 1
      src/components/bill-ui/components/floating/index.vue
  8. 0 1
      src/components/bill-ui/components/gate/layer.vue
  9. 95 3
      src/components/bill-ui/components/icon/iconfont/demo_index.html
  10. 19 3
      src/components/bill-ui/components/icon/iconfont/iconfont.css
  11. 1 1
      src/components/bill-ui/components/icon/iconfont/iconfont.js
  12. 28 0
      src/components/bill-ui/components/icon/iconfont/iconfont.json
  13. BIN
      src/components/bill-ui/components/icon/iconfont/iconfont.ttf
  14. BIN
      src/components/bill-ui/components/icon/iconfont/iconfont.woff
  15. BIN
      src/components/bill-ui/components/icon/iconfont/iconfont.woff2
  16. 1 1
      src/components/bill-ui/components/icon/index.vue
  17. 0 1
      src/components/bill-ui/components/input/check-radio.vue
  18. 0 1
      src/components/bill-ui/components/input/checkbox.vue
  19. 1 1
      src/components/bill-ui/components/input/color.vue
  20. 1 1
      src/components/bill-ui/components/input/file.vue
  21. 1 1
      src/components/bill-ui/components/input/number.vue
  22. 0 1
      src/components/bill-ui/components/input/radio.vue
  23. 1 1
      src/components/bill-ui/components/input/range.vue
  24. 1 1
      src/components/bill-ui/components/input/richtext.vue
  25. 1 1
      src/components/bill-ui/components/input/search.vue
  26. 1 1
      src/components/bill-ui/components/input/select.vue
  27. 0 1
      src/components/bill-ui/components/input/switch.vue
  28. 1 1
      src/components/bill-ui/components/input/text.vue
  29. 0 3
      src/components/bill-ui/components/input/textarea.vue
  30. 0 1
      src/components/bill-ui/components/loading/Loading.vue
  31. 1 1
      src/components/bill-ui/components/menu-item/index.vue
  32. 1 1
      src/components/bill-ui/components/message/message.vue
  33. 1 1
      src/components/bill-ui/components/size-animation/index.vue
  34. 1 1
      src/components/bill-ui/components/slide/index.vue
  35. 1 1
      src/components/bill-ui/components/tip/index.vue
  36. 1 1
      src/components/bill-ui/components/tree/index.vue
  37. 1 1
      src/components/list/index.vue
  38. 0 1
      src/components/menu/menu-item-child.vue
  39. 1 1
      src/components/menu/menu-item.vue
  40. 0 1
      src/env/index.ts
  41. 3 3
      src/layout/edit/fuse-edit.vue
  42. 4 10
      src/layout/edit/scene-edit.vue
  43. 1 3
      src/layout/main.vue
  44. 0 83
      src/layout/model/index.vue
  45. 12 10
      src/layout/scene-list/index.vue
  46. 26 0
      src/layout/show/index.vue
  47. 88 0
      src/layout/show/slide-menu.vue
  48. 100 0
      src/model/app.vue
  49. 63 0
      src/model/index.ts
  50. 160 0
      src/model/platform.ts
  51. 41 13
      src/router/config.ts
  52. 47 9
      src/router/constant.ts
  53. 1 1
      src/router/index.ts
  54. 21 10
      src/sdk/association.ts
  55. 4 1
      src/sdk/sdk.ts
  56. 47 9
      src/store/view.ts
  57. 1 0
      src/utils/index.ts
  58. 0 0
      src/views/folder/index.vue
  59. 0 3
      src/views/guide/index.vue
  60. 0 0
      src/views/guide/show.vue
  61. 3 1
      src/views/guide/sign.vue
  62. 0 0
      src/views/measure/show.vue
  63. 8 3
      src/views/merge/index.vue
  64. 1 0
      src/views/record/show.vue
  65. 85 42
      src/views/registration/index.vue
  66. 65 0
      src/views/summary/index.vue
  67. 3 53
      src/views/tagging/index.vue
  68. 0 0
      src/views/tagging/show.vue
  69. 54 4
      src/views/tagging/sign.vue
  70. 28 3
      src/views/view/index.vue
  71. 0 0
      src/views/view/show.vue
  72. 29 44
      src/views/view/sign.vue
  73. 2 0
      src/views/view/style.scss
  74. 3 1
      tsconfig.json
  75. 6 5
      vite.config.ts

+ 1 - 1
src/api/fuse-model.ts

@@ -64,7 +64,7 @@ const serviceToLocal = (serviceModel: ServiceFuseModel): FuseModel => ({
 })
 
 const localToService = (model: FuseModel): Omit<ServiceFuseModel, 'sceneData'> => ({
-  fusionId: Number(model.id),
+  fusionId: model.fusionId,
   hide: Number(!model.show),
   opacity: model.opacity,
   fusionNumId: model.fusionNumId,

+ 23 - 30
src/api/view.ts

@@ -10,7 +10,7 @@ export type View = {
   title: string
   sort: number
   flyData: string,
-} & ({ fusionId: number } | { num: string, numType: Scene['type'] })
+} & ({ fusionId: number, num: null, numType: null } | { fusionId: null, num: string, numType: Scene['type'] })
 
 type ServiceView = {
   viewId: number
@@ -18,37 +18,29 @@ type ServiceView = {
   viewPoint:	string	
   viewImg:	string	
   sort:	number	
-} & ({ fusionId: number } | { num: string, numType: Scene['type'] })
+} & ({ fusionId: string, num: null, numType: null } | { fusionId: null, num: string, numType: Scene['type'] })
 
-const toLocal = (serviceView: ServiceView) : View => {
-  const base = {
-    id: serviceView.viewId.toString(),
-    cover: serviceView.viewImg,
-    title: serviceView.viewTitle,
-    sort: serviceView.sort,
-    flyData: JSON.parse(serviceView.viewPoint),
-  }
-  if ('fusionId' in serviceView) {
-    return { ...base, fusionId: serviceView.fusionId }
-  } else {
-    return { ...base, num: serviceView.num, numType: serviceView.numType }
-  }
-}
+const toLocal = (serviceView: ServiceView) : View => ({
+  id: serviceView.viewId.toString(),
+  cover: serviceView.viewImg,
+  title: serviceView.viewTitle,
+  sort: serviceView.sort,
+  flyData: JSON.parse(serviceView.viewPoint),
+  num: serviceView.num, 
+  numType: serviceView.numType,
+  fusionId: serviceView.fusionId ? Number(serviceView.fusionId) : null
+} as View)
 
-const toService = (view: View, isUpdate = true): PartialProps<ServiceView, 'viewId'> => {
-  const base = {
-    viewId: isUpdate ? Number(view.id) : undefined,
-    viewTitle: view.title,	
-    viewPoint: JSON.stringify(view.flyData),
-    viewImg: view.cover,
-    sort:	view.sort,
-  }
-  if ('fusionId' in view) {
-    return { ...base, fusionId: view.fusionId } as ServiceView
-  } else {
-    return { ...base, num: view.num, numType: view.numType } as ServiceView
-  }
-}
+const toService = (view: View, isUpdate = true): PartialProps<ServiceView, 'viewId'> => ({
+  viewId: isUpdate ? Number(view.id) : undefined,
+  viewTitle: view.title,	
+  viewPoint: JSON.stringify(view.flyData),
+  viewImg: view.cover,
+  sort:	view.sort,
+  num: view.num, 
+  numType: view.numType,
+  fusionId: view.fusionId ? view.fusionId.toString() : null
+})
 
 export type Views = View[]
 
@@ -58,6 +50,7 @@ export const fetchViews = async () => {
 }
 
 export const postAddView = async (view: View) => {
+  console.log(view, { ...toService(view, false), caseId: params.caseId })
   const serviceView = await axios.post<ServiceView>(INSERT_VIEW, { ...toService(view, false), caseId: params.caseId })
   return toLocal(serviceView)
 }

+ 1 - 1
src/components/bill-ui/assets/scss/editor/_toolbox.scss

@@ -2,7 +2,7 @@
 
 .ui-editor-toolbox {
     position: absolute;
-    z-index: 1;
+    z-index: 2;
     right: var(--editor-menu-right);
     padding: 20px;
     width:  var(--editor-toolbox-width);;

+ 1 - 1
src/components/bill-ui/components/audio/index.vue

@@ -8,7 +8,7 @@
 </template>
 
 <script setup>
-import { defineProps, ref, watchEffect, defineExpose } from 'vue'
+import { ref, watchEffect } from 'vue'
 defineProps({
     src: String,
 })

+ 1 - 1
src/components/bill-ui/components/button/index.vue

@@ -9,7 +9,7 @@
 
 
 <script setup>
-import { defineProps, computed } from 'vue'
+import { computed } from 'vue'
 import { normalizeUnitToStyle } from '../../utils/index'
 import UIIcon from '../icon/index.vue'
 

+ 1 - 1
src/components/bill-ui/components/cropper/cropper.vue

@@ -14,7 +14,7 @@
 <script setup>
 import { VueCropper } from 'vue-cropper'
 import Confirm from '../dialog/Confirm.vue'
-import { computed, defineProps, ref } from 'vue'
+import { computed, ref } from 'vue'
 import 'vue-cropper/dist/index.css'
 
 const layerWidth = 500

+ 1 - 1
src/components/bill-ui/components/floating/index.vue

@@ -7,7 +7,7 @@
 </template>
 
 <script setup>
-import { defineProps, defineExpose, onUnmounted, reactive, watch, computed, onUpdated, onActivated, ref, watchEffect } from 'vue'
+import { onUnmounted, reactive, watch, computed, onUpdated, onActivated, ref, watchEffect } from 'vue'
 import { getPostionByTarget, getScrollParents, getZIndex } from '../../utils'
 
 const Horizontal = {

+ 0 - 1
src/components/bill-ui/components/gate/layer.vue

@@ -15,7 +15,6 @@
 <script setup>
 import { 
   ref, 
-  defineProps,
   watchEffect,
   computed,
   provide,

+ 95 - 3
src/components/bill-ui/components/icon/iconfont/demo_index.html

@@ -55,6 +55,30 @@
           <ul class="icon_lists dib-box">
           
             <li class="dib">
+              <span class="icon iconfont">&#xe6e4;</span>
+                <div class="name">list-scene</div>
+                <div class="code-name">&amp;#xe6e4;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe6e5;</span>
+                <div class="name">list-file</div>
+                <div class="code-name">&amp;#xe6e5;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe6e6;</span>
+                <div class="name">list-record</div>
+                <div class="code-name">&amp;#xe6e6;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe6e7;</span>
+                <div class="name">list-view</div>
+                <div class="code-name">&amp;#xe6e7;</div>
+              </li>
+          
+            <li class="dib">
               <span class="icon iconfont">&#xe63b;</span>
                 <div class="name">video</div>
                 <div class="code-name">&amp;#xe63b;</div>
@@ -306,9 +330,9 @@
 <pre><code class="language-css"
 >@font-face {
   font-family: 'iconfont';
-  src: url('iconfont.woff2?t=1661163199866') format('woff2'),
-       url('iconfont.woff?t=1661163199866') format('woff'),
-       url('iconfont.ttf?t=1661163199866') format('truetype');
+  src: url('iconfont.woff2?t=1661479592359') format('woff2'),
+       url('iconfont.woff?t=1661479592359') format('woff'),
+       url('iconfont.ttf?t=1661479592359') format('truetype');
 }
 </code></pre>
           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -335,6 +359,42 @@
         <ul class="icon_lists dib-box">
           
           <li class="dib">
+            <span class="icon iconfont icon-list-scene"></span>
+            <div class="name">
+              list-scene
+            </div>
+            <div class="code-name">.icon-list-scene
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-list-file"></span>
+            <div class="name">
+              list-file
+            </div>
+            <div class="code-name">.icon-list-file
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-list-record"></span>
+            <div class="name">
+              list-record
+            </div>
+            <div class="code-name">.icon-list-record
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-list-view"></span>
+            <div class="name">
+              list-view
+            </div>
+            <div class="code-name">.icon-list-view
+            </div>
+          </li>
+          
+          <li class="dib">
             <span class="icon iconfont icon-video1"></span>
             <div class="name">
               video
@@ -714,6 +774,38 @@
           
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-list-scene"></use>
+                </svg>
+                <div class="name">list-scene</div>
+                <div class="code-name">#icon-list-scene</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-list-file"></use>
+                </svg>
+                <div class="name">list-file</div>
+                <div class="code-name">#icon-list-file</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-list-record"></use>
+                </svg>
+                <div class="name">list-record</div>
+                <div class="code-name">#icon-list-record</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-list-view"></use>
+                </svg>
+                <div class="name">list-view</div>
+                <div class="code-name">#icon-list-view</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
                   <use xlink:href="#icon-video1"></use>
                 </svg>
                 <div class="name">video</div>

+ 19 - 3
src/components/bill-ui/components/icon/iconfont/iconfont.css

@@ -1,8 +1,8 @@
 @font-face {
   font-family: "iconfont"; /* Project id 3549513 */
-  src: url('iconfont.woff2?t=1661163199866') format('woff2'),
-       url('iconfont.woff?t=1661163199866') format('woff'),
-       url('iconfont.ttf?t=1661163199866') format('truetype');
+  src: url('iconfont.woff2?t=1661479592359') format('woff2'),
+       url('iconfont.woff?t=1661479592359') format('woff'),
+       url('iconfont.ttf?t=1661479592359') format('truetype');
 }
 
 .iconfont {
@@ -13,6 +13,22 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-list-scene:before {
+  content: "\e6e4";
+}
+
+.icon-list-file:before {
+  content: "\e6e5";
+}
+
+.icon-list-record:before {
+  content: "\e6e6";
+}
+
+.icon-list-view:before {
+  content: "\e6e7";
+}
+
 .icon-video1:before {
   content: "\e63b";
 }

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
src/components/bill-ui/components/icon/iconfont/iconfont.js


+ 28 - 0
src/components/bill-ui/components/icon/iconfont/iconfont.json

@@ -6,6 +6,34 @@
   "description": "",
   "glyphs": [
     {
+      "icon_id": "31485449",
+      "name": "list-scene",
+      "font_class": "list-scene",
+      "unicode": "e6e4",
+      "unicode_decimal": 59108
+    },
+    {
+      "icon_id": "31485450",
+      "name": "list-file",
+      "font_class": "list-file",
+      "unicode": "e6e5",
+      "unicode_decimal": 59109
+    },
+    {
+      "icon_id": "31485451",
+      "name": "list-record",
+      "font_class": "list-record",
+      "unicode": "e6e6",
+      "unicode_decimal": 59110
+    },
+    {
+      "icon_id": "31485452",
+      "name": "list-view",
+      "font_class": "list-view",
+      "unicode": "e6e7",
+      "unicode_decimal": 59111
+    },
+    {
       "icon_id": "23781429",
       "name": "video",
       "font_class": "video1",

BIN
src/components/bill-ui/components/icon/iconfont/iconfont.ttf


BIN
src/components/bill-ui/components/icon/iconfont/iconfont.woff


BIN
src/components/bill-ui/components/icon/iconfont/iconfont.woff2


+ 1 - 1
src/components/bill-ui/components/icon/index.vue

@@ -10,7 +10,7 @@
 </template>
 
 <script setup>
-import { defineProps, computed, defineEmits, ref, reactive, defineExpose } from 'vue'
+import { computed, ref, reactive } from 'vue'
 import { normalizeUnitToStyle, os } from '../../utils'
 import Icon from './icon/index.vue'
 import Tip from '../tip'

+ 0 - 1
src/components/bill-ui/components/input/check-radio.vue

@@ -23,7 +23,6 @@
 import icon from '../icon'
 import { checkboxPropsDesc } from './state'
 import { randomId } from '../../utils'
-import { defineProps, defineEmits } from 'vue'
 const props = defineProps(checkboxPropsDesc)
 const emit = defineEmits(['update:modelValue'])
 const id = randomId(4)

+ 0 - 1
src/components/bill-ui/components/input/checkbox.vue

@@ -14,7 +14,6 @@
 import icon from '../icon'
 import { checkboxPropsDesc } from './state'
 import { randomId } from '../../utils'
-import { defineProps, defineEmits } from 'vue'
 const props = defineProps(checkboxPropsDesc)
 const emit = defineEmits(['update:modelValue'])
 const id = randomId(4)

+ 1 - 1
src/components/bill-ui/components/input/color.vue

@@ -10,7 +10,7 @@
 <script setup>
 import { colorPropsDesc } from './state'
 import { randomId } from '../../utils'
-import { defineProps, defineEmits, nextTick } from 'vue'
+import { nextTick } from 'vue'
 const props = defineProps(colorPropsDesc)
 const emit = defineEmits(['update:modelValue'])
 const id = randomId(4)

+ 1 - 1
src/components/bill-ui/components/input/file.vue

@@ -40,7 +40,7 @@
 import { filePropsDesc } from './state'
 import { toRawType } from '../../utils'
 import Message from '../message'
-import { defineProps, defineEmits, defineExpose, ref, computed } from 'vue'
+import { ref, computed } from 'vue'
 
 const props = defineProps({
     ...filePropsDesc,

+ 1 - 1
src/components/bill-ui/components/input/number.vue

@@ -27,7 +27,7 @@
 <script setup>
 import UIText from './text'
 import { numberPropsDesc } from './state'
-import { defineProps, defineEmits, computed, watchEffect, ref } from 'vue'
+import { computed, watchEffect, ref } from 'vue'
 import { toRawType } from '../../utils'
 import Icon from '../icon'
 

+ 0 - 1
src/components/bill-ui/components/input/radio.vue

@@ -21,7 +21,6 @@
 import Icon from '../icon'
 import { radioPropsDesc } from './state'
 import { randomId } from '../../utils'
-import { defineProps, defineEmits } from 'vue'
 const props = defineProps(radioPropsDesc)
 const emit = defineEmits(['update:modelValue'])
 const id = randomId(4)

+ 1 - 1
src/components/bill-ui/components/input/range.vue

@@ -23,7 +23,7 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted, defineProps, watchEffect } from 'vue'
+import { ref, computed, onMounted, watchEffect } from 'vue'
 import { rangePropsDesc } from './state'
 import UInumber from './number.vue'
 import { os } from '../../utils/index'

+ 1 - 1
src/components/bill-ui/components/input/richtext.vue

@@ -27,7 +27,7 @@
 
 <script setup>
 import { richtextPropsDesc } from './state'
-import { defineProps, defineEmits, defineExpose, nextTick, ref, watchEffect } from 'vue'
+import { nextTick, ref, watchEffect } from 'vue'
 const props = defineProps({
     ...richtextPropsDesc,
 })

+ 1 - 1
src/components/bill-ui/components/input/search.vue

@@ -13,7 +13,7 @@
 </template>
 
 <script setup>
-import { ref, watchEffect, defineEmits, onUnmounted } from 'vue'
+import { ref, watchEffect, onUnmounted } from 'vue'
 import { searchPropsDesc, textEmitsDesc } from './state'
 import UISelect from './select.vue'
 

+ 1 - 1
src/components/bill-ui/components/input/select.vue

@@ -63,7 +63,7 @@
 <script setup>
 import UItext from './text.vue'
 import UIFloating from '../floating/index.vue'
-import { ref, onUnmounted, computed, defineExpose, watchEffect } from 'vue'
+import { ref, onUnmounted, computed, watchEffect } from 'vue'
 import { selectPropsDesc, selectEmitsDesc } from './state'
 import icon from '../icon'
 

+ 0 - 1
src/components/bill-ui/components/input/switch.vue

@@ -15,7 +15,6 @@
 <script setup>
 import { switchPropsDesc } from './state'
 import { randomId } from '../../utils'
-import { defineProps, defineEmits } from 'vue'
 const props = defineProps(switchPropsDesc)
 const emit = defineEmits(['update:modelValue'])
 const id = randomId(4)

+ 1 - 1
src/components/bill-ui/components/input/text.vue

@@ -40,7 +40,7 @@
 
 <script setup>
 import { textEmitsDesc, textPropsDesc } from './state'
-import { defineProps, defineEmits, defineExpose, nextTick, ref } from 'vue'
+import { nextTick, ref } from 'vue'
 const props = defineProps({
     type: {
         type: String,

+ 0 - 3
src/components/bill-ui/components/input/textarea.vue

@@ -30,9 +30,6 @@
 <script setup>
 import { textareaPropsDesc } from './state'
 import { 
-  defineProps, 
-  defineEmits, 
-  defineExpose,
   nextTick,
   ref
 } from 'vue'

+ 0 - 1
src/components/bill-ui/components/loading/Loading.vue

@@ -12,7 +12,6 @@
     </teleport>
 </template>
 <script setup>
-import { defineProps } from 'vue'
 import getZIndex from '../../utils/zindex'
 
 defineProps({

+ 1 - 1
src/components/bill-ui/components/menu-item/index.vue

@@ -14,7 +14,7 @@
 
 <script setup>
 import UIIcon from '../icon'
-import { defineProps, defineExpose, defineEmits, ref } from 'vue'
+import { ref } from 'vue'
 
 const self = ref(null)
 const props = defineProps({

+ 1 - 1
src/components/bill-ui/components/message/message.vue

@@ -18,7 +18,7 @@
 <script setup>
 import uiIcon from '../icon'
 import getZindex from '../../utils/zindex'
-import { defineProps, onMounted, ref, nextTick } from 'vue'
+import { onMounted, ref, nextTick } from 'vue'
 
 const props = defineProps({
   msg: {

+ 1 - 1
src/components/bill-ui/components/size-animation/index.vue

@@ -6,7 +6,7 @@
 
 <script setup>
 import { changeWHFactory } from '../../utils'
-import { defineExpose, ref, watchEffect, defineProps } from 'vue'
+import { ref, watchEffect } from 'vue'
 
 const props = defineProps({
     attr: {

+ 1 - 1
src/components/bill-ui/components/slide/index.vue

@@ -21,7 +21,7 @@
 
 <script setup>
 import { Gate, GateContent } from '../gate'
-import { defineProps, ref, watchEffect, computed } from 'vue'
+import { ref, watchEffect, computed } from 'vue'
 import UIIcon from '../icon'
 import { nextTick } from 'vue';
 

+ 1 - 1
src/components/bill-ui/components/tip/index.vue

@@ -6,7 +6,7 @@
 </template>
 
 <script setup>
-import { defineProps, computed, defineEmits } from 'vue'
+import { computed } from 'vue'
 import { os } from '../../utils'
 
 const props = defineProps({

+ 1 - 1
src/components/bill-ui/components/tree/index.vue

@@ -36,7 +36,7 @@
 export default { name: 'ui-tree' }
 </script>
 <script setup>
-import { defineProps, ref, computed, watch, onDeactivated, onActivated, watchEffect } from 'vue'
+import { ref, computed, watch, onDeactivated, onActivated, watchEffect } from 'vue'
 import UISizeAnimation from '../size-animation'
 import { inRevise } from '../../utils'
 

+ 1 - 1
src/components/list/index.vue

@@ -11,7 +11,7 @@
         v-for="(item, i) in data" 
         :key="key ? item[key] : i" 
         :class="{select: item.select}"
-        @click="$emit('changeSelect', item)"
+        @click.stop="$emit('changeSelect', item)"
       >
         <div class="atom-content">
           <slot name="atom" :item="item"></slot>

+ 0 - 1
src/components/menu/menu-item-child.vue

@@ -23,7 +23,6 @@
 </template>
 
 <script setup lang="ts">
-import { defineProps, defineEmits } from 'vue'
 import MenuChild from './menu-item.vue'
 
 import type { Item, Items } from './index.vue'

+ 1 - 1
src/components/menu/menu-item.vue

@@ -45,7 +45,7 @@
 
 <script setup lang="ts">
 import MenuChildItem from './menu-item-child.vue'
-import { defineProps, defineEmits, computed, ref } from 'vue'
+import { computed, ref } from 'vue'
 
 import type { Item, Items } from './index.vue'
 

+ 0 - 1
src/env/index.ts

@@ -18,7 +18,6 @@ export const currentModelStack = stackFactory(ref<FuseModel | null>(null))
 export const showModelsMapStack = stackFactory(ref<Map<FuseModel, boolean>>(new Map))
 export const modelsChangeStoreStack = stackFactory(ref<boolean>(false))
 export const showTaggingPositionsStack = stackFactory(ref<WeakSet<TaggingPosition>>(new WeakSet()))
-// export const showModelsChangeStoreStack = stackFactory
 
 export const custom = flatStacksValue({
   viewMode: viewModeStack,

+ 3 - 3
src/layout/edit/fuse-edit.vue

@@ -1,6 +1,5 @@
 <template>
   <template v-if="loaded" style="height: 100%">
-    <Model :type="FUSE" />
     <Header></Header>
     <router-view v-slot="{ Component }">
       <keep-alive>
@@ -15,7 +14,7 @@ import { ref, watch } from 'vue'
 import { currentMeta, router } from '@/router'
 import { showLeftPanoStack, showRightPanoStack } from '@/env'
 import { togetherCallback } from '@/utils'
-import { Model, FUSE } from '../model/index.vue'
+import { loadModel, fuseModel } from '@/model'
 import { 
   enterEdit, 
   isOld, 
@@ -35,6 +34,7 @@ Promise.all([
   initialGuides(),
   initialMeasures()
 ])
+.then(() => loadModel(fuseModel))
 .then(() => loaded.value = true)
 
 router.beforeEach(async (to, from, next) => {
@@ -51,5 +51,5 @@ watch(currentMeta, (meta, _, onClean) => {
       showRightPanoStack.push(ref(false)),
     ]))
   }
-}, { flush: 'post' })
+}, { flush: 'post', immediate: true })
 </script>

+ 4 - 10
src/layout/edit/scene-edit.vue

@@ -1,9 +1,8 @@
 <template>
   <Header></Header>
   <LeftPano>
-    <SceneList v-model:current="currentModelType" />
+    <SceneList :current="currentModel" @update:current="loadModel" />
   </LeftPano>
-  <Model :type="currentModelType" />
 
   <router-view v-slot="{ Component }">
     <keep-alive>
@@ -14,20 +13,15 @@
 </template>
 
 <script setup lang="ts">
-import { ref, watchEffect } from 'vue'
 import Header from './header/index.vue'
 import SceneList from '../scene-list/index.vue'
-import { Model, FUSE } from '../model/index.vue'
 import { LeftPano } from '@/layout'
 import { custom } from '@/env'
+import { onMounted } from 'vue'
+import { currentModel, loadModel, fuseModel } from '@/model'
 
-import type { ModelType } from '../model/index.vue'
-
-const currentModelType = ref<ModelType>(FUSE)
-
-watchEffect(() => console.log(currentModelType.value))
+onMounted(() => loadModel(fuseModel))
 custom.showLeftPano = true
-
 </script>
 
 <style>

+ 1 - 3
src/layout/main.vue

@@ -18,11 +18,9 @@
 
 <script lang="ts" setup>
 import { custom } from '@/env'
-import { computed, watchEffect } from 'vue'
+import { computed } from 'vue'
 import { isEdit, appEl } from '@/store'
 
-watchEffect(() => console.log(appEl.value))
-
 const layoutClassNames = computed(() => {
   return {
     [`sys-view-${custom.viewMode}`]: true,

+ 0 - 83
src/layout/model/index.vue

@@ -1,83 +0,0 @@
-<template>
-  <iframe class="external" :src="url" v-if="url"></iframe>
-  <div class="laser-layer" v-show="!url">
-    <div class="scene-canvas" ref="fuseRef"></div>
-  </div>
-</template>
-
-<script lang="ts">
-import { defineComponent, ref, watchEffect, computed } from 'vue'
-import { initialSDK, initialed } from '@/sdk'
-import { getScene, SceneType } from '@/store'
-import { showModelsMapStack } from '@/env'
-
-import type { PropType } from 'vue'
-import type { Scene } from '@/store'
-
-export const FUSE = Symbol('fuse')
-export type ModelType = symbol | Scene['id']
-
-export const Model = defineComponent({
-  name: 'model',
-  props: {
-    type: {
-      type: [Number, Symbol] as PropType<ModelType>,
-      required: true
-    }
-  },
-  setup(props) {
-    const fuseRef = ref<HTMLDivElement>()
-    const stopSDKEffect = watchEffect(async () => {
-      if (!initialed && props.type === FUSE && fuseRef.value) {
-        await initialSDK({ layout: fuseRef.value })
-        stopSDKEffect()
-      }
-    })
-    const scene = computed(() => props.type !== FUSE && getScene(props.type as number))
-    const url = computed(() => {
-      if (!scene.value) return;
-      const type = scene.value.type
-      const kk = [SceneType.SWKK || SceneType.SWKJ]
-      const pathname = kk.includes(type) ? 'swkk/spg.html' : 'swss/uat/index.html'
-
-      return `/${pathname}?m=${scene.value.num}`
-    })
-    watchEffect((onCleanup) => {
-      if (url.value) {
-        onCleanup(showModelsMapStack.push(ref(new Map())))
-      }
-    })
-
-    return {
-      FUSE,
-      scene,
-      fuseRef,
-      url
-    }
-  }
-})
-
-export default Model
-</script>
-
-<style scoped lang="scss">
-.external,
-.laser-layer {
-  position: absolute;
-  z-index: 1;
-  left: 0;
-  top: 0;
-  width: 100%;
-  height: 100%;
-
-  .scene-canvas {
-    width: 100%;
-    height: 100%;
-    background-color: #ccc;
-  }
-}
-
-.external {
-  border: none;
-}
-</style>

+ 12 - 10
src/layout/scene-list/index.vue

@@ -5,23 +5,23 @@
     </template>
     <template #atom="{ item }">
       <div 
-        v-if="item.raw === FUSE" 
+        v-if="item.raw === fuseModel" 
         @click="$emit('update:current', current === item.raw ? null : item.raw)"
       >
         <ModelList
           class="model-list"
-          title="融合场景"
-          :show-content="current === FUSE"
+          :title="getModelTypeDesc(fuseModel as any)"
+          :show-content="current === fuseModel"
         >
           <template #action>
             <ui-icon 
-              :type="`pull-${current === FUSE ? 'up' : 'down'}`" 
+              :type="`pull-${current === fuseModel ? 'up' : 'down'}`" 
               ctrl 
             />
           </template>
         </ModelList>
       </div>
-      <div class="scene" @click="$emit('update:current', item.raw.id)" v-else>
+      <div class="scene" @click="$emit('update:current', {type: item.raw.type, num: item.raw.num})" v-else>
         <p>{{ item.raw.name }}</p>
         <p>{{ SceneTypeDesc[item.raw.type as SceneType] }}</p>
       </div>
@@ -30,13 +30,13 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, ref } from 'vue'
+import { computed } from 'vue'
 import { scenes, SceneType, SceneTypeDesc } from '@/store'
-import { FUSE } from '../model/index.vue'
 import List from '@/components/list/index.vue'
 import ModelList from '../model-list/index.vue'
+import { fuseModel, getModelTypeDesc } from '@/model'
 
-import type { ModelType } from '../model/index.vue'
+import type { ModelType } from '@/model'
 
 defineEmits<{ (e: 'update:current', data: ModelType): void }>()
 const props = defineProps<{ current: ModelType }>()
@@ -44,9 +44,11 @@ const props = defineProps<{ current: ModelType }>()
 const list = computed(() => {
   const sceneList = scenes.value.map(scene => ({
     raw: scene,
-    select: props.current === scene.id
+    select: props.current !== fuseModel 
+      && props.current.num === scene.num 
+      && props.current.type === scene.type
   }))
-  return [{ raw: FUSE }, ...sceneList]
+  return [{ raw: fuseModel }, ...sceneList]
 })
 
 </script>

+ 26 - 0
src/layout/show/index.vue

@@ -0,0 +1,26 @@
+<template>
+  <SlideMenu />
+  
+  <router-view v-slot="{ Component }">
+    <keep-alive>
+      <component :is="Component" />
+    </keep-alive>
+  </router-view>
+</template>
+
+<script lang="ts" setup>
+import { custom } from '@/env'
+import { onMounted } from 'vue'
+import { loadModel, fuseModel } from '@/model'
+import SlideMenu from './slide-menu.vue'
+
+onMounted(() => loadModel(fuseModel))
+custom.showLeftPano = true
+</script>
+
+<style>
+:root {
+  --editor-menu-width: 80px;
+  --editor-head-height: 0px;
+}
+</style>

+ 88 - 0
src/layout/show/slide-menu.vue

@@ -0,0 +1,88 @@
+<template>
+  <div class="slide-menu">
+    <div 
+      v-for="item in items" 
+      :class="{active: item.name === router.currentRoute.value.name}" 
+      :key="item.name"
+      @click="router.push({ name: item.name })"
+    >
+      <ui-icon :type="item.icon" :tip="item.title" class="icon" />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { metas, RoutesName, router } from '@/router'
+
+const items = [
+  {
+    name: RoutesName.summaryShow,
+    ...metas[RoutesName.summaryShow]
+  },
+  {
+    name: RoutesName.viewShow,
+    ...metas[RoutesName.viewShow]
+  },
+  {
+    name: RoutesName.recordShow,
+    ...metas[RoutesName.recordShow]
+  },
+  {
+    name: RoutesName.folderShow,
+    ...metas[RoutesName.folderShow]
+  }
+]
+</script>
+
+<style lang="scss" scoped>
+
+.slide-menu {
+  width: var(--editor-menu-width);
+  filter: var(--editor-menu-filter); 
+  background-color: var(--editor-menu-back);
+  position: fixed;
+  left: var(--editor-menu-left);
+  top: var(--editor-head-height);
+  bottom: 0;
+  z-index: 2000;
+  overflow: hidden;
+  backdrop-filter: blur(4px);
+
+  > div {
+    height: 70px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+    color: rgba(255, 255, 255, 0.6);
+    transition: color .3s ease;
+    cursor: pointer;
+
+    &::before {
+      content: '';
+      position: absolute;
+      left: 0;
+      top: 0;
+      bottom: 0;
+      width: 0;
+      background: currentColor;
+      transition: width .3s ease;
+    }
+
+    &.active,
+    &:hover {
+      color: #00C8AF;
+    }
+
+    &.active::before {
+      width: 4px;
+    }
+
+    .icon {
+      font-size: 24px;
+      color: currentColor;
+    }
+    
+  }
+}
+</style>

+ 100 - 0
src/model/app.vue

@@ -0,0 +1,100 @@
+<template>
+  <iframe class="external" :src="url" ref="iframeRef" v-if="url"></iframe>
+  <div class="laser-layer" v-show="!url">
+    <div class="scene-canvas" ref="fuseRef"></div>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, watchEffect, computed, watch, onMounted, nextTick } from 'vue'
+import { SceneType } from '@/store'
+import { showModelsMapStack } from '@/env'
+import { fuseModel, modelProps } from './index'
+import { modelSDKFactory } from './platform'
+
+const typeChange = () => {
+  const oldType = modelProps.type
+  let stopWatch = null as unknown as () => void
+
+  const typePromise = new Promise((_, reject) => {
+    stopWatch = watchEffect(() => {
+      if (modelProps.type !== oldType) {
+        reject(new Error('当前模型未加载完已切换到下个'))
+        stopWatch!()
+      }
+    })
+  })
+  return { typePromise, typeCleanup: stopWatch }
+}
+
+export const Model = defineComponent({
+  name: 'model',
+  setup() {
+    const scene = computed(() => modelProps.type !== fuseModel && modelProps.type)
+    const url = computed(() => {
+      if (!scene.value) return;
+      const type = scene.value.type
+      const kk = [SceneType.SWKK || SceneType.SWKJ]
+      const pathname = kk.includes(type) ? 'swkk/spg.html' : 'swss/index.html'
+
+      return `/${pathname}?m=${scene.value.num}`
+    })
+    const fuseRef = ref<HTMLDivElement>()
+    const iframeRef = ref<HTMLIFrameElement>()
+
+    watch(
+      () => modelProps.type, 
+      async (type, oldType, onCleanup) => {
+        const callback = modelProps.callback
+
+        if (oldType === fuseModel) {
+          onCleanup(showModelsMapStack.push(ref(new Map())))
+        }
+
+        await nextTick()
+        const { typePromise, typeCleanup } = typeChange()
+        const modelPromise = modelSDKFactory(type, type === fuseModel ? fuseRef.value! : iframeRef.value!)
+        let result: any = null, error = null
+        try {
+          result = await Promise.race([typePromise, modelPromise])
+        } catch (err: Error) {
+          error = err
+        }
+        typeCleanup()
+        callback && callback(result, error)
+      }, 
+      { immediate: true, flush: 'post' }
+    )
+
+    return {
+      iframeRef,
+      fuseRef,
+      url
+    }
+  }
+})
+
+export default Model
+</script>
+
+<style scoped lang="scss">
+.external,
+.laser-layer {
+  position: absolute;
+  z-index: 1;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+
+  .scene-canvas {
+    width: 100%;
+    height: 100%;
+    background-color: #ccc;
+  }
+}
+
+.external {
+  border: none;
+}
+</style>

+ 63 - 0
src/model/index.ts

@@ -0,0 +1,63 @@
+import App from './app.vue'
+import { appEl, SceneTypeDesc } from '@/store'
+import { mount, deepIsRevise } from '@/utils'
+import { reactive, ref } from 'vue'
+
+import type { Scene } from '@/store'
+import type { ModelExpose } from './platform'
+
+export type FuseModelType = typeof fuseModel
+export type SceneModelType = Pick<Scene, 'type' | 'num'>
+export type ModelType = FuseModelType | SceneModelType
+export type ModelProps = { type: ModelType, callback: ((expose?: ModelExpose, err?: Error) => void) | null }
+export type { ModelExpose }
+
+export const fuseModel = Symbol('fuse')
+export const currentModel = ref<ModelType>(fuseModel)
+export const modelProps: ModelProps = reactive({ type: currentModel, callback: null })
+export const getModelTypeDesc = (model: ModelType) => {
+  if (model === fuseModel) {
+    return '融合场景'
+  } else {
+    return SceneTypeDesc[model.type]
+  }
+}
+
+const _loadModel = (() => {
+  let oldModelType: ModelType
+  let oldResult: Promise<ModelExpose>
+
+  return (modelType: ModelType) => {
+    if (!deepIsRevise(oldModelType, modelType)) {
+      return oldResult
+    }
+    console.log('加载', modelType)
+
+    oldModelType = modelType
+    return oldResult = new Promise<any>((resolve, reject) => {
+      modelProps.callback = (data: any, err) => {
+        if (err) {
+          reject(err)
+        } else {
+          resolve(data)
+        }
+      }
+      currentModel.value = modelType
+    })
+  }
+})();
+
+
+let isInitial = false
+export const loadModel = (modelType: ModelType): Promise<ModelExpose> => {
+  const modelPromise = _loadModel(modelType)
+  if (!isInitial) {
+    if (!appEl.value) {
+      throw new Error('appEl 未初始化')
+    } else {
+      mount(appEl.value, App)
+      isInitial = true
+    }
+  }
+  return modelPromise
+}

+ 160 - 0
src/model/platform.ts

@@ -0,0 +1,160 @@
+import { watchEffect, nextTick } from 'vue'
+import { fuseModelsLoaded, SceneType } from '@/store'
+import { fuseModel } from './'
+import { initialSDK, initialed as fuseInitialed, sdk as fuseSDK } from '@/sdk'
+import { asyncTimeout } from '@/utils'
+
+import type { ModelType } from './'
+
+export async function modelSDKFactory (
+  type: ModelType, 
+  dom: HTMLDivElement | HTMLIFrameElement
+): Promise<ModelExpose> {
+  if (type === fuseModel) {
+    if (!fuseInitialed) {
+      await initialSDK({ layout: dom })
+    }
+    return exposeFactory(fuseModel)
+  } else {
+    const iframe = dom as HTMLIFrameElement
+    const win = await new Promise<Window | null>((resolve, reject) => {
+      const loadedHandler = () => {
+        resolve(iframe.contentWindow)
+        cleanup()
+      }
+      const errorHandler = (err: any) => {
+        reject(err)
+        cleanup()
+      }
+      const cleanup = () => {
+        iframe.removeEventListener('load', loadedHandler)
+        iframe.removeEventListener('error', errorHandler)
+      }
+      iframe.addEventListener('load', loadedHandler)
+      iframe.addEventListener('error', errorHandler)
+    })
+
+    if (!win) {
+      throw new Error('场景加载失败')
+    }
+    return await exposeFactory(type, win)
+  }
+}
+
+
+const findObjectAttr = <T, K extends keyof T>(data: T, key: K): Promise<T[K]> => {
+  return new Promise<T[K]>(resolve => {
+    const query = () => {
+      if (key in data) {
+        resolve(data[key])
+      } else {
+        setTimeout(query, 6)
+      }
+    }
+    query()
+  })
+}
+
+const fuseLoaded = new Promise<void>(resolve => {
+  const stop = watchEffect(() => {
+    if (fuseModelsLoaded.value) {
+      resolve()
+      nextTick(() => stop())
+    }
+  })
+})
+
+export interface ModelExpose {
+  getView: () => Promise<{ image: Blob, flyData: string }>
+  setView: (flyData: string) => void
+}
+
+export async function exposeFactory(type: ModelType, win?: any): Promise<ModelExpose> {
+  const sceneType = type === fuseModel ? fuseModel : type.type
+  const platforms: {[key in any]: {getSDK: () => Promise<void>, expose: ModelExpose}} = {
+    [fuseModel]: {
+      getSDK: () => fuseLoaded,
+      expose: {
+        async getView() {
+          const dataURL = await fuseSDK.screenshot(260, 160)
+          const res = await fetch(dataURL)
+          const image = await res.blob()
+          const pose = fuseSDK.getPose()
+          return {
+            image,
+            flyData: JSON.stringify(pose)
+          }
+        },
+        async setView(flyData: string) {
+          const pose = JSON.parse(flyData)
+          fuseSDK.comeTo({ dur: 300, ...pose })
+        }
+      }
+    },
+    [SceneType.SWKK]: {
+      getSDK: async () => {
+        const sdk = await findObjectAttr(win , '__sdk')
+        if (!sdk.Scene.loaded) {
+          await new Promise(reoslve => sdk.Scene.on('loaded', reoslve))
+        }
+        return sdk
+      },
+      expose: {
+        async getView() {
+          const pose = sdk.Camera.getPose()
+          const images = await sdk.Camera.screenshot(
+            [{ width: 260, height: 160, name: '2k' }], 
+            true
+          )
+          return {
+            image: images[0].data, 
+            flyData: JSON.stringify(pose)
+          }
+        },
+        async setView(flyData: string) {
+          const pose = JSON.parse(flyData)
+          console.log('===>?', pose)
+          sdk.Camera.setPose({ dur: 300, ...pose })
+        }
+      }
+    },
+    [SceneType.SWSS]: {
+      getSDK: async () => {
+        await findObjectAttr(win, 'laserLoaded')
+        return await findObjectAttr(win, '__sdk')
+      },
+      expose: {
+        async getView() {
+          const dataURL = await sdk.scene.screenshot(260, 160)
+          const res = await fetch(dataURL)
+          const image = await res.blob()
+          const pose = await sdk.scene.getPose()
+          const mode = sdk.customMap.mode
+
+          return {
+            image, 
+            flyData: JSON.stringify({ pose, mode })
+          }
+        },
+        async setView(flyData: string) {
+          const { pose, mode } = JSON.parse(flyData)
+          sdk.customMap.mode  = mode
+          sdk.scene.setPose(pose, 300)
+          console.error('setView')
+        }
+      }
+    }
+  }
+  platforms[SceneType.SWKJ] = platforms[SceneType.SWKK]
+
+  if (!(sceneType in platforms)) {
+    throw new Error('不支持该类型场景!')
+  }
+
+  const sdk: any = await Promise.race([
+    asyncTimeout(10000)
+      .then(() => Promise.reject(new Error('加载超时'))),
+    platforms[sceneType].getSDK()
+  ])
+  return platforms[sceneType].expose
+}

+ 41 - 13
src/router/config.ts

@@ -4,40 +4,40 @@ import type { RouteRecordRaw } from 'vue-router'
 
 export const routes: RouteRecordRaw[] = [
   {
-    path: paths.fuseEdit,
+    path: paths[RoutesName.fuseEdit],
     name: RoutesName.fuseEdit,
     component: () => import('@/layout/edit/fuse-edit.vue'),
     children: [
       {
-        path: paths.switch,
-        name: RoutesName.switch,
+        path: paths[RoutesName.fuseEditSwitch],
+        name: RoutesName.fuseEditSwitch,
         component: () => import('@/layout/edit/fuse-switch.vue'),
         children: [
           {
-            path: paths.merge,
+            path: paths[RoutesName.merge],
             name: RoutesName.merge,
             meta: metas.merge,
             component: () => import('@/views/merge/index.vue')
           },
           {
-            path: paths.tagging,
+            path: paths[RoutesName.tagging],
             name: RoutesName.tagging,
             meta: metas.tagging,
             component: () => import('@/views/tagging/index.vue')
           },
           {
-            path: paths.taggingPosition,
+            path: paths[RoutesName.taggingPosition],
             name: RoutesName.taggingPosition,
             component: () => import('@/views/tagging-position/index.vue')
           },
           {
-            path: paths.measure,
+            path: paths[RoutesName.measure],
             name: RoutesName.measure,
             meta: metas.measure,
             component: () => import('@/views/measure/index.vue')
           },
           {
-            path: paths.guide,
+            path: paths[RoutesName.guide],
             name: RoutesName.guide,
             meta: metas.guide,
             component: () => import('@/views/guide/index.vue')
@@ -45,33 +45,61 @@ export const routes: RouteRecordRaw[] = [
         ]
       },
       {
-        path: paths.registration,
+        path: paths[RoutesName.registration],
         name: RoutesName.registration,
         component: () => import('@/views/registration/index.vue')
       },
       {
-        path: paths.proportion,
+        path: paths[RoutesName.proportion],
         name: RoutesName.proportion,
         component: () => import('@/views/proportion/index.vue')
       }
     ]
   },
   {
-    path: paths.sceneEdit,
+    path: paths[RoutesName.sceneEdit],
     name: RoutesName.sceneEdit,
     component: () => import('@/layout/edit/scene-edit.vue'),
     children: [
       {
-        path: paths.record,
+        path: paths[RoutesName.record],
         name: RoutesName.record,
         component: () => import('@/views/record/index.vue')
       },
       {
-        path: paths.view,
+        path: paths[RoutesName.view],
         name: RoutesName.view,
         component: () => import('@/views/view/index.vue')
       }
     ]
+  },
+  {
+    path: paths[RoutesName.show],
+    name: RoutesName.show,
+    component: () => import('@/layout/show/index.vue'),
+    children: [
+      {
+        path: paths[RoutesName.summaryShow],
+        name: RoutesName.summaryShow,
+        component: () => import('@/views/summary/index.vue')
+      },
+      {
+        path: paths[RoutesName.viewShow],
+        name: RoutesName.viewShow,
+        component: () => import('@/views/view/show.vue')
+      },
+      {
+        path: paths[RoutesName.recordShow],
+        name: RoutesName.recordShow,
+        component: () => import('@/views/record/show.vue')
+      },
+      {
+        path: paths[RoutesName.folderShow],
+        name: RoutesName.folderShow,
+        component: () => import('@/views/folder/index.vue')
+      },
+      
+    ]
   }
 ]
 

+ 47 - 9
src/router/constant.ts

@@ -1,26 +1,40 @@
 export enum RoutesName {
-  merge = 'merge',
+  // 编辑融合页面
+  fuseEdit = 'fuseEdit',
+  
+  // 独立,配准、设置比例
   registration = 'registration',
   proportion = 'proportion',
 
+  // 菜单,独立编辑融合页面
+  fuseEditSwitch = 'fuseEditSwitch',
+  merge = 'merge',
   tagging = 'tagging',
   taggingPosition = 'taggingPosition',
   guide = 'guide',
   measure = 'measure',
-
-  fuseEdit = 'fuseEdit',
-  switch = 'switch',
-
+  
+  // 编辑场景,提取视图,录制视频
   sceneEdit = 'sceneEdit',
   record = 'record',
-  view = 'view'
+  view = 'view',
+
+
+
+  // 展示界面,包括融合和独立场景
+  show = 'show',
+  // 汇总
+  summaryShow = 'summaryShow',
+  recordShow = 'recordShow',
+  viewShow = 'viewShow',
+  folderShow = 'folderShow',
 }
 
 
 export const paths = {
   [RoutesName.fuseEdit]: '/fuseEdit',
 
-  [RoutesName.switch]: '',
+  [RoutesName.fuseEditSwitch]: '',
   [RoutesName.merge]: 'merge',
   [RoutesName.registration]: 'registration/:id',
   [RoutesName.proportion]: 'proportion/:id',
@@ -32,7 +46,14 @@ export const paths = {
   
   [RoutesName.sceneEdit]: '/sceneEdit',
   [RoutesName.record]: 'record',
-  [RoutesName.view]: 'view'
+  [RoutesName.view]: 'view',
+
+  [RoutesName.show]: '/show',
+  [RoutesName.summaryShow]: 'summary',
+  [RoutesName.recordShow]: 'record',
+  [RoutesName.viewShow]: 'view',
+  [RoutesName.folderShow]: 'folder',
+  
 }
 
 export const metas = {
@@ -54,6 +75,23 @@ export const metas = {
     icon: 'nav-measure',
     title: '测量'
   },
-}
 
+
+  [RoutesName.summaryShow]: {
+    icon: 'list-scene',
+    title: '汇总'
+  },
+  [RoutesName.recordShow]: {
+    icon: 'list-view',
+    title: '视图'
+  },
+  [RoutesName.viewShow]: {
+    icon: 'list-record',
+    title: '录屏'
+  },
+  [RoutesName.folderShow]: {
+    icon: 'list-file',
+    title: '卷宗'
+  },
+}
 export const ViewHome = RoutesName.merge

+ 1 - 1
src/router/index.ts

@@ -31,7 +31,7 @@ export const currentRouteNames = computed(() => {
 
 export const currentLayout = computed(() => {
   const names = currentRouteNames.value
-  const layoutNames = [RoutesName.switch] as const
+  const layoutNames = [RoutesName.fuseEditSwitch] as const
   return layoutNames.find(name => names.includes(name))
 })
 

+ 21 - 10
src/sdk/association.ts

@@ -1,6 +1,6 @@
 import { sdk } from './sdk'
 import { fuseModels, taggings, isEdit, sysBus, getFuseModelShowVariable, SceneType } from '@/store'
-import { toRaw, watchEffect, ref, watch } from 'vue'
+import { toRaw, watchEffect, ref, watch, nextTick } from 'vue'
 import { viewModeStack, custom, getResource } from '@/env'
 import { 
   mount, 
@@ -13,19 +13,28 @@ import {
 
 import TaggingComponent from '@/components/tagging/list.vue'
 
+import type { SDK, SceneModel, SceneGuidePath, ModelAttrRange } from '.'
+import { FuseModel, Tagging } from '@/store'
+
 export const modelRange: ModelAttrRange  = {
   opacityRange: { min: 0, max: 100, step: 0.1 },
   bottomRange: { min: -30, max: 70, step: 0.1 },
   scaleRange: { min: 0, max: 200, step: 0.1 }
 }
 
-import type { SDK, SceneModel, SceneGuidePath, ModelAttrRange } from '.'
-import { FuseModel, Tagging } from '@/store'
-
 const sceneModelMap = new WeakMap<FuseModel, SceneModel>()
-export const getSceneModel = (model: FuseModel | null) => model && sceneModelMap.get(toRaw(model))
+export const getSceneModel = (model?: FuseModel | null) => model && sceneModelMap.get(toRaw(model))
 
 const associationModels = (sdk: SDK) => {
+  let isUnSet = false
+  const unSet = ((fn: () => void) => {
+    nextTick(() => {
+      isUnSet = true
+      fn()
+      nextTick(() => isUnSet = false)
+    })
+  })
+
   const getModels = () => fuseModels.value
   shallowWatchArray(getModels, (models, oldModels) => {
     const { added, deleted } = diffArrayChange(models, oldModels)
@@ -44,7 +53,7 @@ const associationModels = (sdk: SDK) => {
       sceneModelMap.set(itemRaw, sceneModel)
 
       sceneModel.bus.on('transformChanged', transform => {
-        Object.assign(item, transform)
+        unSet(() => Object.assign(item, transform))
       })
       sceneModel.bus.on('changeSelect', select => {
         if (custom.currentModel === item && !select) {
@@ -77,10 +86,12 @@ const associationModels = (sdk: SDK) => {
       (loaded) => {
         if (loaded) {
           const modelShow = getFuseModelShowVariable(item)
-          watchEffect(() => getSceneModel(item)?.changeBottom(item.bottom))
-          watchEffect(() => getSceneModel(item)?.changeOpacity(item.opacity))
-          watchEffect(() => getSceneModel(item)?.changeScale(item.scale))
-          watchEffect(() => getSceneModel(item)?.changeShow(modelShow.value))
+          watch(() => item.bottom, () => isUnSet || getSceneModel(item)?.changeBottom(item.bottom), {immediate: true})
+          watch(() => item.opacity, () => isUnSet || getSceneModel(item)?.changeOpacity(item.opacity), {immediate: true})
+          watch(() => item.scale, () => isUnSet || getSceneModel(item)?.changeScale(item.scale), {immediate: true})
+          watch(() => item.position, () => isUnSet || getSceneModel(item)?.changePosition(item.position), {immediate: true})
+          watch(() => item.rotation, () => isUnSet || getSceneModel(item)?.changeRotation(item.rotation), {immediate: true})
+          watch(() => modelShow.value, () => isUnSet || getSceneModel(item)?.changeShow(modelShow.value), {immediate: true})
           stopLoadedWatch()
         }
       }

+ 4 - 1
src/sdk/sdk.ts

@@ -6,7 +6,7 @@ import type { Emitter } from 'mitt'
 
 
 type SceneModelAttrs = FuseModelAttrs & { select: boolean }
-export type SceneModel = ToChangeAPI<Omit<SceneModelAttrs, 'position' | 'rotation'>>
+export type SceneModel = ToChangeAPI<SceneModelAttrs>
   & { 
     bus: Emitter<
       Pick<SceneModelAttrs, 'select'> & 
@@ -27,7 +27,10 @@ export type SceneModel = ToChangeAPI<Omit<SceneModelAttrs, 'position' | 'rotatio
     enterRotateMode: () => void
     enterMoveMode: () => void
     leaveTransform: () => void
+    enterAlignment: () => void
+    leaveAlignment: () => void
   }
+  
 
 export type ModelAttrRange = {
   [key in 'opacity' | 'bottom' | 'scale' as `${key}Range`]: {

+ 47 - 9
src/store/view.ts

@@ -16,22 +16,55 @@ import {
   postDeleteView,
   uploadFile
 } from '@/api'
+import { fuseModel } from '@/model'
 
 import type { View as SView } from '@/api'
+import type { ModelType } from '@/model'
 
 export type View = LocalMode<SView, 'cover'>
 export type Views = View[]
 
 export const views = ref<Views>([])
 
-export const createView = (): View => ({
-  id: createTemploraryID(),
-  title: '视图',
-  cover: 'https://4dkk.4dage.com/scene_view_data/KK-t-F8e5M46wcQ/images/floor_0.png?t=1659422513133?v=0&rnd=0.9219648338739086&x-oss-process=image/resize,m_fill,w_80,h_60/quality,q_70&rnd=0.25420557086595965',
-  flyData: '',
-  fusionId: fuseModels.value[0].fusionId,
-  sort: Math.min(...views.value.map(item => item.sort)) - 1,
-})
+export const createView = (view: Partial<View> = {}): View => {
+  const base = {
+    id: createTemploraryID(),
+    title: '视图',
+    cover: 'https://4dkk.4dage.com/scene_view_data/KK-t-F8e5M46wcQ/images/floor_0.png?t=1659422513133?v=0&rnd=0.9219648338739086&x-oss-process=image/resize,m_fill,w_80,h_60/quality,q_70&rnd=0.25420557086595965',
+    flyData: '',
+    num: null,
+    numType: null,
+    fusionId: fuseModels.value[0].fusionId,
+    sort: Math.min(...views.value.map(item => item.sort)) - 1,
+    ...view,
+  }
+  if (typeof view.fusionId === 'number') {
+    return {
+      ...base,
+      ...view,
+      fusionId: view.fusionId,
+      num: null,
+      numType: null
+    }
+  } else if (view.num && typeof view.numType === 'number') {
+    return {
+      ...base,
+      num: view.num,
+      numType: view.numType,
+      fusionId: null,
+    }
+  } else {
+    return base as View
+  }
+}
+
+export const viewToModelType = (view: View): ModelType => {
+  if (typeof view.fusionId === 'number') {
+    return fuseModel
+  } else {
+    return { num: view.num!, type: view.numType! }
+  }
+}
 
 
 let bcViews: Views = []
@@ -68,5 +101,10 @@ export const saveViews = saveStoreItems(
 export const autoSaveViews = autoSetModeCallback(views, {
   backup: backupViews,
   recovery: recoverViews,
-  save: saveViews
+  save: async () => {
+    for (let i = 0; i < views.value.length; i++) {
+      views.value[i].sort = i
+    }
+    await saveViews()
+  }
 })

+ 1 - 0
src/utils/index.ts

@@ -60,6 +60,7 @@ export const jsonToForm = (data: { [key in string]: any }) => {
   return formData
 }
 
+
 export * from './store-help'
 export * from "./stack";
 export * from "./loading";

+ 0 - 0
src/views/folder/index.vue


+ 0 - 3
src/views/guide/index.vue

@@ -13,7 +13,6 @@
         v-for="guide in guides" 
         :key="guide.id" 
         :guide="guide" 
-        @play="playSceneGuide(getGuidePaths(guide))"
         @edit="edit(guide)"
         @delete="deleteGuide(guide)"
       />
@@ -31,13 +30,11 @@ import { ref } from 'vue';
 import GuideSign from './sign.vue'
 import EditPaths from './edit-paths.vue'
 import { useViewStack } from '@/hook'
-import { playSceneGuide } from '@/sdk'
 import { 
   guides, 
   createGuide, 
   enterEdit, 
   sysBus, 
-  getGuidePaths,
   autoSaveGuides
 } from '@/store'
 

+ 0 - 0
src/views/guide/show.vue


+ 3 - 1
src/views/guide/sign.vue

@@ -7,7 +7,8 @@
           type="preview" 
           class="icon" 
           ctrl 
-          @click="emit('play')" v-if="paths.length" 
+          @click="playSceneGuide(paths)" 
+          v-if="paths.length" 
         />
       </div>
       <div>
@@ -29,6 +30,7 @@ import { Guide, getGuidePaths } from '@/store'
 import { getFileUrl } from '@/utils'
 import { getResource } from '@/env'
 import { computed } from 'vue';
+import { playSceneGuide } from '@/sdk'
 
 const props = defineProps<{ guide: Guide }>()
 const emit = defineEmits<{ 

+ 0 - 0
src/views/measure/show.vue


+ 8 - 3
src/views/merge/index.vue

@@ -61,14 +61,14 @@
 </template>
 
 <script lang="ts" setup>
-import { RoutesName, router } from '@/router'
+import { RoutesName, router, currentMeta } from '@/router'
 import { RightPano } from '@/layout'
 import { autoSaveFuseModels, defaultFuseModelAttrs } from '@/store'
 import { togetherCallback } from '@/utils'
 import { getSceneModel, modelRange } from '@/sdk'
 import { useViewStack, useActive } from '@/hook'
 import { showLeftCtrlPanoStack, showLeftPanoStack, custom, modelsChangeStoreStack } from '@/env'
-import { ref, nextTick } from 'vue'
+import { ref, nextTick, watchEffect } from 'vue'
 import { Dialog } from 'bill/expose-common'
 
 import Actions from '@/components/actions/index.vue'
@@ -76,7 +76,6 @@ import Actions from '@/components/actions/index.vue'
 import type { ActionsProps, ActionsItem } from '@/components/actions/index.vue'
 
 const active = useActive()
-const currentItem = ref<ActionsItem | null>(null)
 const actionItems: ActionsProps['items'] = [
   {
     icon: 'move',
@@ -95,6 +94,12 @@ const actionItems: ActionsProps['items'] = [
     }
   },
 ]
+const currentItem = ref<ActionsItem | null>(null)
+watchEffect(() => {
+  if (!custom.currentModel) {
+    currentItem.value = null
+  }
+})
 
 const reset = async () => {
   if (custom.currentModel && await Dialog.confirm('确定恢复默认?此操作无法撤销')) {

+ 1 - 0
src/views/record/show.vue

@@ -0,0 +1 @@
+

+ 85 - 42
src/views/registration/index.vue

@@ -1,78 +1,121 @@
 <template>
-  <ControlPanl 
-    :group="[{ items: options }]" 
-    v-model="selectOptions" 
-    ref="selectExpose"
-  />
-  <ui-floating
-    v-if="selectOptions.some(({key}) => key === 'opacity')"
-    :refer="opacityOptionEl"
-    isTransform
-    dire="right-center"
-  >
-    <div class="floating-range strengthen">
+  <template v-if="model && sceneModel">
+    <ControlPanl 
+      :group="[{ items: options }]" 
+      v-model="selectOptions" 
+      ref="selectExpose"
+    />
+    <ui-floating
+      v-if="selectOptions.some(({key}) => key === 'opacity')"
+      :refer="opacityOptionEl"
+      isTransform
+      dire="right-center"
+    >
+      <div class="floating-range strengthen">
+        <div class="range-content">
+          <ui-input 
+            type="range" 
+            v-model="model.opacity"
+            v-bind="modelRange.opacityRange" 
+            :ctrl="false" 
+            :input="false"
+            width="100%"
+          />
+          <span class="num" :style="{left: `${model.opacity}%`}">{{model.opacity}}%</span>
+        </div>
+      </div>
+    </ui-floating>
+
+    <div class="right-range floating-range strengthen">
       <div class="range-content">
+        <span class="fun-ctrl" @click="model!.bottom += modelRange.bottomRange.step">+</span>
         <ui-input 
           type="range" 
-          v-model="a"
-          v-bind="modelRange.opacityRange" 
+          v-model="model.bottom"
+          v-bind="modelRange.bottomRange" 
+          :moveCallback="changeRange"
           :ctrl="false" 
           :input="false"
           width="100%"
         />
-        <span class="num" :style="{left: `${a}%`}">{{a}}%</span>
+        <span class="fun-ctrl" @click="model!.bottom -= modelRange.bottomRange.step">-</span>
       </div>
     </div>
-  </ui-floating>
-
-  <div class="right-range floating-range strengthen">
-    <div class="range-content">
-      <span class="fun-ctrl" @click="a += modelRange.opacityRange.step">+</span>
-      <ui-input 
-        type="range" 
-        v-model="a"
-        v-bind="modelRange.opacityRange" 
-        :moveCallback="changeRange"
-        :ctrl="false" 
-        :input="false"
-        width="100%"
-      />
-      <span class="fun-ctrl" @click="a -= modelRange.opacityRange.step">-</span>
-    </div>
-  </div>
 
-  <div class="ui-message tip-left">请在当前窗口调整水平方向位置</div>
-  <div class="ui-message tip-right">请在当前窗口调整垂直方向位置</div>
+    <div class="ui-message tip-left">请在当前窗口调整水平方向位置</div>
+    <div class="ui-message tip-right">请在当前窗口调整垂直方向位置</div>
+  </template>
 </template>
 
 <script setup lang="ts">
 import { ref, computed, watch } from 'vue'
 import { ControlPanl } from '@/components/control-panl/'
-import { modelRange } from '@/sdk'
+import { modelRange, getSceneModel } from '@/sdk'
 import { diffArrayChange } from '@/utils'
+import { useViewStack } from '@/hook'
+import { autoSaveFuseModels, getFuseModel, leave } from '@/store'
+import { router } from '@/router'
 
-import type { Items, ControlExpose } from '@/components/control-panl'
+import type { ControlExpose } from '@/components/control-panl'
+
+const model = computed(() => {
+  const modelId = router.currentRoute.value.params.id as string
+  if (modelId) {
+    return getFuseModel(modelId)
+  }
+})
 
-const options: Items = [
+const sceneModel = computed(() => model.value && getSceneModel(model.value))
+const options = [
   { desc: '移动', icon: 'move', key: 'move' },
   { desc: '旋转', icon: 'flip', key: 'rotate' },
   { desc: '透明度', icon: 'transparency', key: 'opacity' },
 ]
-const selectOptions = ref<Items>([])
+const selectOptions = ref<typeof options>([])
 const selectExpose = ref<ControlExpose>()
 const opacityOptionEl = computed(
   () => selectExpose.value?.dom?.querySelector('div[data-key="opacity"]')
 )
-const a = ref(1)
 
 const changeRange = (sp: ScreenLocalPos, cp: ScreenLocalPos, info: { start: number, locusWidth: number }) => 
   info.start + ((sp.y - cp.y) / info.locusWidth)
 
 watch(selectOptions, (nOptions, oOptions = []) => {
   const { added, deleted } = diffArrayChange(nOptions, oOptions)
-  console.log(added, deleted)
+  const setKeys = ['move', 'rotate']
+  const addOptions = added.filter(option => setKeys.includes(option.key))
+  const delOptions = deleted.filter(option => setKeys.includes(option.key))
+
+  if (sceneModel.value) {
+    if (!addOptions.length && delOptions.length) {
+        sceneModel.value.leaveTransform()
+    } else if (addOptions.length) {
+      if (addOptions[0].key === 'move') {
+        sceneModel.value.enterMoveMode()
+      } else {
+        sceneModel.value.enterRotateMode()
+      }
+    }
+  }
 }, { immediate: true })
 
+useViewStack(() => {
+  if (sceneModel.value) {
+    const model = sceneModel.value
+    model.enterAlignment()
+    return () => {
+      if (selectOptions.value.length) {
+        model.leaveTransform()
+        selectOptions.value = []
+      }
+      model.leaveAlignment()
+    }
+  } else {
+    leave()
+  }
+})
+useViewStack(autoSaveFuseModels)
+
 </script>
 
 <style lang="scss" scoped>
@@ -114,7 +157,7 @@ watch(selectOptions, (nOptions, oOptions = []) => {
   height: 40px;
   right: 10px;
   top: 50%;
-  z-index: 1;
+  z-index: 2;
   transform: translateX(40%) rotate(-90deg);
 
   .range-content {
@@ -132,7 +175,7 @@ watch(selectOptions, (nOptions, oOptions = []) => {
 
 .tip-left,.tip-right {
   top: calc(var(--editor-head-height) + var(--header-top) + 11px);
-  z-index: 1;
+  z-index: 2;
 }
 
 .tip-left {

+ 65 - 0
src/views/summary/index.vue

@@ -0,0 +1,65 @@
+<template>
+  <LeftPano>
+    <SceneList :current="currentModel" @update:current="loadModel" />
+  </LeftPano>
+
+  <RightFillPano>
+    <ui-group>
+      <TaggingSign 
+        v-for="tagging in taggings" 
+        :key="tagging.id" 
+        :tagging="tagging" 
+      />
+    </ui-group>
+    <ui-group>
+      <MeasureSign 
+        v-for="measure in measures" 
+        :key="measure.id" 
+        :measure="measure" 
+      />
+    </ui-group>
+    <ui-group>
+      <GuideSign 
+        v-for="guide in guides" 
+        :key="guide.id" 
+        :guide="guide" 
+      />
+    </ui-group>
+  </RightFillPano>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useViewStack } from '@/hook'
+import { togetherCallback } from '@/utils'
+import { showRightCtrlPanoStack, showRightPanoStack } from '@/env'
+import { currentModel, fuseModel, loadModel } from '@/model'
+import { LeftPano, RightFillPano } from '@/layout'
+import SceneList from '@/layout/scene-list/index.vue'
+import TaggingSign from '@/views/tagging/sign.vue'
+import MeasureSign from '@/views/measure/sign.vue'
+import GuideSign from '@/views/guide/sign.vue'
+import { 
+  taggings, 
+  guides, 
+  measures, 
+  initialTaggings, 
+  initialGuides, 
+  initialMeasures,
+  initialTaggingStyles,
+} from '@/store'
+
+initialTaggingStyles()
+initialTaggings() 
+initialGuides()
+initialMeasures() 
+
+const showRightPano = computed(() => currentModel.value === fuseModel)
+
+useViewStack(
+  () => togetherCallback([
+    showRightCtrlPanoStack.push(showRightPano), 
+    showRightPanoStack.push(showRightPano)
+  ])
+)
+</script>

+ 3 - 53
src/views/tagging/index.vue

@@ -30,7 +30,7 @@
         :selected="selectTagging === tagging"
         @edit="editTagging = tagging"
         @delete="deleteTagging(tagging)"
-        @select="selectTagging = tagging"
+        @select="selected => selectTagging = selected ? tagging : null"
         @fixed="fixedTagging(tagging)"
       />
     </ui-group>
@@ -49,23 +49,20 @@ import Edit from './edit.vue'
 import TagingSign from './sign.vue'
 import { RightFillPano } from '@/layout'
 import { useViewStack } from '@/hook'
-import { computed, nextTick, ref, watchEffect } from 'vue';
-import { sdk } from '@/sdk'
+import { computed, ref } from 'vue';
 import { router, RoutesName } from '@/router'
+import { custom } from '@/env'
 import { 
   taggings, 
   isTemploraryID, 
   Tagging, 
   autoSaveTaggings, 
   createTagging,
-  getFuseModel,
-  getFuseModelShowVariable,
   getTaggingPositions,
   taggingPositions,
   isOld,
   save
 } from '@/store'
-import { custom, showTaggingPositionsStack } from '@/env'
 
 const keyword = ref('')
 const filterTaggings = computed(() => taggings.value.filter(tagging => tagging.title.includes(keyword.value)))
@@ -95,54 +92,7 @@ const fixedTagging = async (tagging: Tagging) => {
   router.push({ name: RoutesName.taggingPosition, params: { id: tagging.id } })
 }
 
-const flyTaggingPositions = (tagging: Tagging, callback?: () => void) => {
-  const positions = getTaggingPositions(tagging)
-
-  let isStop = false
-  const flyIndex = (i: number) => {
-    if (isStop || i >= positions.length) {
-      callback && nextTick(callback)
-      return;
-    }
-    const position = positions[i]
-    const model = getFuseModel(position.modelId)
-    if (!model || !getFuseModelShowVariable(model).value) {
-      flyIndex(i + 1)
-      return;
-    }
-
-    const pop = showTaggingPositionsStack.push(ref(new WeakSet([position])))
-    sdk.comeTo({ 
-      position: position.localPos, 
-      modelId: position.modelId,
-      dur: 300,
-      distance: 3
-    })
-    
-    setTimeout(() => {
-      pop()
-      flyIndex(i + 1)
-    }, 2000)
-  }
-  flyIndex(0)
-  return () => isStop = true
-}
 
 const selectTagging = ref<Tagging | null>(null)
-watchEffect((onCleanup) => {
-  if (selectTagging.value) {
-    const success = () => selectTagging.value = null
-    const stop = flyTaggingPositions(selectTagging.value, success)
-    const keyupHandler = (ev: KeyboardEvent) => ev.code === 'Escape' && success()
-
-    document.documentElement.addEventListener('keyup', keyupHandler, false)
-    onCleanup(() => {
-      stop()
-      console.log('removeHandler')
-      document.documentElement.removeEventListener('keyup', keyupHandler, false)
-    })
-  }
-})
-
 useViewStack(autoSaveTaggings)
 </script>

+ 0 - 0
src/views/tagging/show.vue


+ 54 - 4
src/views/tagging/sign.vue

@@ -1,5 +1,9 @@
 <template>
-  <ui-group-option class="sign-tagging" :class="{active: selected}" @click="!disabledFly && emit('select')">
+  <ui-group-option 
+    class="sign-tagging" 
+    :class="{active: selected}" 
+    @click="!disabledFly && emit('select', true)"
+  >
     <div class="info">
       <img :src="getResource(getFileUrl(tagging.images.length ? tagging.images[0] : style.icon))" v-if="style">
       <div>
@@ -20,8 +24,9 @@
 
 <script setup lang="ts">
 import { getFileUrl } from '@/utils'
-import { computed } from 'vue';
-import { getResource } from '@/env'
+import { computed, ref, watchEffect, nextTick } from 'vue';
+import { getResource, showTaggingPositionsStack } from '@/env'
+import { sdk } from '@/sdk'
 import { 
   getTaggingStyle, 
   getTaggingPositions, 
@@ -43,7 +48,7 @@ const disabledFly = computed(() =>
 const emit = defineEmits<{ 
   (e: 'delete'): void 
   (e: 'edit'): void
-  (e: 'select'): void
+  (e: 'select', selected: boolean): void
   (e: 'fixed'): void
 }>()
 
@@ -56,7 +61,52 @@ const actions = {
   delete: () => emit('delete')
 }
 
+const flyTaggingPositions = (tagging: Tagging, callback?: () => void) => {
+  const positions = getTaggingPositions(tagging)
 
+  let isStop = false
+  const flyIndex = (i: number) => {
+    if (isStop || i >= positions.length) {
+      callback && nextTick(callback)
+      return;
+    }
+    const position = positions[i]
+    const model = getFuseModel(position.modelId)
+    if (!model || !getFuseModelShowVariable(model).value) {
+      flyIndex(i + 1)
+      return;
+    }
+
+    const pop = showTaggingPositionsStack.push(ref(new WeakSet([position])))
+    sdk.comeTo({ 
+      position: position.localPos, 
+      modelId: position.modelId,
+      dur: 300,
+      distance: 3
+    })
+    
+    setTimeout(() => {
+      pop()
+      flyIndex(i + 1)
+    }, 2000)
+  }
+  flyIndex(0)
+  return () => isStop = true
+}
+watchEffect((onCleanup) => {
+  if (props.selected) {
+    const success = () => emit('select', false)
+    const stop = flyTaggingPositions(props.tagging, success)
+    const keyupHandler = (ev: KeyboardEvent) => ev.code === 'Escape' && success()
+
+    document.documentElement.addEventListener('keyup', keyupHandler, false)
+    onCleanup(() => {
+      stop()
+      console.log('removeHandler')
+      document.documentElement.removeEventListener('keyup', keyupHandler, false)
+    })
+  }
+})
 </script>
 
 <style lang="scss" scoped src="./style.scss"></style>

+ 28 - 3
src/views/view/index.vue

@@ -1,7 +1,7 @@
 <template>
   <RightFillPano>
     <div class="btns header-btns">
-      <ui-button class="start" @click="start">
+      <ui-button class="start" @click="getView">
         <ui-icon type="add" />
         视图提取
       </ui-button>
@@ -26,15 +26,40 @@
 <script lang="ts" setup>
 import { views, createView, autoSaveViews, initialViews } from '@/store'
 import { RightFillPano } from '@/layout'
+import { useViewStack } from '@/hook'
 import Draggable from 'vuedraggable'
 import Sign from './sign.vue'
-import { useViewStack } from '@/hook'
+import { loadModel, currentModel, fuseModel } from '@/model'
+import { loadPack } from '@/utils'
+import { Message } from 'bill/index'
 
 import type { View } from '@/store'
 
 initialViews()
 
-const start = () => views.value.push(createView())
+const getView = async () => {
+  try {
+    const { image, flyData } = await loadPack(async () => {
+      const modelSDK = await loadModel(currentModel.value)
+      return await modelSDK.getView()
+    })
+
+    const type = currentModel.value !== fuseModel 
+      ? { numType: currentModel.value.type, num: currentModel.value.num }
+      : {}
+
+    views.value.push(createView({
+      flyData,
+      cover: {
+        blob: image,
+        url: URL.createObjectURL(image)
+      },
+      ...type
+    }))
+  } catch (e: Error) {
+    Message.error(e.message)
+  }
+}
 const deleteView = (record: View) => {
   const index = views.value.indexOf(record)
   if (~index) {

+ 0 - 0
src/views/view/show.vue


+ 29 - 44
src/views/view/sign.vue

@@ -1,7 +1,7 @@
 <template>
   <ui-group-option class="sign">
     <div class="content">
-      <span class="cover">
+      <span class="cover" @click="fly">
         <img :src="getResource(getFileUrl(view.cover))" alt="">
       </span>
       <ui-input 
@@ -12,9 +12,9 @@
         ref="inputRef" 
         height="28px" 
       />
-      <div class="title" v-show="!isEditTitle">
+      <div class="title" v-show="!isEditTitle" @click="fly">
         <p>{{ view.title }}</p>
-        <span>{{ view.title }}</span>
+        <span>{{ getModelTypeDesc(modelType as ModelType) }}</span>
       </div>
     </div>
     <div class="action">
@@ -28,54 +28,39 @@
   </ui-group-option>
 </template>
 
-<script lang="ts">
-import { defineComponent, ref, computed } from 'vue'
+<script lang="ts" setup>
+import { ref, computed } from 'vue'
 import { useFocus } from 'bill/hook/useFocus'
-import { Preview } from '@/components/static-preview/index.vue'
 import { getResource } from '@/env'
 import { getFileUrl } from '@/utils'
+import { loadModel, getModelTypeDesc, ModelType } from '@/model'
+import { viewToModelType } from '@/store'
 
-import type { PropType } from 'vue'
 import type { View } from '@/store'
 
-export default defineComponent({
-  props: {
-    view: {
-      type: Object as PropType<View>,
-      required: true
-    }
-  },
-  emits: {
-    'updateCover': (cover: string) => true,
-    'updateTitle': (title: string) => true,
-    'delete': () => true
-  },
-  setup(props, { emit }) {
-    const menus = [
-      { label: '编辑', value: 'rename' },
-      { label: '删除', value: 'delete' },
-    ]
-    
-    const inputRef = ref()
-    const isEditTitle = useFocus(computed(() => inputRef.value?.vmRef.root))
-    const actions = {
-      delete: () => emit('delete'),
-      rename: () => isEditTitle.value = true
-    }
+const props = defineProps<{ view: View }>()
+const emit = defineEmits<{
+    (e: 'updateCover', cover: string): void,
+    (e: 'updateTitle', title: string): void,
+    (e: 'delete'): void,
+}>()
 
-    return {
-      menus,
-      actions,
-      isEditTitle,
-      getFileUrl,
-      inputRef,
-      getResource
-    }
-  },
-  components: {
-    Preview
-  }
-})
+const menus = [
+  { label: '编辑', value: 'rename' },
+  { label: '删除', value: 'delete' },
+]
+
+const inputRef = ref()
+const isEditTitle = useFocus(computed(() => inputRef.value?.vmRef.root))
+const actions = {
+  delete: () => emit('delete'),
+  rename: () => isEditTitle.value = true
+}
+const modelType = viewToModelType(props.view)
+const fly = async () => {
+  const sdk = await loadModel(modelType)
+  sdk.setView(props.view.flyData)
+}
 </script>
 
 

+ 2 - 0
src/views/view/style.scss

@@ -54,6 +54,7 @@
     color: #fff;
     font-size: 16px;
     margin-right: 10px;
+    cursor: pointer;
 
     img {
       display: block;
@@ -63,6 +64,7 @@
   }
 
   .title {
+    cursor: pointer;
     p {
       font-size: 14px;
     }

+ 3 - 1
tsconfig.json

@@ -17,8 +17,10 @@
     "paths": {
       "bill/*": ["src/components/bill-ui/*"],
       "@/*": ["src/*"]
-    }
+    },
+    "outDir": "./dist"
   },
   "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
+  "exclude": ["src/components/bill-ui/*"],
   "references": [{ "path": "./tsconfig.node.json" }]
 }

+ 6 - 5
vite.config.ts

@@ -31,6 +31,11 @@ export default defineConfig({
         changeOrigin: true,
         rewrite: path => path.replace(/^\/local/, '')
       },
+      '/api/laser': {
+        target: 'https://uat-laser.4dkankan.com/',
+        changeOrigin: true,
+        rewrite: path => path.replace(/^\/api/, '')
+      },
       '/api': {
         target: 'http://192.168.0.47:8808',
         changeOrigin: true,
@@ -46,13 +51,9 @@ export default defineConfig({
         changeOrigin: true,
       },
       '/swss': {
-        target: 'https://uat-laser.4dkankan.com/',
+        target: 'http://localhost:8080/',
         changeOrigin: true,
         rewrite: path => path.replace(/^\/swss/, '')
-      },
-      '/laser': {
-        target: 'https://uat-laser.4dkankan.com/',
-        changeOrigin: true,
       }
     }
   }