refactor: 重构元素动画

This commit is contained in:
pipipi-pikachu 2022-05-13 15:12:51 +08:00
parent b5383c45fe
commit a67370c48d
12 changed files with 374 additions and 435 deletions

View File

@ -1,4 +1,8 @@
export const ANIMATIONS = [ export const ANIMATION_DEFAULT_DURATION = 1000
export const ANIMATION_DEFAULT_TRIGGER = 'click'
export const ANIMATION_CLASS_PREFIX = 'animate__'
export const ENTER_ANIMATIONS = [
{ {
type: 'bounce', type: 'bounce',
name: '弹跳', name: '弹跳',
@ -9,7 +13,6 @@ export const ANIMATIONS = [
{ name: '向上弹入', value: 'bounceInUp' }, { name: '向上弹入', value: 'bounceInUp' },
{ name: '向下弹入', value: 'bounceInDown' }, { name: '向下弹入', value: 'bounceInDown' },
], ],
hiddenElement: []
}, },
{ {
type: 'fade', type: 'fade',
@ -29,19 +32,17 @@ export const ANIMATIONS = [
{ name: '从左下浮入', value: 'fadeInBottomLeft' }, { name: '从左下浮入', value: 'fadeInBottomLeft' },
{ name: '从右下浮入', value: 'fadeInBottomRight' }, { name: '从右下浮入', value: 'fadeInBottomRight' },
], ],
hiddenElement: []
}, },
{ {
type: 'rotate', type: 'rotate',
name: '旋转', name: '旋转',
children: [ children: [
{ name: '旋转进入', value: 'rotateIn' }, { name: '旋转进入', value: 'rotateIn' },
{ name: '绕左下旋转进入', value: 'rotateInDownLeft' }, { name: '绕左下进入', value: 'rotateInDownLeft' },
{ name: '绕右下旋转进入', value: 'rotateInDownRight' }, { name: '绕右下进入', value: 'rotateInDownRight' },
{ name: '绕左上旋转进入', value: 'rotateInUpLeft' }, { name: '绕左上进入', value: 'rotateInUpLeft' },
{ name: '绕右上旋转进入', value: 'rotateInUpRight' }, { name: '绕右上进入', value: 'rotateInUpRight' },
], ],
hiddenElement: []
}, },
{ {
type: 'zoom', type: 'zoom',
@ -53,7 +54,6 @@ export const ANIMATIONS = [
{ name: '从右放大进入', value: 'zoomInRight' }, { name: '从右放大进入', value: 'zoomInRight' },
{ name: '向上放大进入', value: 'zoomInUp' }, { name: '向上放大进入', value: 'zoomInUp' },
], ],
hiddenElement: []
}, },
{ {
type: 'slide', type: 'slide',
@ -64,7 +64,6 @@ export const ANIMATIONS = [
{ name: '从左滑入', value: 'slideInRight' }, { name: '从左滑入', value: 'slideInRight' },
{ name: '向上滑入', value: 'slideInUp' }, { name: '向上滑入', value: 'slideInUp' },
], ],
hiddenElement: []
}, },
{ {
type: 'flip', type: 'flip',
@ -73,7 +72,6 @@ export const ANIMATIONS = [
{ name: 'X轴翻转进入', value: 'flipInX' }, { name: 'X轴翻转进入', value: 'flipInX' },
{ name: 'Y轴翻转进入', value: 'flipInY' }, { name: 'Y轴翻转进入', value: 'flipInY' },
], ],
hiddenElement: []
}, },
{ {
type: 'back', type: 'back',
@ -84,7 +82,6 @@ export const ANIMATIONS = [
{ name: '从右放大滑入', value: 'backInRight' }, { name: '从右放大滑入', value: 'backInRight' },
{ name: '向上放大滑入', value: 'backInUp' }, { name: '向上放大滑入', value: 'backInUp' },
], ],
hiddenElement: []
}, },
{ {
type: 'lightSpeed', type: 'lightSpeed',
@ -93,11 +90,10 @@ export const ANIMATIONS = [
{ name: '从右飞入', value: 'lightSpeedInRight' }, { name: '从右飞入', value: 'lightSpeedInRight' },
{ name: '从左飞入', value: 'lightSpeedInLeft' }, { name: '从左飞入', value: 'lightSpeedInLeft' },
], ],
hiddenElement: []
}, },
] ]
export const ANIMATIONS_EXITS = [ export const EXIT_ANIMATIONS = [
{ {
type: 'bounce', type: 'bounce',
name: '弹跳', name: '弹跳',
@ -108,7 +104,6 @@ export const ANIMATIONS_EXITS = [
{ name: '向上弹出', value: 'bounceOutUp' }, { name: '向上弹出', value: 'bounceOutUp' },
{ name: '向下弹出', value: 'bounceOutDown' }, { name: '向下弹出', value: 'bounceOutDown' },
], ],
hiddenElement: []
}, },
{ {
type: 'fade', type: 'fade',
@ -128,19 +123,17 @@ export const ANIMATIONS_EXITS = [
{ name: '从左下浮出', value: 'fadeOutBottomLeft' }, { name: '从左下浮出', value: 'fadeOutBottomLeft' },
{ name: '从右下浮出', value: 'fadeOutBottomRight' }, { name: '从右下浮出', value: 'fadeOutBottomRight' },
], ],
hiddenElement: []
}, },
{ {
type: 'rotate', type: 'rotate',
name: '旋转', name: '旋转',
children: [ children: [
{ name: '旋转退出', value: 'rotateOut' }, { name: '旋转退出', value: 'rotateOut' },
{ name: '绕左下旋转退出', value: 'rotateOutDownLeft' }, { name: '绕左下退出', value: 'rotateOutDownLeft' },
{ name: '绕右下旋转退出', value: 'rotateOutDownRight' }, { name: '绕右下退出', value: 'rotateOutDownRight' },
{ name: '绕左上旋转退出', value: 'rotateOutUpLeft' }, { name: '绕左上退出', value: 'rotateOutUpLeft' },
{ name: '绕右上旋转退出', value: 'rotateOutUpRight' }, { name: '绕右上退出', value: 'rotateOutUpRight' },
], ],
hiddenElement: []
}, },
{ {
type: 'zoom', type: 'zoom',
@ -152,7 +145,6 @@ export const ANIMATIONS_EXITS = [
{ name: '从右缩小退出', value: 'zoomOutRight' }, { name: '从右缩小退出', value: 'zoomOutRight' },
{ name: '向上缩小退出', value: 'zoomOutUp' }, { name: '向上缩小退出', value: 'zoomOutUp' },
], ],
hiddenElement: []
}, },
{ {
type: 'slide', type: 'slide',
@ -163,7 +155,6 @@ export const ANIMATIONS_EXITS = [
{ name: '从右滑出', value: 'slideOutRight' }, { name: '从右滑出', value: 'slideOutRight' },
{ name: '向上滑出', value: 'slideOutUp' }, { name: '向上滑出', value: 'slideOutUp' },
], ],
hiddenElement: []
}, },
{ {
type: 'flip', type: 'flip',
@ -172,7 +163,6 @@ export const ANIMATIONS_EXITS = [
{ name: 'X轴翻转退出', value: 'flipOutX' }, { name: 'X轴翻转退出', value: 'flipOutX' },
{ name: 'Y轴翻转退出', value: 'flipOutY' }, { name: 'Y轴翻转退出', value: 'flipOutY' },
], ],
hiddenElement: []
}, },
{ {
type: 'back', type: 'back',
@ -183,7 +173,6 @@ export const ANIMATIONS_EXITS = [
{ name: '从右缩小滑出', value: 'backOutRight' }, { name: '从右缩小滑出', value: 'backOutRight' },
{ name: '向上缩小滑出', value: 'backOutUp' }, { name: '向上缩小滑出', value: 'backOutUp' },
], ],
hiddenElement: []
}, },
{ {
type: 'lightSpeed', type: 'lightSpeed',
@ -192,6 +181,5 @@ export const ANIMATIONS_EXITS = [
{ name: '从右飞出', value: 'lightSpeedOutRight' }, { name: '从右飞出', value: 'lightSpeedOutRight' },
{ name: '从左飞出', value: 'lightSpeedOutLeft' }, { name: '从左飞出', value: 'lightSpeedOutLeft' },
], ],
hiddenElement: []
}, },
] ]

View File

@ -33,7 +33,6 @@ import {
Drawer, Drawer,
Spin, Spin,
Alert, Alert,
Tabs,
} from 'ant-design-vue' } from 'ant-design-vue'
const app = createApp(App) const app = createApp(App)
@ -63,8 +62,6 @@ app.component('Checkbox', Checkbox)
app.component('Drawer', Drawer) app.component('Drawer', Drawer)
app.component('Spin', Spin) app.component('Spin', Spin)
app.component('Alert', Alert) app.component('Alert', Alert)
app.component('Tabs', Tabs)
app.component('TabPane', Tabs.TabPane)
app.use(Icon) app.use(Icon)
app.use(Component) app.use(Component)

View File

@ -1,7 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import tinycolor from 'tinycolor2' import tinycolor from 'tinycolor2'
import { omit } from 'lodash' import { omit } from 'lodash'
import { Slide, SlideTheme, PPTElement } from '@/types/slides' import { Slide, SlideTheme, PPTElement, PPTAnimation } from '@/types/slides'
import { slides } from '@/mocks/slides' import { slides } from '@/mocks/slides'
import { theme } from '@/mocks/theme' import { theme } from '@/mocks/theme'
import { layouts } from '@/mocks/layout' import { layouts } from '@/mocks/layout'
@ -16,6 +16,11 @@ interface UpdateElementData {
props: Partial<PPTElement>; props: Partial<PPTElement>;
} }
interface FormatedAnimation {
animations: PPTAnimation[];
autoNext: boolean;
}
export interface SlidesState { export interface SlidesState {
theme: SlideTheme; theme: SlideTheme;
slides: Slide[]; slides: Slide[];
@ -38,13 +43,43 @@ export const useSlidesStore = defineStore('slides', {
currentSlideAnimations(state) { currentSlideAnimations(state) {
const currentSlide = state.slides[state.slideIndex] const currentSlide = state.slides[state.slideIndex]
if (!currentSlide) return null if (!currentSlide?.animations) return []
const animations = currentSlide.animations
if (!animations) return null
const els = currentSlide.elements const els = currentSlide.elements
const elIds = els.map(el => el.id) const elIds = els.map(el => el.id)
return animations.filter(animation => elIds.includes(animation.elId)) return currentSlide.animations.filter(animation => elIds.includes(animation.elId))
},
// 格式化的当前页动画
// 将触发条件为“与上一动画同时”的项目向上合并到序列中的同一位置
// 为触发条件为“上一动画之后”项目的上一项添加自动向下执行标记
formatedAnimations(state) {
const currentSlide = state.slides[state.slideIndex]
if (!currentSlide?.animations) return []
const els = currentSlide.elements
const elIds = els.map(el => el.id)
const animations = currentSlide.animations.filter(animation => elIds.includes(animation.elId))
const formatedAnimations: FormatedAnimation[] = []
for (const animation of animations) {
if (animation.trigger === 'click') {
formatedAnimations.push({ animations: [animation], autoNext: false })
}
if (animation.trigger === 'meantime') {
const last = formatedAnimations[formatedAnimations.length - 1] || { animations: [], autoNext: false }
last.animations = last.animations.filter(item => item.elId !== animation.elId)
last.animations.push(animation)
formatedAnimations[formatedAnimations.length - 1] = last
}
if (animation.trigger === 'auto') {
const last = formatedAnimations[formatedAnimations.length - 1] || { animations: [], autoNext: false }
last.autoNext = true
formatedAnimations[formatedAnimations.length - 1] = last
formatedAnimations.push({ animations: [animation], autoNext: false })
}
}
return formatedAnimations
}, },
layouts(state) { layouts(state) {

View File

@ -572,24 +572,21 @@ export type PPTElement = PPTTextElement | PPTImageElement | PPTShapeElement | PP
* *
* elId: 元素ID * elId: 元素ID
* *
* type:
*
* effect: 动画效果 * effect: 动画效果
* *
* type: 退
*
* duration: 动画持续时间 * duration: 动画持续时间
* *
* delay: 动画延迟时间 * trigger: 动画触发方式(click - meantime - auto - )
*
* implement: 动画启动方式(0->1->2->)
*/ */
export interface PPTAnimation { export interface PPTAnimation {
id: string; id: string;
elId: string; elId: string;
type: string;
effect: string; effect: string;
type: 'in' | 'out';
duration: number; duration: number;
delay: number; trigger: 'click' | 'meantime' | 'auto';
implement: number;
} }
/** /**

View File

@ -4,8 +4,6 @@ export const enum EmitterEvents {
RICH_TEXT_COMMAND = 'RICH_TEXT_COMMAND', RICH_TEXT_COMMAND = 'RICH_TEXT_COMMAND',
OPEN_CHART_DATA_EDITOR = 'OPEN_CHART_DATA_EDITOR', OPEN_CHART_DATA_EDITOR = 'OPEN_CHART_DATA_EDITOR',
OPEN_LATEX_EDITOR = 'OPEN_LATEX_EDITOR', OPEN_LATEX_EDITOR = 'OPEN_LATEX_EDITOR',
RUN_ANIMATION = 'RUN_ANIMATION',
END_RUN_ANIMATION = 'END_RUN_ANIMATION',
} }
export interface RichTextAction { export interface RichTextAction {
@ -22,8 +20,6 @@ type Events = {
[EmitterEvents.RICH_TEXT_COMMAND]: RichTextCommand; [EmitterEvents.RICH_TEXT_COMMAND]: RichTextCommand;
[EmitterEvents.OPEN_CHART_DATA_EDITOR]: void; [EmitterEvents.OPEN_CHART_DATA_EDITOR]: void;
[EmitterEvents.OPEN_LATEX_EDITOR]: void; [EmitterEvents.OPEN_LATEX_EDITOR]: void;
[EmitterEvents.RUN_ANIMATION]: void;
[EmitterEvents.END_RUN_ANIMATION]: void;
} }
const emitter: Emitter<Events> = mitt<Events>() const emitter: Emitter<Events> = mitt<Events>()

View File

@ -21,9 +21,9 @@
<div <div
class="animation-index" class="animation-index"
v-if="toolbarState === 'elAnimation' && elementIndexInAnimation !== -1" v-if="toolbarState === 'elAnimation' && elementIndexListInAnimation.length"
> >
{{elementIndexInAnimation + 1}} <div class="index-item" v-for="index in elementIndexListInAnimation" :key="index">{{index + 1}}</div>
</div> </div>
<LinkHandler <LinkHandler
@ -96,7 +96,7 @@ export default defineComponent({
}, },
setup(props) { setup(props) {
const { canvasScale, toolbarState } = storeToRefs(useMainStore()) const { canvasScale, toolbarState } = storeToRefs(useMainStore())
const { currentSlide } = storeToRefs(useSlidesStore()) const { formatedAnimations } = storeToRefs(useSlidesStore())
const currentOperateComponent = computed(() => { const currentOperateComponent = computed(() => {
const elementTypeMap = { const elementTypeMap = {
@ -113,9 +113,13 @@ export default defineComponent({
return elementTypeMap[props.elementInfo.type] || null return elementTypeMap[props.elementInfo.type] || null
}) })
const elementIndexInAnimation = computed(() => { const elementIndexListInAnimation = computed(() => {
const animations = currentSlide.value.animations || [] const indexList = []
return animations.findIndex(animation => animation.elId === props.elementInfo.id) for (let i = 0; i < formatedAnimations.value.length; i++) {
const elIds = formatedAnimations.value[i].animations.map(item => item.elId)
if (elIds.includes(props.elementInfo.id)) indexList.push(i)
}
return indexList
}) })
const rotate = computed(() => 'rotate' in props.elementInfo ? props.elementInfo.rotate : 0) const rotate = computed(() => 'rotate' in props.elementInfo ? props.elementInfo.rotate : 0)
@ -125,7 +129,7 @@ export default defineComponent({
currentOperateComponent, currentOperateComponent,
canvasScale, canvasScale,
toolbarState, toolbarState,
elementIndexInAnimation, elementIndexListInAnimation,
rotate, rotate,
height, height,
} }
@ -148,13 +152,20 @@ export default defineComponent({
top: 0; top: 0;
left: -24px; left: -24px;
font-size: 12px; font-size: 12px;
width: 18px;
height: 18px; .index-item {
background-color: #fff; width: 18px;
color: $themeColor; height: 18px;
border: 1px solid $themeColor; background-color: #fff;
display: flex; color: $themeColor;
justify-content: center; border: 1px solid $themeColor;
align-items: center; display: flex;
justify-content: center;
align-items: center;
& + .index-item {
margin-top: 5px;
}
}
} }
</style> </style>

View File

@ -7,69 +7,40 @@
@visibleChange="visible => handlePopoverVisibleChange(visible)" @visibleChange="visible => handlePopoverVisibleChange(visible)"
> >
<template #content> <template #content>
<Tabs v-model:activeKey="tabsActiveKey" tab-position="left" type="card"> <div class="tabs">
<TabPane key="animation_in" tab="进场动效"> <div
<div class="animation-pool"> :class="['tab', tab.key, { 'active': activeTab === tab.key }]"
<div class="pool-type" :key="type.name" v-for="type in animations"> v-for="tab in tabs"
<div v-if="!type.hiddenElement.includes(handleElement.type)"> :key="tab.key"
<div class="type-title">{{type.name}}</div> @click="activeTab = tab.key"
<div class="pool-item-wrapper"> >{{tab.label}}</div>
<div </div>
class="pool-item" <template v-for="key in Object.keys(animations)">
v-for="item in type.children" :key="item.name" <div :class="['animation-pool', key]" :key="key" v-if="activeTab === key">
@mouseenter="hoverPreviewAnimation = item.value" <div class="pool-type" :key="effect.name" v-for="effect in animations[key]">
@mouseleave="hoverPreviewAnimation = ''" <div class="type-title">{{effect.name}}</div>
@click="addAnimation(item.value, 'in')" <div class="pool-item-wrapper">
> <div
<div class="pool-item"
class="animation-box" v-for="item in effect.children" :key="item.name"
:class="[ @mouseenter="hoverPreviewAnimation = item.value"
'animate__animated', @mouseleave="hoverPreviewAnimation = ''"
'animate__faster', @click="addAnimation(key, item.value)"
hoverPreviewAnimation === item.value && `animate__${item.value}`, >
]" <div
> class="animation-box"
{{item.name}} :class="[
</div> `${prefix}animated`,
</div> `${prefix}faster`,
</div> hoverPreviewAnimation === item.value && `${prefix}${item.value}`,
]"
>{{item.name}}</div>
</div> </div>
</div> </div>
<div class="mask" v-if="!popoverMaskHide"></div>
</div> </div>
</TabPane> <div class="mask" v-if="!popoverMaskHide"></div>
<TabPane key="animation_out" tab="退场动效"> </div>
<div class="animation-pool"> </template>
<div class="pool-type" :key="type.name" v-for="type in animationsExits">
<div v-if="!type.hiddenElement.includes(handleElement.type)">
<div class="type-title">{{type.name}}</div>
<div class="pool-item-wrapper">
<div
class="pool-item"
v-for="item in type.children" :key="item.name"
@mouseenter="onAnimationOut(item)"
@mouseleave="hoverPreviewAnimation = ''"
@click="addAnimation(item.value, 'out')"
>
<div
class="animation-box"
:class="[
'animate__animated',
'animate__faster',
hoverPreviewAnimation === item.value && `animate__${item.value}`,
]"
>
{{item.name}}
</div>
</div>
</div>
</div>
</div>
<div class="mask" v-if="!popoverMaskHide"></div>
</div>
</TabPane>
</Tabs>
</template> </template>
<Button class="element-animation-btn" @click="handleAnimationId = ''"> <Button class="element-animation-btn" @click="handleAnimationId = ''">
<IconEffects style="margin-right: 5px;" /> 添加动画 <IconEffects style="margin-right: 5px;" /> 添加动画
@ -87,17 +58,18 @@
:animation="300" :animation="300"
:scroll="true" :scroll="true"
:scrollSensitivity="50" :scrollSensitivity="50"
handle=".sequence-content"
@end="handleDragEnd" @end="handleDragEnd"
itemKey="id" itemKey="id"
> >
<template #item="{ element, index }"> <template #item="{ element }">
<div class="sequence-item" :class="{ 'active': handleElement?.id === element.elId }"> <div class="sequence-item" :class="[element.type, { 'active': handleElement?.id === element.elId }]">
<div class="sequence-content"> <div class="sequence-content">
<div class="index">{{index + 1}}</div> <div class="index">{{element.index}}</div>
<div class="text">{{element.elType}}{{element.animationType}}</div> <div class="text">{{element.elType}}{{element.animationEffect}}</div>
<div class="handler"> <div class="handler">
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="预览"> <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="预览">
<IconPlayOne class="handler-btn" @click="runAnimation(element.elId, element.type, element.duration, element.delay)" /> <IconPlayOne class="handler-btn" @click="runAnimation(element.elId, element.effect, element.duration)" />
</Tooltip> </Tooltip>
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="删除"> <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="删除">
<IconCloseSmall class="handler-btn" @click="deleteAnimation(element.id)" /> <IconCloseSmall class="handler-btn" @click="deleteAnimation(element.id)" />
@ -105,51 +77,34 @@
</div> </div>
</div> </div>
<div class="configs" v-if="handleElementAnimation.length && handleElementAnimation[0].elId === element.elId"> <div class="configs" v-if="handleElementAnimation[0]?.elId === element.elId">
<Divider /> <Divider style="margin: 16px 0;" />
<div class="duration"> <div class="config-item">
<div style="flex: 3 1 0%;">动画时长</div> <div style="flex: 3;">持续时长</div>
<InputNumber <InputNumber
:min="100" :min="500"
:max="5000" :max="3000"
:step="100" :step="500"
:value="element.duration" :value="element.duration"
@change="value => updateElementAnimationDuration(element, value)" @change="value => updateElementAnimationDuration(element.id, value)"
style="flex: 4 1 20%;" style="flex: 5;"
/> />
<div class="duration-r"> 毫秒</div>
</div> </div>
<div class="config-item">
<div class="duration"> <div style="flex: 3;">触发方式</div>
<div style="flex: 3;">动画启动方式</div>
<Select <Select
v-model:value="element.implement" :value="element.trigger"
@change="value => updateElementAnimationImplement(element, value)" @change="value => updateElementAnimationTrigger(element.id, value)"
style="flex: 4;" style="flex: 5;"
> >
<SelectOption :value="0">单击时</SelectOption> <SelectOption value="click">主动触发</SelectOption>
<SelectOption :value="1">与上一个一起</SelectOption> <SelectOption value="meantime">与上一动画同时</SelectOption>
<SelectOption :value="2">在上一个之后</SelectOption> <SelectOption value="auto">上一动画之后</SelectOption>
</Select> </Select>
</div> </div>
<div class="config-item">
<div class="duration" v-if="element.implement !== 1"> <Button style="flex: 1;" @click="openAnimationPool(element.id)">更换动画</Button>
<div style="flex: 1;">延迟</div>
<InputNumber
:min="0"
:max="5000"
:step="100"
:value="element.delay"
@change="value => updateElementAnimationDelay(element, value)"
style="flex: 2;"
/>
<div class="duration-r">毫秒启动效果</div>
</div>
<div class="duration duration-btn">
<Button @click="handlePopoverVisibleChange(true), animationPoolVisible = true, handleAnimationId = element.id">更换特效</Button>
<Button @click="updateElementAnimationAll(element)">应用到本页所有</Button>
</div> </div>
</div> </div>
</div> </div>
@ -161,33 +116,39 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, ref, watch } from 'vue' import { computed, defineComponent, ref, watch } from 'vue'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
import emitter, { EmitterEvents } from '@/utils/emitter'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store' import { useMainStore, useSlidesStore } from '@/store'
import { PPTAnimation } from '@/types/slides' import { PPTAnimation } from '@/types/slides'
import { ANIMATIONS, ANIMATIONS_EXITS } from '@/configs/animation' import {
ENTER_ANIMATIONS,
EXIT_ANIMATIONS,
ANIMATION_DEFAULT_DURATION,
ANIMATION_DEFAULT_TRIGGER,
ANIMATION_CLASS_PREFIX,
} from '@/configs/animation'
import { ELEMENT_TYPE_ZH } from '@/configs/element' import { ELEMENT_TYPE_ZH } from '@/configs/element'
import useHistorySnapshot from '@/hooks/useHistorySnapshot' import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import { message } from 'ant-design-vue'
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
const defaultDuration = 1000 const animationEffects: { [key: string]: string } = {}
const defaultDelay = 0 for (const effect of ENTER_ANIMATIONS) {
const defaultImplement = 0 // (0->1->2->) for (const animation of effect.children) {
animationEffects[animation.value] = animation.name
const animationTypes: { [key: string]: string } = {}
for (const type of ANIMATIONS) {
for (const animation of type.children) {
animationTypes[animation.value] = animation.name
} }
} }
for (const type of ANIMATIONS_EXITS) { for (const effect of EXIT_ANIMATIONS) {
for (const animation of type.children) { for (const animation of effect.children) {
animationTypes[animation.value] = animation.name animationEffects[animation.value] = animation.name
} }
} }
type AnimationType = 'in' | 'out'
interface TabItem {
key: AnimationType;
label: string;
}
export default defineComponent({ export default defineComponent({
name: 'element-animation-panel', name: 'element-animation-panel',
components: { components: {
@ -196,54 +157,61 @@ export default defineComponent({
setup() { setup() {
const slidesStore = useSlidesStore() const slidesStore = useSlidesStore()
const { handleElement, handleElementId } = storeToRefs(useMainStore()) const { handleElement, handleElementId } = storeToRefs(useMainStore())
const { currentSlide, currentSlideAnimations } = storeToRefs(slidesStore) const { currentSlide, formatedAnimations, currentSlideAnimations } = storeToRefs(slidesStore)
const tabs: TabItem[] = [
{ key: 'in', label: '入场' },
{ key: 'out', label: '退场' },
]
const activeTab = ref('in')
watch(() => handleElementId.value, () => { watch(() => handleElementId.value, () => {
animationPoolVisible.value = false animationPoolVisible.value = false
}) })
const hoverPreviewAnimation = ref('') const hoverPreviewAnimation = ref('')
const tabsActiveKey = ref('animation_in')
const animationPoolVisible = ref(false) const animationPoolVisible = ref(false)
const { addHistorySnapshot } = useHistorySnapshot() const { addHistorySnapshot } = useHistorySnapshot()
const animations = ANIMATIONS
const animationsExits = ANIMATIONS_EXITS
// //
const animationSequence = computed(() => { const animationSequence = computed(() => {
if (!currentSlideAnimations.value) return []
const animationSequence = [] const animationSequence = []
for (const animation of currentSlideAnimations.value) { for (let i = 0; i < formatedAnimations.value.length; i++) {
const el = currentSlide.value.elements.find(el => el.id === animation.elId) const item = formatedAnimations.value[i]
if (!el) continue for (let j = 0; j < item.animations.length; j++) {
const elType = ELEMENT_TYPE_ZH[el.type] const animation = item.animations[j]
const animationType = animationTypes[animation.type] const el = currentSlide.value.elements.find(el => el.id === animation.elId)
animationSequence.push({ if (!el) continue
...animation,
elType, const elType = ELEMENT_TYPE_ZH[el.type]
animationType, const animationEffect = animationEffects[animation.effect]
}) animationSequence.push({
...animation,
index: j === 0 ? i + 1 : '',
elType,
animationEffect,
})
}
} }
return animationSequence return animationSequence
}) })
// //
const handleElementAnimation = computed(() => { const handleElementAnimation = computed(() => {
const animations = currentSlideAnimations.value || [] const animations = currentSlideAnimations.value
const animation = animations.filter(item => item.elId === handleElementId.value) const animation = animations.filter(item => item.elId === handleElementId.value)
return animation || [] return animation || []
}) })
// //
const deleteAnimation = (id: string) => { const deleteAnimation = (id: string) => {
const animations = (currentSlideAnimations.value as PPTAnimation[]).filter(item => item.id !== id) const animations = currentSlideAnimations.value.filter(item => item.id !== id)
slidesStore.updateSlide({ animations }) slidesStore.updateSlide({ animations })
addHistorySnapshot() addHistorySnapshot()
} }
// //
const handleDragEnd = (eventData: { newIndex: number; oldIndex: number }) => { const handleDragEnd = (eventData: { newIndex: number; oldIndex: number }) => {
const { newIndex, oldIndex } = eventData const { newIndex, oldIndex } = eventData
if (oldIndex === newIndex) return if (oldIndex === newIndex) return
@ -257,88 +225,46 @@ export default defineComponent({
addHistorySnapshot() addHistorySnapshot()
} }
// //
const runAnimation = (elId: string, animationType: string, duration: number, delay: number) => { const runAnimation = (elId: string, effect: string, duration: number) => {
const prefix = 'animate__'
const elRef = document.querySelector(`#editable-element-${elId} [class^=editable-element-]`) const elRef = document.querySelector(`#editable-element-${elId} [class^=editable-element-]`)
if (elRef) { if (elRef) {
setTimeout(() => { const animationName = `${ANIMATION_CLASS_PREFIX}${effect}`
const animationName = `${prefix}${animationType}` document.documentElement.style.setProperty('--animate-duration', `${duration}ms`)
document.documentElement.style.setProperty('--animate-duration', `${duration}ms`) elRef.classList.add(`${ANIMATION_CLASS_PREFIX}animated`, animationName)
elRef.classList.add(`${prefix}animated`, animationName)
emitter.emit(EmitterEvents.RUN_ANIMATION) const handleAnimationEnd = () => {
document.documentElement.style.removeProperty('--animate-duration')
const handleAnimationEnd = () => { elRef.classList.remove(`${ANIMATION_CLASS_PREFIX}animated`, animationName)
document.documentElement.style.removeProperty('--animate-duration') }
elRef.classList.remove(`${prefix}animated`, animationName) elRef.addEventListener('animationend', handleAnimationEnd, { once: true })
emitter.emit(EmitterEvents.END_RUN_ANIMATION)
}
elRef.addEventListener('animationend', handleAnimationEnd, { once: true })
}, delay)
} }
} }
// //
const updateElementAnimationDuration = (element: { id: string }, duration: number) => { const updateElementAnimationDuration = (id: string, duration: number) => {
if (!currentSlideAnimations.value) return
if (duration < 100 || duration > 5000) return if (duration < 100 || duration > 5000) return
const animations = currentSlideAnimations.value.map(item => { const animations = currentSlideAnimations.value.map(item => {
if (item.id === element.id) return { ...item, duration } if (item.id === id) return { ...item, duration }
return item return item
}) })
slidesStore.updateSlide({ animations }) slidesStore.updateSlide({ animations })
addHistorySnapshot() addHistorySnapshot()
} }
// //
const updateElementAnimationImplement = (element: { id: string }, implement: number) => { const updateElementAnimationTrigger = (id: string, trigger: 'click' | 'meantime' | 'auto') => {
if (!currentSlideAnimations.value) return
const animations = currentSlideAnimations.value.map(item => { const animations = currentSlideAnimations.value.map(item => {
if (item.id === element.id) return { ...item, implement } if (item.id === id) return { ...item, trigger }
return item return item
}) })
slidesStore.updateSlide({ animations }) slidesStore.updateSlide({ animations })
addHistorySnapshot() addHistorySnapshot()
} }
// //
const updateElementAnimationDelay = (element: { id: string }, delay: number) => { const updateElementAnimation = (type: AnimationType, effect: string) => {
if (!currentSlideAnimations.value) return
if (delay < 0 || delay > 5000) return
const animations = currentSlideAnimations.value.map(item => {
if (item.id === element.id) return { ...item, delay }
return item
})
slidesStore.updateSlide({ animations })
addHistorySnapshot()
}
//
const updateElementAnimationAll = (element: { duration: number; delay: number; implement: number }) => {
if (!handleElementAnimation.value) return
const animations: PPTAnimation[] = currentSlideAnimations.value ? JSON.parse(JSON.stringify(currentSlideAnimations.value)) : []
const handleElementAnimationMap = {
duration: element.duration,
delay: element.delay,
implement: element.implement
}
const animationsUpdate: PPTAnimation[] = animations.map(x => Object.assign({}, x, handleElementAnimationMap))
slidesStore.updateSlide({ animations: animationsUpdate })
addHistorySnapshot()
message.success({ content: '已应用本页其他动画', duration: 3 })
}
//
const updateElementAnimation = (type: string, effect: string) => {
if (!currentSlideAnimations.value) return
const animations = currentSlideAnimations.value.map(item => { const animations = currentSlideAnimations.value.map(item => {
if (item.id === handleAnimationId.value) return { ...item, type, effect } if (item.id === handleAnimationId.value) return { ...item, type, effect }
return item return item
@ -348,35 +274,33 @@ export default defineComponent({
addHistorySnapshot() addHistorySnapshot()
const animationItem = currentSlideAnimations.value.find(item => item.elId === handleElementId.value) const animationItem = currentSlideAnimations.value.find(item => item.elId === handleElementId.value)
const duration = animationItem?.duration || defaultDuration const duration = animationItem?.duration || ANIMATION_DEFAULT_DURATION
const delay = animationItem?.delay || defaultDelay
runAnimation(handleElementId.value, type, duration, delay) runAnimation(handleElementId.value, effect, duration)
} }
const handleAnimationId = ref('') const handleAnimationId = ref('')
// //
const addAnimation = (type: string, effect: string) => { const addAnimation = (type: AnimationType, effect: string) => {
if (handleAnimationId.value) { if (handleAnimationId.value) {
updateElementAnimation(type, effect) updateElementAnimation(type, effect)
return return
} }
const animations: PPTAnimation[] = currentSlideAnimations.value ? JSON.parse(JSON.stringify(currentSlideAnimations.value)) : [] const animations: PPTAnimation[] = JSON.parse(JSON.stringify(currentSlideAnimations.value))
animations.push({ animations.push({
id: nanoid(10), id: nanoid(10),
elId: handleElementId.value, elId: handleElementId.value,
type, type,
effect, effect,
duration: defaultDuration, duration: ANIMATION_DEFAULT_DURATION,
delay: defaultDelay, trigger: ANIMATION_DEFAULT_TRIGGER,
implement: defaultImplement,
}) })
slidesStore.updateSlide({ animations }) slidesStore.updateSlide({ animations })
animationPoolVisible.value = false animationPoolVisible.value = false
addHistorySnapshot() addHistorySnapshot()
runAnimation(handleElementId.value, type, defaultDuration, defaultDelay) runAnimation(handleElementId.value, effect, ANIMATION_DEFAULT_DURATION)
} }
// 500ms // 500ms
@ -388,52 +312,73 @@ export default defineComponent({
else popoverMaskHide.value = false else popoverMaskHide.value = false
} }
const animationOutItem = ref({}) const openAnimationPool = (elementId: string) => {
const animationOutTimer = ref(0) animationPoolVisible.value = true
const onAnimationOut = (item: { value: string }) => { handleAnimationId.value = elementId
if (item.value === animationOutItem.value) { handlePopoverVisibleChange(true)
return
}
clearTimeout(animationOutTimer.value)
animationOutItem.value = item
hoverPreviewAnimation.value = item.value
animationOutTimer.value = window.setTimeout(() => {
hoverPreviewAnimation.value = ''
}, 500)
} }
return { return {
tabs,
activeTab,
handleAnimationId, handleAnimationId,
handleElement, handleElement,
animationPoolVisible, animationPoolVisible,
animations,
animationsExits,
animationSequence, animationSequence,
hoverPreviewAnimation, hoverPreviewAnimation,
handleElementAnimation, handleElementAnimation,
popoverMaskHide, popoverMaskHide,
animations: {
in: ENTER_ANIMATIONS,
out: EXIT_ANIMATIONS,
},
prefix: ANIMATION_CLASS_PREFIX,
addAnimation, addAnimation,
deleteAnimation, deleteAnimation,
handleDragEnd, handleDragEnd,
runAnimation, runAnimation,
updateElementAnimationDuration, updateElementAnimationDuration,
updateElementAnimationImplement, updateElementAnimationTrigger,
updateElementAnimationDelay,
handlePopoverVisibleChange, handlePopoverVisibleChange,
updateElementAnimationAll, openAnimationPool,
tabsActiveKey,
onAnimationOut,
} }
}, },
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
$inColor: #68a490;
$outColor: #d86344;
.element-animation-panel { .element-animation-panel {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.tabs {
display: flex;
justify-content: flex-start;
align-items: center;
border-bottom: 1px solid $borderColor;
margin-bottom: 20px;
}
.tab {
width: 50%;
padding-bottom: 8px;
border-bottom: 2px solid transparent;
text-align: center;
cursor: pointer;
&.active {
border-bottom: 2px solid $themeColor;
}
&.in.active {
border-bottom-color: $inColor;
}
&.out.active {
border-bottom-color: $outColor;
}
}
.element-animation { .element-animation {
height: 32px; height: 32px;
display: flex; display: flex;
@ -442,19 +387,12 @@ export default defineComponent({
.element-animation-btn { .element-animation-btn {
width: 100%; width: 100%;
} }
.duration { .config-item {
margin-top: 5px;
width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
word-break: keep-all;
.duration-r { & + .config-item {
flex: 2; margin-top: 5px;
text-indent: 8px;
}
&.duration-btn {
margin-top: 20px;
justify-content: space-between;
} }
} }
.tip { .tip {
@ -474,11 +412,16 @@ export default defineComponent({
position: relative; position: relative;
.mask { .mask {
width: 400px; @include absolute-0();
height: 500px; }
position: absolute;
top: 0; &.in .type-title {
left: 0; border-left-color: $inColor;
background-color: rgba($color: $inColor, $alpha: .15);
}
&.out .type-title {
border-left-color: $outColor;
background-color: rgba($color: $outColor, $alpha: .15);
} }
} }
.type-title { .type-title {
@ -513,27 +456,31 @@ export default defineComponent({
@include overflow-overlay(); @include overflow-overlay();
} }
.sequence-item { .sequence-item {
// height: 35px;
border: 1px solid $borderColor; border: 1px solid $borderColor;
padding: 9px 6px; padding: 10px 6px;
border-radius: $borderRadius; border-radius: $borderRadius;
margin-bottom: 8px; margin-bottom: 8px;
transition: all .5s; transition: all .5s;
cursor: grab;
&:active { &.in.active {
cursor: grabbing; border-color: $inColor;
}
&.out.active {
border-color: $outColor;
} }
&.active { &.active {
border-color: $themeColor;
height: auto; height: auto;
} }
.sequence-content { .sequence-content {
display: flex; display: flex;
align-items: center; align-items: center;
// height: 23px; cursor: grab;
&:active {
cursor: grabbing;
}
.index { .index {
flex: 1; flex: 1;
} }

View File

@ -48,7 +48,7 @@ export default defineComponent({
}, },
animationIndex: { animationIndex: {
type: Number, type: Number,
default: -1, required: true,
}, },
turnSlideToId: { turnSlideToId: {
type: Function as PropType<(id: string) => void>, type: Function as PropType<(id: string) => void>,
@ -75,47 +75,28 @@ export default defineComponent({
return elementTypeMap[props.elementInfo.type] || null return elementTypeMap[props.elementInfo.type] || null
}) })
const { currentSlide, theme } = storeToRefs(useSlidesStore()) const { formatedAnimations, theme } = storeToRefs(useSlidesStore())
// //
const needWaitAnimation = computed(() => { const needWaitAnimation = computed(() => {
const animations = currentSlide.value.animations || [] //
const elementIndexInAnimation = animations.findIndex(animation => animation.elId === props.elementInfo.id) const elementIndexInAnimation = formatedAnimations.value.findIndex(item => {
if (elementIndexInAnimation === -1) { const elIds = item.animations.map(item => item.elId)
return false return elIds.includes(props.elementInfo.id)
} })
// effect in //
const firstAnimation = animations.find(animation => animation.elId === props.elementInfo.id) if (elementIndexInAnimation === -1) return false
if (!firstAnimation) {
return false
}
// animationIndex
const findAnimationsList = animations.filter((animation, index) => index < props.animationIndex && animation.elId === props.elementInfo.id)
if (findAnimationsList.length) {
const lastAnimation = findAnimationsList[findAnimationsList.length - 1]
const effect = lastAnimation.effect || 'in' // VERSION 2022/5/7 : effect
if (effect === 'out') {
return false
}
if (elementIndexInAnimation >= props.animationIndex) { //
return true // 退退
} if (elementIndexInAnimation < props.animationIndex) return false
return false
}
// effect //
const firstEffectAnimation = animations.find(animation => animation.elId === props.elementInfo.id) //
if (firstEffectAnimation) { const firstAnimation = formatedAnimations.value[elementIndexInAnimation].animations.find(item => item.elId === props.elementInfo.id)
const effect = firstEffectAnimation.effect || 'in' // VERSION 2022/5/7 : effect if (firstAnimation?.type === 'in') return true
if (effect === 'out') { return false
return false
}
return true
}
return true
}) })
// //

View File

@ -47,7 +47,7 @@ export default defineComponent({
}, },
animationIndex: { animationIndex: {
type: Number, type: Number,
default: -1, required: true,
}, },
turnSlideToId: { turnSlideToId: {
type: Function as PropType<(id: string) => void>, type: Function as PropType<(id: string) => void>,

View File

@ -20,7 +20,7 @@
width: slideWidth + 'px', width: slideWidth + 'px',
height: slideHeight + 'px', height: slideHeight + 'px',
}" }"
v-if="Math.abs(slideIndex - index) < 2" v-if="Math.abs(slideIndex - index) < 2 || slide.animations?.length"
> >
<ScreenSlide <ScreenSlide
:slide="slide" :slide="slide"
@ -59,7 +59,7 @@ export default defineComponent({
}, },
animationIndex: { animationIndex: {
type: Number, type: Number,
default: -1, required: true,
}, },
turnSlideToId: { turnSlideToId: {
type: Function as PropType<(id: string) => void>, type: Function as PropType<(id: string) => void>,

View File

@ -1,93 +1,92 @@
import { computed, onMounted, onUnmounted, ref } from 'vue' import { onMounted, onUnmounted, ref } from 'vue'
import { throttle } from 'lodash' import { throttle } from 'lodash'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store' import { useSlidesStore } from '@/store'
import { KEYS } from '@/configs/hotkey' import { KEYS } from '@/configs/hotkey'
import { ANIMATION_CLASS_PREFIX } from '@/configs/animation'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
export default () => { export default () => {
const slidesStore = useSlidesStore() const slidesStore = useSlidesStore()
const { slides, slideIndex, currentSlide } = storeToRefs(slidesStore) const { slides, slideIndex, formatedAnimations } = storeToRefs(slidesStore)
// 当前页的元素动画列表和当前执行到的位置 // 当前页的元素动画执行到的位置
const animations = computed(() => currentSlide.value.animations || [])
const animationIndex = ref(0) const animationIndex = ref(0)
const inAnimation = ref(false)
const awaitRef = ref<number | null>(-1) // 执行元素动画
const awaitTimer = (delay: number) => { const runAnimation = () => {
return new Promise((resolve) => { // 正在执行动画时,禁止其他新的动画开始
awaitRef.value = window.setTimeout(() => { if (inAnimation.value) return
awaitRef.value = null
resolve(delay)
}, delay)
})
}
// 执行元素的入场动画 const { animations, autoNext } = formatedAnimations.value[animationIndex.value]
const runAnimation = async () => { animationIndex.value += 1
const prefix = 'animate__'
const animation = animations.value[animationIndex.value]
const elRef = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`) // 标记开始执行动画
if (elRef) { inAnimation.value = true
if (awaitRef.value) {
clearTimeout(awaitRef.value) let endAnimationCount = 0
awaitRef.value = null
// 依次执行该位置中的全部动画
for (const animation of animations) {
const elRef: HTMLElement | null = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`)
if (!elRef) {
endAnimationCount += 1
continue
} }
else if (animation.implement !== 1) {
// 判断执行动画 与上一动画在一起则不延迟 const animationName = `${ANIMATION_CLASS_PREFIX}${animation.effect}`
await awaitTimer(animation.delay || 0)
// 执行动画前先清除原有的动画状态(如果有)
elRef.style.removeProperty('--animate-duration')
for (const classname of elRef.classList) {
if (classname.indexOf(ANIMATION_CLASS_PREFIX) !== -1) elRef.classList.remove(classname, `${ANIMATION_CLASS_PREFIX}animated`)
} }
// eslint-disable-next-line require-atomic-updates // 执行动画
animationIndex.value += 1 elRef.style.setProperty('--animate-duration', `${animation.duration}ms`)
elRef.classList.add(animationName, `${ANIMATION_CLASS_PREFIX}animated`)
const animationName = `${prefix}${animation.type}`
document.documentElement.style.setProperty('--animate-duration', `${animation.duration}ms`)
// 判断如果存在非进场动画保留,就去除原动画
elRef.classList.remove(`${prefix}animated`)
for (let i = 0; i < elRef.classList.length; i++) {
if (elRef.classList[i].indexOf(prefix) > -1) {
elRef.classList.remove(elRef.classList[i])
}
}
elRef.classList.add(animationName, `${prefix}animated`)
// 执行动画结束,将“退场”以外的动画状态清除
const handleAnimationEnd = () => { const handleAnimationEnd = () => {
document.documentElement.style.removeProperty('--animate-duration') if (animation.type !== 'out') {
if (animation.effect === 'in') { // 如果是进场动画就去除动画 elRef.style.removeProperty('--animate-duration')
elRef.classList.remove(`${prefix}animated`) elRef.classList.remove(animationName, `${ANIMATION_CLASS_PREFIX}animated`)
elRef.classList.remove(animationName) }
// 判断该位置上的全部动画都已经结束后,标记动画执行完成,并尝试继续向下执行(如果有需要)
endAnimationCount += 1
if (endAnimationCount === animations.length) {
inAnimation.value = false
if (autoNext) runAnimation()
} }
} }
elRef.addEventListener('animationend', handleAnimationEnd, { once: true }) elRef.addEventListener('animationend', handleAnimationEnd, { once: true })
// 判断下个动画
if (animations.value.length && animationIndex.value < animations.value.length) {
const nextAnimation = animations.value[animationIndex.value]
if (nextAnimation.implement === 1) { // 一起
runAnimation()
}
else if (nextAnimation.implement === 2) { // 之后
await awaitTimer(animation.duration || 0)
runAnimation()
}
}
} }
else { }
animationIndex.value += 1
runAnimation() // 撤销元素动画,除了将索引前移外,还需要清除动画状态
const revokeAnimation = () => {
animationIndex.value -= 1
const { animations } = formatedAnimations.value[animationIndex.value]
for (const animation of animations) {
const elRef: HTMLElement | null = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`)
if (!elRef) continue
elRef.style.removeProperty('--animate-duration')
for (const classname of elRef.classList) {
if (classname.indexOf(ANIMATION_CLASS_PREFIX) !== -1) elRef.classList.remove(classname, `${ANIMATION_CLASS_PREFIX}animated`)
}
} }
} }
// 关闭自动播放 // 关闭自动播放
const autoPlayTimer = ref<number>(0) const autoPlayTimer = ref(0)
const closeAutoPlay = () => { const closeAutoPlay = () => {
if (autoPlayTimer.value) { if (autoPlayTimer.value) {
window.clearInterval(autoPlayTimer.value) clearInterval(autoPlayTimer.value)
autoPlayTimer.value = 0 autoPlayTimer.value = 0
} }
} }
@ -101,44 +100,32 @@ export default () => {
// 遇到元素动画时,优先执行动画播放,无动画则执行翻页 // 遇到元素动画时,优先执行动画播放,无动画则执行翻页
// 向上播放遇到动画时,仅撤销到动画执行前的状态,不需要反向播放动画 // 向上播放遇到动画时,仅撤销到动画执行前的状态,不需要反向播放动画
const execPrev = () => { const execPrev = () => {
if (animations.value.length && animationIndex.value > 0) { if (formatedAnimations.value.length && animationIndex.value > 0) {
animationIndex.value -= 1 revokeAnimation()
// 判断当前动画是否保留动画效果,有则去除后再上翻
const prefix = 'animate__'
const animation = animations.value[animationIndex.value]
if (animation) {
const elRef = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`)
if (elRef) {
elRef.classList.remove(`${prefix}animated`)
for (let i = 0; i < elRef.classList.length; i++) {
if (elRef.classList[i].indexOf(prefix) > -1) {
elRef.classList.remove(elRef.classList[i])
}
}
}
}
} }
else if (slideIndex.value > 0) { else if (slideIndex.value > 0) {
slidesStore.updateSlideIndex(slideIndex.value - 1) slidesStore.updateSlideIndex(slideIndex.value - 1)
const lastIndex = animations.value ? animations.value.length : 0 animationIndex.value = formatedAnimations.value.length
animationIndex.value = lastIndex inAnimation.value = false
} }
else { else {
throttleMassage('已经是第一页了') throttleMassage('已经是第一页了')
inAnimation.value = false
} }
} }
const execNext = () => { const execNext = () => {
if (animations.value.length && animationIndex.value < animations.value.length) { if (formatedAnimations.value.length && animationIndex.value < formatedAnimations.value.length) {
runAnimation() runAnimation()
} }
else if (slideIndex.value < slides.value.length - 1) { else if (slideIndex.value < slides.value.length - 1) {
slidesStore.updateSlideIndex(slideIndex.value + 1) slidesStore.updateSlideIndex(slideIndex.value + 1)
animationIndex.value = 0 animationIndex.value = 0
inAnimation.value = false
} }
else { else {
throttleMassage('已经是最后一页了') throttleMassage('已经是最后一页了')
closeAutoPlay() closeAutoPlay()
inAnimation.value = false
} }
} }
@ -146,7 +133,7 @@ export default () => {
const autoPlay = () => { const autoPlay = () => {
closeAutoPlay() closeAutoPlay()
message.success('开始自动放映') message.success('开始自动放映')
autoPlayTimer.value = window.setInterval(execNext, 2500) autoPlayTimer.value = setInterval(execNext, 2500)
} }
// 鼠标滚动翻页 // 鼠标滚动翻页

View File

@ -1,6 +1,6 @@
<template> <template>
<div <div
class="editable-element-shape" class="base-element-line"
:style="{ :style="{
top: elementInfo.top + 'px', top: elementInfo.top + 'px',
left: elementInfo.left + 'px', left: elementInfo.left + 'px',
@ -59,7 +59,7 @@ import useElementShadow from '@/views/components/element/hooks/useElementShadow'
import LinePointMarker from './LinePointMarker.vue' import LinePointMarker from './LinePointMarker.vue'
export default defineComponent({ export default defineComponent({
name: 'editable-element-shape', name: 'base-element-line',
components: { components: {
LinePointMarker, LinePointMarker,
}, },
@ -100,7 +100,7 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.editable-element-shape { .base-element-line {
position: absolute; position: absolute;
} }