瀏覽代碼

feat: 制作1.2新功能

bill 5 月之前
父節點
當前提交
a147272554
共有 40 個文件被更改,包括 1241 次插入411 次删除
  1. 7 0
      src/api/constant.ts
  2. 3 3
      src/api/guide.ts
  3. 2 1
      src/api/index.ts
  4. 65 0
      src/api/monitor.ts
  5. 15 1
      src/api/tagging-position.ts
  6. 102 0
      src/components/actions-merge/index.vue
  7. 28 0
      src/components/right-menu/index.ts
  8. 40 0
      src/components/right-menu/index.vue
  9. 2 2
      src/components/tagging/sign-new.vue
  10. 54 0
      src/components/view-setting/index.vue
  11. 2 0
      src/env/index.ts
  12. 17 0
      src/hook/use-fly.ts
  13. 26 13
      src/hook/use-pixel.ts
  14. 4 0
      src/layout/edit/fuse-edit.vue
  15. 2 0
      src/layout/show/index.vue
  16. 1 1
      src/router/constant.ts
  17. 2 1
      src/sdk/sdk.ts
  18. 2 1
      src/store/index.ts
  19. 68 0
      src/store/monitor.ts
  20. 21 54
      src/views/guide/guide/edit-paths.vue
  21. 114 36
      src/views/merge/index.vue
  22. 0 25
      src/views/merge/style.scss
  23. 2 8
      src/views/security/store.ts
  24. 53 0
      src/views/setting/back-item.vue
  25. 5 115
      src/views/setting/index.vue
  26. 137 0
      src/views/setting/select-back.vue
  27. 7 0
      src/views/setting/type.ts
  28. 17 10
      src/views/tagging-position/index.vue
  29. 0 0
      src/views/tagging/hot/edit.vue
  30. 0 0
      src/views/tagging/hot/images.vue
  31. 152 0
      src/views/tagging/hot/index.vue
  32. 0 0
      src/views/tagging/hot/show.vue
  33. 2 8
      src/views/tagging/sign.vue
  34. 0 0
      src/views/tagging/hot/style-type-select.vue
  35. 0 0
      src/views/tagging/hot/style.scss
  36. 0 0
      src/views/tagging/hot/styles.vue
  37. 109 132
      src/views/tagging/index.vue
  38. 62 0
      src/views/tagging/monitor/index.vue
  39. 113 0
      src/views/tagging/monitor/sign.vue
  40. 5 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`

+ 3 - 3
src/api/guide.ts

@@ -13,7 +13,7 @@ interface ServiceGuide {
   title: string
   showTaggings?: boolean
   showMeasure?: boolean
-  showVideo?: boolean
+  showMonitor?: boolean
   showPath?: boolean
 }
 
@@ -26,7 +26,7 @@ export interface Guide {
 
   showTagging: boolean
   showMeasure: boolean
-  showVideo: boolean
+  showMonitor: boolean
   showPath: boolean
 }
 
@@ -35,7 +35,7 @@ export type Guides = Guide[]
 const serviceToLocal = (serviceGuide: ServiceGuide): Guide => ({
   showTagging: true,
   showMeasure: true,
-  showVideo: 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) });
+};

+ 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>

+ 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>

+ 2 - 2
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";
@@ -219,7 +219,7 @@ const iconClickHandler = () => {
 };
 
 onUnmounted(() => {
-  tag.destroy();
+  tag.destory();
 });
 
 defineExpose(tag);

+ 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>

+ 2 - 0
src/env/index.ts

@@ -15,6 +15,7 @@ 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 showPathStack = stackFactory(ref<Path['id']>())
 export const showMeasuresStack = stackFactory(ref<boolean>(true))
@@ -34,6 +35,7 @@ export const custom = flatStacksValue({
   showLeftCtrlPano: showLeftCtrlPanoStack,
   shwoRightCtrlPano: showRightCtrlPanoStack,
   showTaggings: showTaggingsStack,
+  showMonitors: showMonitorsStack,
   showPaths: showPathsStack,
   showPath: showPathStack,
   showMeasures: showMeasuresStack,

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

@@ -0,0 +1,17 @@
+import { TaggingPosition } from "@/api";
+import { sdk, getTaggingPosNode, setPose } from "@/sdk";
+
+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,
+    });
+  }
+};

+ 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;

+ 4 - 0
src/layout/edit/fuse-edit.vue

@@ -34,10 +34,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 +50,8 @@ const initialSys = async () => {
     initialGuides(),
     initialPaths(),
     initialMeasures(),
+    initMonitors(),
+    initialAnimationModels()
   ]);
   await loadModel(fuseModel);
   const stop = watchEffect(() => {

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

@@ -42,6 +42,7 @@ import {
   fuseModels,
   appEl,
   initialPaths,
+  initMonitors,
 } from "@/store";
 
 const hasSingle = new URLSearchParams(location.search).has("single");
@@ -64,6 +65,7 @@ const initialSys = async () => {
     initialTaggingStyles(),
     initialTaggings(),
     initialGuides(),
+    initMonitors(),
   ]);
   await initialPaths();
   await initialMeasures();

+ 1 - 1
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]: {

+ 2 - 1
src/sdk/sdk.ts

@@ -45,6 +45,7 @@ export type SceneModel = ToChangeAPI<SceneModelAttrs> & {
     }
   >;
   destroy: () => void;
+  enterScaleMode: () => void;
   enterRotateMode: () => void;
   enterMoveMode: () => void;
   leaveTransform: () => void;
@@ -356,7 +357,7 @@ export type Tagging3D = {
   // 距离相机位置
   getCameraDisSquared: () => number;
   // 标注销毁
-  destroy: () => void;
+  destory: () => void;
 };
 
 // 动画组对象

+ 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'

+ 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();
+  },
+});

+ 21 - 54
src/views/guide/guide/edit-paths.vue

@@ -16,26 +16,7 @@
         添加视角
       </ui-button>
     </div>
-
-    <Dropdown placement="top">
-      <div class="show-setting strengthen">
-        <span>显示设置</span>
-        <DownOutlined />
-      </div>
-      <template #overlay>
-        <Menu>
-          <menu-item v-for="option in showOptions">
-            <ui-input
-              @click.stop
-              type="checkbox"
-              :modelValue="show.include(option.key)"
-              @update:modelValue="(s: boolean) => show.updateSelectId(option.key, s)"
-              :label="option.text"
-            />
-          </menu-item>
-        </Menu>
-      </template>
-    </Dropdown>
+    <ViewSetting class="show-setting" />
 
     <div class="info" v-if="paths.length">
       <div class="meta">
@@ -104,7 +85,7 @@
 
 <script setup lang="ts">
 import attachAnimation from "./attach-animation.vue";
-import { Dropdown, Menu, MenuItem } from "ant-design-vue";
+import ViewSetting from "@/components/view-setting/index.vue";
 import { loadPack, togetherCallback, getFileUrl, asyncTimeout } from "@/utils";
 import {
   sdk,
@@ -135,51 +116,45 @@ import {
   showTaggingsStack,
   showPathsStack,
   showMeasuresStack,
+  showMonitorsStack,
 } from "@/env";
 
 import type { Guide, GuidePaths, GuidePath } from "@/store";
 import type { CalcPathProps } from "@/sdk";
-import { DownOutlined } from "@ant-design/icons-vue";
-import { useSelects } from "@/hook/ids";
-import { mergeFuns } from "@/components/drawing/hook";
 
 const props = defineProps<{ data: Guide }>();
 const paths = ref<GuidePaths>(getGuidePaths(props.data));
 const current = ref<GuidePath>(paths.value[0]);
 
-const showOptions = [
-  { text: "标签", key: "showTagging" },
-  { text: "监控", key: "showVideo" },
-  { text: "路径", key: "showPath" },
-  { text: "测量", key: "showMeasure" },
-];
-const show = useSelects(ref(showOptions.map((item) => ({ id: item.key }))));
-const showAttrib = showOptions.reduce((t, c) => {
-  t[c.key] = computed(() => show.include(c.key));
-  return t;
-}, {} as Record<string, Ref<boolean>>);
-show.all.value = true;
-
-watchEffect(() => {
-  for (const key in showAttrib) {
-    (props.data as any)[key] = showAttrib[key].value;
-  }
-});
-
 const updatePathInfo = (index: number, calcInfo: CalcPathProps[1]) => {
   const info = sdk.calcPathInfo(paths.value.slice(index, index + 2) as any, calcInfo);
   Object.assign(paths.value[index], info);
 };
 
 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)),
-    showTaggingsStack.push(showAttrib.showTagging),
-    showPathsStack.push(showAttrib.showPath),
-    showMeasuresStack.push(showAttrib.showMeasure),
+    togetherCallback(
+      keys.map((key) =>
+        mapping[key].push(
+          computed({
+            get: () => props.data[key],
+            set: (s: boolean) => (props.data[key] = s),
+          })
+        )
+      )
+    ),
   ]);
 });
 
@@ -327,14 +302,6 @@ onUnmounted(() => {
     right: 20px;
     bottom: 100%;
     margin-bottom: 20px;
-    width: 160px;
-    background: rgba(27, 27, 28, 0.9);
-    border-radius: 4px;
-    height: 34px;
-    display: flex;
-    padding: 8px;
-    align-items: center;
-    justify-content: space-between;
   }
 
   .meta {

+ 114 - 36
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="close"
+            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>1:1</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"] = [
@@ -109,8 +92,39 @@ const actionItems: ActionsProps["items"] = [
       };
     },
   },
+  {
+    icon: "flip",
+    text: "缩放",
+    action: () => {
+      getSceneModel(custom.currentModel)?.enterScaleMode();
+      return () => {
+        getSceneModel(custom.currentModel)?.leaveTransform();
+      };
+    },
+  },
 ];
 
+const othActions = reactive([
+  {
+    icon: "move",
+    text: "配准",
+    disabled: isOld,
+    action: () => {
+      router.push({
+        name: RoutesName.registration,
+        params: { id: custom.currentModel!.id, save: "1" },
+      });
+    },
+  },
+  {
+    icon: "flip",
+    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 }[];
+}[]

+ 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>

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


+ 2 - 8
src/views/tagging/sign.vue

@@ -45,6 +45,7 @@ import {
 } from "@/store";
 
 import type { Tagging } from "@/store";
+import { flyTaggingPosition } from "@/hook/use-fly";
 
 const props = withDefaults(
   defineProps<{ tagging: Tagging; selected?: boolean; edit?: boolean }>(),
@@ -97,14 +98,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();

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


src/views/tagging/style.scss → src/views/tagging/hot/style.scss


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


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

@@ -1,153 +1,130 @@
 <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">
+      <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";
 
-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>

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

@@ -0,0 +1,113 @@
+<template>
+  <ui-group-option
+    class="sign-tagging"
+    :class="{ active: selected, edit }"
+    @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>
+      <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 }>(), {
+  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;
+
+  &.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>

+ 5 - 0
对接文档.txt

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