vue2 + ts 实现透视卡片 + 瀑布上下移动效果
vue2 + ts 实现透视卡片 + 瀑布上下移动效果
vue2 + ts 实现透视卡片 + 瀑布上下移动效果,跟随鼠标透视卡片,附带上下缓慢移动,鼠标移入后暂停,移出后继续,传入delayIndex可实现瀑布上下移动效果。
效果(只是卡片组件功能,内容的样式是通过插槽放进来的):
完整代码:
<!-- 透视卡片组件-CardEffect -->
<template><div class="poster-con-wrapper":class="{ 'glass-card': glassCard }"ref="cardRef":style="{ borderColor: `rgba(${rgb}, 0.5)`, borderRadius: borderRadius }"@mouseenter="onMouseEnter"@mouseleave="onMouseLeave"><slot></slot></div>
</template><script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import abpbase from 'geofly-framework-web-common/libs/abpbase';@Component({name: 'CardEffect',components: {},
})
export default class CardEffect extends abpbase {@Prop({ type: String, default: '0, 160, 160' }) rgb!: string;@Prop({ type: Boolean, default: true }) glassCard!: boolean;@Prop({ type: String, default: '8px' }) borderRadius!: string;@Prop({ type: Number, default: 10 }) maxTilt!: number;// 新增:延迟索引@Prop({ type: Number, default: 0 }) delayIndex!: number;private handleMouseMove!: (e: MouseEvent) => void;private handleMouseLeave!: () => void;private floatY = 0;private targetY = 0;// 最大向下移动距离private floatMaxDown = 20;private floatMaxUp = 5;private floatAnimationId = 0;// 移动速度private floatSpeed = 0.03;mounted() {console.log('mounted-delayIndex', this.delayIndex);this.addMouseTiltEffect();// 延迟启动浮动动画const delayTime = this.delayIndex * 300; // 每张卡片延迟 的ms,可调整setTimeout(() => {this.startFloatEffect();}, delayTime);}beforeDestroy() {console.log('beforeDestroy-delayIndex', this.delayIndex);this.removeMouseTiltEffect();this.stopFloatEffect();this.onMouseLeave();}onClick() {this.$emit('toList');}// ----------------- 鼠标倾斜 -----------------addMouseTiltEffect() {const el = this.$refs.cardRef as HTMLElement;if (!el) return;const { maxTilt } = this;this.handleMouseMove = (e: MouseEvent) => {const rect = el.getBoundingClientRect();const x = (e.clientX - rect.left) / rect.width;const y = (e.clientY - rect.top) / rect.height;const rotateY = (0.5 - x) * 2 * maxTilt;const rotateX = (y - 0.5) * 2 * maxTilt;el.style.transform = `perspective(800px)translateZ(0)translateY(${this.floatY}px)rotateX(${rotateX}deg)rotateY(${rotateY}deg)`;};this.handleMouseLeave = () => {el.style.transform = `perspective(800px) translateZ(0) translateY(${this.floatY}px) rotateX(0deg) rotateY(0deg)`;};el.addEventListener('mousemove', this.handleMouseMove);el.addEventListener('mouseleave', this.handleMouseLeave);}removeMouseTiltEffect() {const el = this.$refs.cardRef as HTMLElement;if (!el) return;el.removeEventListener('mousemove', this.handleMouseMove);el.removeEventListener('mouseleave', this.handleMouseLeave);}// ----------------- 浮动动画 -----------------startFloatEffect() {const el = this.$refs.cardRef as HTMLElement;if (!el) return;let direction = 1; // 1向下 -1向上const animate = () => {if (!this.$root.$data.pauseFloat) {if (direction === 1) {this.targetY = this.floatMaxDown / 1.5;} else {this.targetY = -this.floatMaxUp;}this.floatY += (this.targetY - this.floatY) * this.floatSpeed;if (Math.abs(this.floatY - this.targetY) < 0.1) {direction *= -1;}el.style.transform = `perspective(800px)translateZ(0)translateY(${this.floatY}px)`;}this.floatAnimationId = requestAnimationFrame(animate);};this.floatAnimationId = requestAnimationFrame(animate);}stopFloatEffect() {cancelAnimationFrame(this.floatAnimationId);}// ----------------- 鼠标移入/移出 -----------------onMouseEnter() {this.$root.$data.pauseFloat = true;}// 鼠标移出onMouseLeave() {this.$root.$data.pauseFloat = false;}
}
</script><style lang="less" scoped>
.poster-con-wrapper {position: relative;height: 100%;width: 100%;transition: all 0.1s ease;transform-style: preserve-3d;border: 2px solid rgba(0, 160, 160, 0.7);perspective: 800px;box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);&:hover {box-shadow: 0 12px 24px rgba(0, 0, 0, 0.25);}&::after {content: "";position: absolute; top: 0; left: 0;width: 100%; height: 100%;border-radius: inherit;pointer-events: none;background: linear-gradient(135deg,rgba(255,255,255,0.25) 0%,rgba(255,255,255,0) 60%);}
}.poster-con {background-color: #fff;color: @primary-color;height: 100%;width: 100%;padding: 16px;border-radius: 8px;.poster-content {margin-top: 16px;font-size: 14px;}
}
</style>