CNCEC_APP/uni_modules/uview-pro/components/u-transition/u-transition.vue

438 lines
11 KiB
Vue
Raw Normal View History

2026-03-25 14:54:15 +08:00
<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-outfade
* @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>