news 2026/10/14 6:01:10

小程序滚动卡片实践:从基础轮播到3D翻转与真机避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
小程序滚动卡片实践:从基础轮播到3D翻转与真机避坑

最近好几个朋友都在问小程序里那种“滚动卡片”到底怎么写,明明官方给了swiper组件,看上去一套就完事,但真要做到卡片层叠、3D翻转、边缘吸附这种效果时,又总觉得差点意思。这篇文章我就把这套东西彻底拆开:从最简单的swiper轮播,到层层递进的卡片叠放,再到带透视的 3D 翻转,把每个效果背后的计算逻辑、事件处理和真机踩坑都过一遍。不管你是刚接触小程序前端,还是已经写腻了普通列表页,这篇都值得花十分钟慢慢看。

1. 先把滚动卡片的底层逻辑讲清楚

很多人在做滚动卡片前,第一反应是找swiper的文档,然后照着indicator-dots加个圆点就当完成任务。但等真正去做的时候,会遇到几个绕不开的问题:卡片左右露出的宽度怎么控制、为什么高度总是塌陷、切换时另一张卡片上的点击事件为什么会被触发。这些都不是swiper的 bug,而是我们对它的渲染模型理解得不够。

1.1 一切滚动卡片都建立在同一个交互模型上

swiper组件本质上是一个可以感知手势的容器,里面每个swiper-item是一张独立的“页面级”子节点。它和普通的scroll-view滚动不太一样:scroll-view是你推多少走多少,靠惯性自然停止;而swiper是滑动结束后,根据位移量、速度和目标位置自动吸附到最近的一个swiper-item上。

这就决定了所有滚动卡片的交互核心不是“滚动”,而是“当前项”。它内部维护了一个current状态,每次滑动结束,当前项都会更新。我们做卡片效果时,绝大多数操作都是在监听这个状态的变化,然后驱动卡片做位移、缩放、旋转。

swiper的每个swiper-item之间是独立的,默认情况下,它们之间没有间距,一个紧贴一个。但卡片效果通常需要露出前后卡片的“边”,用来暗示后面还有内容,这就引出了previous-margin和next-margin这两个属性。

<swiper previous-margin="60rpx" next-margin="60rpx"> <swiper-item><view class="card">1</view></swiper-item> <swiper-item><view class="card">2</view></swiper-item> </swiper>

previous-margin表示第一个swiper-item距离容器左边的间距,next-margin表示最后一个swiper-item距离容器右边的间距。加了这个之后,当前卡片就不会占满整个屏幕宽度,左右两侧会露出相邻卡片的局部,给人一种“后面还有一叠卡片”的视觉暗示。

从底层讲,swiper-item的宽度始终是容器的宽度,previous-margin和next-margin只是改变了可视区域边界,卡片内容实际还是按整个swiper-item的宽度去摆放。如果不想让卡片内容占满整屏,那就要让卡片的宽度小于容器的宽度,居中摆放。

1.2 一个公式解决卡片尺寸和间距

实际操作中,我一般不会让卡片占满整个swiper容器,而是留出一个比例。最稳妥的做法是:卡片宽度按屏幕宽度的一定百分比来设置,通常留出两侧的露出边距后,卡片宽度显得比较紧凑。

以 750rpx(等于屏幕宽度)为基准:

  • 左右各留60rpx,也就是卡片可视宽度 =750 - 60 * 2 = 630rpx
  • 希望左右露出的边缘宽度约70rpx,那么卡片实际宽度 =630 - 70 * 2 = 490rpx
  • 卡片高度按比例算,比如高宽比1.4,那么高度 =490 * 1.4 = 686rpx
.card { width: 490rpx; height: 686rpx; margin: 0 auto; border-radius: 24rpx; box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.15); background: #ffffff; overflow: hidden; }

为什么用 rpx 而不用 px?因为 rpx 会根据屏幕宽度自动换算,不同机型显示比例一致。但要注意,小程序里有些属性不支持 rpx,比如box-shadow的模糊半径在部分安卓机型上解析会有偏差,这时可以用px或者用calc做混合计算。更保险的方式是先在onLoad里用wx.getSystemInfoSync()拿到屏幕宽度,再换算成精确的px值,动态绑定到卡片样式上。

卡片间距的控制不只是在previous-margin上做文章,还可以通过给swiper-item内部加间距来调整。我见过不少实现是把间距放在了卡片上,给卡片加margin: 0 16rpx,这种做法的风险在于,当卡片做缩放或位移时,margin会参与布局计算,导致位移距离和视觉距离不一致。建议把间距放在swiper-item的内层容器上,或者干脆用padding而不是margin。

2. 三种主流卡片滚动效果拆解

基础轮播、层叠递进、3D翻转,这三个是滚动卡片里最常见,也最能体现“手感”差异的形态。我一个个拆开讲,附上可以直接用的代码和关键参数。

2.1 基础轮播卡片:从“能滑”到“好滑”

基础轮播是最简单的形态,一个swiper套上swiper-item,每张卡片里面放图片、文字。但很多人做完之后发现和别人的“高级感”差了很多,问题往往出在三个细节上。

第一,卡片要有圆角和阴影,并且要给图片也加同样的圆角,否则图片会顶出圆角边界。第二,卡片内容不要直接放在swiper-item的根节点上,最好内层包一层view,方便控制圆角、阴影和背景色。第三,swiper的高度不能写成auto,它不支持根据内容自适应高度,必须显式写死高度或通过样式计算得到。

<swiper class="card-swiper" previous-margin="60rpx" next-margin="60rpx" circular="{{false}}" duration="400" bindchange="onCardChange"> <swiper-item wx:for="{{list}}" wx:key="id"> <view class="card-wrap"> <image class="card-img" src="{{item.cover}}" mode="aspectFill" lazy-load /> <text class="card-title">{{item.title}}</text> </view> </swiper-item> </swiper>

这里要注意bindchange的触发时机。它在滑动结束、当前项切换之后触发,event.detail.current就是当前卡片的索引。不要在bindchange里做复杂的同步操作,因为手指还在屏幕上、动画还在跑,此时更新数据容易引发二次渲染,导致动画抖一下。

好用的基础轮播往往还加了一个细节:当前卡片有一个“放大”,非当前卡片有“缩小”。这个效果不需要额外动画库,只需要监听current变化,然后给非当前卡片设置一个缩放比例即可。

.card-swiper .card-wrap { transition: transform 0.3s ease-out, opacity 0.3s ease-out; } .card-swiper .card-wrap.inactive { transform: scale(0.9); opacity: 0.6; }

然后在onCardChange里把所有非当前项加上inactive类,当前项去掉。这里要特别提醒:swiper-item默认会缓存附近的节点,切换速度很快时,样式更新可能没跟上,所以最好把current数据也绑定到每个swiper-item上,通过数据驱动而不是直接操作DOM类名。

2.2 层叠递进卡片:像扑克牌抽拉一样的手感

层叠递进效果,就是当前卡片在最上层,下一张卡片在当前卡片的右侧或下方露出一点点,再后面的卡片依次递减,形成一个“叠起来”的视觉。微信里很多内容卡片流用的就是这个形式。

这种效果用swiper-item加previous-margin、next-margin也能做出一点雏形,但要真正达到“后面卡片叠上来”的效果,需要动态调整非当前卡片的位移和缩放。

思路是这样:假设当前项索引为current,对于第i张卡片,当i < current时,说明它已经滑过去了,可以把它往左移动并缩小;当i === current时,它在最上层;当i > current时,它在右边叠着,往右缩进并缩小。

一个可用的方案是在每个swiper-item上动态绑定transform样式,根据i与current的差值计算偏移量和缩放比例。偏移量可以用一个基准值,比如60rpx乘以差值,缩放值可以是0.9 + 差值 * 0.05。注意用wx:if去控制展示隐藏是不行的,因为swiper需要所有卡片都存在于节点树中才能计算滑动距离。

<swiper-item wx:for="{{list}}" wx:key="id"> <view class="layer-card" style="transform: translateX({{itemOffset}}rpx) scale({{itemScale}}); z-index: {{itemZ}};"> <image src="{{item.cover}}" mode="aspectFill" /> </view> </swiper-item>

每个itemOffset、itemScale、itemZ在onCardChange中统一计算,然后通过setData更新。这里最大坑点是:不要直接在swiper-item上设置transform,因为swiper-item的布局是内部控制的,强行加transform会影响它的滑动位置计算。必须再包一层view。

层叠递进还有一个变体:竖向卡片叠放,也就是当前卡片在最上方,下面的卡片依次向右下偏移,形成像扇子一样的展开效果。这种效果用swiper的vertical属性翻转方向,再对非当前项做translateY偏移即可。竖向卡片在手势上和横向不同,需要多测试真机上的惯性效果,安卓机和iOS机的惯性差异特别明显。

2.3 3D翻转卡片:给卡片一个“立体转身”

3D翻转效果是很多展示型页面的首选。它给人的冲击力远大于普通轮播,但实现起来并不复杂,核心是两件事:透视和旋转。

只要是做 3D 效果,父容器必须有perspective,也就是透视距离。perspective越大,物体近大远小的效果越弱;perspective越小,翻转时的纵深感越强。一般取800px ~ 1200px之间比较自然。在小程序里,可以直接给swiper容器加style="perspective: 1000px;"。

然后是卡片本身,用transform-style: preserve-3d让子元素保留3D空间,再对卡片进行rotateY旋转。一个完整的翻转卡片往往有两面:正面和反面。正面是卡片内容,反面是卡背。两面的层级通过backface-visibility: hidden互相遮挡。

<view class="flip-scene"> <view class="flip-card {{current === index ? 'flipped' : ''}}"> <view class="flip-front"> <image src="{{item.cover}}" mode="aspectFill" /> </view> <view class="flip-back"> <text>{{item.description}}</text> </view> </view> </view>
.flip-scene { width: 500rpx; height: 700rpx; perspective: 1000px; } .flip-card { width: 100%; height: 100%; position: relative; transition: transform 0.6s cubic-bezier(0.4, 0.2, 0.2, 1); transform-style: preserve-3d; } .flip-card.flipped { transform: rotateY(180deg); } .flip-front, .flip-back { position: absolute; width: 100%; height: 100%; backface-visibility: hidden; border-radius: 24rpx; overflow: hidden; } .flip-back { transform: rotateY(180deg); display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #f5f7fa, #c3cfe2); }

翻不翻面通常不是根据滑动来控制,而是点击卡片触发切换。如果你希望滑动和翻转同时存在,那要谨慎处理手势:swiper的左右滑动会触发轮播,点击会触发翻转,这两个事件在快速操作时容易冲突。我建议的做法是,翻转只针对当前卡片,通过点击事件触发,而不是让swiper的bindchange直接带动翻转,否则用户滑到一半卡片翻转,体验会非常诡异。

3. 手势与状态同步:这些API其实是有脾气的

很多人在swiper上栽跟头,都是因为对它的事件机制理解不透。bindchange、current、circular这三个东西组合起来,会有不少反直觉的表现。

3.1 current和bindchange到底什么时候触发

先说结论:bindchange只会在滑动结束且当前项发生变化后触发。如果你手指快速连滑两下,bindchange可能只触发一次,而且event.detail.current的数值可能不是从 0 到 1,而是直接跳到 2。这就是为什么很多动态计算偏差的根源。

解决思路是:不要依赖current的“过程值”,只把它当作“结果值”。每次bindchange触发后,用当前的current与上一次的current做差值,如果差值大于 1,说明发生了跳变,这时可以忽略掉中间的卡片动画,直接把状态定位到最终项。

let lastIndex = 0; onCardChange(e) { const current = e.detail.current; const diff = current - lastIndex; // 处理跳变 if (Math.abs(diff) > 1) { // 直接跳到目的卡片,不做逐张递进动画 this.setData({ current, cardStyleList: this.computeLayerStyles(current), }); } else { // 正常相邻切换,做递进动画 this.setData({ current, cardStyleList: this.computeLayerStyles(current), }); } lastIndex = current; }

另外要提一个细节:swiper的duration属性控制的是从一页到另一页的动画时长,但它不会影响手指滑动时的跟手速度。真机上手感偏“肉”还是偏“灵”,主要是easing-function属性在起作用。微信小程序里可以设置easing-function="easeOutCubic",会让滑动结束时有个明显的减速过程,手感更柔和。

3.2 防止点击穿透与滑动误触

滚动卡片上经常会放按钮、链接、图片,用户点击卡片和滑动卡片之间的冲突需要小心处理。默认情况下,swiper-item内部的点击事件可以被正常触发,但如果你在卡片上做了一些自定义手势,比如按住拖动、长按弹菜单,这两个手势会和swiper的滑动争夺事件响应权。

这里涉及到小程序手势体系里两个关键方法:bindtouchstart、bindtouchmove。如果用bindtouchmove并调用preventDefault,在新版本基础库中是不生效的,必须用catchtouchmove来阻止事件冒泡。但一旦用了catchtouchmove,swiper本身也收不到滑动事件了,滑动手势就会失效。

一个业界常用的折中方案是:用bindtouchstart记录手指初始位置,用bindtouchend计算位移距离。如果位移距离小于某个阈值(比如 10px),就判定为点击,执行点击跳转;否则就判定为滑动,交给swiper处理。这种方法不阻止事件冒泡,所以swiper的滑动不会受影响。

onTouchStart(e) { this.startX = e.touches[0].clientX; this.startY = e.touches[0].clientY; }, onTouchEnd(e) { const endX = e.changedTouches[0].clientX; const endY = e.changedTouches[0].clientY; const deltaX = endX - this.startX; const deltaY = endY - this.startY; if (Math.abs(deltaX) < 10 && Math.abs(deltaY) < 10) { this.onCardTap(); } }

这个方法实测下来可以省掉很多边界问题,但有一个小小的副作用:点击事件的响应会比普通tap慢那么几十毫秒,因为需要等touchend来判断是不是点击。在需要极速响应的场景(比如抽奖转盘、卡片抢购)里,建议还是用普通tap,放弃自定义手势。

3.3 swiper-circular 的两个边界卡点

很多卡片效果为了无缝轮播,会把circular设为true,让最后一张滑完后能直接回到第一张。但circular有两个比较隐晦的问题。

第一个是只有两张卡片的时候,circular的表现会异常,严格来说基础库在两张卡片时对循环路径的计算会有问题,滑动的方向和动画的衔接都不自然。第二个问题更隐蔽:当你开启了circular,bindchange里的current在从最后一张跳回第一张时,可能会连续触发两次,第二次的current才回到 0。

对于两张卡片的场景,一个比较稳妥的解决思路是:放弃circular,改为“数据循环”。也就是数据源里不只有两张卡片,而是把数据扩充成四份,让用户感觉是循环的,但swiper本身不做真循环。这在小程序里叫伪循环,实现起来很直接:列表list复制一份拼接,切换到第二份的“第一张”时,用current绑定值瞬间调回真正数据处理逻辑。

pageToData(pageIndex) { // pageIndex是可视轮播位置的索引 const length = this.data.baseList.length; const realIndex = pageIndex % length; return this.data.baseList[realIndex]; }

伪循环的代码量不大,但能避免很多circular的坑,尤其是在图片懒加载和支付宝小程序/百度小程序等不同平台的兼容性上,伪循环的优势非常明显。

4. 一个范例项目:模拟项目X的卡片展示页

理论讲了一堆,不如直接给一套能跑起来的代码。我这里用的场景是一个“模拟项目X”的存储机房设备巡检卡片页,每张卡片代表一台设备,包含设备名称、状态和最近一次巡检时间。用户左右滑动卡片查看设备信息,顶部显示当前设备编号。

4.1 页面结构与 WXML 要点

<view class="page-container"> <view class="header"> <text class="title">设备巡检卡片</text> <text class="page-info">{{current + 1}} / {{total}}</text> </view> <swiper class="device-swiper" previous-margin="40rpx" next-margin="40rpx" duration="300" easing-function="easeOutCubic" bindchange="onCardChange"> <swiper-item wx:for="{{deviceList}}" wx:key="id"> <view class="device-card {{current === index ? 'active' : ''}}"> <image class="device-cover" src="{{item.cover}}" mode="aspectFill" lazy-load /> <view class="device-info"> <text class="device-name">{{item.name}}</text> <text class="device-status status-{{item.status}}"> {{item.statusText}} </text> <text class="device-time">最近巡检: {{item.time}}</text> </view> </view> </swiper-item> </swiper> </view>

注意这里没有用之前讲的复杂层叠样式,原因是基础轮播在这个场景已经够用,而且代码更少。但是我把active类加上了,用于在当前卡片变大、非当前卡片缩小,增加一点层次感。在swiper-item上不要写style去控制大小,因为swiper-item的尺寸是内部管理,写style很容易被覆盖。

4.2 WXSS 动效与兼容性写法

.device-swiper { height: 860rpx; margin-top: 40rpx; } .device-card { width: 600rpx; height: 800rpx; margin: 0 auto; background: #ffffff; border-radius: 32rpx; box-shadow: 0 16rpx 40rpx rgba(0, 0, 0, 0.08); overflow: hidden; transition: transform 0.3s ease-out, box-shadow 0.3s ease-out; display: flex; flex-direction: column; } .device-card.active { transform: scale(1); box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.12); } .device-card:not(.active) { transform: scale(0.92); opacity: 0.7; }

transition写在device-card基类上是关键,只有这样才能保证active类切换时动画连贯。opacity的调整是为了让非当前卡片弱化存在感,引导用户视线集中在当前卡片上。box-shadow在真机上渲染性能略高,不要给太多层,否则在低端安卓机上会产生明显掉帧。

这里还要提一下overflow: hidden的坑。如果卡片内层有一个按钮,按钮的投影或气泡提示需要超出卡片范围,overflow: hidden会把它们裁掉。如果你的卡片不需要背景图延伸到边缘,可以考虑把overflow: hidden去掉,改用border-radius加background-clip: padding-box的方式控制圆角。

4.3 JS 逻辑与数据变化

const mockData = [ { id: 1, name: '机房A区核心交换机', cover: '/images/device-1.png', status: 'normal', statusText: '正常', time: '2025-01-10 08:30' }, { id: 2, name: '机房B区汇聚路由', cover: '/images/device-2.png', status: 'warning', statusText: '告警', time: '2025-01-09 22:10' }, { id: 3, name: '机房C区防火墙', cover: '/images/device-3.png', status: 'error', statusText: '故障', time: '2025-01-10 06:45' }, { id: 4, name: '机房D区负载均衡', cover: '/images/device-4.png', status: 'normal', statusText: '正常', time: '2025-01-08 15:20' }, ]; Page({ data: { deviceList: mockData, current: 0, total: mockData.length, }, onCardChange(e) { this.setData({ current: e.detail.current, }); // 这里可以追加埋点上报或者联动刷新详情区域 this.updateDetailPanel(e.detail.current); }, updateDetailPanel(index) { const device = this.data.deviceList[index]; console.log('当前选中设备', device.name); }, });

数据上没什么花活,就是一个mockData数组。但真实项目中经常会出现一种情况:接口数据只有 3 条,卡片展示需要 10 条,或者第一张卡片是运营位,后面是列表数据。这时候别把两条不同数据结构强行塞进一个deviceList里,建议计算属性displayList来合并原始数据和新增加数据,保证deviceList永远是一个逻辑完整、结构一致的数据源。

另外,需要注意setData的性能。swiper滑动过程中每次bindchange触发setData时,如果数据体很大,比如deviceList里每项都有一张大图 base64,那会造成明显的滑动卡顿。此时只应该更新current和visible相关的局部数据,卡片列表本身用一个只读常量,或者直接用全局变量缓存。

5. 真机踩坑清单:七条值得记下来的教训

小程序开发最特别的地方在于开发工具和真机的差异特别大,有些效果模拟器上看着完美,一到真机就露出原型。下面是我在多个项目里反复踩过的坑,列成一个清单,每条都是真金白银换来的。

5.1 真机图片和视频在卡片里怎么不露馅

卡片内图片建议全部使用mode="aspectFill",这样才能保证宽高比例不同的图片都能填满卡片区域。aspectFill会裁剪多余部分,所以如果你的卡片圆角为24rpx,图片本身不加圆角的话,需要在图片外层套一个圆角的盒子,并且设置overflow: hidden。

视频又是另一个坑。视频组件在 iOS 上属于原生组件,层级位置不受普通view控制,容易从卡片里“穿”出来。解决方案有两个:一是卡片的视频用cover-view覆盖,二是把视频改为video的show-fullscreen-btn="{{false}}"减小层级冲突。但最省事的方案,还是不要在滚动卡片里放视频,用图片加动效代替,性能也更好。

5.2 动态宽高和 rpx 换算陷阱

swiper的高度必须写死或动态计算,不能依赖子元素撑开。如果用rpx写高度,在屏幕宽度 375px 的设备上换算为 750rpx,在宽度 414px 的设备上同样显示为 750rpx,这本来是正确的等比缩放逻辑。但如果你在代码里用windowWidth计算高度,要注意windowWidth拿到的是px值,要和rpx混用的话,必须通过px / windowWidth * 750的方式换算。

真机上常见的另一个问题是横屏。小程序默认竖屏,但如果页面开启了横屏配置,windowWidth会变成高度值,导致所有基于宽度的计算都错乱。如果你的页面不需要横屏,记得在app.json或页面配置里关闭pageOrientation的横屏选项。

5.3 双层scroll-view导致的事件抢占

有些卡片场景里,页面本身是scroll-view纵向滚动,卡片区域是swiper横向滑动。这个组合在滑动方向冲突时通常没问题,因为 X 轴和 Y 轴方向不同,微信会做方向判定。但如果你在卡片内部又嵌套了一个scroll-view,滑动方向就极容易互相抢占事件,表现为“想滑卡片结果页面跟着滚”。

我的建议是:尽可能避免swiper-item内部嵌入scroll-view。如果卡片内容确实太长需要滚动,可以在swiper外层加一个纵向的scroll-view,卡片区域内部如果内容溢出,用截断加省略号的方式处理,而不是强行滚动。

5.4 自定义导航栏区域的点击穿透

如果项目开启了自定义导航栏,页面顶部会有一段状态栏高度,卡片滑动区域如果延伸到这块区域,点击事件容易被状态栏区域吞掉。表现为卡片边缘的按钮偶尔点不中。这个问题可以通过给最外层容器设置padding-top: {{statusBarHeight}}px解决,状态栏高度在wx.getWindowInfo()中可以拿到。

5.5 动画卡顿:关键 transform 属性写法

做卡片缩放动画时,用transform: scale()比修改width和height性能好很多,因为scale不触发重排。但transform在安卓 WebView 上有时候会被解析成一个单独的渲染层,如果同时配合opacity过渡,可能会有文字模糊的情况。解决办法是给动画中的元素加will-change: transform;,虽然新版本基础库已经优化了一些,但显式声明仍然能减少合成层数量。

另外,千万不要在swiper的bindchange里频繁修改大量节点的transform样式,每改一次就是一次完整渲染流程。一次setData尽量批次更新所有卡片样式,而不是分多次更新,这一点对低端机型尤为重要。

5.6 多卡缓存:用 hidden 替代 wx:if

卡片数量多,或者卡片内部有图片,wx:if在切换时会频繁创建和销毁组件,导致闪烁和性能下降。如果你需要保留卡片状态,比如滑走再滑回来,还是保持原来的滚动位置,建议用hidden来控制显隐。hidden只是改变display属性,节点本身不销毁,这样图片缓存和组件状态都能保留。

但hidden也有一个副作用:隐藏的卡片仍然占内存,并且如果内部有自动播放的视频或音频,它们还会继续播放。所以对于包含视频的卡片,还是得用wx:if并配合autoplay="{{false}}"来手动控制,避免多卡同时发声。

5.7 微信开发者工具的“模拟器”和“真机调试”选择

最后一条经验是工作流层面的:尽量基于真机调试开发滚动卡片相关功能。开发者工具的模拟器在触摸事件模拟、动画帧率上都做了“美化”,很多掉帧和事件问题在模拟器上根本看不出来。我的习惯是先用模拟器快速调样式,每改完一个关键交互就去真机跑一遍,特别是在类似“卡片叠放”“3D翻转”这种重动画场景,千万别拖到全部写完再上真机,否则排错成本会翻好几倍。

6. 持续打磨建议:动效、无障碍和强制性降级

滚动卡片上线能用是第一步,真正考验功力的是在不同机型、不同系统设置下的表现。

一个容易被忽略的细节是,微信有“减弱动态效果”的系统设置。如果用户在系统层开启了“减弱动态效果”,你仍然做大幅度 3D 翻转动画,体验会很差。可以通过wx.getSystemSetting()拿到相关配置值,如果检测到用户开启了减弱动态效果,那么卡片切换时自动降级为淡入淡出,不加旋转、不做层叠透出效果,只保留最基本的视觉信息。

卡片交互的无障碍也很重要。除了视觉上的当前项高亮,最好配合读屏能力,加上aria-label描述当前卡片的标题和状态,让屏幕阅读器用户也能准确感知滑动结果。小程序里可以用aria-role和aria-label属性来增强。

同时,卡片下方最好加上“上一张/下一张”的按钮控件作为滑动手势的补充操作。这不仅仅是为了无障碍,很多用户就是不习惯横向滑动,更喜欢点按钮翻卡。两个入口并存,覆盖率更高。

我做这个项目时还发现一个问题:数据加载失败时,卡片页会变成空白。最外层加一层wx:if,数据为空时展示空状态;数据加载时展示骨架屏。骨架屏的样式尽量和卡片高宽一致,骨架闪烁动画用 CSS 关键帧实现,不要用定时器每帧setData,那样既耗电又卡顿。

最后分享一个小技巧:多张卡片内容一致性较差时,可以考虑在swiper外层套一层“锚点指示器”,用几个小圆点来表示卡片位置,但是不采用默认的indicator-dots,而是自己用view实现圆点,并让当前圆点长度拉长,形成一个横向胶囊状指示器。这种指示器视觉上比默认圆点精致很多,且性能开销极低,你只需要监听current更新指示器的宽度类名即可。我一般把这个指示器放在卡片区域偏下方,几乎适配所有卡片场景。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 6:00:54

AI生成PPT的底层能力评估与职场落地实践

1. 为什么“一键生成PPT”正在重构职场人的内容交付逻辑你有没有过这样的经历&#xff1a;周五下午四点&#xff0c;领导突然发来一条消息&#xff1a;“明早九点汇报&#xff0c;把Q3数据整理成PPT&#xff0c;十页以内&#xff0c;风格要专业简洁。”你盯着空白的PowerPoint界…

作者头像 李华
网站建设 2026/10/14 6:00:39

Python函数全解:从普通函数到生成器、装饰器与偏函数的实战指南

写Python写了这么多年&#xff0c;我愈发觉得&#xff1a;函数才是这门语言的灵魂。列表推导式再花哨、类封装再巧妙&#xff0c;最后都得靠函数把逻辑串起来。而很多人写代码效率上不去、代码复用性差、甚至线上出bug&#xff0c;根源往往不是语法不熟&#xff0c;而是对函数的…

作者头像 李华
网站建设 2026/10/14 6:00:26

Kettle三大实操断层:元数据、数据流契约与作业转换分离

1. 为什么“Kettle入门解析&#xff08;二&#xff09;”这个标题背后藏着大量被忽略的实操断层很多人点开“Kettle入门解析&#xff08;二&#xff09;”时&#xff0c;心里默认这是“上一篇的延续”&#xff0c;是“更深入一点的用法”。但我在某高校数据治理实验室带教三年、…

作者头像 李华
网站建设 2026/10/14 5:59:53

C++在物联网开发中的核心价值与实战指南

1. 物联网系统里&#xff0c;C到底在干什么位置1.1 物联网整体架构与C的切入面很多人一聊到物联网&#xff0c;脑子里蹦出来的就是传感器、云端、大屏可视化这些概念。实际上&#xff0c;一套完整的物联网系统从下往上拆&#xff0c;大概可以分成四层&#xff1a;感知层、网络层…

作者头像 李华
网站建设 2026/10/14 5:59:40

90%的人用错了AI:从提示词到人机分工的实操指南

1. 这个标题到底在说什么先把话说在前头&#xff1a;这个标题看着像标题党&#xff0c;但它戳中的问题非常真实。过去一年多&#xff0c;我身边从产品经理到财务、从大学生到退休阿姨&#xff0c;几乎所有人都在用AI工具&#xff0c;但真正把AI用出价值的人&#xff0c;我掰着手…

作者头像 李华