hover-operate.vue 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. <template>
  2. <Teleport :to="`#${DomMountId}`" v-if="stage">
  3. <transition name="pointer-fade">
  4. <div
  5. v-if="pointer"
  6. :style="{ transform: pointer }"
  7. :size="8"
  8. class="propertys-controller"
  9. ref="layout"
  10. >
  11. <ElMenu>
  12. <ElMenuItem v-for="menu in menus" @click="clickHandler(menu.handler)">
  13. <span class="menu-item">{{ menu.label }}</span>
  14. </ElMenuItem>
  15. </ElMenu>
  16. </div>
  17. </transition>
  18. </Teleport>
  19. </template>
  20. <script lang="ts" setup>
  21. import { computed, nextTick, ref, watch } from "vue";
  22. import { useMouseMenusFilter, useStage } from "../../hook/use-global-vars.ts";
  23. import { useMode } from "../../hook/use-status.ts";
  24. import { DC, EntityShape } from "@/deconstruction.js";
  25. import { useViewerTransformConfig } from "../../hook/use-viewer.ts";
  26. import { Transform } from "konva/lib/Util";
  27. import { DomMountId } from "@/constant/index.ts";
  28. import { ElMenu, ElMenuItem } from "element-plus";
  29. import { Mode } from "@/constant/mode.ts";
  30. import { useGlobalOnlyRightClickShape } from "@/core/hook/use-event.ts";
  31. import { useStore } from "@/core/store/index.ts";
  32. const props = defineProps<{
  33. target: DC<EntityShape> | undefined;
  34. data?: Record<string, any>;
  35. menus: Array<{ icon?: any; label?: string; handler: () => void }>;
  36. }>();
  37. const emit = defineEmits<{
  38. (e: "show" | "hide"): void;
  39. }>();
  40. const layout = ref<HTMLDivElement>();
  41. const stage = useStage();
  42. const { getFilter } = useMouseMenusFilter();
  43. const store = useStore();
  44. const id = computed(() => props.target?.getNode().id());
  45. const type = computed(() => id.value && store.getType(id.value));
  46. const menus = computed(() => {
  47. if (id.value && type.value) {
  48. return getFilter(type.value, id.value)(props.menus);
  49. } else {
  50. return props.menus;
  51. }
  52. });
  53. const show = ref(false);
  54. const { add } = useGlobalOnlyRightClickShape();
  55. const mode = useMode();
  56. watch(
  57. () => !mode.value.has(Mode.draw) && props.target?.getNode(),
  58. (shape, _, onCleanup) => {
  59. if (!shape) return;
  60. const del = add(
  61. shape,
  62. async () => {
  63. await nextTick();
  64. show.value = true;
  65. },
  66. () => {
  67. show.value = false;
  68. }
  69. );
  70. onCleanup(del);
  71. }
  72. );
  73. const clickHandler = (handler: () => void) => {
  74. handler();
  75. show.value = false;
  76. };
  77. const move = new Transform();
  78. const pointer = ref<string | null>(null);
  79. const calcPointer = async () => {
  80. if (!show.value) {
  81. pointer.value = null;
  82. return;
  83. } else if (pointer.value) {
  84. return;
  85. }
  86. const $stage = stage.value!.getStage();
  87. const mousePosition = $stage.pointerPos;
  88. if (!mousePosition) {
  89. pointer.value = null;
  90. return;
  91. }
  92. if (!menus.value.length) {
  93. return;
  94. }
  95. const $shape = props.target!.getNode();
  96. const shapeRect = $shape.getClientRect();
  97. const shapeR = shapeRect.x + shapeRect.width;
  98. const shapeB = shapeRect.y + shapeRect.height;
  99. let x = Math.min(Math.max(mousePosition.x, shapeRect.x), shapeR);
  100. let y = Math.min(Math.max(mousePosition.y, shapeRect.y), shapeB);
  101. move.reset();
  102. move.translate(x, y);
  103. pointer.value = `matrix(${move.m.join(",")})`;
  104. await nextTick();
  105. const domRect = layout.value!.getBoundingClientRect();
  106. x = x - domRect.width / 2;
  107. x = Math.max(x, shapeRect.x);
  108. if (x + domRect.width > shapeR) {
  109. x = shapeR - domRect.width;
  110. }
  111. if (y + domRect.height > shapeB) {
  112. y = y - domRect.height;
  113. }
  114. x = Math.max(x, 10);
  115. y = Math.max(y, 10);
  116. if (x + domRect.width > $stage.width() - 10) {
  117. x = $stage.width() - domRect.width - 10;
  118. }
  119. if (y + domRect.height > $stage.height() - 10) {
  120. y = $stage.height() - domRect.height - 10;
  121. }
  122. move.reset();
  123. move.translate(x, y);
  124. pointer.value = `matrix(${move.m.join(",")})`;
  125. };
  126. watch(
  127. () => !!pointer.value,
  128. (show) => {
  129. emit(show ? "show" : "hide");
  130. }
  131. );
  132. let timeout: any;
  133. const resetPointer = () => {
  134. if (!show.value) {
  135. pointer.value = null;
  136. return;
  137. } else if (pointer.value) {
  138. return;
  139. }
  140. clearTimeout(timeout);
  141. timeout = setTimeout(calcPointer, 16);
  142. };
  143. watch(show, resetPointer);
  144. watch(useViewerTransformConfig(), () => {
  145. pointer.value = null;
  146. resetPointer();
  147. });
  148. </script>
  149. <style lang="scss" scoped>
  150. .menu-item {
  151. display: block;
  152. min-width: 60px;
  153. margin-left: 10px;
  154. }
  155. </style>
  156. <style lang="scss">
  157. .propertys-controller {
  158. pointer-events: none;
  159. position: absolute;
  160. border-radius: 4px;
  161. border: 1px solid #e4e7ed;
  162. background-color: #ffffff;
  163. left: 0;
  164. top: 0;
  165. overflow: hidden;
  166. color: #303133;
  167. display: flex;
  168. flex-direction: column;
  169. align-items: center;
  170. box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
  171. min-width: 10px;
  172. padding: 5px 0;
  173. pointer-events: all;
  174. font-size: 14px;
  175. border: 1px solid var(--el-border-color-light);
  176. box-shadow: var(--el-dropdown-menu-box-shadow);
  177. background-color: var(--el-bg-color-overlay);
  178. border-radius: var(--el-border-radius-base);
  179. --el-menu-base-level-padding: 16px;
  180. --el-menu-item-height: 32px;
  181. --el-menu-item-font-size: 14px;
  182. .el-menu-item {
  183. align-items: center;
  184. padding: 5px 16px 5px 6px !important;
  185. color: var(--el-text-color-regular);
  186. }
  187. }
  188. .pointer-fade-enter-active,
  189. .pointer-fade-leave-active {
  190. transition: opacity 0.3s ease;
  191. .item {
  192. pointer-events: none;
  193. }
  194. }
  195. .pointer-fade-enter-from,
  196. .pointer-fade-leave-to {
  197. opacity: 0;
  198. }
  199. </style>