438 lines
11 KiB
Vue
438 lines
11 KiB
Vue
<template>
|
||
<view
|
||
v-if="rendered"
|
||
ref="rootRef"
|
||
class="u-transition"
|
||
:class="[customClass, animationClass]"
|
||
:style="$u.toStyle(animationStyle, customStyle)"
|
||
@animationstart="handleAnimationStart"
|
||
@animationend="handleAnimationEnd"
|
||
>
|
||
<slot />
|
||
</view>
|
||
</template>
|
||
|
||
<script lang="ts">
|
||
export default {
|
||
name: 'u-transition',
|
||
options: {
|
||
addGlobalClass: true,
|
||
// #ifndef MP-TOUTIAO
|
||
virtualHost: true,
|
||
// #endif
|
||
styleIsolation: 'shared'
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, nextTick, ref, watch } from 'vue';
|
||
import { TransitionProps } from './types';
|
||
import { $u } from '../..';
|
||
import type { TransitionDuration } from '../../types/global';
|
||
|
||
/**
|
||
* transition 过渡动画
|
||
* @description 统一的过渡与进出场动效封装,支持多种预设动画和自定义时长。
|
||
* @tutorial https://uviewpro.cn/zh/components/transition.html
|
||
* @property {Boolean} show 是否展示内容(默认true)
|
||
* @property {String} name 预设动画名,可选 fade/slide-up/slide-down/slide-left/slide-right/zoom-in/zoom-out(默认fade)
|
||
* @property {String} mode 进入/离开过渡模式,等同于原生 transition 的 mode(默认空)
|
||
* @property {Number|Object} duration 进入/离开动画时长,单位ms,支持 { enter, leave }(默认300)
|
||
* @property {String} timing-function 动画曲线(默认cubic-bezier(0.2,0.8,0.2,1))
|
||
* @property {Number} delay 动画延迟,单位ms(默认0)
|
||
* @property {Boolean} appear 首次渲染时是否执行动画(默认false)
|
||
* @property {String} custom-class 自定义 class
|
||
* @property {String|Object} custom-style 自定义样式
|
||
* @example <u-transition :show="visible" name="slide-up"><view>content</view></u-transition>
|
||
*/
|
||
|
||
const props = defineProps(TransitionProps);
|
||
|
||
const emit = defineEmits([
|
||
'before-enter',
|
||
'enter',
|
||
'after-enter',
|
||
'enter-cancelled',
|
||
'before-leave',
|
||
'leave',
|
||
'after-leave',
|
||
'leave-cancelled'
|
||
]);
|
||
|
||
const normalizeDuration = (duration: number | TransitionDuration) => {
|
||
if (typeof duration === 'number') {
|
||
return {
|
||
enter: duration,
|
||
leave: duration
|
||
};
|
||
}
|
||
return {
|
||
enter: duration?.enter ?? 300,
|
||
leave: duration?.leave ?? duration?.enter ?? 300
|
||
};
|
||
};
|
||
|
||
const rootRef = ref();
|
||
const rendered = ref<boolean>(props.show);
|
||
const animationPhase = ref<'enter' | 'leave' | ''>('');
|
||
const animating = ref(false);
|
||
const initialized = ref(false);
|
||
|
||
const transitionDuration = computed(() => normalizeDuration(props.duration));
|
||
|
||
const animationStyle = computed(() => {
|
||
const currentDuration =
|
||
animationPhase.value === 'leave' ? transitionDuration.value.leave : transitionDuration.value.enter;
|
||
return {
|
||
'--u-transition-duration-enter': `${transitionDuration.value.enter}ms`,
|
||
'--u-transition-duration-leave': `${transitionDuration.value.leave}ms`,
|
||
'--u-transition-delay': `${props.delay}ms`,
|
||
'--u-transition-timing': props.timingFunction,
|
||
animationDuration: `${currentDuration}ms`,
|
||
animationDelay: `${props.delay}ms`,
|
||
animationTimingFunction: props.timingFunction
|
||
};
|
||
});
|
||
|
||
const animationClass = computed(() => {
|
||
if (!animationPhase.value) {
|
||
return '';
|
||
}
|
||
return `u-transition-${props.name}-${animationPhase.value}`;
|
||
});
|
||
|
||
const getEl = () => rootRef.value as any;
|
||
|
||
const startEnter = () => {
|
||
if (animating.value && animationPhase.value === 'enter') {
|
||
return;
|
||
}
|
||
if (animating.value && animationPhase.value === 'leave') {
|
||
emit('leave-cancelled', getEl());
|
||
}
|
||
rendered.value = true;
|
||
animationPhase.value = 'enter';
|
||
animating.value = true;
|
||
emit('before-enter', getEl());
|
||
};
|
||
|
||
const startLeave = () => {
|
||
if (!rendered.value) {
|
||
return;
|
||
}
|
||
if (animating.value && animationPhase.value === 'leave') {
|
||
return;
|
||
}
|
||
if (animating.value && animationPhase.value === 'enter') {
|
||
emit('enter-cancelled', getEl());
|
||
}
|
||
animationPhase.value = 'leave';
|
||
animating.value = true;
|
||
emit('before-leave', getEl());
|
||
};
|
||
|
||
const handleAnimationStart = () => {
|
||
if (animationPhase.value === 'enter') {
|
||
emit('enter', getEl());
|
||
} else if (animationPhase.value === 'leave') {
|
||
emit('leave', getEl());
|
||
}
|
||
};
|
||
|
||
const handleAnimationEnd = () => {
|
||
if (animationPhase.value === 'enter') {
|
||
animating.value = false;
|
||
animationPhase.value = '';
|
||
emit('after-enter', getEl());
|
||
return;
|
||
}
|
||
if (animationPhase.value === 'leave') {
|
||
animating.value = false;
|
||
animationPhase.value = '';
|
||
rendered.value = false;
|
||
emit('after-leave', getEl());
|
||
}
|
||
};
|
||
|
||
// 根据mode处理动画顺序(主要用于快速切换时的时序控制)
|
||
const shouldWaitForAnimation = (newPhase: 'enter' | 'leave') => {
|
||
if (!animating.value) return false;
|
||
|
||
const currentPhase = animationPhase.value;
|
||
|
||
// 如果当前正在进行相反的动画,根据mode决定是否需要等待
|
||
if (props.mode === 'out-in' && currentPhase === 'leave' && newPhase === 'enter') {
|
||
return true; // 等待离开动画完成
|
||
}
|
||
if (props.mode === 'in-out' && currentPhase === 'enter' && newPhase === 'leave') {
|
||
return true; // 等待进入动画完成
|
||
}
|
||
|
||
return false;
|
||
};
|
||
|
||
watch(
|
||
() => props.show,
|
||
value => {
|
||
if (!initialized.value) {
|
||
initialized.value = true;
|
||
if (value) {
|
||
rendered.value = true;
|
||
if (props.appear) {
|
||
nextTick(() => startEnter());
|
||
}
|
||
} else {
|
||
rendered.value = false;
|
||
}
|
||
return;
|
||
}
|
||
if (value) {
|
||
if (shouldWaitForAnimation('enter')) {
|
||
// 根据mode等待当前动画完成后再开始进入动画
|
||
// 简单的方式:延迟到下一个tick检查
|
||
nextTick(() => {
|
||
if (!animating.value) {
|
||
startEnter();
|
||
}
|
||
});
|
||
} else {
|
||
startEnter();
|
||
}
|
||
} else {
|
||
if (shouldWaitForAnimation('leave')) {
|
||
// 根据mode等待当前动画完成后再开始离开动画
|
||
nextTick(() => {
|
||
if (!animating.value) {
|
||
startLeave();
|
||
}
|
||
});
|
||
} else {
|
||
startLeave();
|
||
}
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import '../../libs/css/style.components.scss';
|
||
|
||
.u-transition {
|
||
// display: inline-flex;
|
||
// width: auto;
|
||
}
|
||
|
||
@mixin animation-base {
|
||
animation-fill-mode: both;
|
||
}
|
||
|
||
.u-transition-fade-enter {
|
||
@include animation-base;
|
||
animation-name: u-transition-fade-in;
|
||
}
|
||
.u-transition-fade-leave {
|
||
@include animation-base;
|
||
animation-name: u-transition-fade-out;
|
||
}
|
||
|
||
.u-transition-slide-up-enter {
|
||
@include animation-base;
|
||
animation-name: u-transition-slide-up-in;
|
||
}
|
||
.u-transition-slide-up-leave {
|
||
@include animation-base;
|
||
animation-name: u-transition-slide-up-out;
|
||
}
|
||
|
||
.u-transition-slide-down-enter {
|
||
@include animation-base;
|
||
animation-name: u-transition-slide-down-in;
|
||
}
|
||
.u-transition-slide-down-leave {
|
||
@include animation-base;
|
||
animation-name: u-transition-slide-down-out;
|
||
}
|
||
|
||
.u-transition-slide-left-enter {
|
||
@include animation-base;
|
||
animation-name: u-transition-slide-left-in;
|
||
}
|
||
.u-transition-slide-left-leave {
|
||
@include animation-base;
|
||
animation-name: u-transition-slide-left-out;
|
||
}
|
||
|
||
.u-transition-slide-right-enter {
|
||
@include animation-base;
|
||
animation-name: u-transition-slide-right-in;
|
||
}
|
||
.u-transition-slide-right-leave {
|
||
@include animation-base;
|
||
animation-name: u-transition-slide-right-out;
|
||
}
|
||
|
||
.u-transition-zoom-in-enter {
|
||
@include animation-base;
|
||
animation-name: u-transition-zoom-in-in;
|
||
}
|
||
.u-transition-zoom-in-leave {
|
||
@include animation-base;
|
||
animation-name: u-transition-zoom-in-out;
|
||
}
|
||
|
||
.u-transition-zoom-out-enter {
|
||
@include animation-base;
|
||
animation-name: u-transition-zoom-out-in;
|
||
}
|
||
.u-transition-zoom-out-leave {
|
||
@include animation-base;
|
||
animation-name: u-transition-zoom-out-out;
|
||
}
|
||
|
||
@keyframes u-transition-fade-in {
|
||
0% {
|
||
opacity: 0;
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
@keyframes u-transition-fade-out {
|
||
0% {
|
||
opacity: 1;
|
||
}
|
||
100% {
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
@keyframes u-transition-slide-up-in {
|
||
0% {
|
||
opacity: 0;
|
||
transform: translate3d(0, 40rpx, 0);
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
transform: translate3d(0, 0, 0);
|
||
}
|
||
}
|
||
@keyframes u-transition-slide-up-out {
|
||
0% {
|
||
opacity: 1;
|
||
transform: translate3d(0, 0, 0);
|
||
}
|
||
100% {
|
||
opacity: 0;
|
||
transform: translate3d(0, 40rpx, 0);
|
||
}
|
||
}
|
||
|
||
@keyframes u-transition-slide-down-in {
|
||
0% {
|
||
opacity: 0;
|
||
transform: translate3d(0, -40rpx, 0);
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
transform: translate3d(0, 0, 0);
|
||
}
|
||
}
|
||
@keyframes u-transition-slide-down-out {
|
||
0% {
|
||
opacity: 1;
|
||
transform: translate3d(0, 0, 0);
|
||
}
|
||
100% {
|
||
opacity: 0;
|
||
transform: translate3d(0, -40rpx, 0);
|
||
}
|
||
}
|
||
|
||
@keyframes u-transition-slide-left-in {
|
||
0% {
|
||
opacity: 0;
|
||
transform: translate3d(40rpx, 0, 0);
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
transform: translate3d(0, 0, 0);
|
||
}
|
||
}
|
||
@keyframes u-transition-slide-left-out {
|
||
0% {
|
||
opacity: 1;
|
||
transform: translate3d(0, 0, 0);
|
||
}
|
||
100% {
|
||
opacity: 0;
|
||
transform: translate3d(40rpx, 0, 0);
|
||
}
|
||
}
|
||
|
||
@keyframes u-transition-slide-right-in {
|
||
0% {
|
||
opacity: 0;
|
||
transform: translate3d(-40rpx, 0, 0);
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
transform: translate3d(0, 0, 0);
|
||
}
|
||
}
|
||
@keyframes u-transition-slide-right-out {
|
||
0% {
|
||
opacity: 1;
|
||
transform: translate3d(0, 0, 0);
|
||
}
|
||
100% {
|
||
opacity: 0;
|
||
transform: translate3d(-40rpx, 0, 0);
|
||
}
|
||
}
|
||
|
||
@keyframes u-transition-zoom-in-in {
|
||
0% {
|
||
opacity: 0;
|
||
transform: scale(0.85);
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
@keyframes u-transition-zoom-in-out {
|
||
0% {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
100% {
|
||
opacity: 0;
|
||
transform: scale(0.85);
|
||
}
|
||
}
|
||
|
||
@keyframes u-transition-zoom-out-in {
|
||
0% {
|
||
opacity: 0;
|
||
transform: scale(1.1);
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
@keyframes u-transition-zoom-out-out {
|
||
0% {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
100% {
|
||
opacity: 0;
|
||
transform: scale(1.1);
|
||
}
|
||
}
|
||
</style>
|