index.vue 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. <template>
  2. <div
  3. class="editable-element-image"
  4. :class="{ 'lock': elementInfo.lock }"
  5. :style="{
  6. top: elementInfo.top + 'px',
  7. left: elementInfo.left + 'px',
  8. width: elementInfo.width + 'px',
  9. height: elementInfo.height + 'px',
  10. }"
  11. >
  12. <div
  13. class="rotate-wrapper"
  14. :style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
  15. >
  16. <ImageClipHandler
  17. v-if="isCliping"
  18. :src="elementInfo.src"
  19. :clipData="elementInfo.clip"
  20. :width="elementInfo.width"
  21. :height="elementInfo.height"
  22. :top="elementInfo.top"
  23. :left="elementInfo.left"
  24. :rotate="elementInfo.rotate"
  25. :clipPath="clipShape.style"
  26. @clip="range => handleClip(range)"
  27. />
  28. <div
  29. class="element-content"
  30. v-else
  31. :style="{
  32. filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
  33. transform: flipStyle,
  34. }"
  35. v-contextmenu="contextmenus"
  36. @mousedown="$event => handleSelectElement($event)"
  37. @touchstart="$event => handleSelectElement($event)"
  38. >
  39. <ImageOutline :elementInfo="elementInfo" />
  40. <div class="image-content" :style="{ clipPath: clipShape.style }">
  41. <img
  42. :src="elementInfo.src"
  43. :draggable="false"
  44. :style="{
  45. top: imgPosition.top,
  46. left: imgPosition.left,
  47. width: imgPosition.width,
  48. height: imgPosition.height,
  49. filter: filter,
  50. }"
  51. @dragstart.prevent
  52. alt=""
  53. />
  54. <div class="color-mask"
  55. v-if="elementInfo.colorMask"
  56. :style="{
  57. backgroundColor: elementInfo.colorMask,
  58. }"
  59. ></div>
  60. </div>
  61. </div>
  62. </div>
  63. </div>
  64. </template>
  65. <script lang="ts" setup>
  66. import { computed, watch } from 'vue'
  67. import { storeToRefs } from 'pinia'
  68. import { useMainStore, useSlidesStore } from '@/store'
  69. import type { ImageElementClip, PPTImageElement } from '@/types/slides'
  70. import type { ImageClipedEmitData } from '@/types/edit'
  71. import type { ContextmenuItem } from '@/components/Contextmenu/types'
  72. import useElementShadow from '@/views/components/element/hooks/useElementShadow'
  73. import useElementFlip from '@/views/components/element/hooks/useElementFlip'
  74. import useHistorySnapshot from '@/hooks/useHistorySnapshot'
  75. import useClipImage from './useClipImage'
  76. import useFilter from './useFilter'
  77. import ImageOutline from './ImageOutline/index.vue'
  78. import ImageClipHandler from './ImageClipHandler.vue'
  79. const props = defineProps<{
  80. elementInfo: PPTImageElement
  81. selectElement: (e: MouseEvent | TouchEvent, element: PPTImageElement, canMove?: boolean) => void
  82. contextmenus: () => ContextmenuItem[] | null
  83. }>()
  84. const mainStore = useMainStore()
  85. const slidesStore = useSlidesStore()
  86. const { clipingImageElementId } = storeToRefs(mainStore)
  87. const isCliping = computed(() => clipingImageElementId.value === props.elementInfo.id)
  88. const { addHistorySnapshot } = useHistorySnapshot()
  89. const shadow = computed(() => props.elementInfo.shadow)
  90. const { shadowStyle } = useElementShadow(shadow)
  91. const flipH = computed(() => props.elementInfo.flipH)
  92. const flipV = computed(() => props.elementInfo.flipV)
  93. const { flipStyle } = useElementFlip(flipH, flipV)
  94. const imageElement = computed(() => props.elementInfo)
  95. const { clipShape, imgPosition } = useClipImage(imageElement)
  96. const filters = computed(() => props.elementInfo.filters)
  97. const { filter } = useFilter(filters)
  98. const handleSelectElement = (e: MouseEvent | TouchEvent) => {
  99. if (props.elementInfo.lock) return
  100. e.stopPropagation()
  101. props.selectElement(e, props.elementInfo)
  102. }
  103. const handleClip = (data: ImageClipedEmitData | null) => {
  104. mainStore.setClipingImageElementId('')
  105. if (!data) return
  106. const { range, position } = data
  107. const originClip: ImageElementClip = props.elementInfo.clip || { shape: 'rect', range: [[0, 0], [100, 100]] }
  108. const left = props.elementInfo.left + position.left
  109. const top = props.elementInfo.top + position.top
  110. const width = props.elementInfo.width + position.width
  111. const height = props.elementInfo.height + position.height
  112. let centerOffsetX = 0
  113. let centerOffsetY = 0
  114. if (props.elementInfo.rotate) {
  115. const centerX = (left + width / 2) - (props.elementInfo.left + props.elementInfo.width / 2)
  116. const centerY = -((top + height / 2) - (props.elementInfo.top + props.elementInfo.height / 2))
  117. const radian = -props.elementInfo.rotate * Math.PI / 180
  118. const rotatedCenterX = centerX * Math.cos(radian) - centerY * Math.sin(radian)
  119. const rotatedCenterY = centerX * Math.sin(radian) + centerY * Math.cos(radian)
  120. centerOffsetX = rotatedCenterX - centerX
  121. centerOffsetY = -(rotatedCenterY - centerY)
  122. }
  123. const _props = {
  124. clip: { ...originClip, range },
  125. left: left + centerOffsetX,
  126. top: top + centerOffsetY,
  127. width,
  128. height,
  129. }
  130. slidesStore.updateElement({ id: props.elementInfo.id, props: _props })
  131. addHistorySnapshot()
  132. }
  133. // 监听 src 变化,确保图片实时更新
  134. // watch(() => props.elementInfo.src, (newSrc, oldSrc) => {
  135. // if (newSrc !== oldSrc) {
  136. // // 当 src 变化时,确保图片能够重新加载
  137. // const imgElement = document.querySelector(`img[src="${oldSrc}"]`) as HTMLImageElement
  138. // if (imgElement) {
  139. // imgElement.src = newSrc
  140. // }
  141. // }
  142. // }, { deep: true })
  143. </script>
  144. <style lang="scss" scoped>
  145. .editable-element-image {
  146. position: absolute;
  147. &.lock .element-content {
  148. cursor: default;
  149. }
  150. }
  151. .rotate-wrapper {
  152. width: 100%;
  153. height: 100%;
  154. }
  155. .element-content {
  156. width: 100%;
  157. height: 100%;
  158. position: relative;
  159. cursor: move;
  160. .image-content {
  161. width: 100%;
  162. height: 100%;
  163. overflow: hidden;
  164. position: relative;
  165. }
  166. img {
  167. position: absolute;
  168. }
  169. }
  170. .color-mask {
  171. position: absolute;
  172. top: 0;
  173. bottom: 0;
  174. left: 0;
  175. right: 0;
  176. }
  177. </style>