ElementAnimationPanel.vue 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. <template>
  2. <div class="element-animation-panel">
  3. <div class="element-animation" v-if="handleElement">
  4. <Popover trigger="click" v-model:visible="animationPoolVisible">
  5. <template #content>
  6. <div class="animation-pool">
  7. <div class="pool-type" v-for="type in animations" :key="type.name">
  8. <div class="type-title">{{type.name}}:</div>
  9. <div class="pool-item-wrapper">
  10. <div
  11. class="pool-item"
  12. v-for="item in type.children" :key="item.name"
  13. :class="[
  14. 'animate__animated',
  15. 'animate__faster',
  16. hoverPreviewAnimation === item.value && `animate__${item.value}`,
  17. ]"
  18. @mouseover="hoverPreviewAnimation = item.value"
  19. @click="addAnimation(item.value)"
  20. >
  21. {{item.name}}
  22. </div>
  23. </div>
  24. </div>
  25. </div>
  26. </template>
  27. <Button class="element-animation-btn">
  28. <IconEffects style="margin-right: 5px;" /> {{handleElementAnimation || '点击选择动画'}}
  29. </Button>
  30. </Popover>
  31. </div>
  32. <div class="tip" v-else><IconClick /> 选中画布中的元素添加动画</div>
  33. <Divider />
  34. <Draggable
  35. class="animation-sequence"
  36. :modelValue="animationSequence"
  37. :animation="300"
  38. :scroll="true"
  39. :scrollSensitivity="50"
  40. @end="handleDragEnd"
  41. itemKey="id"
  42. >
  43. <template #item="{ element, index }">
  44. <div class="sequence-item" :class="{ 'active': handleElement?.id === element.elId }">
  45. <div class="index">{{index + 1}}</div>
  46. <div class="text">【{{element.elType}}】{{element.animationType}}</div>
  47. <div class="handler">
  48. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="预览">
  49. <IconPlayOne class="handler-btn" @click="runAnimation(element.elId, element.type)" />
  50. </Tooltip>
  51. <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="删除">
  52. <IconCloseSmall class="handler-btn" @click="deleteAnimation(element.elId)" />
  53. </Tooltip>
  54. </div>
  55. </div>
  56. </template>
  57. </Draggable>
  58. </div>
  59. </template>
  60. <script lang="ts">
  61. import { computed, defineComponent, ref, Ref } from 'vue'
  62. import { useStore } from 'vuex'
  63. import { MutationTypes, State } from '@/store'
  64. import { PPTAnimation, PPTElement, Slide } from '@/types/slides'
  65. import { ANIMATIONS } from '@/configs/animation'
  66. import { ELEMENT_TYPE } from '@/configs/element'
  67. import useHistorySnapshot from '@/hooks/useHistorySnapshot'
  68. import Draggable from 'vuedraggable'
  69. const animationTypes: { [key: string]: string } = {}
  70. for(const type of ANIMATIONS) {
  71. for(const animation of type.children) {
  72. animationTypes[animation.value] = animation.name
  73. }
  74. }
  75. export default defineComponent({
  76. name: 'element-animation-panel',
  77. components: {
  78. Draggable,
  79. },
  80. setup() {
  81. const store = useStore<State>()
  82. const handleElement: Ref<PPTElement> = computed(() => store.getters.handleElement)
  83. const currentSlideAnimations: Ref<PPTAnimation[] | null> = computed(() => store.getters.currentSlideAnimations)
  84. const currentSlide: Ref<Slide> = computed(() => store.getters.currentSlide)
  85. const hoverPreviewAnimation = ref('')
  86. const animationPoolVisible = ref(false)
  87. const { addHistorySnapshot } = useHistorySnapshot()
  88. const animations = ANIMATIONS
  89. const animationSequence = computed(() => {
  90. if(!currentSlideAnimations.value) return []
  91. const animationSequence = []
  92. for(const animation of currentSlideAnimations.value) {
  93. const el = currentSlide.value.elements.find(el => el.id === animation.elId)
  94. if(!el) continue
  95. const elType = ELEMENT_TYPE[el.type]
  96. const animationType = animationTypes[animation.type]
  97. animationSequence.push({
  98. ...animation,
  99. elType,
  100. animationType,
  101. })
  102. }
  103. return animationSequence
  104. })
  105. const handleElementAnimation = computed(() => {
  106. if(!handleElement.value) return null
  107. const animations = currentSlideAnimations.value || []
  108. const animation = animations.find(item => item.elId === handleElement.value.id)
  109. if(!animation) return null
  110. return animationTypes[animation.type]
  111. })
  112. const updateElementAnimation = (type: string) => {
  113. const animations = (currentSlideAnimations.value as PPTAnimation[]).map(item => {
  114. if(item.elId === handleElement.value.id) return { ...item, type }
  115. return item
  116. })
  117. store.commit(MutationTypes.UPDATE_SLIDE, { animations })
  118. animationPoolVisible.value = false
  119. addHistorySnapshot()
  120. }
  121. const addAnimation = (type: string) => {
  122. if(handleElementAnimation.value) {
  123. updateElementAnimation(type)
  124. return
  125. }
  126. const animations: PPTAnimation[] = currentSlideAnimations.value ? JSON.parse(JSON.stringify(currentSlideAnimations.value)) : []
  127. animations.push({
  128. elId: handleElement.value.id,
  129. type,
  130. duration: 1000,
  131. })
  132. store.commit(MutationTypes.UPDATE_SLIDE, { animations })
  133. animationPoolVisible.value = false
  134. addHistorySnapshot()
  135. }
  136. const deleteAnimation = (elId: string) => {
  137. const animations = (currentSlideAnimations.value as PPTAnimation[]).filter(item => item.elId !== elId)
  138. store.commit(MutationTypes.UPDATE_SLIDE, { animations })
  139. addHistorySnapshot()
  140. }
  141. const handleDragEnd = (eventData: { newIndex: number; oldIndex: number }) => {
  142. const { newIndex, oldIndex } = eventData
  143. if(oldIndex === newIndex) return
  144. const animations: PPTAnimation[] = JSON.parse(JSON.stringify(currentSlideAnimations.value))
  145. const animation = animations[oldIndex]
  146. animations.splice(oldIndex, 1)
  147. animations.splice(newIndex, 0, animation)
  148. store.commit(MutationTypes.UPDATE_SLIDE, { animations })
  149. addHistorySnapshot()
  150. }
  151. const runAnimation = (elId: string, animationType: string) => {
  152. const prefix = 'animate__'
  153. const elRef = document.querySelector(`#editable-element-${elId} [class^=editable-element-]`)
  154. if(elRef) {
  155. const animationName = `${prefix}${animationType}`
  156. elRef.classList.add(`${prefix}animated`, animationName)
  157. const handleAnimationEnd = () => {
  158. elRef.classList.remove(`${prefix}animated`, animationName)
  159. }
  160. elRef.addEventListener('animationend', handleAnimationEnd, { once: true })
  161. }
  162. }
  163. return {
  164. handleElement,
  165. animationPoolVisible,
  166. animations,
  167. animationSequence,
  168. hoverPreviewAnimation,
  169. handleElementAnimation,
  170. addAnimation,
  171. deleteAnimation,
  172. handleDragEnd,
  173. runAnimation,
  174. }
  175. },
  176. })
  177. </script>
  178. <style lang="scss" scoped>
  179. .element-animation-btn {
  180. width: 100%;
  181. }
  182. .tip {
  183. text-align: center;
  184. font-style: italic;
  185. padding-top: 12px;
  186. }
  187. .animation-pool {
  188. width: 400px;
  189. height: 500px;
  190. overflow-y: auto;
  191. overflow-x: hidden;
  192. font-size: 12px;
  193. margin-right: -12px;
  194. padding-right: 12px;
  195. }
  196. .type-title {
  197. width: 100%;
  198. font-size: 13px;
  199. margin-bottom: 10px;
  200. border-left: 4px solid #aaa;
  201. background-color: #eee;
  202. padding: 2px 0 3px 10px;
  203. }
  204. .pool-item-wrapper {
  205. @include grid-layout-wrapper();
  206. }
  207. .pool-item {
  208. @include grid-layout-item(4, 24%);
  209. margin-bottom: 10px;
  210. height: 40px;
  211. line-height: 40px;
  212. text-align: center;
  213. background-color: $lightGray;
  214. cursor: pointer;
  215. }
  216. .sequence-item {
  217. height: 36px;
  218. display: flex;
  219. align-items: center;
  220. border: 1px solid $borderColor;
  221. padding: 6px;
  222. border-radius: $borderRadius;
  223. margin-bottom: 8px;
  224. &.active {
  225. border-color: $themeColor;
  226. }
  227. .index {
  228. flex: 1;
  229. }
  230. .text {
  231. flex: 6;
  232. }
  233. .handler {
  234. flex: 2;
  235. font-size: 15px;
  236. text-align: right;
  237. }
  238. .handler-btn {
  239. margin-left: 8px;
  240. cursor: pointer;
  241. }
  242. }
  243. </style>