operate.vue 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. <template>
  2. <Teleport :to="`#draw-renderer`" 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. <Menu>
  12. <MenuItem v-for="menu in menus" @click="clickHandler(menu.handler)">
  13. <ui-icon :type="menu.icon" />
  14. <span>{{ menu.label }}</span>
  15. </MenuItem>
  16. </Menu>
  17. </div>
  18. </transition>
  19. </Teleport>
  20. </template>
  21. <script lang="ts" setup>
  22. import { computed, nextTick, ref, watch, watchEffect } from "vue";
  23. import { Transform } from "konva/lib/Util";
  24. import { Menu, MenuItem } from "ant-design-vue";
  25. import { shapeTreeContain, useStage, useViewerTransformConfig } from "./hook.js";
  26. import { DC, EntityShape } from "./dec.js";
  27. const props = defineProps<{
  28. target: DC<EntityShape> | undefined;
  29. data?: Record<string, any>;
  30. menus: Array<{ icon?: any; label?: string; handler: () => void }>;
  31. }>();
  32. const emit = defineEmits<{
  33. (e: "show" | "hide"): void;
  34. }>();
  35. const layout = ref<HTMLDivElement>();
  36. const stage = useStage();
  37. // const status = useMouseShapeStatus(computed(() => props.target));
  38. const rightClick = ref(false);
  39. const hasRClick = (ev: MouseEvent) => {
  40. if (ev.button !== 2) return false;
  41. const shape = props.target?.getNode();
  42. const pos = stage.value?.getNode().pointerPos;
  43. const layer = stage.value?.getNode().children[0];
  44. if (!shape || !pos || !layer) return false;
  45. let clickShape = layer.getIntersection(pos);
  46. ev.preventDefault();
  47. return !!clickShape && shapeTreeContain(shape, clickShape) === shape;
  48. };
  49. watchEffect((onCleanup) => {
  50. const dom = stage.value?.getStage().container();
  51. if (!dom) return;
  52. const clickHandler = async (ev: MouseEvent) => {
  53. const show = hasRClick(ev);
  54. if (show && rightClick.value) {
  55. rightClick.value = false;
  56. await nextTick();
  57. }
  58. rightClick.value = show;
  59. };
  60. dom.addEventListener("contextmenu", clickHandler);
  61. dom.addEventListener("pointerdown", clickHandler);
  62. onCleanup(() => {
  63. dom.removeEventListener("contextmenu", clickHandler);
  64. dom.removeEventListener("pointerdown", clickHandler);
  65. });
  66. });
  67. const clickHandler = (handler: () => void) => {
  68. handler();
  69. rightClick.value = false;
  70. };
  71. const hidden = computed(
  72. () =>
  73. !stage.value?.getStage() ||
  74. !props.target?.getNode() ||
  75. // !status.value.hover ||
  76. // status.value.active ||
  77. // transformIngShapes.value.length !== 0
  78. !rightClick.value
  79. );
  80. const move = new Transform();
  81. const pointer = ref<string | null>(null);
  82. const calcPointer = async () => {
  83. if (hidden.value) {
  84. pointer.value = null;
  85. return;
  86. } else if (pointer.value) {
  87. return;
  88. }
  89. const $stage = stage.value!.getStage();
  90. const mousePosition = $stage.pointerPos;
  91. if (!mousePosition) {
  92. pointer.value = null;
  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 (hidden.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(hidden, resetPointer);
  144. watch(useViewerTransformConfig(), () => {
  145. pointer.value = null;
  146. resetPointer();
  147. });
  148. // watch(() => props.data, resetPointer);
  149. </script>
  150. <style lang="scss">
  151. .propertys-controller {
  152. pointer-events: none;
  153. position: absolute;
  154. border-radius: 4px;
  155. border: 1px solid #e4e7ed;
  156. background-color: #ffffff;
  157. left: 0;
  158. top: 0;
  159. overflow: hidden;
  160. color: #303133;
  161. display: flex;
  162. flex-direction: column;
  163. align-items: center;
  164. box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
  165. min-width: 10px;
  166. padding: 5px 0;
  167. pointer-events: all;
  168. font-size: 14px;
  169. border: 1px solid var(--el-border-color-light);
  170. box-shadow: var(--el-dropdown-menu-box-shadow);
  171. background-color: var(--el-bg-color-overlay);
  172. border-radius: var(--el-border-radius-base);
  173. --el-menu-base-level-padding: 16px;
  174. --el-menu-item-height: 32px;
  175. --el-menu-item-font-size: 14px;
  176. .el-menu-item {
  177. align-items: center;
  178. padding: 5px 16px 5px 6px !important;
  179. color: var(--el-text-color-regular);
  180. }
  181. .el-menu-item [class^="el-icon"] {
  182. margin: 0;
  183. font-size: 1em;
  184. }
  185. }
  186. .pointer-fade-enter-active,
  187. .pointer-fade-leave-active {
  188. transition: opacity 0.3s ease;
  189. .item {
  190. pointer-events: none;
  191. }
  192. }
  193. .pointer-fade-enter-from,
  194. .pointer-fade-leave-to {
  195. opacity: 0;
  196. }
  197. </style>