mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
Merge branch 'chaoXxxxx-master'
This commit is contained in:
commit
c65a765fb3
@ -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: '弹跳',
|
||||||
@ -34,10 +38,10 @@ export const ANIMATIONS = [
|
|||||||
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' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -88,3 +92,94 @@ export const ANIMATIONS = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const EXIT_ANIMATIONS = [
|
||||||
|
{
|
||||||
|
type: 'bounce',
|
||||||
|
name: '弹跳',
|
||||||
|
children: [
|
||||||
|
{ name: '弹出', value: 'bounceOut' },
|
||||||
|
{ name: '向左弹出', value: 'bounceOutLeft' },
|
||||||
|
{ name: '向右弹出', value: 'bounceOutRight' },
|
||||||
|
{ name: '向上弹出', value: 'bounceOutUp' },
|
||||||
|
{ name: '向下弹出', value: 'bounceOutDown' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'fade',
|
||||||
|
name: '浮现',
|
||||||
|
children: [
|
||||||
|
{ name: '浮出', value: 'fadeOut' },
|
||||||
|
{ name: '向下浮出', value: 'fadeOutDown' },
|
||||||
|
{ name: '向下长距浮出', value: 'fadeOutDownBig' },
|
||||||
|
{ name: '向左浮出', value: 'fadeOutLeft' },
|
||||||
|
{ name: '向左长距浮出', value: 'fadeOutLeftBig' },
|
||||||
|
{ name: '向右浮出', value: 'fadeOutRight' },
|
||||||
|
{ name: '向右长距浮出', value: 'fadeOutRightBig' },
|
||||||
|
{ name: '向上浮出', value: 'fadeOutUp' },
|
||||||
|
{ name: '向上长距浮出', value: 'fadeOutUpBig' },
|
||||||
|
{ name: '从左上浮出', value: 'fadeOutTopLeft' },
|
||||||
|
{ name: '从右上浮出', value: 'fadeOutTopRight' },
|
||||||
|
{ name: '从左下浮出', value: 'fadeOutBottomLeft' },
|
||||||
|
{ name: '从右下浮出', value: 'fadeOutBottomRight' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'rotate',
|
||||||
|
name: '旋转',
|
||||||
|
children: [
|
||||||
|
{ name: '旋转退出', value: 'rotateOut' },
|
||||||
|
{ name: '绕左下退出', value: 'rotateOutDownLeft' },
|
||||||
|
{ name: '绕右下退出', value: 'rotateOutDownRight' },
|
||||||
|
{ name: '绕左上退出', value: 'rotateOutUpLeft' },
|
||||||
|
{ name: '绕右上退出', value: 'rotateOutUpRight' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'zoom',
|
||||||
|
name: '缩放',
|
||||||
|
children: [
|
||||||
|
{ name: '缩小退出', value: 'zoomOut' },
|
||||||
|
{ name: '向下缩小退出', value: 'zoomOutDown' },
|
||||||
|
{ name: '从左缩小退出', value: 'zoomOutLeft' },
|
||||||
|
{ name: '从右缩小退出', value: 'zoomOutRight' },
|
||||||
|
{ name: '向上缩小退出', value: 'zoomOutUp' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'slide',
|
||||||
|
name: '滑出',
|
||||||
|
children: [
|
||||||
|
{ name: '向下滑出', value: 'slideOutDown' },
|
||||||
|
{ name: '从左滑出', value: 'slideOutLeft' },
|
||||||
|
{ name: '从右滑出', value: 'slideOutRight' },
|
||||||
|
{ name: '向上滑出', value: 'slideOutUp' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'flip',
|
||||||
|
name: '翻转',
|
||||||
|
children: [
|
||||||
|
{ name: 'X轴翻转退出', value: 'flipOutX' },
|
||||||
|
{ name: 'Y轴翻转退出', value: 'flipOutY' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'back',
|
||||||
|
name: '缩小滑出',
|
||||||
|
children: [
|
||||||
|
{ name: '向下缩小滑出', value: 'backOutDown' },
|
||||||
|
{ name: '从左缩小滑出', value: 'backOutLeft' },
|
||||||
|
{ name: '从右缩小滑出', value: 'backOutRight' },
|
||||||
|
{ name: '向上缩小滑出', value: 'backOutUp' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'lightSpeed',
|
||||||
|
name: '飞出',
|
||||||
|
children: [
|
||||||
|
{ name: '从右飞出', value: 'lightSpeedOutRight' },
|
||||||
|
{ name: '从左飞出', value: 'lightSpeedOutLeft' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
@ -58,8 +58,10 @@ export default () => {
|
|||||||
element.id = elIdMap[element.id]
|
element.id = elIdMap[element.id]
|
||||||
if (element.groupId) element.groupId = groupIdMap[element.groupId]
|
if (element.groupId) element.groupId = groupIdMap[element.groupId]
|
||||||
}
|
}
|
||||||
|
// 动画id替换
|
||||||
if (slide.animations) {
|
if (slide.animations) {
|
||||||
for (const animation of slide.animations) {
|
for (const animation of slide.animations) {
|
||||||
|
animation.id = nanoid(10)
|
||||||
animation.elId = elIdMap[animation.elId]
|
animation.elId = elIdMap[animation.elId]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -568,16 +568,25 @@ export type PPTElement = PPTTextElement | PPTImageElement | PPTShapeElement | PP
|
|||||||
/**
|
/**
|
||||||
* 元素动画
|
* 元素动画
|
||||||
*
|
*
|
||||||
|
* id: 动画id
|
||||||
|
*
|
||||||
* elId: 元素ID
|
* elId: 元素ID
|
||||||
*
|
*
|
||||||
* type: 动画类型
|
* effect: 动画效果
|
||||||
|
*
|
||||||
|
* type: 动画类型(入场、退场)
|
||||||
*
|
*
|
||||||
* duration: 动画持续时间
|
* duration: 动画持续时间
|
||||||
|
*
|
||||||
|
* trigger: 动画触发方式(click - 单击时、meantime - 与上一动画同时、auto - 上一动画之后)
|
||||||
*/
|
*/
|
||||||
export interface PPTAnimation {
|
export interface PPTAnimation {
|
||||||
|
id: string;
|
||||||
elId: string;
|
elId: string;
|
||||||
type: string;
|
effect: string;
|
||||||
|
type: 'in' | 'out';
|
||||||
duration: number;
|
duration: number;
|
||||||
|
trigger: 'click' | 'meantime' | 'auto';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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>
|
@ -4,44 +4,48 @@
|
|||||||
<Popover
|
<Popover
|
||||||
trigger="click"
|
trigger="click"
|
||||||
v-model:visible="animationPoolVisible"
|
v-model:visible="animationPoolVisible"
|
||||||
v-if="!['chart', 'video'].includes(handleElement.type)"
|
|
||||||
@visibleChange="visible => handlePopoverVisibleChange(visible)"
|
@visibleChange="visible => handlePopoverVisibleChange(visible)"
|
||||||
>
|
>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="animation-pool">
|
<div class="tabs">
|
||||||
<div class="pool-type" v-for="type in animations" :key="type.name">
|
<div
|
||||||
<div class="type-title">{{type.name}}:</div>
|
:class="['tab', tab.key, { 'active': activeTab === tab.key }]"
|
||||||
<div class="pool-item-wrapper">
|
v-for="tab in tabs"
|
||||||
<div
|
:key="tab.key"
|
||||||
class="pool-item"
|
@click="activeTab = tab.key"
|
||||||
v-for="item in type.children" :key="item.name"
|
>{{tab.label}}</div>
|
||||||
@mouseenter="hoverPreviewAnimation = item.value"
|
</div>
|
||||||
@mouseleave="hoverPreviewAnimation = ''"
|
<template v-for="key in Object.keys(animations)">
|
||||||
@click="addAnimation(item.value)"
|
<div :class="['animation-pool', key]" :key="key" v-if="activeTab === key">
|
||||||
>
|
<div class="pool-type" :key="effect.name" v-for="effect in animations[key]">
|
||||||
|
<div class="type-title">{{effect.name}}:</div>
|
||||||
|
<div class="pool-item-wrapper">
|
||||||
<div
|
<div
|
||||||
class="animation-box"
|
class="pool-item"
|
||||||
:class="[
|
v-for="item in effect.children" :key="item.name"
|
||||||
'animate__animated',
|
@mouseenter="hoverPreviewAnimation = item.value"
|
||||||
'animate__faster',
|
@mouseleave="hoverPreviewAnimation = ''"
|
||||||
hoverPreviewAnimation === item.value && `animate__${item.value}`,
|
@click="addAnimation(key, item.value)"
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
{{item.name}}
|
<div
|
||||||
|
class="animation-box"
|
||||||
|
:class="[
|
||||||
|
`${prefix}animated`,
|
||||||
|
`${prefix}faster`,
|
||||||
|
hoverPreviewAnimation === item.value && `${prefix}${item.value}`,
|
||||||
|
]"
|
||||||
|
>{{item.name}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mask" v-if="!popoverMaskHide"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mask" v-if="!popoverMaskHide"></div>
|
</template>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<Button class="element-animation-btn">
|
<Button class="element-animation-btn" @click="handleAnimationId = ''">
|
||||||
<IconEffects style="margin-right: 5px;" /> {{handleElementAnimationName || '点击选择动画'}}
|
<IconEffects style="margin-right: 5px;" /> 添加动画
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Button class="element-animation-btn" v-else disabled>
|
|
||||||
<IconEffects style="margin-right: 5px;" /> 该元素暂不支持动画
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tip" v-else><IconClick style="margin-right: 5px;" /> 选中画布中的元素添加动画</div>
|
<div class="tip" v-else><IconClick style="margin-right: 5px;" /> 选中画布中的元素添加动画</div>
|
||||||
@ -54,62 +58,96 @@
|
|||||||
: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="index">{{index + 1}}</div>
|
<div class="sequence-content">
|
||||||
<div class="text">【{{element.elType}}】{{element.animationType}}</div>
|
<div class="index">{{element.index}}</div>
|
||||||
<div class="handler">
|
<div class="text">【{{element.elType}}】{{element.animationEffect}}</div>
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="预览">
|
<div class="handler">
|
||||||
<IconPlayOne class="handler-btn" @click="runAnimation(element.elId, element.type, element.duration)" />
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="预览">
|
||||||
</Tooltip>
|
<IconPlayOne class="handler-btn" @click="runAnimation(element.elId, element.effect, element.duration)" />
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="删除">
|
</Tooltip>
|
||||||
<IconCloseSmall class="handler-btn" @click="deleteAnimation(element.elId)" />
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="删除">
|
||||||
</Tooltip>
|
<IconCloseSmall class="handler-btn" @click="deleteAnimation(element.id)" />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="configs" v-if="handleElementAnimation[0]?.elId === element.elId">
|
||||||
|
<Divider style="margin: 16px 0;" />
|
||||||
|
|
||||||
|
<div class="config-item">
|
||||||
|
<div style="flex: 3;">持续时长:</div>
|
||||||
|
<InputNumber
|
||||||
|
:min="500"
|
||||||
|
:max="3000"
|
||||||
|
:step="500"
|
||||||
|
:value="element.duration"
|
||||||
|
@change="value => updateElementAnimationDuration(element.id, value)"
|
||||||
|
style="flex: 5;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="config-item">
|
||||||
|
<div style="flex: 3;">触发方式:</div>
|
||||||
|
<Select
|
||||||
|
:value="element.trigger"
|
||||||
|
@change="value => updateElementAnimationTrigger(element.id, value)"
|
||||||
|
style="flex: 5;"
|
||||||
|
>
|
||||||
|
<SelectOption value="click">主动触发</SelectOption>
|
||||||
|
<SelectOption value="meantime">与上一动画同时</SelectOption>
|
||||||
|
<SelectOption value="auto">上一动画之后</SelectOption>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div class="config-item">
|
||||||
|
<Button style="flex: 1;" @click="openAnimationPool(element.id)">更换动画</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Draggable>
|
</Draggable>
|
||||||
|
|
||||||
<div class="configs" v-if="handleElementAnimation">
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<div class="duration">
|
|
||||||
<div style="flex: 4;">持续时间(毫秒):</div>
|
|
||||||
<InputNumber
|
|
||||||
:min="100"
|
|
||||||
:max="5000"
|
|
||||||
:step="100"
|
|
||||||
:value="handleElementAnimation.duration"
|
|
||||||
@change="value => updateElementAnimationDuration(value)"
|
|
||||||
style="flex: 3;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, ref } from 'vue'
|
import { computed, defineComponent, ref, watch } from 'vue'
|
||||||
|
import { nanoid } from 'nanoid'
|
||||||
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 } 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 Draggable from 'vuedraggable'
|
import Draggable from 'vuedraggable'
|
||||||
|
|
||||||
const defaultDuration = 1000
|
const animationEffects: { [key: string]: string } = {}
|
||||||
|
for (const effect of ENTER_ANIMATIONS) {
|
||||||
const animationTypes: { [key: string]: string } = {}
|
for (const animation of effect.children) {
|
||||||
for (const type of ANIMATIONS) {
|
animationEffects[animation.value] = animation.name
|
||||||
for (const animation of type.children) {
|
|
||||||
animationTypes[animation.value] = animation.name
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const effect of EXIT_ANIMATIONS) {
|
||||||
|
for (const animation of effect.children) {
|
||||||
|
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',
|
||||||
@ -119,55 +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, () => {
|
||||||
|
animationPoolVisible.value = false
|
||||||
|
})
|
||||||
|
|
||||||
const hoverPreviewAnimation = ref('')
|
const hoverPreviewAnimation = ref('')
|
||||||
const animationPoolVisible = ref(false)
|
const animationPoolVisible = ref(false)
|
||||||
|
|
||||||
const { addHistorySnapshot } = useHistorySnapshot()
|
const { addHistorySnapshot } = useHistorySnapshot()
|
||||||
|
|
||||||
const animations = ANIMATIONS
|
|
||||||
|
|
||||||
// 当前页面的动画列表
|
// 当前页面的动画列表
|
||||||
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)
|
||||||
|
if (!el) continue
|
||||||
|
|
||||||
animationSequence.push({
|
const elType = ELEMENT_TYPE_ZH[el.type]
|
||||||
...animation,
|
const animationEffect = animationEffects[animation.effect]
|
||||||
elType,
|
animationSequence.push({
|
||||||
animationType,
|
...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.find(item => item.elId === handleElementId.value)
|
const animation = animations.filter(item => item.elId === handleElementId.value)
|
||||||
return animation || null
|
return animation || []
|
||||||
})
|
})
|
||||||
|
|
||||||
// 当前选中元素的入场动画名称
|
// 删除元素动画
|
||||||
const handleElementAnimationName = computed(() => {
|
const deleteAnimation = (id: string) => {
|
||||||
if (!handleElementAnimation.value) return null
|
const animations = currentSlideAnimations.value.filter(item => item.id !== id)
|
||||||
return animationTypes[handleElementAnimation.value.type]
|
|
||||||
})
|
|
||||||
|
|
||||||
// 删除元素入场动画
|
|
||||||
const deleteAnimation = (elId: string) => {
|
|
||||||
const animations = (currentSlideAnimations.value as PPTAnimation[]).filter(item => item.elId !== elId)
|
|
||||||
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
|
||||||
@ -181,29 +225,48 @@ export default defineComponent({
|
|||||||
addHistorySnapshot()
|
addHistorySnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行入场动画预览
|
// 执行动画预览
|
||||||
const runAnimation = (elId: string, animationType: string, duration: 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) {
|
||||||
const animationName = `${prefix}${animationType}`
|
const animationName = `${ANIMATION_CLASS_PREFIX}${effect}`
|
||||||
document.documentElement.style.setProperty('--animate-duration', `${duration}ms`)
|
document.documentElement.style.setProperty('--animate-duration', `${duration}ms`)
|
||||||
elRef.classList.add(`${prefix}animated`, animationName)
|
elRef.classList.add(`${ANIMATION_CLASS_PREFIX}animated`, animationName)
|
||||||
|
|
||||||
const handleAnimationEnd = () => {
|
const handleAnimationEnd = () => {
|
||||||
document.documentElement.style.removeProperty('--animate-duration')
|
document.documentElement.style.removeProperty('--animate-duration')
|
||||||
elRef.classList.remove(`${prefix}animated`, animationName)
|
elRef.classList.remove(`${ANIMATION_CLASS_PREFIX}animated`, animationName)
|
||||||
}
|
}
|
||||||
elRef.addEventListener('animationend', handleAnimationEnd, { once: true })
|
elRef.addEventListener('animationend', handleAnimationEnd, { once: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改元素入场动画,并执行一次预览
|
// 修改元素动画持续时间
|
||||||
const updateElementAnimation = (type: string) => {
|
const updateElementAnimationDuration = (id: string, duration: number) => {
|
||||||
if (!currentSlideAnimations.value) return
|
if (duration < 100 || duration > 5000) return
|
||||||
|
|
||||||
const animations = currentSlideAnimations.value.map(item => {
|
const animations = currentSlideAnimations.value.map(item => {
|
||||||
if (item.elId === handleElementId.value) return { ...item, type }
|
if (item.id === id) return { ...item, duration }
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
slidesStore.updateSlide({ animations })
|
||||||
|
addHistorySnapshot()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改触发方式
|
||||||
|
const updateElementAnimationTrigger = (id: string, trigger: 'click' | 'meantime' | 'auto') => {
|
||||||
|
const animations = currentSlideAnimations.value.map(item => {
|
||||||
|
if (item.id === id) return { ...item, trigger }
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
slidesStore.updateSlide({ animations })
|
||||||
|
addHistorySnapshot()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改元素动画,并执行一次预览
|
||||||
|
const updateElementAnimation = (type: AnimationType, effect: string) => {
|
||||||
|
const animations = currentSlideAnimations.value.map(item => {
|
||||||
|
if (item.id === handleAnimationId.value) return { ...item, type, effect }
|
||||||
return item
|
return item
|
||||||
})
|
})
|
||||||
slidesStore.updateSlide({ animations })
|
slidesStore.updateSlide({ animations })
|
||||||
@ -211,41 +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
|
||||||
|
|
||||||
runAnimation(handleElementId.value, type, duration)
|
runAnimation(handleElementId.value, effect, duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改元素入场动画持续时间
|
const handleAnimationId = ref('')
|
||||||
const updateElementAnimationDuration = (duration: number) => {
|
// 添加元素动画,并执行一次预览
|
||||||
if (!currentSlideAnimations.value) return
|
const addAnimation = (type: AnimationType, effect: string) => {
|
||||||
if (duration < 100 || duration > 5000) return
|
if (handleAnimationId.value) {
|
||||||
|
updateElementAnimation(type, effect)
|
||||||
const animations = currentSlideAnimations.value.map(item => {
|
|
||||||
if (item.elId === handleElementId.value) return { ...item, duration }
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
slidesStore.updateSlide({ animations })
|
|
||||||
addHistorySnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加元素入场动画,并执行一次预览
|
|
||||||
const addAnimation = (type: string) => {
|
|
||||||
if (handleElementAnimationName.value) {
|
|
||||||
updateElementAnimation(type)
|
|
||||||
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),
|
||||||
elId: handleElementId.value,
|
elId: handleElementId.value,
|
||||||
type,
|
type,
|
||||||
duration: defaultDuration,
|
effect,
|
||||||
|
duration: ANIMATION_DEFAULT_DURATION,
|
||||||
|
trigger: ANIMATION_DEFAULT_TRIGGER,
|
||||||
})
|
})
|
||||||
slidesStore.updateSlide({ animations })
|
slidesStore.updateSlide({ animations })
|
||||||
animationPoolVisible.value = false
|
animationPoolVisible.value = false
|
||||||
addHistorySnapshot()
|
addHistorySnapshot()
|
||||||
|
|
||||||
runAnimation(handleElementId.value, type, defaultDuration)
|
runAnimation(handleElementId.value, effect, ANIMATION_DEFAULT_DURATION)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 动画选择面板打开500ms后再移除遮罩层,否则打开面板后迅速滑入鼠标预览会导致抖动
|
// 动画选择面板打开500ms后再移除遮罩层,否则打开面板后迅速滑入鼠标预览会导致抖动
|
||||||
@ -257,32 +312,73 @@ export default defineComponent({
|
|||||||
else popoverMaskHide.value = false
|
else popoverMaskHide.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openAnimationPool = (elementId: string) => {
|
||||||
|
animationPoolVisible.value = true
|
||||||
|
handleAnimationId.value = elementId
|
||||||
|
handlePopoverVisibleChange(true)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
tabs,
|
||||||
|
activeTab,
|
||||||
|
handleAnimationId,
|
||||||
handleElement,
|
handleElement,
|
||||||
animationPoolVisible,
|
animationPoolVisible,
|
||||||
animations,
|
|
||||||
animationSequence,
|
animationSequence,
|
||||||
hoverPreviewAnimation,
|
hoverPreviewAnimation,
|
||||||
handleElementAnimation,
|
handleElementAnimation,
|
||||||
handleElementAnimationName,
|
|
||||||
popoverMaskHide,
|
popoverMaskHide,
|
||||||
|
animations: {
|
||||||
|
in: ENTER_ANIMATIONS,
|
||||||
|
out: EXIT_ANIMATIONS,
|
||||||
|
},
|
||||||
|
prefix: ANIMATION_CLASS_PREFIX,
|
||||||
addAnimation,
|
addAnimation,
|
||||||
deleteAnimation,
|
deleteAnimation,
|
||||||
handleDragEnd,
|
handleDragEnd,
|
||||||
runAnimation,
|
runAnimation,
|
||||||
updateElementAnimationDuration,
|
updateElementAnimationDuration,
|
||||||
|
updateElementAnimationTrigger,
|
||||||
handlePopoverVisibleChange,
|
handlePopoverVisibleChange,
|
||||||
|
openAnimationPool,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</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;
|
||||||
@ -291,10 +387,13 @@ export default defineComponent({
|
|||||||
.element-animation-btn {
|
.element-animation-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.duration {
|
.config-item {
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
& + .config-item {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.tip {
|
.tip {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@ -310,15 +409,19 @@ export default defineComponent({
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-right: -12px;
|
margin-right: -12px;
|
||||||
padding-right: 12px;
|
|
||||||
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 {
|
||||||
@ -353,37 +456,46 @@ export default defineComponent({
|
|||||||
@include overflow-overlay();
|
@include overflow-overlay();
|
||||||
}
|
}
|
||||||
.sequence-item {
|
.sequence-item {
|
||||||
height: 36px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border: 1px solid $borderColor;
|
border: 1px solid $borderColor;
|
||||||
padding: 6px;
|
padding: 10px 6px;
|
||||||
border-radius: $borderRadius;
|
border-radius: $borderRadius;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
cursor: grab;
|
transition: all .5s;
|
||||||
|
|
||||||
&:active {
|
&.in.active {
|
||||||
cursor: grabbing;
|
border-color: $inColor;
|
||||||
|
}
|
||||||
|
&.out.active {
|
||||||
|
border-color: $outColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
border-color: $themeColor;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.index {
|
.sequence-content {
|
||||||
flex: 1;
|
display: flex;
|
||||||
}
|
align-items: center;
|
||||||
.text {
|
cursor: grab;
|
||||||
flex: 6;
|
|
||||||
}
|
&:active {
|
||||||
.handler {
|
cursor: grabbing;
|
||||||
flex: 2;
|
}
|
||||||
font-size: 15px;
|
|
||||||
text-align: right;
|
.index {
|
||||||
}
|
flex: 1;
|
||||||
.handler-btn {
|
}
|
||||||
margin-left: 8px;
|
.text {
|
||||||
cursor: pointer;
|
flex: 6;
|
||||||
|
}
|
||||||
|
.handler {
|
||||||
|
flex: 2;
|
||||||
|
font-size: 15px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.handler-btn {
|
||||||
|
margin-left: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -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,13 +75,27 @@ 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 && elementIndexInAnimation >= props.animationIndex) return true
|
const elIds = item.animations.map(item => item.elId)
|
||||||
|
return elIds.includes(props.elementInfo.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 该元素未设置过动画
|
||||||
|
if (elementIndexInAnimation === -1) return false
|
||||||
|
|
||||||
|
// 若该元素已执行过动画,都无须隐藏
|
||||||
|
// 具体来说:若已执行的最后一个动画为入场,显然无须隐藏;若已执行的最后一个动画为退场,由于保留了退场动画结束状态,也无需额外隐藏
|
||||||
|
if (elementIndexInAnimation < props.animationIndex) return false
|
||||||
|
|
||||||
|
// 若该元素未执行过动画,获取其将要执行的第一个动画
|
||||||
|
// 若将要执行的第一个动画为入场,则需要隐藏,否则无须隐藏
|
||||||
|
const firstAnimation = formatedAnimations.value[elementIndexInAnimation].animations.find(item => item.elId === props.elementInfo.id)
|
||||||
|
if (firstAnimation?.type === 'in') return true
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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>,
|
||||||
|
@ -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>,
|
||||||
|
@ -1,39 +1,87 @@
|
|||||||
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 runAnimation = () => {
|
const runAnimation = () => {
|
||||||
const prefix = 'animate__'
|
// 正在执行动画时,禁止其他新的动画开始
|
||||||
const animation = animations.value[animationIndex.value]
|
if (inAnimation.value) return
|
||||||
|
|
||||||
|
const { animations, autoNext } = formatedAnimations.value[animationIndex.value]
|
||||||
animationIndex.value += 1
|
animationIndex.value += 1
|
||||||
|
|
||||||
const elRef = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`)
|
// 标记开始执行动画
|
||||||
if (elRef) {
|
inAnimation.value = true
|
||||||
const animationName = `${prefix}${animation.type}`
|
|
||||||
document.documentElement.style.setProperty('--animate-duration', `${animation.duration}ms`)
|
|
||||||
elRef.classList.add(`${prefix}animated`, animationName)
|
|
||||||
|
|
||||||
|
let endAnimationCount = 0
|
||||||
|
|
||||||
|
// 依次执行该位置中的全部动画
|
||||||
|
for (const animation of animations) {
|
||||||
|
const elRef: HTMLElement | null = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`)
|
||||||
|
if (!elRef) {
|
||||||
|
endAnimationCount += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const animationName = `${ANIMATION_CLASS_PREFIX}${animation.effect}`
|
||||||
|
|
||||||
|
// 执行动画前先清除原有的动画状态(如果有)
|
||||||
|
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`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行动画
|
||||||
|
elRef.style.setProperty('--animate-duration', `${animation.duration}ms`)
|
||||||
|
elRef.classList.add(animationName, `${ANIMATION_CLASS_PREFIX}animated`)
|
||||||
|
|
||||||
|
// 执行动画结束,将“退场”以外的动画状态清除
|
||||||
const handleAnimationEnd = () => {
|
const handleAnimationEnd = () => {
|
||||||
document.documentElement.style.removeProperty('--animate-duration')
|
if (animation.type !== 'out') {
|
||||||
elRef.classList.remove(`${prefix}animated`, animationName)
|
elRef.style.removeProperty('--animate-duration')
|
||||||
|
elRef.classList.remove(animationName, `${ANIMATION_CLASS_PREFIX}animated`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断该位置上的全部动画都已经结束后,标记动画执行完成,并尝试继续向下执行(如果有需要)
|
||||||
|
endAnimationCount += 1
|
||||||
|
if (endAnimationCount === animations.length) {
|
||||||
|
inAnimation.value = false
|
||||||
|
if (autoNext) runAnimation()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
elRef.addEventListener('animationend', handleAnimationEnd, { once: true })
|
elRef.addEventListener('animationend', handleAnimationEnd, { once: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 撤销元素动画,除了将索引前移外,还需要清除动画状态
|
||||||
|
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(0)
|
const autoPlayTimer = ref(0)
|
||||||
const closeAutoPlay = () => {
|
const closeAutoPlay = () => {
|
||||||
@ -52,29 +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()
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user