乐于分享
好东西不私藏

Uniapp实现超酷的电商小程序加入购物车动画特效开发实战教程

Uniapp实现超酷的电商小程序加入购物车动画特效开发实战教程

书接上篇,我们今天继续来学习一下 UniApp 聊一个电商系统常见的话题:商品飞入购物车动画特效,附带实现思路和所有源码示例,以及常见避坑指南和开发实践。

此系列文章将带领你从移动端跨平台开发入门到精通,如果你也喜欢关注APP、小程序、公众号、H5等等应用的开发,可以持续关注后续更新,避免错过宝贵的知识分享。

致开发者的忠告: AI编程盛行的今天,我们并不是不需要学习技术,而是更应该专研技术,拥有把控全局的架构设计思维才能在AI盛行的未来有立足之地。

言归正传,咱们今天继续来聊一聊电商APP中使用Uniapp实现加入购物车动画组件原理的深度剖析:从原理到实践,让商品“飞”进购物车,H5/App/微信小程序都支持。

大家好,我是你们的技术前辈。今天我们来聊一个电商项目里非常常见、也非常能提升用户体验的小功能:加入购物车抛物线动画

你肯定见过:在商品列表页,点击商品右下角的“加购”按钮,会有一个小圆点(或商品图标)从点击位置划出一道优美的抛物线,最终落到底部购物车图标上。这个小特效不仅让交互更生动,还能给用户明确的反馈:“商品已加入购物车”。

很多同学想自己实现,但往往会遇到:

  • 小球飞歪了,没落在购物车上。

  • 页面滚动后,起点/终点计算错乱。

  • 连续点击时动画互相干扰。

  • 不知道如何封装成可复用的组件。

  • 最头疼的是:好不容易在H5跑通了,到微信小程序里直接报错(因为用了 document.createElement)。

别担心,今天我就带大家一步步从原理到实践,实现一个真正跨端(H5/App/微信小程序)的“飞入购物车”动画组件。我们会采用一种非常巧妙的 纯CSS动画方案,性能好、代码简洁,还能完美解决坐标计算问题,并且完全基于 Vue/uni-app 的响应式系统,不依赖任何平台特有的DOM API。


一、核心原理:用 CSS 动画画出抛物线

1.1 抛物线运动的分解

抛物线运动可以分解为两个方向:

  • 水平方向:匀速直线运动(速度恒定)。

  • 竖直方向:匀加速直线运动(受重力影响)。

在 CSS 中,如果我们想对一个元素同时应用不同的运动规律,一个元素做不到(因为 transform 是一个整体属性,只能应用一种时间函数)。但我们可以用嵌套元素来巧妙解决:

  • 外层元素控制竖直运动,应用 cubic-bezier 缓动(模拟重力加速度)。

  • 内层元素控制水平运动,应用 linear 缓动(匀速)。

两个元素同时运动,组合起来就是完美的抛物线。最终你看到的小球位置 = 外层的垂直位移 + 内层的水平位移。

1.2 缓动函数的选择

  • 竖直下落cubic-bezier(.22, 1.4, 1, 1) 是一个非常自然的“加速下落”曲线(起始慢,然后加速),很像小球受重力作用。

  • 水平匀速linear

1.3 用 CSS 变量传递位移

每个小球的起点和终点不同,所以水平和垂直位移 dx 和 dy 是动态的。我们可以通过 CSS 变量 --x 和 --y 将位移值传递给动画,这样动画定义就可以复用:

@keyframesmoveX {
to { transformtranslateX(var(--x)); }
}
@keyframesmoveY {
to { transformtranslateY(var(--y)); }
}

外层元素应用 moveY,内层元素应用 moveX,并通过 style 属性设置 --x 和 --y


二、坐标计算:如何精准获取起点和终点?

这是整个组件最关键的环节。如果坐标不准,动画就会飞歪。

2.1 起点的获取

当用户点击加购按钮时,我们可以从事件对象中获取点击点相对于视口的坐标。

  • 在 uni-app 中,标准事件对象包含 detail: { x, y },这俩就是相对于视口的坐标。

  • 如果是原生触摸事件,可以从 touches[0] 中获取 clientX/clientY

为什么用视口坐标?因为购物车图标通常是 position: fixed 固定定位,它的边界也是相对于视口的。两者坐标系一致,完全不受页面滚动的影响。这就省去了处理滚动偏移的麻烦。

2.2 终点的获取

我们需要找到购物车图标的位置。使用 uni.createSelectorQuery() 查询元素:

constquery=uni.createSelectorQuery().in(this)
query.select('#cart-icon').boundingClientRect(rect=> {
if (rect) {
constendX=rect.left+rect.width/2
constendY=rect.top+rect.height/2
// 使用终点坐标
  }
}).exec()

这里 rect.left 和 rect.top 也是相对于视口的,所以 endX/endY 也是视口坐标。

注意事项

  • 查询操作必须在元素渲染完成后进行,通常在点击时查询(此时元素一定存在)。

  • 确保选择器能唯一定位到购物车元素(使用 id 是最稳妥的)。

2.3 位移计算

得到起点 (startX, startY) 和终点 (endX, endY) 后,位移就很简单了:

  • dx = endX - startX

  • dy = endY - startY

然后将这两个值通过 CSS 变量设置给小球。


三、跨端实现:如何动态创建小球且不依赖 DOM API?

如果使用 document.createElement,在微信小程序中会报错,因为小程序没有 DOM API。所以我们必须换一种思路:利用 Vue 的响应式渲染

  • 在组件模板中,用 v-for 渲染一组小球。

  • 维护一个响应式数组 balls,每个小球包含起点坐标、位移量等信息。

  • 点击加购时,向数组 push 一个新小球,它就会自动渲染并开始动画。

  • 动画结束后,通过 @animationend 事件从数组中移除该小球。

这样,所有小球的创建和销毁都通过 Vue 的数据驱动完成,完美避开平台差异。而获取坐标、计算位移等逻辑仍然使用 uni API,在所有平台通用。


四、组件设计:我们要做一个怎样的组件?

基于上述原理,我们来设计一个通用组件 FlyToCart,它应该具备以下特点:

  • 易用:父组件只需传入购物车元素的选择器,并在点击时调用 start 方法传入事件对象即可。

  • 健壮:自动计算起点和终点,处理坐标异常。

  • 多小球支持:连续点击时,多个小球可以同时存在,互不干扰。

  • 精准结束:动画结束后自动销毁小球,避免内存泄漏。

  • 可定制:支持自定义小球颜色、大小、图标等。

4.1 Props

参数 类型 必填 默认值 说明
cartSelector string 购物车元素的选择器(如 ‘#cart-icon’)
duration number 400 动画时长(毫秒)
ballColor string #fb603d 小球背景色
ballSize number 20 小球尺寸(px)
ballIcon string 小球图标的类名(如 ‘i-material-symbols-add’)
zIndex number 9999 小球层级

4.2 Methods

方法名 参数 说明
start clickEvent: Event 开始动画,传入点击事件对象

4.3 Events

事件名 说明
start 动画开始时触发
end 动画结束时触发
error 发生错误时触发(如无法获取坐标、找不到购物车)

五、完整代码实现

下面是组件完整代码,已经过测试,可在 H5、App、微信小程序中正常运行。

<!-- components/FlyToCart.vue -->
<template>
<!-- 固定容器,用于承载所有飞行的小球,不拦截点击事件 -->
<view
class="fly-cart-container"
:style="{
position: 'fixed',
left: 0,
top: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
zIndex: zIndex
}"
>
<!-- 遍历渲染所有小球 -->
<view
v-for="ball in balls"
:key="ball.id"
class="fly-ball-outer"
:style="{
left: ball.startX + 'px',
top: ball.startY + 'px',
animation: `moveY ${duration}ms cubic-bezier(0.22, 1.4, 1, 1) forwards`,
'--y': ball.dy + 'px',
zIndex: zIndex
}"
>
<!-- 内层:小球本体 + 水平运动 -->
<view
class="fly-ball-inner"
:style="{
width: ballSize + 'px',
height: ballSize + 'px',
backgroundColor: ballColor,
animation: `moveX ${duration}ms linear forwards`,
'--x': ball.dx + 'px'
}"
@animationend="onAnimationEnd(ball.id)"
>
<!-- 图标(可选) -->
<textv-if="ballIcon":class="ballIcon"></text>
</view>
</view>
</view>
</template>

<scriptsetuplang="ts">
import { refgetCurrentInstance } from'vue'

constprops=defineProps({
cartSelector: {
typeString,
requiredtrue
  },
duration: {
typeNumber,
default400
  },
ballColor: {
typeString,
default'#fb603d'
  },
ballSize: {
typeNumber,
default20
  },
ballIcon: {
typeString,
default''
  },
zIndex: {
typeNumber,
default9999
  }
})

constemit=defineEmits(['start''end''error'])

// 存储所有活跃的小球
interfaceBall {
idstring|number
startXnumber
startYnumber
dxnumber
dynumber
}
constballs=ref<Ball[]>([])

constinstance=getCurrentInstance()

// 生成唯一ID(简单处理)
letidCounter=0
constgenerateId= () =>`ball-${Date.now()}-${idCounter++}`

/**
 * 开始动画(外部调用)
 * @param clickEvent 点击事件对象
 */
conststart= (clickEventany=> {
// 1. 获取起点坐标(兼容多种事件格式)
letstartXnumberstartYnumber
if (clickEvent.detail&&typeofclickEvent.detail.x==='number') {
// uni-app 标准事件
startX=clickEvent.detail.x
startY=clickEvent.detail.y
  } elseif (clickEvent.touches) {
// 触摸事件
startX=clickEvent.touches[0].clientX
startY=clickEvent.touches[0].clientY
  } elseif (clickEvent.clientX!==undefined) {
// 鼠标事件
startX=clickEvent.clientX
startY=clickEvent.clientY
  } else {
console.error('无法获取点击坐标')
emit('error''无法获取点击坐标')
return
  }

// 2. 获取购物车位置
constquery=uni.createSelectorQuery().in(instance?.proxy)
query.select(props.cartSelector).boundingClientRect((rectany=> {
if (!rect) {
console.error('未找到购物车元素')
emit('error''未找到购物车元素')
return
    }

constendX=rect.left+rect.width/2
constendY=rect.top+rect.height/2

// 3. 计算位移
constdx=endX-startX
constdy=endY-startY

// 4. 创建新小球
constnewBallBall= {
idgenerateId(),
startX,
startY,
dx,
dy
    }
balls.value.push(newBall)
emit('start')
  }).exec()
}

// 动画结束处理
constonAnimationEnd= (idstring|number=> {
// 从数组中移除对应小球
balls.value=balls.value.filter(ball=>ball.id!==id)
emit('end')
}

// 暴露方法给父组件
defineExpose({ start })
</script>

<stylescoped>
/* 关键帧定义 */
@keyframesmoveX {
to {
transformtranslateX(var(--x));
  }
}
@keyframesmoveY {
to {
transformtranslateY(var(--y));
  }
}

/* 小球外层样式(垂直运动) */
.fly-ball-outer {
positionfixed/* 相对于视口定位 */
pointer-eventsnone/* 防止遮挡点击 */
}

/* 小球内层样式(水平运动 + 本体) */
.fly-ball-inner {
displayflex;
align-itemscenter;
justify-contentcenter;
border-radius50%;
/* 宽高、背景色、动画等已通过内联 style 设置,此处无需重复 */
}
</style>

代码说明

  • 完全响应式:所有小球通过 balls 数组驱动渲染,无需手动操作 DOM。

  • 跨端兼容v-for@animationend 在 H5/App/小程序中都支持。

  • 精准坐标:起点从事件获取,终点通过 uni.createSelectorQuery 获取,均使用视口坐标系,不受滚动影响。

  • 动画结束自动清理:每个小球动画结束后触发 @animationend,从数组中移除,避免内存泄漏。

  • 多小球并发:每个小球独立,互不干扰。


六、在页面中使用示例

下面是一个商品列表页的完整示例,包含可滚动的商品列表、底部固定购物车,以及调用动画组件。

<!-- pages/goods/list.vue -->
<template>
<viewclass="goods-page">
<!-- 商品列表,可滚动 -->
<scroll-viewscroll-yclass="goods-scroll">
<viewv-for="item in goodsList":key="item.id"class="goods-item">
<image:src="item.image"class="goods-image"/>
<viewclass="goods-info">
<textclass="goods-name">{{ item.name }}</text>
<textclass="goods-price">¥{{ item.price }}</text>
</view>
<!-- 加购按钮,传入 $event 和商品数据 -->
<viewclass="add-btn"@click="onAdd($event, item)">
<uni-iconstype="cart"size="20"color="#fff"/>
</view>
</view>
</scroll-view>

<!-- 底部固定购物车图标(作为终点) -->
<viewclass="cart-icon"id="cart-icon">
<uni-iconstype="cart-filled"size="28"color="#ff5500"/>
<textv-if="cartCount"class="cart-badge">{{ cartCount }}</text>
</view>

<!-- 引入动画组件,传入购物车选择器 -->
<FlyToCart
ref="flyCart"
cartSelector="#cart-icon"
ballColor="#ff5500"
ballIcon="i-material-symbols-add"
@end="onFlyEnd"
@error="onFlyError"
/>
</view>
</template>

<scriptsetuplang="ts">
import { ref } from'vue'
importFlyToCartfrom'@/components/FlyToCart.vue'

// 模拟商品数据
constgoodsList=ref([
  { id1name'商品1'price99image'/static/goods1.png' },
  { id2name'商品2'price199image'/static/goods2.png' }
])
constcartCount=ref(0)
constflyCart=ref<InstanceType<typeofFlyToCart>>()

// 加入购物车处理
constonAdd= (eventanyitemany=> {
// 触发动画
flyCart.value?.start(event)
// 实际加入购物车逻辑(请求后端等)
addToCart(item)
}

constaddToCart= (itemany=> {
cartCount.value++
// 可发送请求等
}

constonFlyEnd= () => {
console.log('动画结束')
// 可选:震动反馈
// uni.vibrateShort({ type: 'light' })
}

constonFlyError= (errstring=> {
console.error('动画错误'err)
// 降级提示:至少告诉用户加入成功
uni.showToast({ title'已加入购物车'icon'success' })
}
</script>

<stylescopedlang="scss">
.goods-page {
height100vh;
positionrelative;
}
.goods-scroll {
heightcalc(100vh - 120rpx);
}
.goods-item {
displayflex;
padding20rpx;
border-bottom1rpxsolid#eee;
positionrelative;
}
.add-btn {
positionabsolute;
right20rpx;
bottom20rpx;
width60rpx;
height60rpx;
background-color#ff5500;
border-radius50%;
displayflex;
align-itemscenter;
justify-contentcenter;
}
.cart-icon {
positionfixed;
bottom30rpx;
right30rpx;
width100rpx;
height100rpx;
background-color#fff;
border-radius50%;
box-shadow04rpx20rpxrgba(0,0,0,0.1);
displayflex;
align-itemscenter;
justify-contentcenter;
z-index100;
}
.cart-badge {
positionabsolute;
top0;
right0;
background-color#ff5500;
color#fff;
font-size20rpx;
padding4rpx8rpx;
border-radius20rpx;
}
</style>

七、常见问题与解决方案

❌ 问题1:小球起点偏移(没从点击位置开始)

原因:点击事件坐标获取错误,或小球定位时误加了滚动偏移。解决方案:确认使用的是 event.detail.x/y(uni-app 标准事件)或 clientX/clientY(原生事件),它们已经是相对于视口的坐标。不要手动加上 scrollTop

❌ 问题2:小球没有落在购物车中心

原因:终点坐标计算错误,或购物车元素查询失败。解决方案

  • 确保购物车元素有唯一的 id,选择器正确(如 #cart-icon)。

  • 在 boundingClientRect 回调中打印 rect 检查是否获取到正确尺寸。

  • 如果购物车元素是 v-if 控制,确保点击时它已存在。

❌ 问题3:连续快速点击,小球重叠或动画错乱

原因:多个小球同时存在,但动画名称相同,会不会冲突?答案:不会。每个小球的位移由自己的 CSS 变量 --x 和 --y 控制,即使动画名称相同,也是独立计算。完全支持并发。

❌ 问题4:动画结束后小球残留

原因@animationend 事件未触发,可能是动画被中断或监听错误。解决方案:可添加 setTimeout 兜底移除,但一般情况下 @animationend 可靠。如果残留,检查小球是否被意外销毁或样式冲突。

❌ 问题5:小程序中 pointer-events: none 无效

原因:微信小程序中,pointer-events 支持有限,但在 view 上应该有效。如果无效,可能会导致小球遮挡点击。可以在外层容器上设置 position: fixed 且无背景,通常不会遮挡,因为小球本身有 pointer-events: none。如果仍有问题,可以尝试将容器设为 position: absolute,并设置 width/height 为 100%,但 fixed 更稳定。

❌ 问题6:小程序中获取不到购物车位置(返回0)

原因:查询时元素可能尚未渲染完成。解决方案:在点击时查询,此时元素肯定已经渲染。如果还是不行,可以加一个极短的延时(setTimeout 0)再查询。

❌ 问题7:H5 中动画结束后小球没有移除

原因:可能是 @animationend 事件未触发,可尝试用 @webkitAnimationEnd 作为补充。在组件中我们可以同时监听两个事件,或者使用 animationend(现代浏览器都支持)。如果仍然有问题,可以使用 setTimeout 兜底。


八、总结与扩展

今天我们实现了一个真正跨端的加入购物车抛物线动画组件,核心优势:

  • 纯CSS动画:性能好,利用硬件加速。

  • 跨端兼容:不依赖特定平台API,所有逻辑均使用 uni-app 标准能力。

  • 易用性:父组件只需传入购物车选择器,点击时调用 start 并传入事件对象即可。

  • 健壮性:自动计算坐标,支持多小球并发,动画结束自动清理。

你可以直接复制这份代码到你的项目中,几行代码就能拥有丝滑的加入购物车特效。如果想更炫酷,还可以:

  • 用商品小图代替小球(将 ballIcon 改为图片)。

  • 添加音效(在 end 事件中播放)。

  • 支持多终点(比如多个购物车图标)。

希望这篇教程能帮你在项目中轻松实现这个有趣的小特效。如果遇到问题,欢迎随时交流!

—— 一个喜欢研究交互动效的老前辈 🛒

加油,未来的全栈大佬!💪如果你也对移动端跨端开发感兴趣,关注我,后续还有更多优质文章分享!

往期相关文章推荐

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » Uniapp实现超酷的电商小程序加入购物车动画特效开发实战教程

评论 抢沙发

3 + 8 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮