浏览代码

Merge branch 'v1.2.0-ga' of http://192.168.0.115:3000/bill/fuse-code into v1.2.0-ga

xzw 5 月之前
父节点
当前提交
30d1721393
共有 94 个文件被更改,包括 2909 次插入756 次删除
  1. 7 0
      src/api/constant.ts
  2. 4 0
      src/api/guide-path.ts
  3. 14 0
      src/api/guide.ts
  4. 2 1
      src/api/index.ts
  5. 65 0
      src/api/monitor.ts
  6. 1 0
      src/api/sys.ts
  7. 15 1
      src/api/tagging-position.ts
  8. 102 0
      src/components/actions-merge/index.vue
  9. 164 3
      src/components/bill-ui/components/icon/iconfont/demo_index.html
  10. 31 3
      src/components/bill-ui/components/icon/iconfont/iconfont.css
  11. 1 1
      src/components/bill-ui/components/icon/iconfont/iconfont.js
  12. 49 0
      src/components/bill-ui/components/icon/iconfont/iconfont.json
  13. 二进制
      src/components/bill-ui/components/icon/iconfont/iconfont.ttf
  14. 二进制
      src/components/bill-ui/components/icon/iconfont/iconfont.woff
  15. 二进制
      src/components/bill-ui/components/icon/iconfont/iconfont.woff2
  16. 14 4
      src/components/drawing-time/current.vue
  17. 7 3
      src/components/drawing/hook.ts
  18. 10 1
      src/components/drawing/renderer.vue
  19. 11 0
      src/components/global-search/guide.vue
  20. 239 0
      src/components/global-search/index.vue
  21. 10 0
      src/components/global-search/map.vue
  22. 16 0
      src/components/global-search/measure.vue
  23. 11 0
      src/components/global-search/model.vue
  24. 10 0
      src/components/global-search/monitor.vue
  25. 10 0
      src/components/global-search/path.vue
  26. 11 0
      src/components/global-search/tagging.vue
  27. 19 0
      src/components/global-search/view.vue
  28. 28 0
      src/components/right-menu/index.ts
  29. 40 0
      src/components/right-menu/index.vue
  30. 1 1
      src/components/tagging/sign-new.vue
  31. 54 0
      src/components/view-setting/index.vue
  32. 63 50
      src/env/index.ts
  33. 8 1
      src/hook/ids.ts
  34. 139 0
      src/hook/use-fly.ts
  35. 26 13
      src/hook/use-pixel.ts
  36. 13 5
      src/layout/edit/fuse-edit.vue
  37. 4 19
      src/layout/model-list/index.vue
  38. 3 2
      src/layout/model-list/sign.vue
  39. 15 0
      src/layout/show/index.vue
  40. 2 2
      src/router/constant.ts
  41. 7 7
      src/sdk/association/animation.ts
  42. 49 27
      src/sdk/association/guide.ts
  43. 2 0
      src/sdk/association/index.ts
  44. 18 3
      src/sdk/association/path.ts
  45. 6 4
      src/sdk/sdk.ts
  46. 1 0
      src/store/guide-path.ts
  47. 4 0
      src/store/guide.ts
  48. 2 1
      src/store/index.ts
  49. 39 0
      src/store/map.ts
  50. 68 0
      src/store/monitor.ts
  51. 6 1
      src/store/path.ts
  52. 7 2
      src/utils/full.ts
  53. 3 3
      src/utils/store-help.ts
  54. 38 28
      src/views/animation/bottom.vue
  55. 3 4
      src/views/animation/index.vue
  56. 6 5
      src/views/animation/right/am.vue
  57. 1 1
      src/views/animation/right/path.vue
  58. 58 0
      src/views/guide/guide/attach-animation-sam.vue
  59. 116 0
      src/views/guide/guide/attach-animation.vue
  60. 79 23
      src/views/guide/guide/edit-paths.vue
  61. 9 8
      src/views/guide/guide/edit.vue
  62. 15 1
      src/views/guide/guide/show.vue
  63. 16 8
      src/views/guide/guide/sign.vue
  64. 8 4
      src/views/guide/index.vue
  65. 52 12
      src/views/guide/path/edit.vue
  66. 30 1
      src/views/guide/path/show.vue
  67. 28 12
      src/views/guide/path/sign.vue
  68. 3 3
      src/views/guide/show.vue
  69. 18 12
      src/views/measure/show.vue
  70. 20 11
      src/views/measure/sign.vue
  71. 115 37
      src/views/merge/index.vue
  72. 0 25
      src/views/merge/style.scss
  73. 2 8
      src/views/security/store.ts
  74. 53 0
      src/views/setting/back-item.vue
  75. 5 115
      src/views/setting/index.vue
  76. 137 0
      src/views/setting/select-back.vue
  77. 7 0
      src/views/setting/type.ts
  78. 60 41
      src/views/summary/index.vue
  79. 17 10
      src/views/tagging-position/index.vue
  80. 0 0
      src/views/tagging/hot/edit.vue
  81. 0 0
      src/views/tagging/hot/images.vue
  82. 152 0
      src/views/tagging/hot/index.vue
  83. 3 0
      src/views/tagging/show.vue
  84. 12 40
      src/views/tagging/sign.vue
  85. 0 0
      src/views/tagging/hot/style-type-select.vue
  86. 6 0
      src/views/tagging/style.scss
  87. 0 0
      src/views/tagging/hot/styles.vue
  88. 110 132
      src/views/tagging/index.vue
  89. 62 0
      src/views/tagging/monitor/index.vue
  90. 45 0
      src/views/tagging/monitor/show.vue
  91. 126 0
      src/views/tagging/monitor/sign.vue
  92. 51 56
      src/views/view/sign.vue
  93. 6 1
      src/views/view/style.scss
  94. 9 0
      对接文档.txt

+ 7 - 0
src/api/constant.ts

@@ -78,6 +78,13 @@ export const INSERT_GUIDE_PATH = `${namespace}/fusionGuidePath/add`
 export const UPDATE_GUIDE_PATH = `${namespace}/fusionGuidePath/update`
 export const DELETE_GUIDE_PATH = `${namespace}/fusionGuidePath/delete`
 
+
+// 监控
+export const GUIDE_MONITOR_LIST = `${namespace}/monitor/allList`
+export const UPDATE_MONITOR = `${namespace}/monitor/update`
+export const INSERT_MONITOR = `${namespace}/monitor/update`
+export const DELETE_MONITOR = `${namespace}/monitor/delete`
+
 // 屏幕录制
 export const RECORD_LIST = `${namespace}/caseVideoFolder/allList`
 export const RECORD_STATUS = `${namespace}/caseVideo/uploadAddVideoProgress`

+ 4 - 0
src/api/guide-path.ts

@@ -19,6 +19,8 @@ interface ServiceGuidePath {
   speed: number
   panoInfo?: string
   cover: string
+
+  playAnimation?: boolean
 }
 
 export interface GuidePath {
@@ -36,6 +38,8 @@ export interface GuidePath {
   sort: number
   speed: number
   cover: string
+
+  playAnimation?: boolean
 }
 
 export type GuidePaths = GuidePath[]

+ 14 - 0
src/api/guide.ts

@@ -11,6 +11,10 @@ interface ServiceGuide {
   fusionGuideId: number
   cover: string
   title: string
+  showTaggings?: boolean
+  showMeasure?: boolean
+  showMonitor?: boolean
+  showPath?: boolean
 }
 
 export interface Guide {
@@ -18,11 +22,21 @@ export interface Guide {
   cover: string
   title: string
   recoveryContent?: string
+  changeAnimationStatus?: boolean
+
+  showTagging: boolean
+  showMeasure: boolean
+  showMonitor: boolean
+  showPath: boolean
 }
 
 export type Guides = Guide[]
 
 const serviceToLocal = (serviceGuide: ServiceGuide): Guide => ({
+  showTagging: true,
+  showMeasure: true,
+  showMonitor: true,
+  showPath: true,
   ...serviceGuide,
   id: serviceGuide.fusionGuideId.toString(),
 })

+ 2 - 1
src/api/index.ts

@@ -36,4 +36,5 @@ export * from './folder-type'
 export * from './floder'
 export * from './setting'
 export * from './path'
-export * from './animation'
+export * from './animation'
+export * from './monitor'

+ 65 - 0
src/api/monitor.ts

@@ -0,0 +1,65 @@
+import axios from "./instance";
+import { params } from "@/env";
+import {
+  GUIDE_MONITOR_LIST,
+  UPDATE_MONITOR,
+  DELETE_MONITOR,
+  INSERT_MONITOR,
+} from "./constant";
+
+import type { Guide } from "./guide";
+
+interface ServiceMonitor {
+  id: string;
+  title: string;
+  content: string;
+}
+
+export interface Monitor {
+  id: string;
+  title: string;
+  content: string;
+}
+
+export type Monitors = Monitor[];
+
+const serviceToLocal = (servicePath: ServiceMonitor): Monitor => ({
+  ...servicePath,
+});
+
+const localToService = (path: Monitor): ServiceMonitor => ({
+  ...path,
+});
+
+export const fetchMonitors = async () => {
+  return [
+    {
+      id: 1,
+      title: "室内监控",
+      content: "",
+    },
+    {
+      id: 2,
+      title: "室内监控",
+      content: "",
+    },
+  ];
+
+  const monitors = await axios.get<ServiceMonitor[]>(GUIDE_MONITOR_LIST);
+  return monitors.map(serviceToLocal);
+};
+export const postInsertMonitor = async (monitor: Monitor) => {
+  const smonitor = await axios.post<ServiceMonitor>(INSERT_MONITOR, {
+    ...localToService(monitor),
+    caseId: params.caseId,
+  });
+  return serviceToLocal(smonitor);
+};
+
+export const postUpdateMonitor = async (monitor: Monitor) => {
+  return axios.post<undefined>(UPDATE_MONITOR, { ...localToService(monitor) });
+};
+
+export const postDeleteMonitor = (id: Monitor["id"]) => {
+  return axios.post<undefined>(DELETE_MONITOR, { id: Number(id) });
+};

+ 1 - 0
src/api/sys.ts

@@ -71,3 +71,4 @@ export const getCaseInfo = async () => {
 // 校验密码
 export const authSharePassword = (randCode: string) =>
   axios<boolean>(AUTH_PWD, { params: { randCode, caseId: params.caseId } });
+

+ 15 - 1
src/api/tagging-position.ts

@@ -24,6 +24,8 @@ interface ServicePosition {
   fontSize: number,
   lineHeight: number,
   visibilityRange: number
+  pose?: string;
+  
 }
 
 export enum TaggingPositionType {
@@ -43,6 +45,16 @@ export interface TaggingPosition {
 
   type: TaggingPositionType
   mat: Tagging3DProps['mat']
+  pose?: {
+    position: SceneLocalPos;
+    target: SceneLocalPos;
+    panoInfo?: {
+      panoId: any;
+      modelId: string;
+      posInModel: SceneLocalPos;
+      rotInModel: SceneLocalPos;
+    };
+  };
 }
 
 export type TaggingPositions = TaggingPosition[]
@@ -63,6 +75,7 @@ const serviceToLocal = (position: ServicePosition, taggingId?: Tagging['id']): T
   visibilityRange: position.visibilityRange || 30,
   fontSize: position.fontSize || 12,
   lineHeight: position.lineHeight || 1,
+  pose: position.pose && JSON.parse(position.pose)
 })
 
 const localToService = (position: TaggingPosition, update = false): PartialProps<ServicePosition, 'tagPointId'> => ({
@@ -76,7 +89,8 @@ const localToService = (position: TaggingPosition, update = false): PartialProps
   normal: JSON.stringify(position.normal),
   fontSize: position.fontSize,
   lineHeight: position.lineHeight,
-  visibilityRange: position.visibilityRange 
+  visibilityRange: position.visibilityRange ,
+  pose: position.pose && JSON.stringify(position.pose)
 })
 
 

+ 102 - 0
src/components/actions-merge/index.vue

@@ -0,0 +1,102 @@
+<template>
+  <div class="actions">
+    <span
+      v-for="(action, i) in items"
+      :class="{ active: equal(selected, action), disabled: action.disabled }"
+      :key="action.key || i"
+      @click="clickHandler(action)"
+    >
+      <ui-icon :type="action.icon" class="icon" :tip="action.text" />
+    </span>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { useActive } from "@/hook";
+import { ref, toRaw, watchEffect, onBeforeUnmount, nextTick, watch } from "vue";
+
+export type ActionsItem<T = any> = {
+  icon: string;
+  key?: T;
+  text: string;
+  disabled?: boolean;
+  action?: () => (() => void) | void;
+};
+export type ActionsProps = {
+  items: ActionsItem[];
+  current?: ActionsItem | null;
+  single?: boolean;
+};
+
+const props = defineProps<ActionsProps>();
+const emit = defineEmits<{ (e: "update:current", data: ActionsItem | null): void }>();
+const equal = (a: ActionsItem | null, b: ActionsItem | null) => toRaw(a) === toRaw(b);
+const selected = ref<ActionsItem | null>(null);
+const clickHandler = (select: ActionsItem) => {
+  selected.value = equal(selected.value, select) ? null : select;
+  emit("update:current", selected.value);
+  if (props.single) {
+    nextTick(() => selected.value && clickHandler(selected.value));
+  }
+};
+
+watch(
+  () => props.current,
+  () => {
+    if (!props.current && selected.value) {
+      clickHandler(selected.value);
+    }
+  }
+);
+
+watch(
+  selected,
+  (_n, _o, onCleanup) => {
+    if (selected.value?.action) {
+      const cleanup = selected.value.action();
+      cleanup && onCleanup(cleanup);
+    }
+  },
+  { flush: "sync" }
+);
+
+onBeforeUnmount(() => {
+  selected.value = null;
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+  display: flex;
+  gap: 3px;
+  background: rgba(27, 27, 28, 0.8);
+  box-shadow: inset 0px 0px 0px 2px rgba(255, 255, 255, 0.1);
+  border-radius: 4px 4px 4px 4px;
+  padding: 4px 10px;
+
+  span {
+    flex: 1;
+    height: 32px;
+    width: 32px;
+    border-radius: 4px 4px 4px 4px;
+    opacity: 1;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: rgba(255, 255, 255, 0.6);
+    font-size: 14px;
+    cursor: pointer;
+    transition: all 0.3s ease;
+
+    .icon {
+      font-size: 22px;
+    }
+
+    &:hover,
+    &.active {
+      background: rgba(0, 200, 175, 0.16);
+      color: #00c8af;
+    }
+  }
+}
+</style>

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

@@ -55,6 +55,48 @@
           <ul class="icon_lists dib-box">
           
             <li class="dib">
+              <span class="icon iconfont">&#xe79b;</span>
+                <div class="name">view</div>
+                <div class="code-name">&amp;#xe79b;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe799;</span>
+                <div class="name">ratio</div>
+                <div class="code-name">&amp;#xe799;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe79a;</span>
+                <div class="name">1b1</div>
+                <div class="code-name">&amp;#xe79a;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe65a;</span>
+                <div class="name">reset</div>
+                <div class="code-name">&amp;#xe65a;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe798;</span>
+                <div class="name">menu</div>
+                <div class="code-name">&amp;#xe798;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe797;</span>
+                <div class="name">keys_a</div>
+                <div class="code-name">&amp;#xe797;</div>
+              </li>
+          
+            <li class="dib">
+              <span class="icon iconfont">&#xe796;</span>
+                <div class="name">add_a</div>
+                <div class="code-name">&amp;#xe796;</div>
+              </li>
+          
+            <li class="dib">
               <span class="icon iconfont">&#xe78a;</span>
                 <div class="name">rectification</div>
                 <div class="code-name">&amp;#xe78a;</div>
@@ -684,9 +726,9 @@
 <pre><code class="language-css"
 >@font-face {
   font-family: 'iconfont';
-  src: url('iconfont.woff2?t=1741331539860') format('woff2'),
-       url('iconfont.woff?t=1741331539860') format('woff'),
-       url('iconfont.ttf?t=1741331539860') format('truetype');
+  src: url('iconfont.woff2?t=1741831605477') format('woff2'),
+       url('iconfont.woff?t=1741831605477') format('woff'),
+       url('iconfont.ttf?t=1741831605477') format('truetype');
 }
 </code></pre>
           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -713,6 +755,69 @@
         <ul class="icon_lists dib-box">
           
           <li class="dib">
+            <span class="icon iconfont icon-view"></span>
+            <div class="name">
+              view
+            </div>
+            <div class="code-name">.icon-view
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-ratio"></span>
+            <div class="name">
+              ratio
+            </div>
+            <div class="code-name">.icon-ratio
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-a-1b1"></span>
+            <div class="name">
+              1b1
+            </div>
+            <div class="code-name">.icon-a-1b1
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-reset"></span>
+            <div class="name">
+              reset
+            </div>
+            <div class="code-name">.icon-reset
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-menu"></span>
+            <div class="name">
+              menu
+            </div>
+            <div class="code-name">.icon-menu
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-keys_a"></span>
+            <div class="name">
+              keys_a
+            </div>
+            <div class="code-name">.icon-keys_a
+            </div>
+          </li>
+          
+          <li class="dib">
+            <span class="icon iconfont icon-add_a"></span>
+            <div class="name">
+              add_a
+            </div>
+            <div class="code-name">.icon-add_a
+            </div>
+          </li>
+          
+          <li class="dib">
             <span class="icon iconfont icon-rectification"></span>
             <div class="name">
               rectification
@@ -1659,6 +1764,62 @@
           
             <li class="dib">
                 <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-view"></use>
+                </svg>
+                <div class="name">view</div>
+                <div class="code-name">#icon-view</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-ratio"></use>
+                </svg>
+                <div class="name">ratio</div>
+                <div class="code-name">#icon-ratio</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-a-1b1"></use>
+                </svg>
+                <div class="name">1b1</div>
+                <div class="code-name">#icon-a-1b1</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-reset"></use>
+                </svg>
+                <div class="name">reset</div>
+                <div class="code-name">#icon-reset</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-menu"></use>
+                </svg>
+                <div class="name">menu</div>
+                <div class="code-name">#icon-menu</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-keys_a"></use>
+                </svg>
+                <div class="name">keys_a</div>
+                <div class="code-name">#icon-keys_a</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
+                  <use xlink:href="#icon-add_a"></use>
+                </svg>
+                <div class="name">add_a</div>
+                <div class="code-name">#icon-add_a</div>
+            </li>
+          
+            <li class="dib">
+                <svg class="icon svg-icon" aria-hidden="true">
                   <use xlink:href="#icon-rectification"></use>
                 </svg>
                 <div class="name">rectification</div>

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

@@ -1,8 +1,8 @@
 @font-face {
   font-family: "iconfont"; /* Project id 4647199 */
-  src: url('iconfont.woff2?t=1741331539860') format('woff2'),
-       url('iconfont.woff?t=1741331539860') format('woff'),
-       url('iconfont.ttf?t=1741331539860') format('truetype');
+  src: url('iconfont.woff2?t=1741831605477') format('woff2'),
+       url('iconfont.woff?t=1741831605477') format('woff'),
+       url('iconfont.ttf?t=1741831605477') format('truetype');
 }
 
 .iconfont {
@@ -13,6 +13,34 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-view:before {
+  content: "\e79b";
+}
+
+.icon-ratio:before {
+  content: "\e799";
+}
+
+.icon-a-1b1:before {
+  content: "\e79a";
+}
+
+.icon-reset:before {
+  content: "\e65a";
+}
+
+.icon-menu:before {
+  content: "\e798";
+}
+
+.icon-keys_a:before {
+  content: "\e797";
+}
+
+.icon-add_a:before {
+  content: "\e796";
+}
+
 .icon-rectification:before {
   content: "\e78a";
 }

文件差异内容过多而无法显示
+ 1 - 1
src/components/bill-ui/components/icon/iconfont/iconfont.js


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

@@ -6,6 +6,55 @@
   "description": "",
   "glyphs": [
     {
+      "icon_id": "43623992",
+      "name": "view",
+      "font_class": "view",
+      "unicode": "e79b",
+      "unicode_decimal": 59291
+    },
+    {
+      "icon_id": "43616883",
+      "name": "ratio",
+      "font_class": "ratio",
+      "unicode": "e799",
+      "unicode_decimal": 59289
+    },
+    {
+      "icon_id": "43616882",
+      "name": "1b1",
+      "font_class": "a-1b1",
+      "unicode": "e79a",
+      "unicode_decimal": 59290
+    },
+    {
+      "icon_id": "25654903",
+      "name": "reset",
+      "font_class": "reset",
+      "unicode": "e65a",
+      "unicode_decimal": 58970
+    },
+    {
+      "icon_id": "43615153",
+      "name": "menu",
+      "font_class": "menu",
+      "unicode": "e798",
+      "unicode_decimal": 59288
+    },
+    {
+      "icon_id": "43559284",
+      "name": "keys_a",
+      "font_class": "keys_a",
+      "unicode": "e797",
+      "unicode_decimal": 59287
+    },
+    {
+      "icon_id": "43559283",
+      "name": "add_a",
+      "font_class": "add_a",
+      "unicode": "e796",
+      "unicode_decimal": 59286
+    },
+    {
       "icon_id": "43549167",
       "name": "rectification",
       "font_class": "rectification",

二进制
src/components/bill-ui/components/icon/iconfont/iconfont.ttf


二进制
src/components/bill-ui/components/icon/iconfont/iconfont.woff


二进制
src/components/bill-ui/components/icon/iconfont/iconfont.woff2


+ 14 - 4
src/components/drawing-time/current.vue

@@ -13,6 +13,7 @@
     }"
   />
   <v-line
+    v-if="!hideLine"
     :config="{
         points: [currentX, size!.height, currentX, 10],
         stroke: currentColor,
@@ -37,11 +38,20 @@ import { Transform } from "konva/lib/Util";
 import { Arrow } from "konva/lib/shapes/Arrow";
 import { DC } from "../drawing/dec";
 
-const props = withDefaults(defineProps<{ currentTime: number; follow?: boolean }>(), {
-  follow: false,
-});
+const props = withDefaults(
+  defineProps<{
+    currentTime: number;
+    follow?: boolean;
+    hideLine?: boolean;
+    currentColor?: string;
+  }>(),
+  {
+    follow: false,
+    currentColor: "#fff",
+  }
+);
 
-const currentColor = "#fff";
+const currentColor = props.currentColor;
 const { misPixel } = useGlobalVar();
 const emit = defineEmits<{ (e: "update:currentTime", num: number): void }>();
 

+ 7 - 3
src/components/drawing/hook.ts

@@ -20,6 +20,7 @@ import { Transform } from "konva/lib/Util";
 import { lineLen } from "./math";
 import { Viewer } from "./viewer";
 import { KonvaEventObject } from "konva/lib/Node";
+import { asyncTimeout } from "@/utils";
 
 export const rendererName = "renderer";
 export const rendererMap = new WeakMap<any, { unmounteds: (() => void)[] }>();
@@ -46,7 +47,8 @@ export const installGlobalVar = <T>(
     if (!(key in instance)) {
       let val = create() as any;
       if (typeof val === "object" && "var" in val && "onDestroy" in val) {
-        val.onDestory && unmounteds.push(val.onDestory);
+        console.error('val.onDestory', val, key, val.onDestroy)
+        val.onDestroy && unmounteds.push(val.onDestroy);
         if (import.meta.env.DEV) {
           unmounteds.push(() => {
             console.log("销毁变量", key);
@@ -158,7 +160,7 @@ export const listener = <
 export const useGlobalResize = installGlobalVar(() => {
   const stage = useStage();
   const size = ref<Size>();
-  const setSize = () => {
+  const setSize = async () => {
     if (fix.value) return;
     const container = stage.value?.getStage().container();
     if (container) {
@@ -166,6 +168,7 @@ export const useGlobalResize = installGlobalVar(() => {
     }
 
     const dom = stage.value!.getNode().container().parentElement!;
+    await asyncTimeout(16)
     size.value = {
       width: dom.offsetWidth,
       height: dom.offsetHeight,
@@ -215,7 +218,8 @@ export const useGlobalResize = installGlobalVar(() => {
       fix,
     },
     onDestroy: () => {
-      fix || unResize();
+      console.error('size onDest')
+      unResize();
       unWatch && unWatch();
     },
   };

+ 10 - 1
src/components/drawing/renderer.vue

@@ -1,5 +1,10 @@
 <template>
-  <div class="draw-layout" id="draw-renderer" ref="layout" :style="{ cursor: cursorStyle }">
+  <div
+    class="draw-layout"
+    id="draw-renderer"
+    ref="layout"
+    :style="{ cursor: cursorStyle }"
+  >
     <template v-if="layout">
       <v-stage ref="stage" :config="size">
         <v-layer :config="viewerConfig" id="formal">
@@ -79,6 +84,10 @@ const config = computed(
       ...size.value,
     }
 );
+const { updateSize } = useGlobalResize();
+defineExpose({
+  updateSize,
+});
 </script>
 
 <style scoped lang="scss">

+ 11 - 0
src/components/global-search/guide.vue

@@ -0,0 +1,11 @@
+<template>
+  <GuideSign :guide="data" :edit="false" search @click="flyPlayGuide(data)" />
+</template>
+
+<script lang="ts" setup>
+import { flyPlayGuide } from "@/hook/use-fly";
+import { Guide } from "@/store";
+import GuideSign from "@/views/guide/guide/sign.vue";
+
+defineProps<{ data: Guide }>();
+</script>

+ 239 - 0
src/components/global-search/index.vue

@@ -0,0 +1,239 @@
+<template>
+  <div id="global-search" v-if="custom.showSearch">
+    <Select
+      :filter-option="filter"
+      v-model:value="value"
+      size="large"
+      optionLabelProp="label"
+      :listHeight="440"
+      style="width: 340px"
+      show-search
+      @search="handleSearch"
+      :dropdownMatchSelectWidth="false"
+      popupClassName="global-search-menu"
+      allowClear
+      placeholder="搜索"
+    >
+      <template v-for="item in options" :key="item.key">
+        <SelectOptGroup v-if="item.options.length" class="group-item" :key="item.key">
+          <template #label>
+            <span class="group-item-title">{{ item.name }}</span>
+          </template>
+          <SelectOption
+            v-for="(option, ndx) in item.options"
+            :value="item.key + option.id"
+            :label="item.getLabel(option as any)"
+            :key="item.key + option.id"
+            :class="{ 'last-item': ndx + 1 === item.options.length }"
+          >
+            <component :is="item.comp" :data="(option as any)" />
+          </SelectOption>
+        </SelectOptGroup>
+      </template>
+    </Select>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import TaggingComp from "./tagging.vue";
+import PathComp from "./path.vue";
+import MeasureComp from "./measure.vue";
+import GuideComp from "./guide.vue";
+import ViewComp from "./view.vue";
+import MonitorComp from "./monitor.vue";
+import MapComp from "./map.vue";
+import ModelComp from "./model.vue";
+import {
+  FuseModel,
+  Guide,
+  guides,
+  Measure,
+  measures,
+  Monitor,
+  monitors,
+  Path,
+  paths,
+  Tagging,
+  taggings,
+  View,
+  views,
+} from "@/store";
+import { Select, SelectOptGroup, SelectOption } from "ant-design-vue";
+import { computed, ref } from "vue";
+import { custom } from "@/env";
+import { debounce } from "@/utils";
+import { searchAddress, Address } from "@/store/map";
+import { fuseModels } from "@/store";
+
+const addressItems = ref<Address[]>([]);
+const options = computed(() => [
+  {
+    key: "mode-",
+    name: "模型",
+    options: fuseModels.value,
+    getLabel: (tag: FuseModel) => tag.title,
+    comp: ModelComp,
+  },
+  {
+    key: "tagging-",
+    name: "标签",
+    options: taggings.value,
+    getLabel: (tag: Tagging) => tag.title,
+    comp: TaggingComp,
+  },
+  {
+    key: "path-",
+    name: "路径",
+    options: paths.value,
+    getLabel: (tag: Path) => tag.name,
+    comp: PathComp,
+  },
+  {
+    key: "measure-",
+    name: "测量",
+    options: measures.value,
+    getLabel: (tag: Measure) => tag.title,
+    comp: MeasureComp,
+  },
+  {
+    key: "guide-",
+    name: "导览",
+    options: guides.value,
+    getLabel: (tag: Guide) => tag.title,
+    comp: GuideComp,
+  },
+  {
+    key: "view-",
+    name: "视图提取",
+    options: views.value,
+    getLabel: (tag: View) => tag.title,
+    comp: ViewComp,
+  },
+  {
+    key: "monitor-",
+    name: "监控",
+    options: monitors.value,
+    getLabel: (tag: Monitor) => tag.title,
+    comp: MonitorComp,
+  },
+  {
+    key: "map-",
+    name: "地址",
+    options: [...addressItems.value],
+    getLabel: (tag: Address) => tag.address,
+    comp: MapComp,
+  },
+]);
+
+const filterOption = (input: string, key: string) => {
+  if (key.indexOf("map-") === 0) return true;
+  const option = options.value.find((option) => key.indexOf(option.key) === 0);
+  if (!option) return false;
+  const id = key.substring(option.key.length);
+  const item = option.options.find((item) => item.id.toString() === id)!;
+  if (!item) return false;
+  return option.getLabel(item as any).indexOf(input) >= 0;
+};
+
+const filter = (input: string, option: any) => {
+  if (option.options) {
+    const fOptions = option.options.filter((option: any) =>
+      filterOption(input, option.value)
+    );
+    return fOptions.length === option.options.length;
+  } else {
+    return filterOption(input, option.value);
+  }
+};
+
+const value = ref();
+const fetchIng = ref(false);
+let timeout: any;
+let count = 0;
+const handleSearch = (val: string) => {
+  if (!val) {
+    addressItems.value = [];
+  }
+  const currentCount = ++count;
+  clearTimeout(timeout);
+  timeout = setTimeout(async () => {
+    const data = await searchAddress(val);
+    if (currentCount === count) {
+      addressItems.value = data;
+      fetchIng.value = false;
+    }
+  }, 500);
+};
+</script>
+<style lang="scss" scoped>
+#global-search {
+  position: absolute;
+  z-index: 99;
+  left: calc(var(--left-pano-left) + var(--left-pano-width) + 20px);
+  
+  top: calc(var(--editor-head-height) + var(--header-top) + 20px);
+  // background: #000;
+  transition: all 0.3s ease;
+}
+</style>
+
+<style lang="scss">
+#global-search {
+  .ant-select-selector {
+    background-color: var(--editor-toolbox-back);
+    backdrop-filter: blur(4px);
+
+    box-shadow: inset 0px 0px 0px 2px rgba(255, 255, 255, 0.1);
+    border-radius: 4px;
+
+    font-size: 14px;
+
+    padding-left: 30px;
+    &::before {
+      font-family: "iconfont" !important;
+      content: "\e64c";
+      position: absolute;
+      left: 10px;
+      top: 50%;
+      transform: translateY(-50%);
+      color: var(--colors-color);
+    }
+  }
+  input {
+    padding-left: 20px;
+  }
+  .anticon-search {
+    color: #fff;
+    display: none;
+  }
+  .ant-select-clear {
+    margin-right: 2px;
+  }
+  .ant-select-clear,
+  .ant-select-arrow {
+    font-size: 16px;
+  }
+}
+
+.global-search-menu {
+  background-color: var(--editor-toolbox-back);
+  backdrop-filter: blur(4px);
+
+  .ant-empty-description {
+    color: rgba(255, 255, 255, 0.7);
+  }
+  .ant-empty-image * {
+    fill: rgba(255, 255, 255, 0.7);
+  }
+}
+
+.group-item-title {
+  font-weight: bold;
+  font-size: 14px;
+  color: rgba(255, 255, 255, 0.7);
+}
+
+.last-item {
+  border-bottom: 1px solid rgba(var(--colors-primary-fill), 0.16);
+}
+</style>

+ 10 - 0
src/components/global-search/map.vue

@@ -0,0 +1,10 @@
+<template>
+  <p @click="flyLatLng(data.latlng)">{{ data.address }}</p>
+</template>
+
+<script lang="ts" setup>
+import { flyLatLng } from "@/hook/use-fly";
+import { Address } from "@/store/map";
+
+defineProps<{ data: Address }>();
+</script>

+ 16 - 0
src/components/global-search/measure.vue

@@ -0,0 +1,16 @@
+<template>
+  <MeasureSign
+    :measure="data"
+    :edit="false"
+    search
+    @click="flyMeasure(data)"
+  />
+</template>
+
+<script lang="ts" setup>
+import { flyMeasure } from "@/hook/use-fly";
+import { Measure } from "@/store";
+import MeasureSign from "@/views/measure/sign.vue";
+
+defineProps<{ data: Measure }>();
+</script>

+ 11 - 0
src/components/global-search/model.vue

@@ -0,0 +1,11 @@
+<template>
+  <ModelSign :model="data" search @click="(mode: any) => flyModel(data, mode, true)" />
+</template>
+
+<script lang="ts" setup>
+import { flyModel } from "@/hook/use-fly";
+import { FuseModel } from "@/store";
+import ModelSign from "@/layout/model-list/sign.vue";
+
+defineProps<{ data: FuseModel }>();
+</script>

+ 10 - 0
src/components/global-search/monitor.vue

@@ -0,0 +1,10 @@
+<template>
+  <ViewSign :monitor="data" :edit="false" search />
+</template>
+
+<script lang="ts" setup>
+import { Monitor } from "@/store";
+import ViewSign from "@/views/tagging/monitor/sign.vue";
+
+defineProps<{ data: Monitor }>();
+</script>

+ 10 - 0
src/components/global-search/path.vue

@@ -0,0 +1,10 @@
+<template>
+  <PathSign :path="data" :edit="false" search />
+</template>
+
+<script lang="ts" setup>
+import { Path } from "@/store";
+import PathSign from "@/views/guide/path/sign.vue";
+
+defineProps<{ data: Path }>();
+</script>

+ 11 - 0
src/components/global-search/tagging.vue

@@ -0,0 +1,11 @@
+<template>
+  <TaggingSign :tagging="data" :edit="false" @select="flyTagging(data)" search />
+</template>
+
+<script lang="ts" setup>
+import { flyTagging } from "@/hook/use-fly";
+import { Tagging } from "@/store";
+import TaggingSign from "@/views/tagging/hot/sign.vue";
+
+defineProps<{ data: Tagging }>();
+</script>

+ 19 - 0
src/components/global-search/view.vue

@@ -0,0 +1,19 @@
+<template>
+  <ViewSign :view="data" :edit="false" search class="aitem" />
+</template>
+
+<script lang="ts" setup>
+import { View } from "@/store";
+import ViewSign from "@/views/view/sign.vue";
+
+defineProps<{ data: View }>();
+</script>
+
+<style>
+.aitem .content {
+  width: 100%;
+}
+.aitem .content .title {
+  flex: 1;
+}
+</style>

+ 28 - 0
src/components/right-menu/index.ts

@@ -0,0 +1,28 @@
+import { mount } from "@/utils";
+import { Pos } from "../drawing/dec";
+import RMenuComp from "./index.vue";
+import { reactive } from "vue";
+
+export type RMenu = {
+  label: string;
+  icon: string;
+  handler: () => void;
+};
+
+export const useRMenus = (
+  pixel: Pos,
+  menus: RMenu[],
+  dom = document.querySelector("#app")!
+) => {
+  const unMount = mount(
+    dom as HTMLDivElement,
+    RMenuComp,
+    reactive({
+      pixel,
+      menus,
+      onClose() {
+        unMount()
+      }
+    })
+  );
+};

+ 40 - 0
src/components/right-menu/index.vue

@@ -0,0 +1,40 @@
+<template>
+  <Dropdown v-model:open="open">
+    <span class="proce" :style="{ left: pixel.x + 'px', top: pixel.y + 'px' }"></span>
+    <template #overlay>
+      <Menu>
+        <MenuItem @click="clickHandler(menu)" v-for="menu in menus">
+          <template #icon><ui-icon :type="menu.icon" /></template>
+          {{ menu.label }}
+        </MenuItem>
+      </Menu>
+    </template>
+  </Dropdown>
+</template>
+
+<script lang="ts" setup>
+import { Dropdown, Menu, MenuItem } from "ant-design-vue";
+import { RMenu } from ".";
+import { Pos } from "../drawing/dec";
+import { ref, watchEffect } from "vue";
+
+const props = defineProps<{ menus: RMenu[]; pixel: Pos; onClose: () => void }>();
+
+const open = ref(true);
+watchEffect(() => {
+  if (!open.value) {
+    props.onClose();
+  }
+});
+
+const clickHandler = (menu: RMenu) => {
+  menu.handler();
+  props.onClose()
+};
+</script>
+
+<style lang="scss" scoped>
+.proce {
+  position: absolute;
+}
+</style>

+ 1 - 1
src/components/tagging/sign-new.vue

@@ -63,7 +63,7 @@
 <script lang="ts" setup>
 import { computed, markRaw, onUnmounted, ref, watch, watchEffect } from "vue";
 import UIBubble from "bill/components/bubble/index.vue";
-import Images from "@/views/tagging/images.vue";
+import Images from "@/views/tagging/hot/images.vue";
 import Preview from "../static-preview/index.vue";
 import { getTaggingStyle } from "@/store";
 import { getFileUrl } from "@/utils";

+ 54 - 0
src/components/view-setting/index.vue

@@ -0,0 +1,54 @@
+<template>
+  <div>
+    <Dropdown placement="top">
+      <div class="strengthen show-setting">
+        <span>显示设置</span>
+        <DownOutlined />
+      </div>
+      <template #overlay>
+        <Menu>
+          <menu-item v-for="option in showOptions">
+            <ui-input
+              @click.stop
+              type="checkbox"
+              :modelValue="option.stack.current.value.value"
+              @update:modelValue="(s: boolean) => option.stack.current.value.value = s"
+              :label="option.text"
+            />
+          </menu-item>
+        </Menu>
+      </template>
+    </Dropdown>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { Dropdown, Menu, MenuItem } from "ant-design-vue";
+import {
+  showMeasuresStack,
+  showMonitorsStack,
+  showPathsStack,
+  showTaggingsStack,
+} from "@/env";
+import { DownOutlined } from "@ant-design/icons-vue";
+
+const showOptions = [
+  { text: "标签", stack: showTaggingsStack },
+  { text: "监控", stack: showMonitorsStack },
+  { text: "路径", stack: showPathsStack },
+  { text: "测量", stack: showMeasuresStack },
+];
+</script>
+
+<style lang="scss" scoped>
+.show-setting {
+  width: 160px;
+  height: 34px;
+  background: rgba(27, 27, 28, 0.9);
+  border-radius: 4px;
+  display: flex;
+  padding: 8px;
+  align-items: center;
+  justify-content: space-between;
+}
+</style>

+ 63 - 50
src/env/index.ts

@@ -1,31 +1,37 @@
-import { stackFactory, flatStacksValue, strToParams } from '@/utils'
-import { reactive, ref } from 'vue'
+import { stackFactory, flatStacksValue, strToParams } from "@/utils";
+import { reactive, ref } from "vue";
 
-import type { FuseModel, Path, TaggingPosition, View } from '@/store'
-export const namespace = '/fusion'
-export const viewModeStack = stackFactory(ref<'full' | 'auto'>('auto'))
-export const showToolbarStack = stackFactory(ref<boolean>(false))
-export const showHeadBarStack = stackFactory(ref<boolean>(true))
-export const showRightPanoStack = stackFactory(ref<boolean>(true))
-export const showLeftPanoStack = stackFactory(ref<boolean>(false))
-export const moundLeftPanoStack = stackFactory(ref<boolean>(true))
-export const showLeftCtrlPanoStack = stackFactory(ref<boolean>(true))
+import type { FuseModel, Path, TaggingPosition, View } from "@/store";
+export const namespace = "/fusion";
+export const viewModeStack = stackFactory(ref<"full" | "auto">("auto"));
+export const showToolbarStack = stackFactory(ref<boolean>(false));
+export const showHeadBarStack = stackFactory(ref<boolean>(true));
+export const showRightPanoStack = stackFactory(ref<boolean>(true));
+export const showLeftPanoStack = stackFactory(ref<boolean>(false));
+export const moundLeftPanoStack = stackFactory(ref<boolean>(true));
+export const showLeftCtrlPanoStack = stackFactory(ref<boolean>(true));
 export const showModeStack = stackFactory(ref<"pano" | "fuse">("fuse"));
-export const showRightCtrlPanoStack = stackFactory(ref<boolean>(true))
-export const showBottomBarStack = stackFactory(ref<boolean>(false), true)
-export const bottomBarHeightStack = stackFactory(ref<string>('60px'))
-export const showTaggingsStack = stackFactory(ref<boolean>(true))
-export const showPathsStack = stackFactory(ref<boolean>(true))
-export const showPathStack = stackFactory(ref<Path['id']>())
-export const showMeasuresStack = stackFactory(ref<boolean>(true))
-export const currentModelStack = stackFactory(ref<FuseModel | null>(null))
-export const showModelsMapStack = stackFactory(ref<WeakMap<FuseModel, boolean>>(new Map()), true)
-export const modelsChangeStoreStack = stackFactory(ref<boolean>(false))
-export const showTaggingPositionsStack = stackFactory(ref<WeakSet<TaggingPosition>>(new WeakSet()))
-export const currentViewStack = stackFactory(ref<View>())
+export const showRightCtrlPanoStack = stackFactory(ref<boolean>(true));
+export const showBottomBarStack = stackFactory(ref<boolean>(false), true);
+export const bottomBarHeightStack = stackFactory(ref<string>("60px"));
+export const showTaggingsStack = stackFactory(ref<boolean>(true));
+export const showMonitorsStack = stackFactory(ref<boolean>(true));
+export const showPathsStack = stackFactory(ref<boolean>(true));
+export const showSearchStack = stackFactory(ref<boolean>(true));
+export const showPathStack = stackFactory(ref<Path["id"]>());
+export const showMeasuresStack = stackFactory(ref<boolean>(true));
+export const currentModelStack = stackFactory(ref<FuseModel | null>(null));
+export const showModelsMapStack = stackFactory(
+  ref<WeakMap<FuseModel, boolean>>(new Map()),
+  true
+);
+export const modelsChangeStoreStack = stackFactory(ref<boolean>(false));
+export const showTaggingPositionsStack = stackFactory(
+  ref<WeakSet<TaggingPosition>>(new WeakSet())
+);
+export const currentViewStack = stackFactory(ref<View>());
 
 export const custom = flatStacksValue({
-  
   viewMode: viewModeStack,
   showToolbar: showToolbarStack,
   showRightPano: showRightPanoStack,
@@ -34,6 +40,7 @@ export const custom = flatStacksValue({
   showLeftCtrlPano: showLeftCtrlPanoStack,
   shwoRightCtrlPano: showRightCtrlPanoStack,
   showTaggings: showTaggingsStack,
+  showMonitors: showMonitorsStack,
   showPaths: showPathsStack,
   showPath: showPathStack,
   showMeasures: showMeasuresStack,
@@ -46,37 +53,43 @@ export const custom = flatStacksValue({
   showHeadBar: showHeadBarStack,
   currentView: currentViewStack,
   showMode: showModeStack,
-})
+  showSearch: showSearchStack,
+});
 
+export const params = reactive(
+  strToParams(location.search)
+) as unknown as Params;
+params.caseId = Number(params.caseId);
+params.share = Boolean(Number(params.share));
+params.single = Boolean(Number(params.single));
 
-export const params = reactive(strToParams(location.search)) as unknown as Params
-params.caseId = Number(params.caseId)
-params.share = Boolean(Number(params.share))
-params.single = Boolean(Number(params.single))
+export type Params = {
+  caseId: number;
+  baseURL?: string;
+  modelId?: string;
+  mapKey?: string;
+  mapPlatform?: string;
+  fileUrl?: string;
+  sign?: string;
+  type?: string;
+  testMap?: boolean;
+  title?: string;
+  m?: string;
+  share?: boolean;
+  single?: boolean;
+  token?: string;
+};
 
-export type Params = { 
-  caseId: number,
-  baseURL?: string,
-  modelId?: string,
-  fileUrl?: string
-  sign?: string
-  type?: string
-  testMap?: boolean
-  title?: string
-  m?: string
-  share?: boolean,
-  single?: boolean
-  token?: string
-}
-
-export const baseURL = params.baseURL ? params.baseURL : ''
+export const baseURL = params.baseURL ? params.baseURL : "";
 
 export const getResource = (uri: string) => {
-  if (~uri.indexOf('base64') || ~uri.indexOf('bolb') || ~uri.indexOf('//')) return uri
+  if (~uri.indexOf("base64") || ~uri.indexOf("bolb") || ~uri.indexOf("//"))
+    return uri;
 
-  if (uri[0] === '/') {
-    return `${baseURL}${uri}`
+  if (uri[0] === "/") {
+    return `${baseURL}${uri}`;
   } else {
-    return `${baseURL}/${uri}`
+    return `${baseURL}/${uri}`;
   }
-}
+};
+

+ 8 - 1
src/hook/ids.ts

@@ -11,7 +11,6 @@ export const useSelects = <T extends { id: any }>(items: Ref<T[]>) => {
     } else {
       ~ndx && selects.value.splice(ndx, 1);
     }
-    console.log(selects.value)
   };
   const updateSelectId = (id: any, select: boolean) => {
     const item = items.value.find((s) => s.id === id);
@@ -37,7 +36,15 @@ export const useSelects = <T extends { id: any }>(items: Ref<T[]>) => {
   return {
     selects,
     unSelects: computed(() => items.value.filter(item => !selects.value.includes(item as any))),
+    all: computed({
+      get: () => items.value.length === selects.value.length,
+      set: (select: boolean) => {
+        items.value.forEach(item => updateSelect(item, select))
+      } 
+    }),
+    include: (id: string) => selects.value.some(item => item.id === id),
     updateSelect,
     updateSelectId,
   };
 };
+

+ 139 - 0
src/hook/use-fly.ts

@@ -0,0 +1,139 @@
+import { TaggingPosition } from "@/api";
+import { custom, showTaggingPositionsStack } from "@/env";
+import { currentModel, fuseModel, loadModel } from "@/model";
+import { sdk, getTaggingPosNode, setPose, getSceneMeasure, playSceneGuide, pauseSceneGuide, activeModel } from "@/sdk";
+import { getPathNode, pauseScenePath, playScenePath } from "@/sdk/association/path";
+import {
+  getFuseModel,
+  getFuseModelShowVariable,
+  getTaggingPositions,
+  Measure,
+  Tagging,
+  Path,
+  View,
+  viewToModelType,
+  Guide,
+  getGuidePaths,
+  FuseModel,
+} from "@/store";
+import { nextTick, ref } from "vue";
+
+let stopFly: (() => void) | null = null;
+export const flyTagging = (tagging: Tagging, callback?: () => void) => {
+  stopFly && stopFly();
+  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])));
+    flyTaggingPosition(position);
+
+    setTimeout(() => {
+      pop();
+      flyIndex(i + 1);
+    }, 2000);
+  };
+  flyIndex(0);
+  stopFly = () => {
+    isStop = true;
+    stopFly = null;
+  };
+  return stopFly;
+};
+
+export const flyTaggingPosition = (position: TaggingPosition) => {
+  if (position.pose) {
+    setPose(position.pose);
+  } else {
+    sdk.comeTo({
+      position: getTaggingPosNode(position)!.getImageCenter(),
+      modelId: position.modelId,
+      dur: 300,
+      // distance: 3,
+      maxDis: 15,
+      isFlyToTag: true,
+    });
+  }
+};
+
+export const flyMeasure = (data: Measure) => {
+  stopFly && stopFly();
+  getSceneMeasure(data)?.fly();
+};
+
+export const flyPath = (path: Path) => {
+  stopFly && stopFly();
+  getPathNode(path.id)?.fly();
+  getPathNode(path.id)?.focus(true);
+};
+
+export const flyView = (view: View) => {
+  stopFly && stopFly();
+  let isStop = false;
+  stopFly = () => {
+    isStop = true;
+    stopFly = null
+  };
+  const modelType = viewToModelType(view);
+  loadModel(modelType).then((sdk) => {
+    if (!isStop) {
+      custom.currentView = view;
+      sdk.setView(view.flyData);
+    }
+  });
+  return stopFly;
+};
+
+export const flyPlayGuide = (guide: Guide) => {
+  stopFly && stopFly()
+  stopFly = () => {
+    stopFly = null
+    pauseSceneGuide()
+  }
+  playSceneGuide(getGuidePaths(guide), undefined, true, guide)
+  return stopFly
+}
+
+export const flyPlayPath = (path: Path) => {
+  stopFly && stopFly()
+  stopFly = () => {
+    stopFly = null
+    pauseScenePath()
+  }
+  playScenePath(path, true);
+  return stopFly
+}
+
+export const flyLatLng = (latlng: number[]) => {
+  stopFly && stopFly();
+  sdk.comeToByLatLng(latlng)
+}
+
+export const flyModel = (model: FuseModel, mode: "pano" | "fuse", f = false) => {
+  stopFly && stopFly();
+
+  if (getFuseModelShowVariable(model).value) {
+    if (custom.currentModel === model && mode === custom.showMode) {
+      if (!f) return;
+
+      activeModel({ showMode: "fuse", active: undefined, fore: f });
+    } else {
+      activeModel({ showMode: mode, active: model, fore: f });
+    }
+  }
+
+  if (currentModel.value !== fuseModel) {
+    loadModel(fuseModel);
+  }
+};

+ 26 - 13
src/hook/use-pixel.ts

@@ -4,21 +4,26 @@ import { useViewStack } from "./viewStack";
 
 export const useCameraChange = <T>(change: () => T) => {
   const data = ref(change());
-  let isPause = false
+  let isPause = false;
   const update = () => {
     if (isPause) return;
     data.value = change() as UnwrapRef<T>;
   };
   useViewStack(() => {
+    update()
     sdk.sceneBus.on("cameraChange", update);
     return () => {
       sdk.sceneBus.off("cameraChange", update);
     };
   });
-  return [data, () => isPause = true, () => {
-    isPause = false
-    update()
-}] as const;
+  return [
+    data,
+    () => (isPause = true),
+    () => {
+      isPause = false;
+      update();
+    },
+  ] as const;
 };
 
 export const usePixel = (
@@ -42,9 +47,13 @@ export const usePixel = (
 
   useViewStack(() => {
     watch(getter, (val) => (pos.value = val));
-    return watch(pos, () => {
-      pixel.value = getPosStyle()
-    }, { deep: true });
+    return watch(
+      pos,
+      () => {
+        pixel.value = getPosStyle();
+      },
+      { deep: true }
+    );
   });
 
   return [pixel, pos, pause, recovery] as const;
@@ -59,7 +68,7 @@ export const usePixels = (
   });
 
   const getPosStyle = () => {
-    const pixels: ({left: string, top: string} | null)[] = [];
+    const pixels: ({ left: string; top: string } | null)[] = [];
     for (let i = 0; i < positions.value.length; i++) {
       const pos = positions.value[i];
       const screenPos = sdk.getScreenByPosition(pos.localPos, pos.modelId);
@@ -73,13 +82,17 @@ export const usePixels = (
       }
     }
   };
-  const [pixels, pause, recovery] = useCameraChange(getPosStyle)
+  const [pixels, pause, recovery] = useCameraChange(getPosStyle);
 
   useViewStack(() => {
     watch(getter, (val) => (positions.value = val));
-    return watch(positions, () => {
-      pixels.value = getPosStyle()
-    }, { deep: true });
+    return watch(
+      positions,
+      () => {
+        pixels.value = getPosStyle();
+      },
+      { deep: true }
+    );
   });
 
   return [pixels, positions, pause, recovery] as const;

+ 13 - 5
src/layout/edit/fuse-edit.vue

@@ -1,11 +1,14 @@
 <template>
   <template v-if="loaded" style="height: 100%">
     <Header></Header>
-    <router-view v-slot="{ Component }" v-if="fuseModels.length">
-      <!-- <keep-alive> -->
-      <component :is="Component" />
-      <!-- </keep-alive> -->
-    </router-view>
+    <template v-if="fuseModels.length">
+      <router-view v-slot="{ Component }">
+        <!-- <keep-alive> -->
+        <component :is="Component" />
+        <!-- </keep-alive> -->
+      </router-view>
+      <GlobalSearch />
+    </template>
 
     <SelectModel v-else>
       <ui-button type="primary" class="add-fuse-model">
@@ -21,6 +24,7 @@ import { currentMeta, router, RoutesName } from "@/router";
 import { showLeftPanoStack, showRightPanoStack } from "@/env";
 import { asyncTimeout, togetherCallback } from "@/utils";
 import { loadModel, fuseModel } from "@/model";
+import GlobalSearch from "@/components/global-search/index.vue";
 import {
   enterEdit,
   isOld,
@@ -34,10 +38,12 @@ import {
   initialMeasures,
   fuseModelsLoaded,
   initialPaths,
+  initMonitors,
 } from "@/store";
 
 import Header from "./header/index.vue";
 import SelectModel from "./scene-select.vue";
+import { initialAnimationModels } from "@/store/animation";
 
 const loaded = ref(false);
 const initialSys = async () => {
@@ -48,6 +54,8 @@ const initialSys = async () => {
     initialGuides(),
     initialPaths(),
     initialMeasures(),
+    initMonitors(),
+    initialAnimationModels(),
   ]);
   await loadModel(fuseModel);
   const stop = watchEffect(() => {

+ 4 - 19
src/layout/model-list/index.vue

@@ -15,7 +15,7 @@
         :canChange="canChange"
         :model="item.raw"
         @delete="modelDelete(item.raw)"
-        @click="(mode: any) => modelChangeSelect(item.raw, mode, true)"
+        @click="(mode: any) => flyModel(item.raw, mode, true)"
       />
     </template>
   </List>
@@ -24,14 +24,14 @@
     <div class="mode-tab strengthen">
       <div
         class="mode-icon-layout"
-        @click="modelChangeSelect(panoModel, 'fuse')"
+        @click="flyModel(panoModel, 'fuse')"
         :class="{ active: custom.showMode === 'fuse' }"
       >
         <ui-icon type="show_3d_n" class="icon" ctrl tip="三维模型" tipV="top" />
       </div>
       <div
         class="mode-icon-layout"
-        @click="modelChangeSelect(panoModel, 'pano')"
+        @click="flyModel(panoModel, 'pano')"
         :class="{ active: custom.showMode === 'pano' }"
       >
         <ui-icon type="show_roaming_n" class="icon" ctrl tip="全景图" tipV="top" />
@@ -49,6 +49,7 @@ import { activeModel, getSupperPanoModel } from "@/sdk/association";
 import { fuseModels, getFuseModelShowVariable } from "@/store";
 import type { FuseModel } from "@/store";
 import { currentModel, fuseModel, loadModel } from "@/model";
+import { flyModel } from "@/hook/use-fly";
 import { sdk } from "@/sdk/sdk";
 
 export type ModelListProps = {
@@ -75,22 +76,6 @@ const modelList = computed(() =>
   }))
 );
 
-const modelChangeSelect = (model: FuseModel, mode: "pano" | "fuse", f = false) => {
-  if (getFuseModelShowVariable(model).value) {
-    if (custom.currentModel === model && mode === custom.showMode) {
-      if (!f) return;
-
-      activeModel({ showMode: "fuse", active: undefined, fore: f });
-    } else {
-      activeModel({ showMode: mode, active: model, fore: f });
-    }
-  }
-
-  if (currentModel.value !== fuseModel) {
-    loadModel(fuseModel);
-  }
-};
-
 watchEffect(() => {
   if (custom.currentModel && !getFuseModelShowVariable(custom.currentModel).value) {
     activeModel({ showMode: "fuse" });

+ 3 - 2
src/layout/model-list/sign.vue

@@ -16,6 +16,7 @@
           v-if="supportPano"
         />
         <ui-input
+          v-if="!search"
           type="checkbox"
           v-model="show"
           @click.stop
@@ -24,7 +25,7 @@
           }"
         />
         <ui-icon
-          v-if="custom.modelsChangeStore"
+          v-if="custom.modelsChangeStore && !search"
           type="del"
           ctrl
           @click="$emit('delete')"
@@ -50,7 +51,7 @@ import type { FuseModel } from "@/store";
 import { computed, ref } from "vue";
 import { currentModel, fuseModel } from "@/model";
 
-type ModelProps = { model: FuseModel; canChange?: boolean };
+type ModelProps = { model: FuseModel; canChange?: boolean; search?: boolean };
 const props = defineProps<ModelProps>();
 
 const active = computed(

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

@@ -15,11 +15,15 @@
           <component :is="Component" />
         </keep-alive>
       </router-view>
+      <GlobalSearch />
     </div>
+    <ViewSetting class="show-setting" />
   </template>
 </template>
 
 <script lang="ts" setup>
+import GlobalSearch from "@/components/global-search/index.vue";
+import ViewSetting from "@/components/view-setting/index.vue";
 import { custom, showRightPanoStack } from "@/env";
 import { ref, watchEffect } from "vue";
 import { router, RoutesName } from "@/router";
@@ -42,6 +46,7 @@ import {
   fuseModels,
   appEl,
   initialPaths,
+  initMonitors,
 } from "@/store";
 
 const hasSingle = new URLSearchParams(location.search).has("single");
@@ -64,6 +69,7 @@ const initialSys = async () => {
     initialTaggingStyles(),
     initialTaggings(),
     initialGuides(),
+    initMonitors(),
   ]);
   await initialPaths();
   await initialMeasures();
@@ -97,3 +103,12 @@ watchEffect((onCleanup) => {
   ) !important;
 }
 </style>
+
+<style lang="scss" scoped>
+.show-setting {
+  position: absolute;
+  bottom: 20px;
+  z-index: 99;
+  right: calc(var(--editor-menu-right) + var(--editor-toolbox-width) + 20px);
+}
+</style>

+ 2 - 2
src/router/constant.ts

@@ -84,7 +84,7 @@ export const metas = {
   [RoutesName.registration]: { full: true, sysTitle: "多元融合" },
   [RoutesName.tagging]: {
     icon: "label",
-    title: "标",
+    title: "标",
     sysTitle: "多元融合",
   },
   [RoutesName.guide]: {
@@ -110,7 +110,7 @@ export const metas = {
 
   [RoutesName.view]: {
     sysTitle: "视图提取",
-    icon: "nav-setup",
+    icon: "view",
     title: "视图提取",
     left: 'scene-list'
   },

+ 7 - 7
src/sdk/association/animation.ts

@@ -48,10 +48,10 @@ export const addAM = (data: AnimationModel): Promise<AnimationModel3D> => {
       if (!exixts) {
         const des = amMap[key];
         if (!des) return;
-        Object.values(des.frames || {}).forEach((frame) => frame.destory());
-        Object.values(des.actions || {}).forEach((frame) => frame.destory());
-        Object.values(des.paths || {}).forEach((frame) => frame.destory());
-        des.am?.destory();
+        Object.values(des.frames || {}).forEach((frame) => frame.destroy());
+        Object.values(des.actions || {}).forEach((frame) => frame.destroy());
+        Object.values(des.paths || {}).forEach((frame) => frame.destroy());
+        des.am?.destroy();
         delete amMap[key];
       } else if (!amMap[key]) {
         amMap[key] = {
@@ -122,7 +122,7 @@ export const addFrame = (
       if (exists && !map.frames[data.id]) {
         map.frames[data.id] = map.am.addFrame(data);
       } else if (!exists && map.frames[data.id]) {
-        map.frames[data.id].destory();
+        map.frames[data.id].destroy();
         delete map.frames[data.id];
       }
     }
@@ -176,7 +176,7 @@ export const addAction = (
       if (exists && !map.actions[data.id]) {
         map.actions[data.id] = map.am.addAction(data);
       } else if (!exists && map.actions[data.id]) {
-        map.actions[data.id].destory();
+        map.actions[data.id].destroy();
         delete map.actions[data.id];
       }
     }
@@ -238,7 +238,7 @@ export const addPath = (
       if (exists && !map.paths[data.id]) {
         map.paths[data.id] = map.am.addPath({ ...data, path });
       } else if (!exists && map.paths[data.id]) {
-        map.paths[data.id].destory();
+        map.paths[data.id].destroy();
         delete map.paths[data.id];
       }
     }

+ 49 - 27
src/sdk/association/guide.ts

@@ -1,11 +1,12 @@
 import { SceneGuide, sdk } from "../sdk";
-import { toRaw, ref, watch, watchEffect } from "vue";
-import { viewModeStack, showLeftPanoStack, custom } from "@/env";
+import { toRaw, ref, watch, watchEffect, computed } from "vue";
+import { viewModeStack, showLeftPanoStack, custom, showTaggingsStack, showPathsStack, showMeasuresStack, showSearchStack } from "@/env";
 import { togetherCallback, asyncTimeout } from "@/utils";
 import { fuseModels, isEdit, sysBus, fuseModelsLoaded } from "@/store";
 import type { FuseModel, FuseModels, Guide, GuidePath } from "@/store";
 import { analysisPoseInfo } from ".";
 import { fullView, isScenePlayRun, pauseScene, playScene } from "@/utils/full";
+import { animationGroup, currentTime } from "./animation";
 
 // -----------------导览关联--------------------
 
@@ -80,34 +81,55 @@ export const recovery = async (guide: Guide) => {
     );
 };
 
-
 export const playSceneGuide = (
   paths: GuidePath[],
   changeIndexCallback?: (index: number) => void,
-  forceFull = false
+  forceFull = false,
+  guide?: Guide
 ) => {
-  let sceneGuide: SceneGuide
-  return playScene({
-    create: () => {
-      sceneGuide = sdk.enterSceneGuide(
-        paths.map((path) => ({ ...path, ...analysisPoseInfo(path) }))
-      );
-      changeIndexCallback && sceneGuide.bus.on("changePoint", changeIndexCallback);
-    },
-    play: () => {
-      sceneGuide.play();
-      return new Promise((resolve) => sceneGuide.bus.on("playComplete", resolve))
-    },
-    pause: () => {
-      console.error('pause??')
-      sceneGuide.pause();
+  let sceneGuide: SceneGuide;
+  currentTime.value = 0
+  let pop: (() => void) | null = null
+  return playScene(
+    {
+      create: () => {
+        sceneGuide = sdk.enterSceneGuide(
+          paths.map((path) => ({ ...path, ...analysisPoseInfo(path) }))
+        );
+        sceneGuide.bus.on("changePoint", (index) => {
+          console.log(index)
+          if (paths[index - 1].playAnimation) {
+            animationGroup && animationGroup.play()
+          }
+          changeIndexCallback && changeIndexCallback(index);
+        });
+        pop = togetherCallback([
+          showTaggingsStack.push(computed(() => guide ? guide.showTagging : true)),
+          showPathsStack.push(computed(() => guide ? guide.showPath : true)),
+          showMeasuresStack.push(computed(() => guide ? guide.showMeasure : true)),
+          showSearchStack.push(ref(false))
+        ])
+      },
+      play: () => {
+        sceneGuide.play();
+        return new Promise((resolve) =>
+          sceneGuide.bus.on("playComplete", resolve)
+        );
+      },
+      pause: () => {
+        sceneGuide.pause();
+        animationGroup && animationGroup.pause()
+      },
+      clear: () => {
+        currentTime.value = 0
+        sceneGuide.clear();
+        pop && pop()
+        sceneGuide.bus.off("changePoint");
+      },
     },
-    clear: () => {
-      sceneGuide.clear();
-      sceneGuide.bus.off("changePoint");
-    }
-  }, forceFull)
-}
+    forceFull
+  );
+};
 
-export const pauseSceneGuide = pauseScene
-export const isScenePlayIng = isScenePlayRun
+export const pauseSceneGuide = pauseScene;
+export const isScenePlayIng = isScenePlayRun;

+ 2 - 0
src/sdk/association/index.ts

@@ -12,6 +12,7 @@ import { associationSetting } from "./setting";
 import { associationMessaures } from "./measure";
 import { custom } from "@/env";
 import { associationPaths } from "./path";
+import { associationAnimation } from "./animation";
 
 export const getSupperPanoModel = () => {
   const supperModel = ref<FuseModel | null>(null);
@@ -120,6 +121,7 @@ export const setupAssociation = (mountEl: HTMLDivElement, sdk: SDK) => {
       associationMessaures(sdk);
       associationSetting(sdk, mountEl);
       associationPaths(sdk, mountEl)
+      // associationAnimation(sdk, mountEl)
       nextTick(() => stopWatch());
     }
   });

+ 18 - 3
src/sdk/association/path.ts

@@ -1,12 +1,12 @@
 import { diffArrayChange, mount, shallowWatchArray } from "@/utils";
 import TaggingComponent from "@/components/path/list.vue";
-import { Path as PathData, paths } from "@/store/path";
+import { Path as PathData, paths, selectPaths } from "@/store/path";
 import { sdk, Path, SDK } from "../sdk";
 import { nextTick, reactive, ref, watch, watchEffect } from "vue";
 import { groupProxy } from "@/store/group";
 import { isScenePlayRun, pauseScene, playScene } from "@/utils/full";
 import { analysisPose, setPose } from ".";
-import { custom, showPathsStack, showPathStack } from "@/env";
+import { custom, showPathsStack, showPathStack, showSearchStack } from "@/env";
 import { Message } from "bill/expose-common";
 
 // -----------------导览线关联--------------------
@@ -52,7 +52,7 @@ export const playScenePath = async (
   const node = getPathNode(path)
   if (!node) {
     console.error('un', path.id)
-    return Message.error('路径所在模型被删除,无法播放');
+    return Message.error('路径所在模型被隐藏活删除,无法播放');
   }
 
   showPathsStack.push(ref(false))
@@ -60,7 +60,11 @@ export const playScenePath = async (
 
   
   let initPose: any;
+  let pop: null | (() => void) = null
   await playScene({
+    create: () => {
+      pop = showSearchStack.push(ref(false))
+    },
     play: () => {
       return new Promise(resolve => {
         initPose = analysisPose(sdk.getPose());
@@ -70,6 +74,9 @@ export const playScenePath = async (
     pause: () => {
       setPose(initPose)
       node.pause();
+    },
+    clear: () => {
+      pop && pop()
     }
   }, forceFull)
 
@@ -101,4 +108,12 @@ export const associationPaths = (sdk: SDK, el: HTMLDivElement) => {
       {immediate: true}
     );
   });
+
+  watchEffect(() => {
+    selectPaths.selects.value.forEach(item => pathNodes.get(item)?.visibility(true))
+    selectPaths.unSelects.value.forEach(item => {
+      pathNodes.get(item)?.visibility(false)
+    })
+  })
+  
 };

+ 6 - 4
src/sdk/sdk.ts

@@ -45,6 +45,7 @@ export type SceneModel = ToChangeAPI<SceneModelAttrs> & {
     }
   >;
   destroy: () => void;
+  enterScaleMode: () => void;
   enterRotateMode: () => void;
   enterMoveMode: () => void;
   leaveTransform: () => void;
@@ -198,6 +199,7 @@ export interface SDK {
   screenshot: (width: number, height: number) => Promise<string>;
   getPose: () => Pose;
   comeTo: (pos: CameraComeToProps) => void;
+  comeToByLatLng: (pos: number[]) => void;
 
   enterSceneGuide: (data: SceneGuidePath[]) => SceneGuide;
 
@@ -378,7 +380,7 @@ export type AnimationGroup = {
 
 export type AnimationModel3D = {
   // 销毁动画模型
-  destory: () => void;
+  destroy: () => void;
   // 更改动画模型可见性
   changeShow: (show: boolean) => void;
   // 更改动画可见范围  不传为全局可见
@@ -431,7 +433,7 @@ export type AnimationModel3D = {
 
 export type AnimationModelFrame3D = {
   // 销毁动画模型帧
-  destory: () => void;
+  destroy: () => void;
   // 修改帧播放时间 单位为秒
   changeTime: (s: number) => void;
   setMat: (mat: any) => void
@@ -439,7 +441,7 @@ export type AnimationModelFrame3D = {
 
 export type AnimationModelAction3D = {
   // 销毁动画模型动作
-  destory: () => void;
+  destroy: () => void;
   // 修改动作播放时间 单位为秒
   changeTime: (s: number) => void;
   // 修改动作幅度
@@ -452,7 +454,7 @@ export type AnimationModelAction3D = {
 
 export type AnimationModelPath3D = {
   // 销毁动画模型路径
-  destory: () => void;
+  destroy: () => void;
   // 修改路径 传入参数为你之前返回的路径对象
   changePath: (path: Path | undefined) => void;
   // 修改播放是否要反向

+ 1 - 0
src/store/guide-path.ts

@@ -33,6 +33,7 @@ export const createGuidePath = (path: Partial<GuidePath> = {}): GuidePath => ({
   time: 1,
   sort: 999,
   speed: 1,
+  
   position: {x: 0, y: 0, z: 0},
   target: {x: 0, y: 0, z: 0},
   ...path

+ 4 - 0
src/store/guide.ts

@@ -36,6 +36,10 @@ export const createGuide = (guide: Partial<Guide> = {}): Guide => ({
   id: createTemploraryID(),
   title: `路径${guides.value.length + 1}`,
   cover: '',
+  showMeasure: true,
+  showTagging: true,
+  showPath: true,
+  showVideo: true,
   ...guide
 })
 

+ 2 - 1
src/store/index.ts

@@ -14,4 +14,5 @@ export * from './floder'
 export * from './floder-type'
 export * from './setting'
 export * from './case'
-export * from './path'
+export * from './path'
+export * from './monitor'

+ 39 - 0
src/store/map.ts

@@ -0,0 +1,39 @@
+import { params } from "@/env";
+
+export type Address = { address: string; latlng: number[], id: string };
+const platform = {
+  gaode(val: string) {
+    const key = params.mapKey || "3bddec1685d461c2271a6099cde02fd2";
+    return fetch(
+      `https://restapi.amap.com/v3/geocode/geo?address=${encodeURIComponent(
+        val
+      )}&key=${key}`
+    )
+      .then((res) => res.json())
+      .then((res) => {
+        if (res.info !== "OK") {
+          throw res.info;
+        }
+        console.log(res)
+        const items = res.geocodes.map((item: any) => ({
+          id: item.location,
+          address: item.formatted_address,
+          latlng: item.location
+            .split(",")
+            .map((item: string) => Number(item.trim())),
+        })).slice(0, 10);
+        return items;
+      });
+  },
+};
+
+export const searchAddress = (val: string): Promise<Address[]> => {
+  if (!val) return Promise.resolve([])
+  const p = (
+    params.mapPlatform && params.mapPlatform in platform
+      ? params.mapPlatform
+      : "gaode"
+  ) as keyof typeof platform;
+
+  return platform[p](val);
+};

+ 68 - 0
src/store/monitor.ts

@@ -0,0 +1,68 @@
+import { ref } from "vue";
+import { autoSetModeCallback, createTemploraryID } from "./sys";
+import { fetchMonitors, postUpdateMonitor, postDeleteMonitor, postInsertMonitor } from "@/api";
+import {
+  togetherCallback,
+  deleteStoreItem,
+  addStoreItem,
+  updateStoreItem,
+  saveStoreItems,
+  recoverStoreItems,
+} from "@/utils";
+
+import type { Monitor, Monitors } from "@/api";
+export type { Monitors, Monitor } from "@/api";
+
+export const monitors = ref<Monitors>([]);
+
+export const initMonitors = async () => {
+  monitors.value = await fetchMonitors();
+};
+
+export const createMonitor = (
+  am: Partial<Monitor> = {}
+): Monitor => ({
+  id: createTemploraryID(),
+  title: `模型`,
+  content: '',
+  ...am,
+});
+
+let bcMonitors: Monitors = [];
+export const getBackupMonitors = () => bcMonitors;
+export const backupMonitors = () => {
+  bcMonitors = monitors.value.map((monitor) => ({ ...monitor }));
+};
+export const addMonitor = addStoreItem(monitors, postInsertMonitor);
+export const updateMonitors = updateStoreItem(
+  monitors,
+  postUpdateMonitor
+);
+export const deleteMonitor = deleteStoreItem(monitors, ({ id }) =>
+  postDeleteMonitor(id)
+);
+export const initialMonitors = async () => {
+  monitors.value = await fetchMonitors();
+  backupMonitors();
+};
+
+export const recoverMonitors = recoverStoreItems(
+  monitors,
+  getBackupMonitors
+);
+export const saveMonitors = saveStoreItems(
+  monitors,
+  getBackupMonitors,
+  {
+    add: addMonitor,
+    update: updateMonitors,
+    delete: deleteMonitor,
+  }
+);
+export const autoSaveMonitor = autoSetModeCallback([monitors], {
+  backup: togetherCallback([backupMonitors]),
+  recovery: togetherCallback([recoverMonitors]),
+  save: async () => {
+    await saveMonitors();
+  },
+});

+ 6 - 1
src/store/path.ts

@@ -21,11 +21,13 @@ import {
 import type { Path } from '@/api'
 import { custom, params } from '@/env'
 import { Message } from 'bill/expose-common'
+import { useSelects } from '@/hook/ids'
 
 export  type { Path } from '@/api'
 export type Paths = Path[]
 
 export const paths = ref<Paths>([])
+export const selectPaths = useSelects(paths)
 export const getPath = (id: Path['id']) => paths.value.find(path => path.id === id)
 
 export const getPathIsShow = (path: Path) => {
@@ -71,7 +73,10 @@ export const backupPaths = () => {
 export const initialPaths = fetchStoreItems(paths, async () => {
   const paths = await fetchPaths()
   return paths
-}, backupPaths)
+}, () => {
+  backupPaths()
+  selectPaths.all.value = true
+})
 export const recoverPaths = async () => {
   const backupItems = bcPaths;
   paths.value.length = 0

+ 7 - 2
src/utils/full.ts

@@ -3,20 +3,24 @@ import { togetherCallback } from ".";
 import { ref, watch } from "vue";
 import { isEdit, sysBus } from "@/store";
 
+export const currentIsFullView = ref(false)
 export const fullView = async (fn: () => void) => {
   const popViewMode = togetherCallback([
     viewModeStack.push(ref("full")),
     showLeftPanoStack.push(ref(false)),
   ]);
   let isFull = false;
+  currentIsFullView.value = false
   try {
     await document.documentElement.requestFullscreen();
+    currentIsFullView.value = true
     isFull = true;
   } catch {}
 
-  const driving = () => document.fullscreenElement || fn();
+  const driving = () => {
+    document.fullscreenElement || fn()
+  };
   const stop = (ev: KeyboardEvent) => ev.key == "Escape" && fn();
-
   if (isFull) {
     document.addEventListener("fullscreenchange", driving);
     document.addEventListener("fullscreenerror", fn);
@@ -27,6 +31,7 @@ export const fullView = async (fn: () => void) => {
   return () => {
     popViewMode();
     if (isFull) {
+      currentIsFullView.value = false
       document.fullscreenElement && document.exitFullscreen();
       document.removeEventListener("fullscreenchange", driving);
       document.removeEventListener("fullscreenerror", fn);

+ 3 - 3
src/utils/store-help.ts

@@ -129,7 +129,7 @@ export function fetchStoreItems<
 >(
   items: Ref<T[]>,
   fetchAction: () => Promise<T[]>,
-  callback: (() => void) | null
+  callback: () => void
 ): () => Promise<void>;
 export function fetchStoreItems<
   T extends { id: any },
@@ -137,7 +137,7 @@ export function fetchStoreItems<
 >(
   items: Ref<T[]>,
   fetchAction: () => Promise<K[]>,
-  callback: (() => void) | null,
+  callback: () => void,
   transform: (items: K[]) => Promise<T[]> | T[]
 ): () => Promise<void>;
 export function fetchStoreItems<
@@ -146,7 +146,7 @@ export function fetchStoreItems<
 >(
   items: Ref<T[]>,
   fetchAction: () => Promise<K[]>,
-  callback?: (() => void) | null,
+  callback?: () => void,
   transform?: (items: K[]) => Promise<T[]> | T[]
 ) {
   return async () => {

+ 38 - 28
src/views/animation/bottom.vue

@@ -37,33 +37,37 @@
       </div>
     </div>
     <div class="oper-bar" :class="{ disabled: play }">
-      <Renderer v-model:scale="scale">
+      <Renderer v-model:scale="scale" ref="renderer">
         <v-group>
-          <template v-for="prop in tlProps">
-            <TimeLine
-              v-if="am[prop.attr].length"
-              :items="am[prop.attr]"
-              :height="prop.height"
-              :background="prop.background"
-              :opacity="prop.opacity"
-              :top="prop.top"
-              :itemsRenderer="prop.component"
-              @update="({ ndx, time }) => (am[prop.attr][ndx].time = time)"
-              @add="
-                (item) => {
-                  am[prop.attr].push(item);
-                  $emit('update:active', {
-                    key: prop.attr,
-                    ndx: am[prop.attr].length - 1,
-                  });
-                }
-              "
-              @del="(ndx) => am[prop.attr].splice(ndx, 1)"
-              :active="prop.attr === active?.key ? am[prop.attr][active.ndx] : undefined"
-              @update:active="(active: any) => $emit('update:active', active && { key: prop.attr, ndx: am[prop.attr].indexOf(active) })"
-            />
+          <template v-if="am">
+            <template v-for="prop in tlProps">
+              <TimeLine
+                v-if="am[prop.attr].length"
+                :items="am[prop.attr]"
+                :height="prop.height"
+                :background="prop.background"
+                :opacity="prop.opacity"
+                :top="prop.top"
+                :itemsRenderer="prop.component"
+                @update="({ ndx, time }) => (am![prop.attr][ndx].time = time)"
+                @add="
+                  (item) => {
+                    am![prop.attr].push(item);
+                    $emit('update:active', {
+                      key: prop.attr,
+                      ndx: am![prop.attr].length - 1,
+                    });
+                  }
+                "
+                @del="(ndx) => am![prop.attr].splice(ndx, 1)"
+                :active="
+                  prop.attr === active?.key ? am[prop.attr][active.ndx] : undefined
+                "
+                @update:active="(active: any) => $emit('update:active', active && { key: prop.attr, ndx: am![prop.attr].indexOf(active) })"
+              />
+            </template>
+            <empty v-if="!count" />
           </template>
-          <empty v-if="!count" />
         </v-group>
         <v-group>
           <Time @update-current-time="(time) => $emit('update:currentTime', time)">
@@ -81,7 +85,7 @@
 
 <script lang="ts" setup>
 import { Slider } from "ant-design-vue";
-import { computed, ref, watch } from "vue";
+import { computed, ref, watch, watchEffect } from "vue";
 import { AnimationModel } from "@/store/animation";
 import Renderer from "@/components/drawing/renderer.vue";
 import Time from "@/components/drawing-time/time.vue";
@@ -93,7 +97,7 @@ import empty from "@/components/drawing-time-line/empty.vue";
 import { Active } from "./type";
 
 const props = defineProps<{
-  am: AnimationModel;
+  am?: AnimationModel;
   active?: Active;
   currentTime: number;
   follow: boolean;
@@ -142,7 +146,7 @@ const tlProps = [
 ] as const;
 
 const count = computed(() =>
-  Object.values(tlProps).reduce((t, c) => t + props.am[c.attr].length, 0)
+  Object.values(tlProps).reduce((t, c) => (props.am ? t + props.am[c.attr].length : 0), 0)
 );
 
 const play = ref(false);
@@ -161,6 +165,12 @@ watch(play, (_a, _b, onCleanup) => {
   animation();
   onCleanup(() => (isDes = true));
 });
+
+const renderer = ref<any>();
+watch(
+  () => props.am,
+  () => renderer.value.updateSize()
+);
 </script>
 
 <style scoped lang="scss">

+ 3 - 4
src/views/animation/index.vue

@@ -21,7 +21,6 @@
       @apply-global="k => ams.forEach((am: any) => (am[k] = focusAM![k]))"
     />
     <Bottom
-      v-if="focusAM"
       :am="focusAM"
       v-model:follow="follow"
       v-model:current-time="currentTime"
@@ -51,7 +50,7 @@ import { getAddTLItemAttr } from "@/components/drawing-time-line/check";
 import { Message } from "bill/expose-common";
 import { uuid } from "@/components/drawing/hook";
 import { title } from "./type";
-import { amMap, getAMKey } from "@/sdk/association/animation";
+import { amMap, getAMKey, currentTime } from "@/sdk/association/animation";
 
 enterEdit(() => router.back());
 initialAnimationModels();
@@ -60,7 +59,7 @@ useViewStack(autoSaveAnimationModel);
 
 const focusAM = ref<AnimationModel>();
 const activeAttrib = ref<Active>();
-const currentTime = ref(0);
+
 const follow = ref(false);
 const frameAction = ref<string>();
 
@@ -155,7 +154,7 @@ const deleteAm = (am: AnimationModel) => {
 
 <style lang="scss" scoped>
 .animation-layout {
-  --bottom-height: 0px;
+  --bottom-height: 70px;
 
   &.focusAM {
     --bottom-height: 225px;

+ 6 - 5
src/views/animation/right/am.vue

@@ -53,19 +53,19 @@
         <ui-group-option class="item">
           <span class="label">加帧</span>
           <span class="oper" @click="$emit('addFrame')">
-            <ui-icon type="keys" ctrl />
+            <ui-icon type="keys_a" ctrl />
           </span>
         </ui-group-option>
         <ui-group-option class="item">
           <span class="label">路径</span>
           <span class="oper">
-            <ui-icon @click="visibleSelectPath = true" type="add" ctrl />
+            <ui-icon @click="visibleSelectPath = true" type="add_a" ctrl />
           </span>
         </ui-group-option>
         <ui-group-option class="item">
           <span class="label">字幕</span>
           <span class="oper">
-            <ui-icon @click="$emit('addSubtitle')" type="add" ctrl />
+            <ui-icon @click="$emit('addSubtitle')" type="add_a" ctrl />
           </span>
         </ui-group-option>
       </ui-group>
@@ -77,7 +77,7 @@
           <span class="label">{{ action.title }}</span>
           <span class="oper">
             <ui-icon
-              type="add"
+              type="add_a"
               ctrl
               @click="$emit('addAction', { key: action.action, name: action.title })"
             />
@@ -146,7 +146,8 @@ const selectPathHandler = () => {
     return;
   }
   const name = options.value.find(({ value }) => value === pathId.value)!.label;
-  emit("addPath", { name });
+  console.log(pathId.value);
+  emit("addPath", { name, pathId: pathId.value });
   visibleSelectPath.value = false;
 };
 </script>

+ 1 - 1
src/views/animation/right/path.vue

@@ -22,7 +22,7 @@
               type="select"
               :options="options"
               placeholder="请选择路径"
-              v-model="data.id"
+              v-model="data.pathId"
             />
           </SignItem>
         </ui-group-option>

+ 58 - 0
src/views/guide/guide/attach-animation-sam.vue

@@ -0,0 +1,58 @@
+<template>
+  <div
+    class="anima fun-ctrl"
+    :class="{ disabled: disableAttach, active: isPlayIng }"
+    @click="attachAnimation"
+  >
+    <ui-icon type="a-animation_s" />
+    <span>激活动画</span>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { currentTime } from "@/sdk/association/animation";
+import { computed, ref, watchEffect } from "vue";
+import { GuidePath } from "@/store";
+
+const props = defineProps<{ current: GuidePath; paths: GuidePath[] }>();
+
+const playNdx = computed(() => props.paths.findIndex((path) => path.playAnimation));
+const curNdx = computed(() => props.paths.indexOf(props.current));
+const isCurrentPlay = computed(
+  () => playNdx.value !== -1 && curNdx.value === playNdx.value
+);
+const disableAttach = computed(() => playNdx.value !== -1 && !isCurrentPlay.value);
+const isPlayIng = computed(() => playNdx.value !== -1 && curNdx.value >= playNdx.value);
+
+const getNdxTime = (curNdx: number) => {
+  if (playNdx.value === -1 || curNdx < playNdx.value) return 0;
+  let mis = 0;
+  for (let i = playNdx.value; i < curNdx; i++) {
+    mis += props.paths[i].time;
+  }
+  return mis;
+};
+
+watchEffect(() => {
+  currentTime.value = getNdxTime(curNdx.value);
+});
+
+const attachAnimation = () => {
+  props.current.playAnimation = !props.current.playAnimation;
+};
+</script>
+
+<style lang="scss" scoped>
+.anima {
+  display: flex;
+  align-items: center;
+  margin: 0 30px;
+  &.active {
+    color: var(--colors-primary-base) !important;
+  }
+
+  .icon {
+    font-size: 1.4em;
+  }
+}
+</style>

+ 116 - 0
src/views/guide/guide/attach-animation.vue

@@ -0,0 +1,116 @@
+<template>
+  <div class="animation-layout">
+    <div class="info">
+      <div>
+        <ui-icon
+          type="a-animation_s"
+          ctrl
+          @click="attachAnimation"
+          :class="{ disabled: disableAttach, active: isPlayIng }"
+        />
+      </div>
+    </div>
+    <div class="renderer">
+      <Renderer v-model:scale="scale">
+        <v-group>
+          <Time @update-current-time="(val) => (currentTime = val)">
+            <!-- <v-group>
+              <template v-for="(_, ndx) in paths">
+                <TimeCurrent
+                  v-if="ndx >= playNdx"
+                  hideLine
+                  current-color="#00c8af"
+                  :currentTime="getNdxTime(ndx)"
+                />
+              </template>
+            </v-group> -->
+            <TimeCurrent
+              hideLine
+              :currentTime="currentTime"
+              :follow="follow"
+              @update:current-time="(val) => (currentTime = val)"
+            />
+          </Time>
+        </v-group>
+      </Renderer>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import Renderer from "@/components/drawing/renderer.vue";
+import Time from "@/components/drawing-time/time.vue";
+import TimeCurrent from "@/components/drawing-time/current.vue";
+import { currentTime } from "@/sdk/association/animation";
+import { computed, ref, watchEffect } from "vue";
+import { GuidePath } from "@/store";
+
+const props = defineProps<{ current: GuidePath; paths: GuidePath[] }>();
+const scale = ref(1);
+const follow = ref(true);
+
+const playNdx = computed(() => props.paths.findIndex((path) => path.playAnimation));
+const curNdx = computed(() => props.paths.indexOf(props.current));
+const isCurrentPlay = computed(
+  () => playNdx.value !== -1 && curNdx.value === playNdx.value
+);
+const disableAttach = computed(() => playNdx.value !== -1 && !isCurrentPlay.value);
+const isPlayIng = computed(() => playNdx.value !== -1 && curNdx.value >= playNdx.value);
+
+const getNdxTime = (curNdx: number) => {
+  if (playNdx.value === -1 || curNdx < playNdx.value) return 0;
+  let mis = 0;
+  for (let i = playNdx.value; i < curNdx; i++) {
+    mis += props.paths[i].time;
+  }
+  return mis;
+};
+
+watchEffect(() => {
+  currentTime.value = getNdxTime(curNdx.value);
+});
+
+const attachAnimation = () => {
+  props.current.playAnimation = !props.current.playAnimation;
+};
+</script>
+
+<style lang="scss" scoped>
+.animation-layout {
+  height: 30px;
+  display: flex;
+  position: relative;
+  // &::before {
+  //   content: "";
+  //   position: absolute;
+  //   left: 0;
+  //   bottom: 0;
+  //   height: 50%;
+  //   background: rgba(0, 0, 0, 0.5);
+  //   width: 100%;
+  // }
+
+  .info {
+    flex: 0 0 auto;
+    width: 33px;
+    color: #fff;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    flex-direction: column;
+    .icon {
+      display: block;
+      padding: 5px;
+      font-size: 16px;
+
+      &.active {
+        color: var(--colors-primary-base) !important;
+      }
+    }
+  }
+  .renderer {
+    flex: 1;
+    pointer-events: none;
+  }
+}
+</style>

+ 79 - 23
src/views/guide/guide/edit-paths.vue

@@ -1,12 +1,6 @@
 <template>
   <div class="video" ref="videoRef">
     <div class="overflow">
-      <ui-icon
-        ctrl
-        :type="isScenePlayIng ? 'pause' : 'preview'"
-        :disabled="!paths.length"
-        @click="play"
-      />
       <ui-button
         type="primary"
         @click="addPath"
@@ -16,26 +10,39 @@
         添加视角
       </ui-button>
     </div>
+    <ViewSetting class="show-setting" />
+
     <div class="info" v-if="paths.length">
       <div class="meta">
         <div class="length">
           <span>视频时长</span>{{ paths.reduce((t, c) => t + c.time, 0).toFixed(1) }}s
         </div>
-        <div
-          class="fun-ctrl clear"
-          @click="deleteAll"
-          :class="{ disabled: isScenePlayIng }"
-        >
-          <ui-icon type="del" />
-          <span>清空画面</span>
+        <ui-icon
+          ctrl
+          :type="isScenePlayIng ? 'a-pause' : 'a-play'"
+          :disabled="!paths.length"
+          @click="play"
+          style="font-size: 16px"
+        />
+        <div>
+          <attachAnimation :current="current" :paths="paths" />
+          <div
+            class="fun-ctrl clear"
+            @click="deleteAll"
+            :class="{ disabled: isScenePlayIng }"
+          >
+            <ui-icon type="del" />
+            <span>清空画面</span>
+          </div>
         </div>
       </div>
+      <!-- <attachAnimation :current="current" :paths="paths" /> -->
 
-      <div class="photo-list" ref="listVm">
+      <div class="photo-list" ref="listVm" :class="{ disabled: isScenePlayIng }">
         <template v-for="(path, i) in paths" :key="path.id">
           <div
             class="photo"
-            :class="{ active: current === path, disabled: isScenePlayIng }"
+            :class="{ active: current === path }"
             @click="changeCurrent(path)"
           >
             <ui-icon
@@ -81,6 +88,8 @@
 </template>
 
 <script setup lang="ts">
+import attachAnimation from "./attach-animation-sam.vue";
+import ViewSetting from "@/components/view-setting/index.vue";
 import { loadPack, togetherCallback, getFileUrl, asyncTimeout } from "@/utils";
 import {
   sdk,
@@ -100,7 +109,7 @@ import {
 } from "@/store";
 import { Dialog } from "bill/index";
 import { useViewStack } from "@/hook";
-import { nextTick, onUnmounted, ref, watch, watchEffect } from "vue";
+import { computed, nextTick, onUnmounted, Ref, ref, watch, watchEffect } from "vue";
 import {
   showRightPanoStack,
   showLeftCtrlPanoStack,
@@ -108,6 +117,10 @@ import {
   showRightCtrlPanoStack,
   getResource,
   custom,
+  showTaggingsStack,
+  showPathsStack,
+  showMeasuresStack,
+  showMonitorsStack,
 } from "@/env";
 
 import type { Guide, GuidePaths, GuidePath } from "@/store";
@@ -122,14 +135,32 @@ const updatePathInfo = (index: number, calcInfo: CalcPathProps[1]) => {
   Object.assign(paths.value[index], info);
 };
 
-useViewStack(() =>
-  togetherCallback([
+useViewStack(() => {
+  const mapping = {
+    showTagging: showTaggingsStack,
+    showMonitor: showMonitorsStack,
+    showMeasure: showMeasuresStack,
+    showPath: showPathsStack,
+  };
+  const keys = Object.keys(mapping) as (keyof typeof mapping)[];
+
+  return togetherCallback([
     showRightPanoStack.push(ref(false)),
     showLeftCtrlPanoStack.push(ref(false)),
     showLeftPanoStack.push(ref(false)),
     showRightCtrlPanoStack.push(ref(false)),
-  ])
-);
+    togetherCallback(
+      keys.map((key) =>
+        mapping[key].push(
+          computed({
+            get: () => props.data[key],
+            set: (s: boolean) => (props.data[key] = s),
+          })
+        )
+      )
+    ),
+  ]);
+});
 
 useAutoSetMode(
   paths,
@@ -204,9 +235,14 @@ const play = async () => {
   } else {
     changeCurrent(paths.value[0]);
     await asyncTimeout(400);
-    playSceneGuide(paths.value, (index) => {
-      current.value = paths.value[index - 1];
-    });
+    playSceneGuide(
+      paths.value,
+      (index) => {
+        current.value = paths.value[index - 1];
+      },
+      false,
+      props.data
+    );
   }
 };
 
@@ -265,20 +301,33 @@ onUnmounted(() => {
     }
   }
 
+  .show-setting {
+    position: absolute;
+    right: 20px;
+    bottom: 100%;
+    margin-bottom: 20px;
+  }
+
   .meta {
     font-size: 12px;
     border-bottom: 1px solid rgba(255, 255, 255, 0.16);
     padding: 10px 20px;
     display: flex;
     justify-content: space-between;
+    align-items: center;
 
     .length span {
       margin-right: 10px;
     }
+    > div {
+      display: flex;
+      align-items: center;
+    }
 
     .clear {
       display: flex;
       align-items: center;
+
       .icon {
         font-size: 1.4em;
         margin-right: 5px;
@@ -307,12 +356,14 @@ onUnmounted(() => {
         top: 50%;
         transform: translateY(-50%);
       }
+
       &::before {
         left: 0;
         right: 7px;
         height: 2px;
         background-color: currentColor;
       }
+
       &::after {
         right: -5px;
         width: 0;
@@ -355,6 +406,7 @@ onUnmounted(() => {
     }
   }
 }
+
 .un-video {
   height: 100px;
   line-height: 100px;
@@ -369,16 +421,20 @@ onUnmounted(() => {
   .ui-input .text input {
     padding: 8px 4px;
   }
+
   .ui-input .text {
     font-size: 12px;
   }
+
   .ui-input .text.suffix .retouch {
     right: 4px;
   }
+
   .ui-input .text.suffix input {
     padding-right: 28px;
     text-align: right;
   }
+
   .ui-input.time .text.suffix input {
     padding-right: 18px;
     text-align: right;

+ 9 - 8
src/views/guide/guide/edit.vue

@@ -18,14 +18,14 @@
     />
   </ui-group>
   <Teleport to="#layout-app">
-    <ui-editor-toolbar :toolbar="!!currentGuide" class="video-toolbar">
+    <ui-editor-toolbar :toolbar="!!currentGuide" class="video-toolbar" disabledAnimation>
       <EditPaths :data="currentGuide" v-if="currentGuide" />
     </ui-editor-toolbar>
   </Teleport>
 </template>
 
 <script lang="ts" setup>
-import { ref } from "vue";
+import { ref, watchEffect } from "vue";
 import GuideSign from "./sign.vue";
 import EditPaths from "./edit-paths.vue";
 import { useViewStack } from "@/hook";
@@ -35,12 +35,14 @@ import {
   enterEdit,
   sysBus,
   autoSaveGuides,
-  enterOld,
 } from "@/store";
 
 import type { Guide } from "@/store";
 
-const currentGuide = ref<Guide | null>();
+const currentGuide = ref<Guide | null>(null);
+const emit = defineEmits<{(e: 'update:current', v: Guide | null): void}>()
+watchEffect(() => emit('update:current', currentGuide.value))
+
 const leaveEdit = () => (currentGuide.value = null);
 const edit = (guide: Guide) => {
   currentGuide.value = guide;
@@ -57,10 +59,9 @@ useViewStack(autoSaveGuides);
 
 defineExpose({
   add: () => {
-    edit(createGuide())
-  }
-})
-
+    edit(createGuide());
+  },
+});
 </script>
 
 <style lang="scss" scoped>

+ 15 - 1
src/views/guide/guide/show.vue

@@ -1,8 +1,22 @@
 <template>
-  <GuideSign v-for="guide in guides" :key="guide.id" :guide="guide" :edit="false" />
+  <ui-group title="导览列表" class="show-taggings">
+    <GuideSign
+      v-for="guide in filterGuides"
+      :key="guide.id"
+      :guide="guide"
+      :edit="false"
+    />
+  </ui-group>
 </template>
 
 <script setup lang="ts">
+import { computed } from "vue";
 import GuideSign from "./sign.vue";
 import { guides } from "@/store";
+
+const props = withDefaults(defineProps<{ keyword?: string }>(), { keyword: "" });
+
+const filterGuides = computed(() =>
+  guides.value.filter((guide) => guide.title.includes(props.keyword))
+);
 </script>

+ 16 - 8
src/views/guide/guide/sign.vue

@@ -1,5 +1,5 @@
 <template>
-  <ui-group-option class="sign-guide">
+  <ui-group-option class="sign-guide" :class="{ search }">
     <div class="info">
       <div class="guide-cover">
         <img :src="getResource(getFileUrl(guide.cover))" />
@@ -7,7 +7,7 @@
           type="preview"
           class="icon"
           ctrl
-          @click="playSceneGuide(paths, undefined, true)"
+          @click="flyPlayGuide(guide)"
           v-if="paths.length"
         />
       </div>
@@ -44,10 +44,15 @@ import { playSceneGuide, isScenePlayIng, pauseSceneGuide } from "@/sdk";
 import { VideoRecorder } from "simaqcore";
 import useFocus from "bill/hook/useFocus";
 import { Message } from "bill/expose-common";
+import { flyPlayGuide } from "@/hook/use-fly";
 
-const props = withDefaults(defineProps<{ guide: Guide; edit?: boolean }>(), {
-  edit: true,
-});
+const props = withDefaults(
+  defineProps<{ guide: Guide; edit?: boolean; search?: boolean }>(),
+  {
+    edit: true,
+    search: false,
+  }
+);
 
 const inputRef = ref();
 const isEditTitle = useFocus(computed(() => inputRef.value?.vmRef.root));
@@ -114,7 +119,7 @@ const actions = {
 
     videoRecorder.off("*");
     videoRecorder.on("startRecord", () => {
-      playSceneGuide(paths.value, undefined, true);
+      flyPlayGuide(props.guide);
       stopWatch = watchEffect(() => {
         if (!isScenePlayIng.value) {
           videoRecorder.endRecord();
@@ -136,8 +141,11 @@ const paths = computed(() => getGuidePaths(props.guide));
   align-items: center;
   padding: 20px 0;
   border-bottom: 1px solid var(--colors-border-color);
-  &:first-child {
-    border-top: 1px solid var(--colors-border-color);
+
+  &.search {
+    padding: 0;
+    border: none;
+    margin-bottom: 5px;
   }
 
   .info {

+ 8 - 4
src/views/guide/index.vue

@@ -16,17 +16,17 @@
     <PathEdit v-if="current === 'path'" ref="quiskObj" />
   </RightFillPano>
   <Teleport to=".laser-layer">
-    <div class="quisks">
+    <div class="quisks" v-if="!isEdit && !currentIsFullView">
       <div class="quisk-item fun-ctrl" @click="quiskAdd('guide')">
-        <ui-icon type="close" />
+        <ui-icon type="a-guide_s" />
         <span>导览</span>
       </div>
       <div class="quisk-item fun-ctrl" @click="quiskAdd('animation')">
-        <ui-icon type="close" />
+        <ui-icon type="a-animation_s" />
         <span>动画</span>
       </div>
       <div class="quisk-item fun-ctrl" @click="quiskAdd('path')">
-        <ui-icon type="close" />
+        <ui-icon type="a-path_s" />
         <span>路线</span>
       </div>
     </div>
@@ -40,6 +40,7 @@ import PathEdit from "./path/edit.vue";
 import { nextTick, reactive, ref, watchEffect } from "vue";
 import { guides, isEdit, paths } from "@/store";
 import router from "@/router";
+import { currentIsFullView } from "@/utils/full";
 
 const current = ref("path");
 const tabs = reactive([
@@ -129,6 +130,9 @@ const quiskAdd = async (key: string) => {
       margin-top: 6px;
       font-size: 14px;
     }
+    .icon {
+      font-size: 22px;
+    }
   }
 }
 </style>

+ 52 - 12
src/views/guide/path/edit.vue

@@ -1,26 +1,36 @@
 <template>
-  <ui-group>
+  <ui-group borderBottom class="path-header">
     <template #header>
-      <ui-button @click="edit()">
+      <!-- <ui-button @click="edit()">
         <ui-icon type="add" />
         新增
-      </ui-button>
+      </ui-button> -->
+      <div class="path-header-content">
+        <ui-input type="checkbox" v-model="all" label="全选" />
+        <ui-icon type="add" ctrl @click="edit()" />
+      </div>
     </template>
   </ui-group>
   <ui-group class="path-group">
-    <PathSign
-      v-for="path in paths"
-      :key="path.id"
-      :path="path"
-      @edit="edit(path)"
-      @delete="deletePath(path)"
-    />
+    <div v-for="path in paths" :key="path.id" class="path-item">
+      <ui-input
+        type="checkbox"
+        :modelValue="selects.includes(path)"
+        @update:modelValue="(select: boolean) => updateSelect(path, select)"
+      />
+      <PathSign
+        :path="path"
+        @edit="edit(path)"
+        @delete="deletePath(path)"
+        class="path-content"
+      />
+    </div>
   </ui-group>
   <EditPath :data="currentPath" v-if="currentPath" @applyGlobal="applyGlobal" />
 </template>
 
 <script lang="ts" setup>
-import { computed, ref } from "vue";
+import { computed, ref, watchEffect } from "vue";
 import PathSign from "./sign.vue";
 import EditPath from "./edit-path.vue";
 import { getPathNode, pathsGroup } from "@/sdk/association/path";
@@ -32,6 +42,7 @@ import {
   autoSavePaths,
   createPath,
   enterOld,
+  selectPaths,
   save,
 } from "@/store";
 
@@ -40,7 +51,13 @@ import { Dialog } from "bill/expose-common";
 import { showPathsStack, showPathStack } from "@/env";
 import { asyncTimeout } from "@/utils";
 
-const currentPath = ref<Path | null>();
+const { all, selects, updateSelect } = selectPaths;
+
+
+const currentPath = ref<Path | null>(null);
+const emit = defineEmits<{(e: 'update:current', v: Path | null): void}>()
+watchEffect(() => emit('update:current', currentPath.value))
+
 const leaveEdit = () => {
   currentPath.value = null;
   pathsGroup.visibility(true);
@@ -103,5 +120,28 @@ defineExpose({
 
 .path-group {
   padding-bottom: 50px;
+  .path-item {
+    width: 100%;
+    display: flex;
+    align-items: center;
+    border-bottom: 1px solid var(--colors-border-color);
+  }
+
+  .path-content {
+    flex: 1;
+  }
+}
+
+.path-header-content {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+</style>
+
+<style>
+.path-header.ui-group .border-bottom,
+.path-header.ui-group {
+  margin-bottom: 0;
 }
 </style>

+ 30 - 1
src/views/guide/path/show.vue

@@ -1,8 +1,37 @@
 <template>
-  <PathSign v-for="path in paths" :key="path.id" :path="path" :edit="false" />
+  <ui-group title="路径列表" class="show-taggings">
+    <template #icon>
+      <ui-icon
+        ctrl
+        :type="custom.showPaths ? 'eye-s' : 'eye-n'"
+        @click="custom.showPaths = !custom.showPaths"
+      />
+    </template>
+    <PathSign
+      v-for="path in filterPath"
+      :key="path.id"
+      :path="path"
+      :edit="false"
+      class="show-path"
+    />
+  </ui-group>
 </template>
 
 <script setup lang="ts">
+import { custom } from "@/env";
 import PathSign from "./sign.vue";
 import { paths } from "@/store";
+import { computed } from "vue";
+
+const props = withDefaults(defineProps<{ keyword?: string }>(), { keyword: "" });
+
+const filterPath = computed(() =>
+  paths.value.filter((path) => path.name.includes(props.keyword))
+);
 </script>
+
+<style lang="scss" scoped>
+.show-path {
+  border-bottom: 1px solid var(--colors-border-color);
+}
+</style>

+ 28 - 12
src/views/guide/path/sign.vue

@@ -1,8 +1,10 @@
 <template>
   <!-- -->
   <ui-group-option
-    :class="`sign-guide ${hover || focus ? 'active' : ''} `"
-    @click.stop="clickHandler"
+    :class="`sign-guide ${!search && (hover || focus) ? 'active' : ''} ${
+      search ? 'search' : ''
+    }`"
+    @click="clickHandler"
     @mouseenter="enterHandler"
     @mouseleave="leaveHandler"
   >
@@ -16,7 +18,7 @@
           type="preview"
           class="icon"
           ctrl
-          @click.stop="playHandler()"
+          @click="playHandler()"
           v-if="path.points.length"
         />
       </div>
@@ -36,13 +38,18 @@
 
 <script setup lang="ts">
 import { Path } from "@/store";
-import { getPathNode, playScenePath } from "@/sdk/association/path";
+import { getPathNode } from "@/sdk/association/path";
 import { computed, ref, watch, watchEffect } from "vue";
 import { custom } from "@/env";
+import { flyPath, flyPlayPath } from "@/hook/use-fly";
 
-const props = withDefaults(defineProps<{ path: Path; edit?: boolean }>(), {
-  edit: true,
-});
+const props = withDefaults(
+  defineProps<{ path: Path; edit?: boolean; search?: boolean }>(),
+  {
+    edit: true,
+    search: false,
+  }
+);
 
 const emit = defineEmits<{
   (e: "delete"): void;
@@ -57,9 +64,11 @@ const actions = {
   edit: () => emit("edit"),
   delete: () => emit("delete"),
 };
+let isCoverClick = false;
 const playHandler = () => {
+  isCoverClick = true;
   node.value?.focus(true);
-  playScenePath(props.path, true);
+  flyPlayPath(props.path);
 };
 const focus = ref(false);
 const hover = ref(false);
@@ -96,8 +105,10 @@ const enterHandler = () => {
 };
 
 const clickHandler = () => {
-  node.value?.fly();
-  node.value?.focus(true);
+  if (!isCoverClick) {
+    flyPath(props.path);
+  }
+  isCoverClick = false;
 };
 </script>
 
@@ -108,11 +119,16 @@ const clickHandler = () => {
   align-items: center;
   padding: 20px 0;
   margin-bottom: 0;
-  border-bottom: 1px solid var(--colors-border-color);
+
+  &.search {
+    padding: 0;
+    border-bottom: none;
+    margin-bottom: 5px;
+  }
+
   position: relative;
   cursor: pointer;
   &:first-child {
-    border-top: 1px solid var(--colors-border-color);
   }
 
   &.active::after {

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

@@ -21,8 +21,8 @@
     <template #icon v-if="currentKey === 'path'">
       <ui-icon
         ctrl
-        :type="custom.showPaths ? 'eye-s' : 'eye-n'"
-        @click="custom.showPaths = !custom.showPaths"
+        :type="selectPaths.all.value ? 'eye-s' : 'eye-n'"
+        @click="selectPaths.all.value = !selectPaths.all.value"
       />
     </template>
     <div class="show-guides">
@@ -36,7 +36,7 @@
 import { computed, ref } from "vue";
 import Guide from "./guide/show.vue";
 import Path from "./path/show.vue";
-import { guides, paths } from "@/store";
+import { guides, paths, selectPaths } from "@/store";
 import { Menu, Dropdown } from "ant-design-vue";
 import { DownOutlined } from "@ant-design/icons-vue";
 import { custom } from "@/env";

+ 18 - 12
src/views/measure/show.vue

@@ -1,31 +1,37 @@
 <template>
   <ui-group title="测量列表" class="show-measures">
     <template #icon>
-      <ui-icon 
+      <ui-icon
         ctrl
-        :type="custom.showMeasures ? 'eye-s' : 'eye-n'" 
-        @click="custom.showMeasures = !custom.showMeasures" 
+        :type="custom.showMeasures ? 'eye-s' : 'eye-n'"
+        @click="custom.showMeasures = !custom.showMeasures"
       />
     </template>
-    <MeasureSign 
-      v-for="measure in measures" 
-      :key="measure.id" 
-      :measure="measure" 
+    <MeasureSign
+      v-for="measure in filterMeasures"
+      :key="measure.id"
+      :measure="measure"
       :edit="false"
     />
   </ui-group>
 </template>
 
 <script setup lang="ts">
-import MeasureSign from '@/views/measure/sign.vue'
-import { measures } from '@/store'
-import { custom } from '@/env'
+import MeasureSign from "@/views/measure/sign.vue";
+import { measures } from "@/store";
+import { custom } from "@/env";
+import { computed } from "vue";
 
-</script>
+const props = withDefaults(defineProps<{ keyword?: string }>(), { keyword: "" });
 
+const filterMeasures = computed(() => {
+  console.log(props.keyword);
+  return measures.value.filter((measure) => measure.title.includes(props.keyword));
+});
+</script>
 
 <style lang="scss">
 .show-measures.ui-group > h3.group-title {
   margin-bottom: 0;
 }
-</style>
+</style>

+ 20 - 11
src/views/measure/sign.vue

@@ -1,14 +1,14 @@
 <template>
   <ui-group-option
     class="sign-measure"
-    :class="{ active: measure.selected }"
+    :class="{ active: measure.selected, search }"
     @mouseenter="measure.selected = true"
     @mouseleave="measure.selected = false"
   >
     <div class="info">
       <ui-icon :type="MeasureTypeMeta[measure.type].icon" class="type" />
       <div v-show="!isEditTitle">
-        <p @click.stop="edit && (isEditTitle = true)">
+        <p @click="edit && (isEditTitle = true)">
           {{ measure.title || MeasureTypeMeta[measure.type].unitDesc }}
         </p>
         <span>{{ desc }} {{ MeasureTypeMeta[measure.type].unit }}</span>
@@ -24,12 +24,12 @@
         height="28px"
       />
     </div>
-    <div class="actions" @click.stop>
+    <div class="actions" v-if="!search">
       <!-- <ui-icon type="del" ctrl @click.stop="$emit('delete')" v-if="edit" /> -->
       <ui-icon
         type="pin"
         ctrl
-        @click.stop="fly"
+        @click="flyMeasure(measure)"
         :class="{ disabled: !getMeasureIsShow(measure) }"
       />
       <ui-more
@@ -51,10 +51,15 @@ import type { Measure } from "@/store";
 import { computed, ref, watch, watchEffect } from "vue";
 import { Message } from "bill/index";
 import { custom } from "@/env";
+import { flyMeasure } from "@/hook/use-fly";
 
-const props = withDefaults(defineProps<{ measure: Measure; edit?: boolean }>(), {
-  edit: true,
-});
+const props = withDefaults(
+  defineProps<{ measure: Measure; edit?: boolean; search?: boolean }>(),
+  {
+    edit: true,
+    search: false,
+  }
+);
 const emit = defineEmits<{
   (e: "delete"): void;
   (e: "updateTitle", title: string): void;
@@ -78,9 +83,6 @@ watchEffect(() => {
   }
 });
 
-const fly = () => {
-  getSceneMeasure(props.measure)?.fly();
-};
 const desc = ref("-");
 watch(
   () => [props.measure, custom.showMeasures, custom.showModelsMap],
@@ -102,7 +104,13 @@ watch(
   border-bottom: 1px solid var(--colors-border-color);
   position: relative;
 
-  &.active::after {
+  &.search {
+    padding: 0;
+    border-bottom: none;
+    margin-bottom: 5px;
+  }
+
+  &.active:not(.search)::after {
     content: "";
     position: absolute;
     pointer-events: none;
@@ -124,6 +132,7 @@ watch(
       overflow: hidden;
       display: flex;
       background: rgba(0, 0, 0, 0.5);
+      flex: 0 0 auto;
       font-size: 18px;
       align-items: center;
       justify-content: center;

+ 115 - 37
src/views/merge/index.vue

@@ -3,44 +3,38 @@
     v-if="custom.currentModel && active && custom.showMode === 'fuse'"
     class="merge-layout"
   >
+    <div class="actions-group">
+      <Actions :items="actionItems" v-model:current="currentItem" />
+      <Actions class="merge-action" :items="othActions" />
+    </div>
     <ui-group>
-      <template #header>
-        <Actions class="edit-header" :items="actionItems" v-model:current="currentItem" />
-      </template>
       <ui-group-option label="等比缩放">
         <template #icon>
-          <a
+          <ui-icon
             class="set-prop"
+            ctrl
             :class="{ disabled: isOld || currentItem }"
             @click="router.push({ 
               name: RoutesName.proportion, 
               params: { id: custom.currentModel!.id, save: '1' },
             })"
-            >设置比例</a
-          >
+            type="ratio"
+            tip="设置比例"
+          />
         </template>
         <ui-input
-          type="range"
+          type="number"
+          class="scale-input"
           v-model="custom.currentModel.scale"
           v-bind="modelRange.scaleRange"
           :ctrl="false"
           width="100%"
         >
+          <template #preIcon><ui-icon type="a-1b1" /></template>
           <template #icon>%</template>
         </ui-input>
       </ui-group-option>
-      <!-- <ui-group-option label="离地高度">
-        <ui-input 
-          type="range" 
-          v-model="custom.currentModel.bottom" 
-          v-bind="modelRange.bottomRange" 
-          :ctrl="false" 
-          width="100%"
-        >
-          <template #icon>m</template>
-        </ui-input>
-      </ui-group-option> -->
-      <ui-group-option label="模型不透明度">
+      <ui-group-option label="不透明度">
         <ui-input
           type="range"
           v-model="custom.currentModel.opacity"
@@ -51,20 +45,6 @@
           <template #icon>%</template>
         </ui-input>
       </ui-group-option>
-      <ui-group-option>
-        <!-- :disabled="currentItem"  -->
-        <ui-button
-          :class="{ disabled: isOld }"
-          @click="router.push({ 
-            name: RoutesName.registration, 
-            params: {id: custom.currentModel!.id, save: '1' } 
-          })"
-          >配准</ui-button
-        >
-      </ui-group-option>
-      <ui-group-option>
-        <ui-button @click="reset">恢复默认</ui-button>
-      </ui-group-option>
     </ui-group>
   </RightPano>
 </template>
@@ -82,12 +62,15 @@ import {
   modelsChangeStoreStack,
   showRightPanoStack,
 } from "@/env";
-import { ref, nextTick, watchEffect, computed, watch } from "vue";
+import { ref, nextTick, watchEffect, computed, watch, reactive } from "vue";
 import { Dialog } from "bill/expose-common";
 
-import Actions from "@/components/actions/index.vue";
+import Actions from "@/components/actions-merge/index.vue";
 
 import type { ActionsProps, ActionsItem } from "@/components/actions/index.vue";
+import { listener } from "@/components/drawing/hook";
+import { getOffset } from "@/utils/event";
+import { useRMenus } from "@/components/right-menu";
 
 const active = useActive();
 const actionItems: ActionsProps["items"] = [
@@ -100,7 +83,7 @@ const actionItems: ActionsProps["items"] = [
     },
   },
   {
-    icon: "flip",
+    icon: "a-rotate",
     text: "旋转",
     action: () => {
       getSceneModel(custom.currentModel)?.enterRotateMode();
@@ -109,8 +92,39 @@ const actionItems: ActionsProps["items"] = [
       };
     },
   },
+  {
+    icon: "a-zoom",
+    text: "缩放",
+    action: () => {
+      getSceneModel(custom.currentModel)?.enterScaleMode();
+      return () => {
+        getSceneModel(custom.currentModel)?.leaveTransform();
+      };
+    },
+  },
 ];
 
+const othActions = reactive([
+  {
+    icon: "rectification",
+    text: "配准",
+    disabled: isOld,
+    action: () => {
+      router.push({
+        name: RoutesName.registration,
+        params: { id: custom.currentModel!.id, save: "1" },
+      });
+    },
+  },
+  {
+    icon: "reset",
+    text: "恢复默认",
+    action: () => {
+      reset();
+    },
+  },
+]);
+
 const currentItem = ref<ActionsItem | null>(null);
 watchEffect(() => {
   if (!custom.currentModel) {
@@ -121,7 +135,6 @@ watchEffect(() => {
 watch(
   () => custom.currentModel,
   () => {
-    console.log("???");
     currentItem.value = null;
   }
 );
@@ -147,6 +160,25 @@ useViewStack(() =>
     showLeftPanoStack.push(ref(true)),
     showRightPanoStack.push(computed(() => !!custom.currentModel)),
     modelsChangeStoreStack.push(ref(true)),
+    listener(
+      document.querySelector("#layout-app") as HTMLElement,
+      "contextmenu",
+      (ev) => {
+        const pixel = getOffset(ev);
+        const pos = sdk.getPositionByScreen(pixel);
+        if (custom.currentModel && pos && custom.currentModel.id !== pos.modelId) {
+          useRMenus(pixel, [
+            {
+              label: "移动到这里",
+              icon: "close",
+              handler() {
+                custom.currentModel!.position = pos.worldPos;
+              },
+            },
+          ]);
+        }
+      }
+    ),
     () => (currentItem.value = null),
   ])
 );
@@ -167,8 +199,49 @@ useViewStack(() => {
 });
 </script>
 
+<style lang="scss" scoped>
+.actions-group {
+  position: absolute;
+  bottom: 100%;
+  left: 0;
+  width: 100%;
+  margin-bottom: 10px;
+  gap: 10px;
+  display: flex;
+}
+.model-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 10px;
+
+  p {
+    font-size: 14px;
+    color: #fff;
+  }
+}
+
+.model-desc {
+  color: rgba(255, 255, 255, 0.6);
+  line-height: 18px;
+  font-size: 12px;
+}
+
+.model-action {
+  display: flex;
+  align-items: center;
+  > * {
+    margin-left: 20px;
+  }
+}
+</style>
+
 <style lang="scss">
 .merge-layout {
+  position: relative;
+  overflow: initial !important;
+  top: calc(var(--editor-head-height) + var(--header-top) + 70px) !important;
+
   .ui-input .text.suffix input {
     padding-left: 5px;
     padding-right: 15px;
@@ -181,4 +254,9 @@ useViewStack(() => {
 .set-prop {
   cursor: pointer;
 }
+
+.scale-input input {
+  text-align: right;
+  padding-right: 20px !important;
+}
 </style>

+ 0 - 25
src/views/merge/style.scss

@@ -1,25 +0,0 @@
-.model-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 10px;
-  
-  p {
-    font-size: 14px;
-    color: #fff;
-  }
-}
-
-.model-desc {
-  color: rgba(255,255,255,0.6);
-  line-height: 18px;
-  font-size: 12px;
-}
-
-.model-action {
-  display: flex;
-  align-items: center;
-  > * {
-    margin-left: 20px;
-  }
-}

+ 2 - 8
src/views/security/store.ts

@@ -13,6 +13,7 @@ import {
 } from "@/store";
 import { nextTick, ref, toRaw } from "vue";
 import store from './data'
+import { flyTaggingPosition } from "@/hook/use-fly";
 
 export const data = store[params.caseId as unknown as "573"];
 
@@ -99,14 +100,7 @@ const flyTaggingPositions = (tagging: Tagging, callback?: () => void) => {
     }
 
     const pop = showTaggingPositionsStack.push(ref(new WeakSet([position])));
-    sdk.comeTo({
-      position: getTaggingPosNode(position)!.getImageCenter(),
-      modelId: position.modelId,
-      dur: 300,
-      // distance: 3,
-      maxDis:15,
-      isFlyToTag: true
-    });
+    flyTaggingPosition(position)
 
     setTimeout(() => {
       pop();

+ 53 - 0
src/views/setting/back-item.vue

@@ -0,0 +1,53 @@
+<template>
+  <div class="back-item" :class="{ [type]: true, active }">
+    <img :src="url" v-if="['img', 'map'].includes(type)" />
+    <i class="iconfont" :class="url" v-else-if="type === 'icon'" />
+    <span :style="{ background: url }" v-else></span>
+    <p class="back-item-desc">{{ label }}</p>
+  </div>
+</template>
+<script lang="ts" setup>
+defineProps<{ type: string; url: string; label: string; active: boolean }>();
+</script>
+
+<style lang="scss" scoped>
+.back-item {
+  > span,
+  .iconfont,
+  img {
+    display: block;
+    height: 88px;
+    cursor: pointer;
+    outline: 2px solid transparent;
+    transition: all 0.3s;
+    border-radius: 4px;
+  }
+
+  .iconfont {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #525252;
+    font-size: 32px;
+  }
+
+  img {
+    object-fit: cover;
+  }
+
+  &.active {
+    > span,
+    .iconfont,
+    img {
+      outline-color: #00c8af;
+    }
+  }
+}
+
+.back-item-desc {
+  font-size: 14px;
+  color: #fff;
+  margin-top: 10px;
+  text-align: center;
+}
+</style>

+ 5 - 115
src/views/setting/index.vue

@@ -11,20 +11,7 @@
 
     <ui-group title="设置天空">
       <ui-group-option>
-        <div class="back-layout">
-          <div
-            v-for="back in backs"
-            :key="back.value"
-            class="back-item"
-            :class="{ [back.type]: true, active: setting!.back === back.value }"
-            @click="setting!.back !== back.value && changeBack(back.value)"
-          >
-            <img :src="back.image" v-if="['img', 'map'].includes(back.type)" />
-            <i class="iconfont" :class="back.image" v-else-if="back.type === 'icon'" />
-            <span :style="{ background: back.image }" v-else></span>
-            <p class="back-item-desc">{{ back.label }}</p>
-          </div>
-        </div>
+        <selectBack :value="setting?.back" @update:value="changeBack" />
       </ui-group-option>
     </ui-group>
   </RightFillPano>
@@ -33,62 +20,11 @@
 <script lang="ts" setup>
 import { RightFillPano } from "@/layout";
 import { enterEdit, enterOld, setting, isEdit, updataSetting } from "@/store";
-import { ref, watchEffect } from "vue";
+import { ref } from "vue";
 import { togetherCallback, getFileUrl, loadPack } from "@/utils";
-import { showRightPanoStack, showRightCtrlPanoStack, custom } from "@/env";
-import { analysisPose, sdk, SettingResourceType } from "@/sdk";
-
-const backs = ref<{ label: string; type: string; image: string; value: string }[]>([]);
-watchEffect(async () => {
-  backs.value = [
-    { label: "无", type: "icon", image: "icon-without", value: "none" },
-    {
-      label: "地图",
-      type: "map",
-      image: "/oss/fusion/default/images/map.png",
-      value: "map",
-    },
-    {
-      label: "蓝天白云",
-      type: "img",
-      image: "/oss/fusion/default/images/pic_ltby@2x.png",
-      value: "/oss/fusion/default/images/蓝天白云.jpg",
-    },
-    {
-      label: "乌云密布",
-      type: "img",
-      image: "/oss/fusion/default/images/pic_wymb@2x.png",
-      value: "/oss/fusion/default/images/乌云密布.jpg",
-    },
-    {
-      label: "夜空",
-      type: "img",
-      image: "/oss/fusion/default/images/pic_yk@2x.png",
-      value: "/oss/fusion/default/images/夜空.jpg",
-    },
-    // {
-    //   label: "草地",
-    //   type: "img",
-    //   image: "/oss/fusion/default/images/pic_cd@2x.png",
-    //   value: "/oss/fusion/default/images/草地.jpg",
-    // },
-    // {
-    //   label: "道路",
-    //   type: "img",
-    //   image: "/oss/fusion/default/images/pic_dl@2x.png",
-    //   value: "/oss/fusion/default/images/道路.jpg",
-    // },
-    {
-      label: "傍晚",
-      type: "img",
-      image: "/oss/fusion/default/images/pic_bw@2x.png",
-      value: "/oss/fusion/default/images/傍晚.jpg",
-    },
-    // { label: "灰色", type: "color", image: "#333333", value: "#333" },
-    // { label: "黑色", type: "color", image: "#000000", value: "#000" },
-    // { label: "白色", type: "color", image: "#ffffff", value: "#fff" },
-  ];
-});
+import { showRightPanoStack, showRightCtrlPanoStack } from "@/env";
+import { analysisPose, sdk } from "@/sdk";
+import selectBack from "./select-back.vue";
 
 const enterSetPic = () => {
   enterEdit(
@@ -161,50 +97,4 @@ const changeBack = (back: string) => {
   text-align: center;
   cursor: pointer;
 }
-
-.back-layout {
-  display: grid;
-  grid-template-columns: repeat(3, 1fr);
-  gap: 20px;
-}
-
-.back-item {
-  > span,
-  .iconfont,
-  img {
-    display: block;
-    height: 88px;
-    cursor: pointer;
-    outline: 2px solid transparent;
-    transition: all 0.3s;
-    border-radius: 4px;
-  }
-
-  .iconfont {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    color: #525252;
-    font-size: 32px;
-  }
-
-  img {
-    object-fit: cover;
-  }
-
-  &.active {
-    > span,
-    .iconfont,
-    img {
-      outline-color: #00c8af;
-    }
-  }
-}
-
-.back-item-desc {
-  font-size: 14px;
-  color: #fff;
-  margin-top: 10px;
-  text-align: center;
-}
 </style>

+ 137 - 0
src/views/setting/select-back.vue

@@ -0,0 +1,137 @@
+<template>
+  <div class="back-layout">
+    <template v-for="back in backs" :key="back.value">
+      <div v-if="back.children" class="child-layout-parent">
+        <Dropdown placement="bottom">
+          <div class="child-layout" :class="{ active: activeParent === back.value }">
+            <ui-icon type="rectification" />
+            <BackItem
+              :type="back.type"
+              :label="back.label"
+              :url="back.image"
+              :active="activeParent === back.value"
+              @click="value !== back.value && $emit('update:value', back.value)"
+            />
+          </div>
+          <template #overlay>
+            <Menu :selectedKeys="[value]">
+              <MenuItem
+                v-for="item in back.children"
+                @click="value !== item.value && $emit('update:value', item.value)"
+                :key="item.value"
+              >
+                {{ item.label }}
+              </MenuItem>
+            </Menu>
+          </template>
+        </Dropdown>
+      </div>
+      <BackItem
+        v-else
+        :type="back.type"
+        :label="back.label"
+        :url="back.image"
+        :active="value === back.value"
+        @click="value !== back.value && $emit('update:value', back.value)"
+      />
+    </template>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { Dropdown, MenuItem, Menu } from "ant-design-vue";
+import { computed, ref } from "vue";
+import BackItem from "./back-item.vue";
+
+const props = defineProps<{ value: string | undefined }>();
+defineEmits<{ (e: "update:value", value: string): void }>();
+
+const backs = ref([
+  { label: "无", type: "icon", image: "icon-without", value: "none" },
+  {
+    label: "地图",
+    type: "map",
+    image: "/oss/fusion/default/images/map.png",
+    value: "dt",
+    children: [
+      { label: "天地图", value: "map" },
+      { label: "高德地图", value: "gdMap" },
+      { label: "谷歌地图", value: "gMap" },
+    ],
+  },
+  {
+    label: "蓝天白云",
+    type: "img",
+    image: "/oss/fusion/default/images/pic_ltby@2x.png",
+    value: "/oss/fusion/default/images/蓝天白云.jpg",
+  },
+  {
+    label: "乌云密布",
+    type: "img",
+    image: "/oss/fusion/default/images/pic_wymb@2x.png",
+    value: "/oss/fusion/default/images/乌云密布.jpg",
+  },
+  {
+    label: "夜空",
+    type: "img",
+    image: "/oss/fusion/default/images/pic_yk@2x.png",
+    value: "/oss/fusion/default/images/夜空.jpg",
+  },
+  {
+    label: "傍晚",
+    type: "img",
+    image: "/oss/fusion/default/images/pic_bw@2x.png",
+    value: "/oss/fusion/default/images/傍晚.jpg",
+  },
+]);
+
+const activeParent = computed(() => {
+  for (const back of backs.value) {
+    if (back.value === props.value) {
+      return back.value;
+    } else if (back.children) {
+      for (const c of back.children) {
+        if (c.value === props.value) {
+          return back.value;
+        }
+      }
+    }
+  }
+});
+</script>
+<style lang="scss" scoped>
+.back-layout {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 20px;
+}
+.child-layout-parent {
+  position: relative;
+}
+.child-layout {
+  position: absolute;
+  top: 0;
+  left: 0;
+  height: 88px;
+  > .icon {
+    position: absolute;
+    font-size: 22px;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    z-index: 2;
+  }
+  &::after {
+    content: "";
+    position: absolute;
+    z-index: 1;
+    background: rgba(0, 0, 0, 0.5);
+    inset: 0;
+    border-radius: 4px;
+    cursor: pointer;
+  }
+  &.active::after {
+    inset: -2px;
+  }
+}
+</style>

+ 7 - 0
src/views/setting/type.ts

@@ -0,0 +1,7 @@
+export type Back = {
+  label: string;
+  type: string;
+  image: string;
+  value: string;
+  children?: { label: string; value: string }[];
+}[]

+ 60 - 41
src/views/summary/index.vue

@@ -5,57 +5,66 @@
 
   <RightFillPano>
     <template #header>
-      <div class="tabs">
-        <span 
-          v-for="tab in tabs"
-          :key="tab.key"
-          :class="{ active: tab.key === current }"
-          @click="current = tab.key"
-        >
-          {{tab.text}}
-        </span>
-      </div>
+      <InputGroup compact style="margin-bottom: 20px" class="head-sam-group">
+        <Select v-model:value="current" style="width: 30%">
+          <SelectOption :value="tab.key" v-for="tab in tabs" :key="tab.key">{{
+            tab.text
+          }}</SelectOption>
+        </Select>
+        <Input v-model:value="keyword" style="width: 70%" />
+      </InputGroup>
     </template>
-    <Taggings v-if="current === TabKey.tagging" />
-    <Guides  v-if="current === TabKey.guide"/>
-    <Measures  v-if="current === TabKey.measure"/>
+    <component :is="comp" v-if="comp" :keyword="keyword" />
   </RightFillPano>
 </template>
 
 <script setup lang="ts">
-import { ref, watchEffect } from 'vue'
-import { useViewStack } from '@/hook'
-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 Taggings from '@/views/tagging/show.vue'
-import Measures from '@/views/measure/show.vue'
-import Guides from '@/views/guide/show.vue'
+import { computed, ref, watchEffect } from "vue";
+import { useViewStack } from "@/hook";
+import { showRightCtrlPanoStack, showRightPanoStack } from "@/env";
+import { currentModel, fuseModel, loadModel } from "@/model";
+import { LeftPano, RightFillPano } from "@/layout";
+import { InputGroup, Select, SelectOption, Input } from "ant-design-vue";
+import SceneList from "@/layout/scene-list/index.vue";
+import Taggings from "@/views/tagging/hot/show.vue";
+import Measures from "@/views/measure/show.vue";
+import Guides from "@/views/guide/guide/show.vue";
+import Paths from "@/views/guide/path/show.vue";
+import Monitor from "@/views/tagging/monitor/show.vue";
 
-enum TabKey { tagging, measure, guide }
+enum TabKey {
+  tagging,
+  monitor,
+  path,
+  measure,
+  guide,
+}
 const tabs = [
-  { key: TabKey.tagging, text: '标签' },
-  { key: TabKey.measure, text: '测量' },
-  { key: TabKey.guide, text: '路径' },
-]
-const current = ref(tabs[0].key)
-const showRightCtrl = ref(true)
+  { comp: Taggings, key: TabKey.tagging, text: "标签" },
+  { comp: Monitor, key: TabKey.monitor, text: "监控" },
+  { comp: Paths, key: TabKey.path, text: "路径" },
+  { comp: Measures, key: TabKey.measure, text: "测量" },
+  { comp: Guides, key: TabKey.guide, text: "导览" },
+];
+const current = ref(tabs[0].key);
+const comp = computed(() => tabs.find((item) => item.key === current.value)?.comp);
+const showRightCtrl = ref(true);
+const keyword = ref("");
 
 watchEffect((onclean) => {
-  const isFuse = currentModel.value === fuseModel
+  const isFuse = currentModel.value === fuseModel;
   if (!isFuse) {
-    onclean(showRightPanoStack.push(ref(false)))
+    onclean(showRightPanoStack.push(ref(false)));
   }
-  showRightCtrl.value = isFuse
-})
-useViewStack(() => showRightCtrlPanoStack.push(showRightCtrl))
+  showRightCtrl.value = isFuse;
+});
+useViewStack(() => showRightCtrlPanoStack.push(showRightCtrl));
 </script>
 
 <style lang="scss" scoped>
 .tabs {
   height: 60px;
-  border-bottom: 1px solid rgba(255,255,255,0.16);
+  border-bottom: 1px solid rgba(255, 255, 255, 0.16);
   display: flex;
   margin: -20px;
   margin-bottom: 20px;
@@ -66,15 +75,15 @@ useViewStack(() => showRightCtrlPanoStack.push(showRightCtrl))
     align-items: center;
     justify-content: center;
     position: relative;
-    transition: color .3s ease;
+    transition: color 0.3s ease;
     cursor: pointer;
     font-size: 16px;
 
     &::after {
-      content: '';
-      transition: height .3s ease;
+      content: "";
+      transition: height 0.3s ease;
       position: absolute;
-      background-color: #00C8AF;
+      background-color: #00c8af;
       left: 0;
       right: 0;
       bottom: 0;
@@ -83,7 +92,7 @@ useViewStack(() => showRightCtrlPanoStack.push(showRightCtrl))
 
     &:hover,
     &.active {
-      color: #00C8AF;
+      color: #00c8af;
     }
 
     &.active::after {
@@ -91,4 +100,14 @@ useViewStack(() => showRightCtrlPanoStack.push(showRightCtrl))
     }
   }
 }
-</style>
+</style>
+
+<style lang="scss">
+.head-sam-group {
+  .ant-select-selector,
+  .ant-input {
+    background-color: rgba(255, 255, 255, 0.1) !important;
+    border-color: rgba(255, 255, 255, 0.2) !important;
+  }
+}
+</style>

+ 17 - 10
src/views/tagging-position/index.vue

@@ -33,8 +33,10 @@ import PositionSign from "./sign.vue";
 import { router } from "@/router";
 import { Dialog, Message } from "bill/index";
 import { RightFillPano } from "@/layout";
-import { asyncTimeout } from "@/utils";
+import { asyncTimeout, debounce } from "@/utils";
 import { useViewStack } from "@/hook";
+import { flyTaggingPosition as flyTaggingPositionRaw } from "@/hook/use-fly";
+
 import {
   computed,
   nextTick,
@@ -60,6 +62,7 @@ import { Collapse } from "ant-design-vue";
 
 import type { TaggingPosition } from "@/store";
 import { clickListener } from "@/utils/event";
+import { useCameraChange } from "@/hook/use-pixel";
 
 const showId = ref<TaggingPosition["id"]>();
 const tagging = computed(() => getTagging(router.currentRoute.value.params.id as string));
@@ -80,11 +83,22 @@ useViewStack(() => {
 
 watch(showId, (id) => {
   const position = positions.value?.find((item) => item.id === id);
-  console.log(custom.showMode);
   if (custom.showMode === "fuse") {
     position && flyTaggingPosition(position);
   }
 });
+const [pose] = useCameraChange(() => sdk.getPose());
+
+watch(
+  [showId, pose],
+  debounce(() => {
+    const position = positions.value?.find((item) => item.id === showId.value);
+    if (position) {
+      position.pose = pose.value;
+      console.log('set Pose')
+    }
+  }, 300)
+);
 
 let pop: () => void;
 const flyTaggingPosition = (position: TaggingPosition) => {
@@ -95,14 +109,7 @@ const flyTaggingPosition = (position: TaggingPosition) => {
   }
 
   pop = showTaggingPositionsStack.push(ref(new WeakSet([position])));
-  sdk.comeTo({
-    position: getTaggingPosNode(position)!.getImageCenter(),
-    modelId: position.modelId,
-    dur: 300,
-    // distance: 3,
-    maxDis: 15,
-    isFlyToTag: true,
-  });
+  flyTaggingPositionRaw(position);
 };
 onUnmounted(() => pop && pop());
 

src/views/tagging/edit.vue → src/views/tagging/hot/edit.vue


src/views/tagging/images.vue → src/views/tagging/hot/images.vue


+ 152 - 0
src/views/tagging/hot/index.vue

@@ -0,0 +1,152 @@
+<template>
+  <ui-group title="标签列表" class="tagging-list">
+    <template #header>
+      <StyleTypeSelect v-model:value="type" all count />
+    </template>
+    <template #icon>
+      <ui-icon
+        ctrl
+        :class="{ active: showSearch }"
+        type="search"
+        @click="showSearch = !showSearch"
+        style="margin-right: 20px"
+      />
+      <ui-icon
+        ctrl
+        :type="custom.showTaggings ? 'eye-s' : 'eye-n'"
+        @click="custom.showTaggings = !custom.showTaggings"
+      />
+    </template>
+    <ui-group-option v-if="showSearch">
+      <ui-input type="text" width="100%" placeholder="搜索" v-model="keyword">
+        <template #preIcon>
+          <ui-icon type="search" />
+        </template>
+      </ui-input>
+    </ui-group-option>
+    <TagingSign
+      v-for="tagging in filterTaggings"
+      :key="tagging.id"
+      :tagging="tagging"
+      :selected="selectTagging === tagging"
+      @edit="editTagging = tagging"
+      @delete="deleteTagging(tagging)"
+      @select="(selected) => (selectTagging = selected ? tagging : null)"
+      @fixed="fixedTagging(tagging)"
+    />
+  </ui-group>
+
+  <Teleport to="#layout-app">
+    <Edit
+      class="edit-layout"
+      v-if="editTagging"
+      :data="editTagging"
+      @quit="editTagging = null"
+      @save="saveHandler"
+    />
+  </Teleport>
+</template>
+
+<script lang="ts" setup>
+import Edit from "./edit.vue";
+import TagingSign from "./sign.vue";
+import StyleTypeSelect from "./style-type-select.vue";
+import { useViewStack } from "@/hook";
+import { computed, ref, watchEffect } from "vue";
+import { router, RoutesName } from "@/router";
+import { custom } from "@/env";
+import {
+  taggings,
+  getTaggingStyle,
+  Tagging,
+  autoSaveTaggings,
+  createTagging,
+  getTaggingPositions,
+  taggingPositions,
+  isOld,
+  save,
+  getTagging,
+  TaggingStyle,
+} from "@/store";
+import { taggingsGroup } from "@/sdk";
+
+const showSearch = ref(false);
+const type = ref<TaggingStyle["typeId"]>(-1);
+const keyword = ref("");
+const filterTaggings = computed(() =>
+  taggings.value.filter((tagging) => {
+    if (!tagging.title.includes(keyword.value)) return false;
+    if (type.value === -1) return true;
+    const style = getTaggingStyle(tagging.styleId);
+    return style?.typeId === type.value;
+  })
+);
+
+const editTagging = ref<Tagging | null>(null);
+const saveHandler = (tagging: Tagging) => {
+  if (!editTagging.value) return;
+  if (!getTagging(editTagging.value.id)) {
+    taggings.value.push(tagging);
+    const style = getTaggingStyle(tagging.styleId);
+    if (style) {
+      style.lastUse = 1;
+    }
+  } else {
+    Object.assign(editTagging.value, tagging);
+  }
+
+  editTagging.value = null;
+};
+
+const deleteTagging = (tagging: Tagging) => {
+  const index = taggings.value.indexOf(tagging);
+  const positions = getTaggingPositions(tagging);
+  taggingPositions.value = taggingPositions.value.filter(
+    (position) => !positions.includes(position)
+  );
+  taggings.value.splice(index, 1);
+};
+
+const fixedTagging = async (tagging: Tagging) => {
+  if (isOld.value) {
+    await save();
+  }
+  router.push({ name: RoutesName.taggingPosition, params: { id: tagging.id } });
+};
+
+const selectTagging = ref<Tagging | null>(null);
+useViewStack(() => {
+  const stopAuth = autoSaveTaggings();
+  const stop = watchEffect((onCleanup) => {
+    taggingsGroup.changeCanMove(true);
+    taggingsGroup.showDelete(true);
+    onCleanup(() => {
+      taggingsGroup.changeCanMove(false);
+      taggingsGroup.showDelete(false);
+    });
+  });
+  return () => {
+    stop();
+    stopAuth();
+  };
+});
+defineExpose({
+  add() {
+    editTagging.value = createTagging();
+  },
+});
+</script>
+
+<style scoped>
+.active {
+  color: var(--color-main-normal) !important;
+}
+
+.tagging-list {
+  padding-bottom: 30px;
+}
+
+.edit-layout {
+  z-index: 999999;
+}
+</style>

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

@@ -31,10 +31,13 @@ import StyleTypeSelect from "./style-type-select.vue";
 
 import type { Tagging, TaggingStyle } from "@/store";
 
+const props = withDefaults(defineProps<{ keyword?: string }>(), { keyword: "" });
+
 const selectTagging = ref<Tagging | null>(null);
 const type = ref<TaggingStyle["typeId"]>(-1);
 const filterTaggings = computed(() =>
   taggings.value.filter((tagging) => {
+    if (!tagging.title.includes(props.keyword)) return false;
     if (type.value === -1) return true;
     const style = getTaggingStyle(tagging.styleId);
     return style?.typeId === type.value;

+ 12 - 40
src/views/tagging/sign.vue

@@ -1,8 +1,8 @@
 <template>
   <ui-group-option
     class="sign-tagging"
-    :class="{ active: selected, edit }"
-    @click="edit && getTaggingIsShow(tagging) && emit('select', true)"
+    :class="{ active: selected, edit, search }"
+    @click="(search || edit) && getTaggingIsShow(tagging) && emit('select', true)"
   >
     <div class="info">
       <img :src="getResource(getFileUrl(findImage))" v-if="findImage" />
@@ -11,7 +11,7 @@
         <span>放置:{{ positions.length }}</span>
       </div>
     </div>
-    <div class="actions" @click.stop>
+    <div class="actions" @click.stop v-if="!search">
       <ui-icon
         v-if="!edit"
         type="pin"
@@ -45,10 +45,16 @@ import {
 } from "@/store";
 
 import type { Tagging } from "@/store";
+import { flyTagging, flyTaggingPosition } from "@/hook/use-fly";
 
 const props = withDefaults(
-  defineProps<{ tagging: Tagging; selected?: boolean; edit?: boolean }>(),
-  { edit: true }
+  defineProps<{
+    tagging: Tagging;
+    selected?: boolean;
+    edit?: boolean;
+    search?: boolean;
+  }>(),
+  { edit: true, search: false }
 );
 const style = computed(() => getTaggingStyle(props.tagging.styleId));
 const positions = computed(() => getTaggingPositions(props.tagging));
@@ -80,44 +86,10 @@ 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: getTaggingPosNode(position)!.getImageCenter(),
-      modelId: position.modelId,
-      dur: 300,
-      // distance: 3,
-      maxDis: 15,
-      isFlyToTag: true
-    });
-
-    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 stop = flyTagging(props.tagging, success);
     const keyupHandler = (ev: KeyboardEvent) => ev.code === "Escape" && success();
 
     document.documentElement.addEventListener("keyup", keyupHandler, false);

src/views/tagging/style-type-select.vue → src/views/tagging/hot/style-type-select.vue


+ 6 - 0
src/views/tagging/style.scss

@@ -7,6 +7,12 @@
   border-bottom: 1px solid var(--colors-border-color);
   position: relative;
 
+  &.search {
+    padding: 0;
+    border-bottom: none;
+    margin-bottom: 5px;
+  }
+
   &.edit{
     cursor: pointer;
 

src/views/tagging/styles.vue → src/views/tagging/hot/styles.vue


+ 110 - 132
src/views/tagging/index.vue

@@ -1,153 +1,131 @@
 <template>
   <RightFillPano>
     <template #header>
-      <ui-group borderBottom>
-        <template #header>
-          <ui-button @click="editTagging = createTagging()">
-            <ui-icon type="add" />
-            新增
-          </ui-button>
-        </template>
-      </ui-group>
+      <div class="tabs" :class="{ disabled: isEdit }">
+        <span
+          v-for="tab in tabs"
+          :key="tab.key"
+          :class="{ active: tab.key === current }"
+          @click="current = tab.key"
+        >
+          {{ tab.text }}
+        </span>
+      </div>
     </template>
-    <ui-group title="标签列表" class="tagging-list">
-      <template #header>
-        <StyleTypeSelect v-model:value="type" all count />
-      </template>
-      <template #icon>
-        <ui-icon
-          ctrl
-          :class="{ active: showSearch }"
-          type="search"
-          @click="showSearch = !showSearch"
-          style="margin-right: 20px"
-        />
-        <ui-icon
-          ctrl
-          :type="custom.showTaggings ? 'eye-s' : 'eye-n'"
-          @click="custom.showTaggings = !custom.showTaggings"
-        />
-      </template>
-      <ui-group-option v-if="showSearch">
-        <ui-input type="text" width="100%" placeholder="搜索" v-model="keyword">
-          <template #preIcon>
-            <ui-icon type="search" />
-          </template>
-        </ui-input>
-      </ui-group-option>
-      <TagingSign
-        v-for="tagging in filterTaggings"
-        :key="tagging.id"
-        :tagging="tagging"
-        :selected="selectTagging === tagging"
-        @edit="editTagging = tagging"
-        @delete="deleteTagging(tagging)"
-        @select="(selected) => (selectTagging = selected ? tagging : null)"
-        @fixed="fixedTagging(tagging)"
-      />
-    </ui-group>
+
+    <Hot ref="quiskObj" v-if="current === 'tagging'" />
+    <Monitor ref="quiskObj" v-if="current === 'monitor'" />
   </RightFillPano>
 
-  <Edit
-    v-if="editTagging"
-    :data="editTagging"
-    @quit="editTagging = null"
-    @save="saveHandler"
-  />
+  <Teleport to=".laser-layer">
+    <div class="quisks" v-if="!isEdit && !currentIsFullView">
+      <div class="quisk-item fun-ctrl" @click="quiskAdd('tagging')">
+        <ui-icon type="a-guide_s" />
+        <span>标签</span>
+      </div>
+      <!-- <div class="quisk-item fun-ctrl" @click="quiskAdd('monitor')">
+        <ui-icon type="a-animation_s" />
+        <span>监控</span>
+      </div> -->
+    </div>
+  </Teleport>
 </template>
 
 <script lang="ts" setup>
-import Edit from "./edit.vue";
-import TagingSign from "./sign.vue";
-import StyleTypeSelect from "./style-type-select.vue";
+import { isEdit, monitors, taggings } from "@/store";
+import Hot from "./hot/index.vue";
+import Monitor from "./monitor/index.vue";
 import { RightFillPano } from "@/layout";
-import { useViewStack } from "@/hook";
-import { computed, ref, watchEffect } from "vue";
-import { router, RoutesName } from "@/router";
-import { custom } from "@/env";
-import {
-  taggings,
-  getTaggingStyle,
-  Tagging,
-  autoSaveTaggings,
-  createTagging,
-  getTaggingPositions,
-  taggingPositions,
-  isOld,
-  save,
-  getTagging,
-  TaggingStyle,
-} from "@/store";
-import { taggingsGroup } from "@/sdk";
+import { nextTick, reactive, ref, watchEffect } from "vue";
+import { currentIsFullView } from "@/utils/full";
 
-const showSearch = ref(false);
-const type = ref<TaggingStyle["typeId"]>(-1);
-const keyword = ref("");
-const filterTaggings = computed(() =>
-  taggings.value.filter((tagging) => {
-    if (!tagging.title.includes(keyword.value)) return false;
-    if (type.value === -1) return true;
-    const style = getTaggingStyle(tagging.styleId);
-    return style?.typeId === type.value;
-  })
-);
+const current = ref("tagging");
+const tabs = reactive([
+  { key: "tagging", text: "标签()" },
+  { key: "monitor", text: "监控()" },
+]);
+watchEffect(() => {
+  tabs[0].text = `标签(${taggings.value.length})`;
+  tabs[1].text = `监控(${monitors.value.length})`;
+});
+const quiskObj = ref<any>();
+const quiskAdd = async (key: string) => {
+  current.value = key;
+  await nextTick();
+  quiskObj.value.add();
+};
+</script>
 
-const editTagging = ref<Tagging | null>(null);
-const saveHandler = (tagging: Tagging) => {
-  if (!editTagging.value) return;
-  if (!getTagging(editTagging.value.id)) {
-    taggings.value.push(tagging);
-    const style = getTaggingStyle(tagging.styleId);
-    if (style) {
-      style.lastUse = 1;
-    }
-  } else {
-    Object.assign(editTagging.value, tagging);
-  }
+<style lang="scss" scoped>
+.tabs {
+  height: 60px;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.16);
+  display: flex;
+  margin: -20px;
+  margin-bottom: 20px;
 
-  editTagging.value = null;
-};
+  > span {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+    transition: color 0.3s ease;
+    cursor: pointer;
+    font-size: 16px;
 
-const deleteTagging = (tagging: Tagging) => {
-  const index = taggings.value.indexOf(tagging);
-  const positions = getTaggingPositions(tagging);
-  taggingPositions.value = taggingPositions.value.filter(
-    (position) => !positions.includes(position)
-  );
-  taggings.value.splice(index, 1);
-};
+    &::after {
+      content: "";
+      transition: height 0.3s ease;
+      position: absolute;
+      background-color: #00c8af;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      height: 0;
+    }
+
+    &:hover,
+    &.active {
+      color: #00c8af;
+    }
 
-const fixedTagging = async (tagging: Tagging) => {
-  if (isOld.value) {
-    await save();
+    &.active::after {
+      height: 3px;
+    }
   }
-  router.push({ name: RoutesName.taggingPosition, params: { id: tagging.id } });
-};
+}
 
-const selectTagging = ref<Tagging | null>(null);
-useViewStack(() => {
-  const stopAuth = autoSaveTaggings();
-  const stop = watchEffect((onCleanup) => {
-    taggingsGroup.changeCanMove(true);
-    taggingsGroup.showDelete(true);
-    onCleanup(() => {
-      taggingsGroup.changeCanMove(false);
-      taggingsGroup.showDelete(false);
-    });
-  });
-  return () => {
-    stop();
-    stopAuth();
-  };
-});
-</script>
+.quisks {
+  position: absolute;
+  bottom: 20px;
+  left: 50%;
+  transform: translateX(-50%);
+  display: flex;
+  align-items: center;
 
-<style scoped>
-.active {
-  color: var(--color-main-normal) !important;
-}
+  .quisk-item {
+    width: 80px;
+    height: 80px;
+    border-radius: 10px;
+    background: rgba(27, 27, 28, 0.8);
+    color: #ffffff;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
 
-.tagging-list {
-  padding-bottom: 30px;
+    &:not(:last-child) {
+      margin-right: 20px;
+    }
+
+    span {
+      margin-top: 6px;
+      font-size: 14px;
+    }
+    .icon {
+      font-size: 22px;
+    }
+  }
 }
 </style>

+ 62 - 0
src/views/tagging/monitor/index.vue

@@ -0,0 +1,62 @@
+<template>
+  <ui-group title="监控列表" class="tagging-list">
+    <template #icon>
+      <ui-icon
+        ctrl
+        :class="{ active: showSearch }"
+        type="search"
+        @click="showSearch = !showSearch"
+        style="margin-right: 20px"
+      />
+      <ui-icon
+        ctrl
+        :type="custom.showMonitors ? 'eye-s' : 'eye-n'"
+        @click="custom.showMonitors = !custom.showMonitors"
+      />
+    </template>
+    <ui-group-option v-if="showSearch">
+      <ui-input type="text" width="100%" placeholder="搜索" v-model="keyword">
+        <template #preIcon>
+          <ui-icon type="search" />
+        </template>
+      </ui-input>
+    </ui-group-option>
+    <MonitorSign
+      v-for="monitor in filterMonitors"
+      :key="monitor.id"
+      :monitor="monitor"
+      @updateTitle="(title) => (monitor.title = title)"
+      :selected="selectMonitor === monitor"
+      @delete="deleteMonitor(monitor)"
+      @select="(selected) => (selectMonitor = selected ? monitor : null)"
+    />
+  </ui-group>
+</template>
+<script setup lang="ts">
+import { custom } from "@/env";
+import { autoSaveMonitor, Monitor, monitors } from "@/store/monitor";
+import { computed, ref } from "vue";
+import MonitorSign from "./sign.vue";
+import { useViewStack } from "@/hook";
+
+const showSearch = ref(false);
+const keyword = ref("");
+const selectMonitor = ref<Monitor | null>(null);
+
+const filterMonitors = computed(() =>
+  monitors.value.filter((monitor) => monitor.title.includes(keyword.value))
+);
+
+const deleteMonitor = (monitor: Monitor) => {
+  const index = monitors.value.indexOf(monitor);
+  monitors.value.splice(index, 1);
+};
+
+useViewStack(autoSaveMonitor);
+</script>
+
+<style scoped>
+.active {
+  color: var(--color-main-normal) !important;
+}
+</style>

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

@@ -0,0 +1,45 @@
+<template>
+  <ui-group title="监控列表" class="show-taggings">
+    <template #icon>
+      <ui-icon
+        ctrl
+        :type="custom.showMonitors ? 'eye-s' : 'eye-n'"
+        @click="custom.showMonitors = !custom.showMonitors"
+      />
+    </template>
+    <MonitorSign
+      v-for="monitor in filterMonitors"
+      :key="monitor.id"
+      :monitor="monitor"
+      :selected="selectMonitor === monitor"
+      :edit="false"
+      @select="(selected) => (selectMonitor = selected ? monitor : null)"
+      class="show-tagging"
+    />
+  </ui-group>
+</template>
+
+<script setup lang="ts">
+import { computed, ref } from "vue";
+import { custom } from "@/env";
+import MonitorSign from "./sign.vue";
+import { monitors } from "@/store";
+
+import type { Monitor } from "@/store";
+
+const props = withDefaults(defineProps<{ keyword?: string }>(), { keyword: "" });
+
+const selectMonitor = ref<Monitor | null>(null);
+const filterMonitors = computed(() =>
+  monitors.value.filter((monitor) => monitor.title.includes(props.keyword))
+);
+</script>
+
+<style lang="scss">
+.show-taggings.ui-group > h3.group-title {
+  margin-bottom: 0;
+}
+.show-tagging.sign-tagging.active::after {
+  display: none;
+}
+</style>

+ 126 - 0
src/views/tagging/monitor/sign.vue

@@ -0,0 +1,126 @@
+<template>
+  <ui-group-option
+    class="sign-tagging"
+    :class="{ active: selected, edit, search }"
+    @click="emit('select', true)"
+  >
+    <div class="info">
+      <p v-show="!isEditTitle">{{ monitor.title }}</p>
+      <ui-input
+        class="view-title-input"
+        type="text"
+        :maxlength="15"
+        :modelValue="monitor.title"
+        @update:modelValue="(title: string) => $emit('updateTitle', title.trim())"
+        v-show="isEditTitle"
+        ref="inputRef"
+        height="28px"
+      />
+    </div>
+    <div class="actions" @click.stop v-if="edit">
+      <ui-more
+        :options="menus"
+        style="margin-left: 20px"
+        @click="(action: keyof typeof actions) => actions[action]()"
+      />
+    </div>
+  </ui-group-option>
+</template>
+
+<script setup lang="ts">
+import type { Monitor } from "@/store";
+import useFocus from "bill/hook/useFocus";
+import { computed, ref } from "vue";
+
+withDefaults(
+  defineProps<{
+    monitor: Monitor;
+    selected?: boolean;
+    edit?: boolean;
+    search?: boolean;
+  }>(),
+  {
+    edit: true,
+  }
+);
+
+const emit = defineEmits<{
+  (e: "delete"): void;
+  (e: "updateTitle", title: string): void;
+  (e: "select", selected: boolean): void;
+}>();
+
+const menus = [
+  { label: "编辑", value: "edit" },
+  { label: "删除", value: "delete" },
+];
+const actions = {
+  edit: () => (isEditTitle.value = true),
+  delete: () => emit("delete"),
+};
+
+const inputRef = ref();
+const isEditTitle = useFocus(computed(() => inputRef.value?.vmRef.root));
+</script>
+
+<style lang="scss" scoped>
+.sign-tagging {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20px 0;
+  margin: 0;
+  border-bottom: 1px solid var(--colors-border-color);
+  position: relative;
+  &.search {
+    padding: 0;
+    border: none !important;
+    margin-bottom: 5px;
+  }
+
+  &.edit {
+    cursor: pointer;
+
+    &.active::after {
+      content: "";
+      position: absolute;
+      pointer-events: none;
+      inset: 0 -20px;
+      background-color: rgba(0, 200, 175, 0.16);
+    }
+  }
+
+  .info {
+    flex: 1;
+
+    display: flex;
+    align-items: center;
+
+    img {
+      width: 48px;
+      height: 48px;
+      object-fit: cover;
+      border-radius: 4px;
+      overflow: hidden;
+      display: block;
+    }
+
+    div {
+      margin-left: 10px;
+
+      p {
+        color: #fff;
+        font-size: 14px;
+      }
+      span {
+        color: rgba(255, 255, 255, 0.6);
+        font-size: 12px;
+      }
+    }
+  }
+
+  .actions {
+    flex: none;
+  }
+}
+</style>

+ 51 - 56
src/views/view/sign.vue

@@ -1,93 +1,88 @@
 <template>
-  <ui-group-option class="sign" :class="{active}">
+  <ui-group-option class="sign" :class="{ active, search }">
     <div class="content">
-      <span class="cover" @click="fly">
-        <img :src="getResource(getFileUrl(view.cover))" alt="">
+      <span class="cover" @click="flyView(view)">
+        <img :src="getResource(getFileUrl(view.cover))" alt="" />
       </span>
-      <ui-input 
+      <ui-input
         class="view-title-input"
-        type="text" 
-        :modelValue="view.title" 
+        type="text"
+        :modelValue="view.title"
         :maxlength="15"
         @update:modelValue="(title: string) => $emit('updateTitle', title.trim())"
-        v-show="isEditTitle" 
-        ref="inputRef" 
-        height="28px" 
+        v-show="isEditTitle"
+        ref="inputRef"
+        height="28px"
       />
-      <div class="title" v-show="!isEditTitle" @click="fly">
+      <div class="title" v-show="!isEditTitle" @click="flyView(view)">
         <p>{{ view.title }}</p>
-        <span>  {{ getModelDesc(modelType as ModelType) }}</span>
+        <span> {{ getModelDesc(modelType as ModelType) }}</span>
       </div>
     </div>
     <div class="action" v-if="edit">
       <ui-icon type="order" ctrl />
-      <ui-more 
-        :options="menus" 
-        style="margin-left: 20px" 
-        @click="(action: keyof typeof actions) => actions[action]()" 
+      <ui-more
+        :options="menus"
+        style="margin-left: 20px"
+        @click="(action: keyof typeof actions) => actions[action]()"
       />
     </div>
   </ui-group-option>
 </template>
 
 <script lang="ts" setup>
-import { ref, computed, watchEffect } from 'vue'
-import { useFocus } from 'bill/hook/useFocus'
-import { custom, getResource } from '@/env'
-import { deepIsRevise, getFileUrl } from '@/utils'
-import { loadModel, getModelDesc, ModelType, currentModel } from '@/model'
-import { viewToModelType } from '@/store'
+import { ref, computed, watchEffect } from "vue";
+import { useFocus } from "bill/hook/useFocus";
+import { custom, getResource } from "@/env";
+import { deepIsRevise, getFileUrl } from "@/utils";
+import { loadModel, getModelDesc, ModelType, currentModel } from "@/model";
+import { viewToModelType } from "@/store";
 
-import type { View } from '@/store'
-import { Message } from 'bill/expose-common'
+import type { View } from "@/store";
+import { Message } from "bill/expose-common";
+import { flyView } from "@/hook/use-fly";
 
 const props = withDefaults(
-  defineProps<{ view: View, edit?: boolean }>(),
+  defineProps<{ view: View; edit?: boolean; search?: boolean }>(),
   { edit: true }
-)
+);
 const emit = defineEmits<{
-    (e: 'updateCover', cover: string): void,
-    (e: 'updateTitle', title: string): void,
-    (e: 'delete'): void,
-}>()
+  (e: "updateCover", cover: string): void;
+  (e: "updateTitle", title: string): void;
+  (e: "delete"): void;
+}>();
 
 const menus = [
-  { label: '重命名', value: 'rename' },
-  { label: '删除', value: 'delete' },
-]
+  { label: "重命名", value: "rename" },
+  { label: "删除", value: "delete" },
+];
 
-const inputRef = ref()
-const isEditTitle = useFocus(computed(() => inputRef.value?.vmRef.root))
+const inputRef = ref();
+const isEditTitle = useFocus(computed(() => inputRef.value?.vmRef.root));
 
 watchEffect(() => {
   if (!isEditTitle.value && !props.view.title.length) {
-    isEditTitle.value = true
-    Message.warning('视图名称不可为空')
+    isEditTitle.value = true;
+    Message.warning("视图名称不可为空");
   }
-})
+});
 
 const actions = {
-  delete: () => emit('delete'),
-  rename: () => isEditTitle.value = true
-}
-const modelType = viewToModelType(props.view)
-const fly = async () => {
-  const sdk = await loadModel(modelType)
-  custom.currentView = props.view
-  sdk.setView(props.view.flyData)
-}
+  delete: () => emit("delete"),
+  rename: () => (isEditTitle.value = true),
+};
+const modelType = viewToModelType(props.view);
 const active = computed(() => {
-  return custom.currentView === props.view && !deepIsRevise(currentModel.value, modelType)
-})
-
+  return (
+    custom.currentView === props.view && !deepIsRevise(currentModel.value, modelType)
+  );
+});
 </script>
 
-
-<style lang="scss" src="./style.scss" scoped>
-</style>
+<style lang="scss" src="./style.scss" scoped></style>
 
 <style>
-  .view-title-input.ui-input .text.suffix input {
-    padding-right: 50px;
-  }
-</style>
+.view-title-input.ui-input .text.suffix input {
+  padding-right: 50px;
+}
+</style>

+ 6 - 1
src/views/view/style.scss

@@ -37,11 +37,16 @@
   margin-bottom: 0 !important;
   position: relative;
 
+  &.search {
+    padding: 0;
+    border: none !important;
+    margin-bottom: 5px;
+  }
   &:last-child {
     border-bottom: 1px solid rgba(255,255,255,0.1600);
   }
 
-  &.active::after {
+  &.active:not(.search)::after {
     content: '';
     position: absolute;
     pointer-events: none;

+ 9 - 0
对接文档.txt

@@ -157,3 +157,12 @@ export type AnimationModelPath3D = {
   // 修改路径续时间 单位为秒
   changeDuration: (n: number) => void
 };
+
+
+// -------配准模块-------
+模型对象多一个enterScaleMode  进入缩放状态
+去除右键点击会选中模型操作
+
+
+sdk增多一个方法
+sdk.comeToByLatLng   飞到指定经纬度