ExcalidrawView.vue 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. <template>
  2. <div ref="canvasContainer" class="excalidraw-wrapper" @click="showCanvasTop" />
  3. <div ref="canvasFloating" v-if="showFullView" class="floating-canvas" @mousedown="onMouseDown" @wheel.prevent="onWheel" @click="onCloseFloating" />
  4. </template>
  5. <script setup lang="ts">
  6. import { onMounted, ref, nextTick, onUnmounted } from 'vue-demi'
  7. import { withBase } from 'vitepress';
  8. import { exportToCanvas } from '@excalidraw/excalidraw'
  9. import { ExportedDataState } from '@excalidraw/excalidraw/data/types'
  10. interface Props {
  11. filePath: string;
  12. }
  13. const props = defineProps<Props>()
  14. const canvasContainer = ref<HTMLCanvasElement | null>(null)
  15. const canvasFloating = ref<HTMLCanvasElement | null>(null)
  16. const showFullView = ref(false);
  17. let excalidrawJson: ExportedDataState | undefined = undefined
  18. let topCanvas: HTMLCanvasElement | undefined = undefined
  19. let clickTimer: ReturnType<typeof setTimeout> | null = null;
  20. let scale = 1;
  21. let origin = { x: 0, y: 0 };
  22. let lastMouse = { x: 0, y: 0 };
  23. let dragging = false;
  24. let hasMoved = false; // 标记是否拖动过
  25. const dblclickCanvasHandler = (e: MouseEvent) => {
  26. if (clickTimer) {
  27. clearTimeout(clickTimer); // 阻止 click 事件
  28. clickTimer = null;
  29. }
  30. origin = { x: 0, y: 0 };
  31. scale = 1;
  32. updateTransform();
  33. };
  34. function showCanvasTop() {
  35. if (showFullView.value) return // 防止重复点击
  36. showFullView.value = true;
  37. // 等待 DOM 渲染后复制内容
  38. nextTick(async () => {
  39. if (canvasFloating.value && excalidrawJson) {
  40. const canvas = await drawCanvas(excalidrawJson)
  41. if (canvas) {
  42. topCanvas = canvas
  43. canvas.style.transform = `translate(0px, 0px) scale(1)`;
  44. canvas.style.pointerEvents = 'auto';
  45. canvas.style.userSelect = 'auto';
  46. origin = { x: 0, y: 0 };
  47. scale = 1;
  48. canvasFloating.value.appendChild(canvas)
  49. // 双击重置
  50. canvas.addEventListener('dblclick', dblclickCanvasHandler);
  51. }
  52. }
  53. });
  54. }
  55. function closeFullView() {
  56. showFullView.value = false;
  57. if (topCanvas) {
  58. topCanvas.removeEventListener('dblclick', dblclickCanvasHandler);
  59. }
  60. if (canvasFloating.value) {
  61. canvasFloating.value.innerHTML = ''; // 清空 canvas
  62. }
  63. }
  64. function onCloseFloating(e: MouseEvent) {
  65. if (clickTimer) clearTimeout(clickTimer);
  66. clickTimer = setTimeout(() => {
  67. // 这是单击
  68. if (hasMoved) {
  69. hasMoved = false;
  70. return; // 用户拖动过,不关闭
  71. }
  72. closeFullView()
  73. }, 500); // 等待双击事件,如果没有才执行
  74. }
  75. function onMouseDown(e: MouseEvent) {
  76. // 如果按的是 canvas,就拖动
  77. if (topCanvas && topCanvas.contains(e.target as Node)) {
  78. dragging = true;
  79. hasMoved = false; // 重置状态
  80. lastMouse = { x: e.clientX, y: e.clientY };
  81. const onMouseMove = (e: MouseEvent) => {
  82. if (!dragging) return;
  83. const dx = e.clientX - lastMouse.x;
  84. const dy = e.clientY - lastMouse.y;
  85. // 如果移动距离大于阈值,认为已经拖动过
  86. if (Math.abs(dx) > 2 || Math.abs(dy) > 2) {
  87. hasMoved = true;
  88. }
  89. origin.x += dx;
  90. origin.y += dy;
  91. lastMouse = { x: e.clientX, y: e.clientY };
  92. updateTransform();
  93. };
  94. const onMouseUp = () => {
  95. dragging = false;
  96. window.removeEventListener('mousemove', onMouseMove);
  97. window.removeEventListener('mouseup', onMouseUp);
  98. };
  99. // 添加鼠标事件监控
  100. window.addEventListener('mousemove', onMouseMove);
  101. window.addEventListener('mouseup', onMouseUp);
  102. }
  103. }
  104. function updateTransform() {
  105. if (topCanvas) {
  106. topCanvas.style.transform = `translate(${origin.x}px, ${origin.y}px) scale(${scale})`;
  107. }
  108. }
  109. function onWheel(e: WheelEvent) {
  110. const delta = -e.deltaY;
  111. const factor = delta > 0 ? 1.1 : 0.9;
  112. scale *= factor;
  113. scale = Math.min(Math.max(0.2, scale), 10); // 限制缩放范围
  114. updateTransform();
  115. }
  116. const getExcalidraw = async (path: string): Promise<ExportedDataState | undefined> => {
  117. const res = await fetch(withBase(path)).catch((err) => {
  118. console.log("excalidraw file err: ", err)
  119. })
  120. if (!res) return undefined
  121. const json = await res.json()
  122. return json
  123. }
  124. // const getExcalidraw = async (path: string): Promise<ExportedDataState | undefined> => {
  125. // try {
  126. // const data = await import(/* @vite-ignore */ withBase(path));
  127. // return data.default;
  128. // } catch (err) {
  129. // console.error('加载 excalidraw 失败:', err);
  130. // return undefined;
  131. // }
  132. // };
  133. const drawCanvas = async (data: ExportedDataState, width?: number, height?: number): Promise<HTMLCanvasElement> => {
  134. // const maxWidthOrHeight = width > height ? height : width
  135. // 改为只限制宽度,因为渲染前父窗口高度可能为0,导致maxWidthOrHeight不生效
  136. const maxWidthOrHeight = width
  137. const canvas = await exportToCanvas({
  138. ...data,
  139. maxWidthOrHeight,// 绘制的最大高度或宽度,设置maxWidthOrHeight后getDimensions则不生效
  140. // getDimensions: (width: number, height: number) => {
  141. // return {
  142. // width,
  143. // height,
  144. // scale: 1
  145. // }
  146. // }, // 参数为原始绘制尺寸,返回尺寸和比例 {width: number; height: number; scale: number}
  147. exportPadding: 10 // 添加边距,默认10
  148. })
  149. return canvas
  150. }
  151. function onKeyDown(e: KeyboardEvent) {
  152. if (e.key === 'Escape') {
  153. closeFullView()
  154. }
  155. }
  156. onMounted(async () => {
  157. console.log("excalidrawJson: ", props.filePath)
  158. excalidrawJson = await getExcalidraw(props.filePath)
  159. if (excalidrawJson && canvasContainer.value) {
  160. let excalidrawCanvas = await drawCanvas(excalidrawJson, canvasContainer.value.clientWidth, canvasContainer.value.clientHeight)
  161. if (excalidrawCanvas) {
  162. canvasContainer.value.appendChild(excalidrawCanvas)
  163. }
  164. // 使用 ResizeObserver 监听父容器宽度变化
  165. const resizeObserver = new ResizeObserver(async (entries: ResizeObserverEntry[]) => {
  166. for (const entry of entries) {
  167. const el = entry.target
  168. if (el === canvasContainer.value && excalidrawJson) {
  169. excalidrawCanvas = await drawCanvas(excalidrawJson, el.clientWidth, el.clientHeight)
  170. }
  171. }
  172. });
  173. resizeObserver.observe(canvasContainer.value!);
  174. }
  175. window.addEventListener('keydown', onKeyDown);
  176. })
  177. onUnmounted(() => {
  178. window.removeEventListener('keydown', onKeyDown);
  179. })
  180. </script>
  181. <style scoped>
  182. .excalidraw-wrapper {
  183. width: 100%;
  184. border: 1px solid #ddd;
  185. }
  186. .floating-canvas {
  187. position: fixed;
  188. top: 0;
  189. left: 0;
  190. width: 100vw;
  191. height: 100vh;
  192. /* background: rgba(0, 0, 0, 0.8); */
  193. z-index: 9999;
  194. display: flex;
  195. justify-content: center;
  196. align-items: center;
  197. overflow: hidden;
  198. cursor: grab;
  199. pointer-events: none; /* ✅ 让这个 div 不接收鼠标事件 */
  200. user-select: none; /* ✅ 禁止选中文本 */
  201. }
  202. .floating-canvas > canvas {
  203. pointer-events: auto; /* ✅ 子元素重新启用事件 */
  204. user-select: auto;
  205. }
  206. .floating-canvas:active {
  207. cursor: grabbing;
  208. }
  209. .floating-canvas canvas {
  210. /* max-width: 90vw; */
  211. max-width: none;
  212. /* max-height: 90vh; */
  213. max-height: none;
  214. box-shadow: 0 0 12px #fff;
  215. transition: transform 0.05s;
  216. }
  217. </style>