useSlideHandler.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import { computed } from 'vue'
  2. import { storeToRefs } from 'pinia'
  3. import { nanoid } from 'nanoid'
  4. import { useMainStore, useSlidesStore } from '@/store'
  5. import type { Slide } from '@/types/slides'
  6. import { copyText, readClipboard } from '@/utils/clipboard'
  7. import { encrypt } from '@/utils/crypto'
  8. import { createElementIdMap } from '@/utils/element'
  9. import { KEYS } from '@/configs/hotkey'
  10. import message from '@/utils/message'
  11. import usePasteTextClipboardData from '@/hooks/usePasteTextClipboardData'
  12. import useHistorySnapshot from '@/hooks/useHistorySnapshot'
  13. import useAddSlidesOrElements from '@/hooks/useAddSlidesOrElements'
  14. export default () => {
  15. const mainStore = useMainStore()
  16. const slidesStore = useSlidesStore()
  17. const { selectedSlidesIndex: _selectedSlidesIndex, activeElementIdList } = storeToRefs(mainStore)
  18. const { currentSlide, slides, theme, slideIndex, viewportSize, viewportRatio } = storeToRefs(slidesStore)
  19. const selectedSlidesIndex = computed(() => [..._selectedSlidesIndex.value, slideIndex.value])
  20. const selectedSlides = computed(() => slides.value.filter((item, index) => selectedSlidesIndex.value.includes(index)))
  21. const selectedSlidesId = computed(() => selectedSlides.value.map(item => item.id))
  22. const { pasteTextClipboardData } = usePasteTextClipboardData()
  23. const { addSlidesFromData } = useAddSlidesOrElements()
  24. const { addHistorySnapshot } = useHistorySnapshot()
  25. // 重置幻灯片
  26. const resetSlides = () => {
  27. const emptySlide: Slide = {
  28. id: nanoid(10),
  29. elements: [],
  30. background: {
  31. type: 'solid',
  32. color: theme.value.backgroundColor,
  33. },
  34. }
  35. slidesStore.updateSlideIndex(0)
  36. mainStore.setActiveElementIdList([])
  37. slidesStore.setSlides([emptySlide])
  38. }
  39. /**
  40. * 移动页面焦点
  41. * @param command 移动页面焦点命令:上移、下移
  42. */
  43. const updateSlideIndex = (command: string) => {
  44. console.log('updateSlideIndex', command)
  45. if ((command === KEYS.UP || command === KEYS.LEFT) && slideIndex.value > 0) {
  46. if (activeElementIdList.value.length) mainStore.setActiveElementIdList([])
  47. slidesStore.updateSlideIndex(slideIndex.value - 1)
  48. }
  49. else if ((command === KEYS.DOWN || command === KEYS.RIGHT) && slideIndex.value < slides.value.length - 1) {
  50. if (activeElementIdList.value.length) mainStore.setActiveElementIdList([])
  51. slidesStore.updateSlideIndex(slideIndex.value + 1)
  52. }
  53. }
  54. // 将当前页面数据加密后复制到剪贴板
  55. const copySlide = () => {
  56. const text = encrypt(JSON.stringify({
  57. type: 'slides',
  58. data: selectedSlides.value,
  59. }))
  60. copyText(text).then(() => {
  61. mainStore.setThumbnailsFocus(true)
  62. })
  63. }
  64. // 尝试将剪贴板页面数据解密后添加到下一页(粘贴)
  65. const pasteSlide = () => {
  66. readClipboard().then(text => {
  67. pasteTextClipboardData(text, { onlySlide: true })
  68. }).catch(err => message.warning(err))
  69. }
  70. // 创建一页空白页并添加到下一页
  71. const createSlide = () => {
  72. const emptySlide: Slide = {
  73. id: nanoid(10),
  74. elements: [],
  75. background: {
  76. type: 'solid',
  77. color: theme.value.backgroundColor,
  78. },
  79. }
  80. mainStore.setActiveElementIdList([])
  81. slidesStore.addSlide(emptySlide)
  82. addHistorySnapshot()
  83. }
  84. // 用一页全新的空白页替换掉当前全部页面(单页模式下代替 createSlide 使用)
  85. //
  86. // 刻意做成一次赋值而不是「先 createSlide 再 deleteSlide 旧页」:
  87. // 1. setSlides 是原子的,不存在「新页建好了但旧页没删掉」这种中间态;
  88. // 2. 不在这里存快照,让后续 createElement 那一笔成为唯一的一笔,Ctrl+Z 一次回到原状;
  89. // 3. 新页保证是空的,createFrameElement 里「当前页已有 frame 就拒绝插入」的守卫必定放行。
  90. const replaceAllSlidesWithNew = () => {
  91. const emptySlide: Slide = {
  92. id: nanoid(10),
  93. elements: [],
  94. background: {
  95. type: 'solid',
  96. color: theme.value.backgroundColor,
  97. },
  98. }
  99. mainStore.setActiveElementIdList([])
  100. slidesStore.setSlides([emptySlide])
  101. slidesStore.updateSlideIndex(0)
  102. }
  103. // 根据模板创建新页面
  104. const createSlideByTemplate = (slide: Slide) => {
  105. const { groupIdMap, elIdMap } = createElementIdMap(slide.elements)
  106. const slideWidth = viewportSize.value
  107. const slideHeight = viewportSize.value * viewportRatio.value
  108. // 模板原始宽高(16:9比例)
  109. const templateWidth = 1280
  110. const templateHeight = 720
  111. // 计算缩放因子
  112. const scaleX = slideWidth / templateWidth
  113. const scaleY = slideHeight / templateHeight
  114. for (const element of slide.elements) {
  115. element.id = elIdMap[element.id]
  116. element.top = (element.top || 0) * scaleY
  117. element.left = (element.left || 0) * scaleX
  118. element.width = (element.width || 0) * scaleX
  119. if (element.type === 'text' || element.type === 'shape') {
  120. element.height = (element.height || 0) * scaleY
  121. }
  122. // 处理 content 中的字体大小
  123. if (element.type === 'text') {
  124. // 匹配 style 中的 font-size 属性
  125. element.content = element.content.replace(/font-size:\s*(\d+)px/g, (match, fontSize) => {
  126. const newFontSize = Math.round(parseInt(fontSize) * scaleY)
  127. return `font-size: ${newFontSize}px`
  128. })
  129. }
  130. if (element.groupId) element.groupId = groupIdMap[element.groupId]
  131. }
  132. const newSlide = {
  133. ...slide,
  134. id: nanoid(10),
  135. }
  136. mainStore.setActiveElementIdList([])
  137. slidesStore.addSlide(newSlide)
  138. addHistorySnapshot()
  139. }
  140. // 将当前页复制一份到下一页
  141. const copyAndPasteSlide = () => {
  142. const slide = JSON.parse(JSON.stringify(currentSlide.value))
  143. addSlidesFromData([slide])
  144. }
  145. // 删除当前页,若将删除全部页面,则执行重置幻灯片操作
  146. const deleteSlide = (targetSlidesId = selectedSlidesId.value) => {
  147. if (slides.value.length === targetSlidesId.length) resetSlides()
  148. else slidesStore.deleteSlide(targetSlidesId)
  149. mainStore.updateSelectedSlidesIndex([])
  150. addHistorySnapshot()
  151. }
  152. // 将当前页复制后删除(剪切)
  153. // 由于复制操作会导致多选状态消失,所以需要提前将需要删除的页面ID进行缓存
  154. const cutSlide = () => {
  155. const targetSlidesId = [...selectedSlidesId.value]
  156. copySlide()
  157. deleteSlide(targetSlidesId)
  158. }
  159. // 选中全部幻灯片
  160. const selectAllSlide = () => {
  161. const newSelectedSlidesIndex = Array.from(Array(slides.value.length), (item, index) => index)
  162. mainStore.setActiveElementIdList([])
  163. mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex)
  164. }
  165. // 拖拽调整幻灯片顺序同步数据
  166. const sortSlides = (newIndex: number, oldIndex: number) => {
  167. if (oldIndex === newIndex) return
  168. const _slides: Slide[] = JSON.parse(JSON.stringify(slides.value))
  169. const movingSlide = _slides[oldIndex]
  170. const movingSlideSection = movingSlide.sectionTag
  171. if (movingSlideSection) {
  172. const movingSlideSectionNext = _slides[oldIndex + 1]
  173. delete movingSlide.sectionTag
  174. if (movingSlideSectionNext && !movingSlideSectionNext.sectionTag) {
  175. movingSlideSectionNext.sectionTag = movingSlideSection
  176. }
  177. }
  178. if (newIndex === 0) {
  179. const firstSection = _slides[0].sectionTag
  180. if (firstSection) {
  181. delete _slides[0].sectionTag
  182. movingSlide.sectionTag = firstSection
  183. }
  184. }
  185. const _slide = _slides[oldIndex]
  186. _slides.splice(oldIndex, 1)
  187. _slides.splice(newIndex, 0, _slide)
  188. slidesStore.setSlides(_slides)
  189. slidesStore.updateSlideIndex(newIndex)
  190. }
  191. const isEmptySlide = computed(() => {
  192. if (slides.value.length > 1) return false
  193. if (slides.value[0].elements.length > 0) return false
  194. return true
  195. })
  196. return {
  197. resetSlides,
  198. updateSlideIndex,
  199. copySlide,
  200. pasteSlide,
  201. createSlide,
  202. replaceAllSlidesWithNew,
  203. createSlideByTemplate,
  204. copyAndPasteSlide,
  205. deleteSlide,
  206. cutSlide,
  207. selectAllSlide,
  208. sortSlides,
  209. isEmptySlide,
  210. }
  211. }