useSlideHandler.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  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. if (command === KEYS.UP && slideIndex.value > 0) {
  45. if (activeElementIdList.value.length) mainStore.setActiveElementIdList([])
  46. slidesStore.updateSlideIndex(slideIndex.value - 1)
  47. }
  48. else if (command === KEYS.DOWN && slideIndex.value < slides.value.length - 1) {
  49. if (activeElementIdList.value.length) mainStore.setActiveElementIdList([])
  50. slidesStore.updateSlideIndex(slideIndex.value + 1)
  51. }
  52. }
  53. // 将当前页面数据加密后复制到剪贴板
  54. const copySlide = () => {
  55. const text = encrypt(JSON.stringify({
  56. type: 'slides',
  57. data: selectedSlides.value,
  58. }))
  59. copyText(text).then(() => {
  60. mainStore.setThumbnailsFocus(true)
  61. })
  62. }
  63. // 尝试将剪贴板页面数据解密后添加到下一页(粘贴)
  64. const pasteSlide = () => {
  65. readClipboard().then(text => {
  66. pasteTextClipboardData(text, { onlySlide: true })
  67. }).catch(err => message.warning(err))
  68. }
  69. // 创建一页空白页并添加到下一页
  70. const createSlide = () => {
  71. const emptySlide: Slide = {
  72. id: nanoid(10),
  73. elements: [],
  74. background: {
  75. type: 'solid',
  76. color: theme.value.backgroundColor,
  77. },
  78. }
  79. mainStore.setActiveElementIdList([])
  80. slidesStore.addSlide(emptySlide)
  81. addHistorySnapshot()
  82. }
  83. // 根据模板创建新页面
  84. const createSlideByTemplate = (slide: Slide) => {
  85. const { groupIdMap, elIdMap } = createElementIdMap(slide.elements)
  86. const slideWidth = viewportSize.value
  87. const slideHeight = viewportSize.value * viewportRatio.value
  88. // 模板原始宽高(16:9比例)
  89. const templateWidth = 1280
  90. const templateHeight = 720
  91. // 计算缩放因子
  92. const scaleX = slideWidth / templateWidth
  93. const scaleY = slideHeight / templateHeight
  94. for (const element of slide.elements) {
  95. element.id = elIdMap[element.id]
  96. element.top = (element.top || 0) * scaleY
  97. element.left = (element.left || 0) * scaleX
  98. element.width = (element.width || 0) * scaleX
  99. if (element.type === 'text' || element.type === 'shape') {
  100. element.height = (element.height || 0) * scaleY
  101. }
  102. // 处理 content 中的字体大小
  103. if (element.type === 'text') {
  104. // 匹配 style 中的 font-size 属性
  105. element.content = element.content.replace(/font-size:\s*(\d+)px/g, (match, fontSize) => {
  106. const newFontSize = Math.round(parseInt(fontSize) * scaleY)
  107. return `font-size: ${newFontSize}px`
  108. })
  109. }
  110. if (element.groupId) element.groupId = groupIdMap[element.groupId]
  111. }
  112. const newSlide = {
  113. ...slide,
  114. id: nanoid(10),
  115. }
  116. mainStore.setActiveElementIdList([])
  117. slidesStore.addSlide(newSlide)
  118. addHistorySnapshot()
  119. }
  120. // 将当前页复制一份到下一页
  121. const copyAndPasteSlide = () => {
  122. const slide = JSON.parse(JSON.stringify(currentSlide.value))
  123. addSlidesFromData([slide])
  124. }
  125. // 删除当前页,若将删除全部页面,则执行重置幻灯片操作
  126. const deleteSlide = (targetSlidesId = selectedSlidesId.value) => {
  127. if (slides.value.length === targetSlidesId.length) resetSlides()
  128. else slidesStore.deleteSlide(targetSlidesId)
  129. mainStore.updateSelectedSlidesIndex([])
  130. addHistorySnapshot()
  131. }
  132. // 将当前页复制后删除(剪切)
  133. // 由于复制操作会导致多选状态消失,所以需要提前将需要删除的页面ID进行缓存
  134. const cutSlide = () => {
  135. const targetSlidesId = [...selectedSlidesId.value]
  136. copySlide()
  137. deleteSlide(targetSlidesId)
  138. }
  139. // 选中全部幻灯片
  140. const selectAllSlide = () => {
  141. const newSelectedSlidesIndex = Array.from(Array(slides.value.length), (item, index) => index)
  142. mainStore.setActiveElementIdList([])
  143. mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex)
  144. }
  145. // 拖拽调整幻灯片顺序同步数据
  146. const sortSlides = (newIndex: number, oldIndex: number) => {
  147. if (oldIndex === newIndex) return
  148. const _slides: Slide[] = JSON.parse(JSON.stringify(slides.value))
  149. const movingSlide = _slides[oldIndex]
  150. const movingSlideSection = movingSlide.sectionTag
  151. if (movingSlideSection) {
  152. const movingSlideSectionNext = _slides[oldIndex + 1]
  153. delete movingSlide.sectionTag
  154. if (movingSlideSectionNext && !movingSlideSectionNext.sectionTag) {
  155. movingSlideSectionNext.sectionTag = movingSlideSection
  156. }
  157. }
  158. if (newIndex === 0) {
  159. const firstSection = _slides[0].sectionTag
  160. if (firstSection) {
  161. delete _slides[0].sectionTag
  162. movingSlide.sectionTag = firstSection
  163. }
  164. }
  165. const _slide = _slides[oldIndex]
  166. _slides.splice(oldIndex, 1)
  167. _slides.splice(newIndex, 0, _slide)
  168. slidesStore.setSlides(_slides)
  169. slidesStore.updateSlideIndex(newIndex)
  170. }
  171. const isEmptySlide = computed(() => {
  172. if (slides.value.length > 1) return false
  173. if (slides.value[0].elements.length > 0) return false
  174. return true
  175. })
  176. return {
  177. resetSlides,
  178. updateSlideIndex,
  179. copySlide,
  180. pasteSlide,
  181. createSlide,
  182. createSlideByTemplate,
  183. copyAndPasteSlide,
  184. deleteSlide,
  185. cutSlide,
  186. selectAllSlide,
  187. sortSlides,
  188. isEmptySlide,
  189. }
  190. }