index.vue 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. <template>
  2. <div
  3. class="canvas"
  4. ref="canvasRef"
  5. @mousewheel="$event => mousewheelScaleCanvas($event)"
  6. @mousedown="$event => handleClickBlankArea($event)"
  7. v-contextmenu="contextmenus"
  8. v-click-outside="removeEditorAreaFocus"
  9. >
  10. <ElementCreateSelection
  11. v-if="creatingElement"
  12. @created="data => insertElementFromCreateSelection(data)"
  13. />
  14. <div
  15. class="viewport-wrapper"
  16. :style="{
  17. width: viewportStyles.width * canvasScale + 'px',
  18. height: viewportStyles.height * canvasScale + 'px',
  19. left: viewportStyles.left + 'px',
  20. top: viewportStyles.top + 'px',
  21. }"
  22. >
  23. <div class="operates">
  24. <AlignmentLine
  25. v-for="(line, index) in alignmentLines"
  26. :key="index"
  27. :type="line.type"
  28. :axis="line.axis"
  29. :length="line.length"
  30. />
  31. <MultiSelectOperate
  32. v-if="activeElementIdList.length > 1"
  33. :elementList="elementList"
  34. :scaleMultiElement="scaleMultiElement"
  35. />
  36. <Operate
  37. v-for="element in elementList"
  38. :key="element.id"
  39. :elementInfo="element"
  40. :isSelected="activeElementIdList.includes(element.id)"
  41. :isActive="handleElementId === element.id"
  42. :isActiveGroupElement="activeGroupElementId === element.id"
  43. :isMultiSelect="activeElementIdList.length > 1"
  44. :rotateElement="rotateElement"
  45. :scaleElement="scaleElement"
  46. :dragLineElement="dragLineElement"
  47. />
  48. <SlideBackground />
  49. </div>
  50. <div
  51. class="viewport"
  52. ref="viewportRef"
  53. :style="{ transform: `scale(${canvasScale})` }"
  54. >
  55. <MouseSelection
  56. v-if="mouseSelectionState.isShow"
  57. :top="mouseSelectionState.top"
  58. :left="mouseSelectionState.left"
  59. :width="mouseSelectionState.width"
  60. :height="mouseSelectionState.height"
  61. :quadrant="mouseSelectionState.quadrant"
  62. />
  63. <EditableElement
  64. v-for="(element, index) in elementList"
  65. :key="element.id"
  66. :elementInfo="element"
  67. :elementIndex="index + 1"
  68. :isMultiSelect="activeElementIdList.length > 1"
  69. :selectElement="selectElement"
  70. />
  71. </div>
  72. </div>
  73. </div>
  74. </template>
  75. <script lang="ts">
  76. import { computed, defineComponent, provide, ref, watch, watchEffect } from 'vue'
  77. import throttle from 'lodash/throttle'
  78. import { MutationTypes, useStore } from '@/store'
  79. import { ContextmenuItem } from '@/components/Contextmenu/types'
  80. import { PPTElement, Slide } from '@/types/slides'
  81. import { AlignmentLineProps } from '@/types/edit'
  82. import { removeAllRanges } from '@/utils/selection'
  83. import useViewportSize from './hooks/useViewportSize'
  84. import useMouseSelection from './hooks/useMouseSelection'
  85. import useDropImageOrText from './hooks/useDropImageOrText'
  86. import useRotateElement from './hooks/useRotateElement'
  87. import useScaleElement from './hooks/useScaleElement'
  88. import useSelectElement from './hooks/useSelectElement'
  89. import useDragElement from './hooks/useDragElement'
  90. import useDragLineElement from './hooks/useDragLineElement'
  91. import useInsertFromCreateSelection from './hooks/useInsertFromCreateSelection'
  92. import useDeleteElement from '@/hooks/useDeleteElement'
  93. import useCopyAndPasteElement from '@/hooks/useCopyAndPasteElement'
  94. import useSelectAllElement from '@/hooks/useSelectAllElement'
  95. import useScaleCanvas from '@/hooks/useScaleCanvas'
  96. import useScreening from '@/hooks/useScreening'
  97. import EditableElement from './EditableElement.vue'
  98. import MouseSelection from './MouseSelection.vue'
  99. import SlideBackground from './SlideBackground.vue'
  100. import AlignmentLine from './AlignmentLine.vue'
  101. import ElementCreateSelection from './ElementCreateSelection.vue'
  102. import MultiSelectOperate from './Operate/MultiSelectOperate.vue'
  103. import Operate from './Operate/index.vue'
  104. export default defineComponent({
  105. name: 'editor-canvas',
  106. components: {
  107. EditableElement,
  108. MouseSelection,
  109. SlideBackground,
  110. AlignmentLine,
  111. ElementCreateSelection,
  112. MultiSelectOperate,
  113. Operate,
  114. },
  115. setup() {
  116. const store = useStore()
  117. const activeElementIdList = computed(() => store.state.activeElementIdList)
  118. const handleElementId = computed(() => store.state.handleElementId)
  119. const editorAreaFocus = computed(() => store.state.editorAreaFocus)
  120. const ctrlKeyState = computed(() => store.state.ctrlKeyState)
  121. const ctrlOrShiftKeyActive = computed<boolean>(() => store.getters.ctrlOrShiftKeyActive)
  122. const viewportRef = ref<HTMLElement>()
  123. const alignmentLines = ref<AlignmentLineProps[]>([])
  124. const activeGroupElementId = ref('')
  125. watch(handleElementId, () => activeGroupElementId.value = '')
  126. const currentSlide = computed<Slide>(() => store.getters.currentSlide)
  127. const elementList = ref<PPTElement[]>([])
  128. const setLocalElementList = () => {
  129. elementList.value = currentSlide.value ? JSON.parse(JSON.stringify(currentSlide.value.elements)) : []
  130. }
  131. watchEffect(setLocalElementList)
  132. const canvasRef = ref<HTMLElement>()
  133. const canvasScale = computed(() => store.state.canvasScale)
  134. const { viewportStyles } = useViewportSize(canvasRef)
  135. useDropImageOrText(canvasRef)
  136. const { mouseSelectionState, updateMouseSelection } = useMouseSelection(elementList, viewportRef)
  137. const { dragElement } = useDragElement(elementList, activeGroupElementId, alignmentLines)
  138. const { dragLineElement } = useDragLineElement(elementList)
  139. const { selectElement } = useSelectElement(elementList, activeGroupElementId, dragElement)
  140. const { scaleElement, scaleMultiElement } = useScaleElement(elementList, activeGroupElementId, alignmentLines)
  141. const { rotateElement } = useRotateElement(elementList, viewportRef)
  142. const { selectAllElement } = useSelectAllElement()
  143. const { deleteAllElements } = useDeleteElement()
  144. const { pasteElement } = useCopyAndPasteElement()
  145. const { enterScreening } = useScreening()
  146. const handleClickBlankArea = (e: MouseEvent) => {
  147. store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [])
  148. if (!ctrlOrShiftKeyActive.value) updateMouseSelection(e)
  149. if (!editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true)
  150. removeAllRanges()
  151. }
  152. const removeEditorAreaFocus = () => {
  153. if (editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, false)
  154. }
  155. const { scaleCanvas } = useScaleCanvas()
  156. const throttleScaleCanvas = throttle(scaleCanvas, 100, { leading: true, trailing: false })
  157. const mousewheelScaleCanvas = (e: WheelEvent) => {
  158. if (!ctrlKeyState.value) return
  159. e.preventDefault()
  160. if (e.deltaY > 0) throttleScaleCanvas('-')
  161. else if (e.deltaY < 0) throttleScaleCanvas('+')
  162. }
  163. const showGridLines = computed(() => store.state.showGridLines)
  164. const toggleGridLines = () => {
  165. store.commit(MutationTypes.SET_GRID_LINES_STATE, !showGridLines.value)
  166. }
  167. const creatingElement = computed(() => store.state.creatingElement)
  168. const { insertElementFromCreateSelection } = useInsertFromCreateSelection(viewportRef)
  169. const contextmenus = (): ContextmenuItem[] => {
  170. return [
  171. {
  172. text: '粘贴',
  173. subText: 'Ctrl + V',
  174. handler: pasteElement,
  175. },
  176. {
  177. text: '全选',
  178. subText: 'Ctrl + A',
  179. handler: selectAllElement,
  180. },
  181. {
  182. text: '网格线',
  183. subText: showGridLines.value ? '√' : '',
  184. handler: toggleGridLines,
  185. },
  186. {
  187. text: '重置当前页',
  188. handler: deleteAllElements,
  189. },
  190. { divider: true },
  191. {
  192. text: '从当前页演示',
  193. subText: 'Ctrl+F',
  194. handler: enterScreening,
  195. },
  196. ]
  197. }
  198. provide('slideScale', canvasScale)
  199. return {
  200. elementList,
  201. activeElementIdList,
  202. handleElementId,
  203. activeGroupElementId,
  204. canvasRef,
  205. viewportRef,
  206. viewportStyles,
  207. canvasScale,
  208. mouseSelectionState,
  209. handleClickBlankArea,
  210. removeEditorAreaFocus,
  211. currentSlide,
  212. creatingElement,
  213. insertElementFromCreateSelection,
  214. alignmentLines,
  215. selectElement,
  216. rotateElement,
  217. scaleElement,
  218. dragLineElement,
  219. scaleMultiElement,
  220. mousewheelScaleCanvas,
  221. contextmenus,
  222. }
  223. },
  224. })
  225. </script>
  226. <style lang="scss" scoped>
  227. .canvas {
  228. height: 100%;
  229. user-select: none;
  230. overflow: hidden;
  231. background-color: $lightGray;
  232. position: relative;
  233. }
  234. .viewport-wrapper {
  235. position: absolute;
  236. box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.1);
  237. }
  238. .viewport {
  239. position: absolute;
  240. top: 0;
  241. left: 0;
  242. transform-origin: 0 0;
  243. }
  244. </style>