use-expose.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import {
  2. installGlobalVar,
  3. useCursor,
  4. useInstanceProps,
  5. useLayers,
  6. useMountMenusAttachs,
  7. useStage,
  8. useTempStatus,
  9. } from "./use-global-vars.ts";
  10. import { useMode, useOperMode } from "./use-status";
  11. import { Stage } from "konva/lib/Stage";
  12. import { useInteractiveProps } from "./use-interactive.ts";
  13. import { useStore } from "../store/index.ts";
  14. import { useGetViewBoxPositionPixel, useSetViewport, useViewer } from "./use-viewer.ts";
  15. import { useGlobalResize, useListener } from "./use-event.ts";
  16. import { useInteractiveDrawShapeAPI } from "./use-draw.ts";
  17. import { useHistory } from "./use-history.ts";
  18. import { watchEffect } from "vue";
  19. import { usePaste } from "./use-paste.ts";
  20. import { useMouseShapesStatus } from "./use-mouse-status.ts";
  21. import { Mode } from "@/constant/mode.ts";
  22. import { ElMessageBox } from "element-plus";
  23. import { mergeFuns } from "@/utils/shared.ts";
  24. import { themeColor } from "@/constant";
  25. import { getImage, isSvgString } from "@/utils/resource.ts";
  26. import { useResourceHandler } from "./use-fetch.ts";
  27. import { useConfig } from "./use-config.ts";
  28. import { useSelectionRevise } from "./use-selection.ts";
  29. import { useFormalLayer, useGetFormalChildren } from "./use-layer.ts";
  30. import { components } from "../components/index.ts";
  31. import { useProportion } from "./use-proportion.ts";
  32. import { ShapeType } from "@/index.ts";
  33. import { useGetDXF } from "./use-dxf.ts";
  34. // 自动粘贴服务
  35. export const useAutoPaste = () => {
  36. const paste = usePaste();
  37. const drawAPI = useInteractiveDrawShapeAPI();
  38. const resourceHandler = useResourceHandler();
  39. paste.push({
  40. ["text/plain"]: {
  41. async handler(pos, val) {
  42. console.log(val);
  43. if (isSvgString(val)) {
  44. const url = await resourceHandler(val, "svg");
  45. drawAPI.addShape("icon", { url, stroke: themeColor }, pos, true);
  46. } else {
  47. drawAPI.addShape("text", { content: val }, pos, true);
  48. }
  49. },
  50. type: "string",
  51. },
  52. ["image"]: {
  53. async handler(pos, val, type) {
  54. const url = await resourceHandler(val, type);
  55. if (type.includes("svg")) {
  56. drawAPI.addShape("icon", { url, stroke: themeColor }, pos, true);
  57. } else {
  58. const image = await getImage(url);
  59. drawAPI.addShape(
  60. "image",
  61. { url, width: image.width, height: image.height },
  62. pos,
  63. true
  64. );
  65. }
  66. },
  67. type: "file",
  68. },
  69. });
  70. };
  71. // 快捷键服务
  72. export const useShortcutKey = () => {
  73. // 自动退出添加模式
  74. const { quitDrawShape, enterDrawShape } = useInteractiveDrawShapeAPI();
  75. const interactiveProps = useInteractiveProps();
  76. const store = useStore();
  77. useListener(
  78. "contextmenu",
  79. (ev) => {
  80. const iProps = interactiveProps.value;
  81. if (!iProps?.type || ev.button !== 2) return;
  82. const addCount = quitDrawShape();
  83. // 钢笔工具需要右键两次才退出,右键一次相当于完成
  84. const isDots = ['dots', 'single-dots'].includes(components[iProps.type].addMode);
  85. if (isDots && addCount > 0) {
  86. setTimeout(() => {
  87. enterDrawShape(iProps.type, iProps.preset, iProps.operate?.single);
  88. }, 10);
  89. }
  90. },
  91. document.documentElement
  92. );
  93. const history = useHistory();
  94. const status = useMouseShapesStatus();
  95. const getChildren = useGetFormalChildren();
  96. const operMode = useOperMode();
  97. useListener(
  98. "keydown",
  99. (ev) => {
  100. if (ev.target !== document.body) return;
  101. if (ev.key === "z" && ev.ctrlKey) {
  102. history.hasUndo.value && history.undo();
  103. } else if (ev.key === "y" && ev.ctrlKey) {
  104. history.hasRedo.value && history.redo();
  105. } else if (ev.key === "s" && ev.ctrlKey) {
  106. // 保存
  107. history.saveLocal();
  108. } else if (ev.key === "Delete" || ev.key === "Backspace") {
  109. // 删除
  110. const isSelect = status.selects.length;
  111. const shapes = isSelect ? status.selects : status.actives;
  112. const delItems = shapes.map((shape) => {
  113. const id = shape.id();
  114. if (!id) return;
  115. const item = store.getItemById(id)
  116. const type = store.getType(id);
  117. if (!item?.disableDelete && type) {
  118. return [type, item] as const
  119. }
  120. }).filter(item => !!item);
  121. history.onceTrack(() => {
  122. delItems.forEach(([type, item]) => {
  123. if (components[type as ShapeType].delItem) {
  124. components[type as ShapeType].delItem!(store, item as any)
  125. } else {
  126. store.delItem(type as ShapeType, item!.id);
  127. }
  128. })
  129. });
  130. if (delItems.length) {
  131. if (isSelect) {
  132. status.selects = [];
  133. } else {
  134. status.actives = [];
  135. }
  136. }
  137. } else if (operMode.value.mulSelection && ev.key === "A") {
  138. if (status.selects.length) {
  139. status.selects = [];
  140. } else {
  141. status.selects = getChildren();
  142. }
  143. }
  144. },
  145. window
  146. );
  147. };
  148. export const useAutoService = () => {
  149. useAutoPaste();
  150. useShortcutKey();
  151. // 鼠标自动变化服务
  152. const status = useMouseShapesStatus();
  153. const operMode = useOperMode();
  154. const mode = useMode();
  155. const cursor = useCursor();
  156. const { set: setCursor } = cursor.push("initial");
  157. watchEffect(() => {
  158. let style: string | null = null;
  159. if (operMode.value.freeView) {
  160. style = "pointer";
  161. } else if (mode.include(Mode.update)) {
  162. style = "./icons/m_move.png";
  163. } else if (status.hovers.length) {
  164. style = "pointer";
  165. } else {
  166. style = "initial";
  167. }
  168. setCursor(style);
  169. });
  170. // 自动保存历史及恢复服务
  171. const history = useHistory();
  172. const instanceProps = useInstanceProps();
  173. const init = (id: any) => {
  174. const quitHooks: (() => void)[] = [];
  175. if (!id) return quitHooks;
  176. const unloadHandler = () => {
  177. if (history.hasRedo.value || history.hasUndo.value) {
  178. history.saveLocal();
  179. }
  180. };
  181. history.setLocalId(id);
  182. window.addEventListener("beforeunload", unloadHandler);
  183. quitHooks.push(() =>
  184. window.removeEventListener("beforeunload", unloadHandler)
  185. );
  186. if (!history.hasLocal()) return quitHooks;
  187. if (!import.meta.env.DEV) {
  188. let isOpen = true;
  189. ElMessageBox.confirm("检测到有历史数据,是否要恢复?", {
  190. type: "info",
  191. confirmButtonText: "恢复",
  192. cancelButtonText: "取消",
  193. })
  194. .then(() => {
  195. history.loadLocalStorage();
  196. })
  197. .catch(() => {
  198. history.clearLocal();
  199. })
  200. .finally(() => {
  201. isOpen = false;
  202. });
  203. quitHooks.push(() => isOpen && ElMessageBox.close());
  204. }
  205. return quitHooks;
  206. };
  207. watchEffect((onCleanup) => {
  208. onCleanup(mergeFuns(init(instanceProps.get().id)));
  209. });
  210. useSelectionRevise();
  211. };
  212. export type DrawExpose = ReturnType<typeof useExpose>;
  213. type PickParams<K extends keyof Stage, O extends string> = Stage[K] extends (
  214. ...args: any
  215. ) => any
  216. ? Omit<Required<Parameters<Stage[K]>>[0], O>
  217. : never;
  218. export const useExpose = installGlobalVar(() => {
  219. const mode = useMode();
  220. const interactiveProps = useInteractiveProps();
  221. const stage = useStage();
  222. const layers = useLayers();
  223. const store = useStore();
  224. const history = useHistory();
  225. const viewer = useViewer().viewer;
  226. const { updateSize } = useGlobalResize();
  227. const exposeBlob = (config?: PickParams<"toBlob", "callback">) => {
  228. const $stage = stage.value!.getStage();
  229. return new Promise<Blob>((resolve) => {
  230. $stage.toBlob({ ...config, resolve } as any);
  231. });
  232. };
  233. const toggleHit = () => {
  234. if (!layers.value) return;
  235. layers.value.forEach((layer) => {
  236. layer.toggleHitCanvas();
  237. });
  238. };
  239. return {
  240. ...useInteractiveDrawShapeAPI(),
  241. get stage() {
  242. const $store = stage.value?.getStage();
  243. return $store;
  244. },
  245. ...useTempStatus(),
  246. exposeBlob,
  247. toggleHit,
  248. formalLayer: useFormalLayer(),
  249. updateSize,
  250. history,
  251. store,
  252. ...useSetViewport(),
  253. mode,
  254. getData() {
  255. return store.data;
  256. },
  257. getViewBoxPositionPixel: useGetViewBoxPositionPixel(),
  258. viewer,
  259. presetAdd: interactiveProps,
  260. config: useConfig(),
  261. mountMenus: useMountMenusAttachs(),
  262. proportion: useProportion(),
  263. getDXF: useGetDXF()
  264. };
  265. });