Vue 3 实现抖音式卡片滑动交互:从零到完整方案

本文介绍如何用 Vue 3 + TypeScript 实现类似抖音的卡片滑动交互,应用于单词记忆场景。完整方案包含手势识别、堆叠动画、实时反馈和状态管理。

在线体验:m.dobell.top(手机浏览器打开)。


效果拆解

目标效果:屏幕中央一张主卡片,后方隐约可见两张堆叠卡片。手指左滑(不认识)或右滑(认识),卡片跟随手指移动并旋转,松开后飞出屏幕,下一张顶上。

核心需求:

  • 3 张可见卡片(z-index 分层,缩放递减)
  • Pointer Events 手势跟踪
  • 滑动过程实时视觉反馈(颜色、图标、标签)
  • 阈值判断 + 飞出动画 + 队列循环

卡片堆叠布局

三张卡片用 v-for 渲染,通过 index 控制层级和变形:

typescript 复制代码
const stackStyle = (stackIndex: number) => {
  if (stackIndex >= 3) return { display: 'none' }

  const scaleMap = [1, 0.95, 0.90]      // 越靠后越小
  const yOffsetMap = [0, 12, 24]         // 越靠后越向下
  const zIndexMap = [30, 29, 28]         // 越靠前越高

  return {
    zIndex: zIndexMap[stackIndex],
    transform: `scale(${scaleMap[stackIndex]}) translateY(${yOffsetMap[stackIndex]}px)`,
    transition: stackIndex === 0 && isDragging.value
      ? 'none'                           // 拖拽中不要过渡
      : 'transform 0.35s ease'
  }
}

数组用 ref<Word[]>([]) 存储当前队列。队列用完时自动从后端拉取下一批。


Pointer Events 手势跟踪

用 Pointer Events 而非 Touch Events,因为 Pointer Events 同时支持触屏和鼠标,方便开发调试:

typescript 复制代码
const SWIPE_THRESHOLD = 72  // px,超过此值触发飞出
const isDragging = ref(false)
const dragX = ref(0)

const onPointerDown = (e: PointerEvent) => {
  isDragging.value = true
  startX.value = e.clientX
  ;(e.target as HTMLElement).setPointerCapture(e.pointerId)
}

const onPointerMove = (e: PointerEvent) => {
  if (!isDragging.value) return
  dragX.value = e.clientX - startX.value
}

const onPointerUp = () => {
  if (!isDragging.value) return
  isDragging.value = false

  if (Math.abs(dragX.value) > SWIPE_THRESHOLD) {
    // 触发飞出
    const direction = dragX.value > 0 ? 'right' : 'left'
    flyAway(direction)
  } else {
    // 复位
    dragX.value = 0
  }
}

关键点:setPointerCapture 确保手指滑出元素边界后依然能跟踪。不用这个 API 的话,手指移出卡片范围就会丢失 tracking。


实时视觉反馈

根据拖拽距离动态计算颜色和图标透明度:

typescript 复制代码
const feedbackColor = computed(() => {
  const ratio = Math.min(Math.abs(dragX.value) / SWIPE_THRESHOLD, 1)
  if (dragX.value > 0) {
    return `rgba(34, 197, 94, ${ratio * 0.3})`  // 绿色 = 认识
  } else if (dragX.value < 0) {
    return `rgba(239, 68, 68, ${ratio * 0.3})`  // 红色 = 不认识
  }
  return 'transparent'
})

const checkIconOpacity = computed(() =>
  dragX.value > 0 ? Math.min(dragX.value / SWIPE_THRESHOLD, 1) : 0
)

const crossIconOpacity = computed(() =>
  dragX.value < 0 ? Math.min(-dragX.value / SWIPE_THRESHOLD, 1) : 0
)

拖拽距离与阈值的比值驱动所有视觉过渡。不需要动画库,computed 全搞定。


飞出动画 + 队列推进

飞出用 CSS transition + 最终位置计算:

typescript 复制代码
const flyAway = (direction: 'left' | 'right') => {
  const targetX = direction === 'right' ? window.innerWidth * 1.5 : -window.innerWidth * 1.5
  flyX.value = targetX
  flyRotation.value = direction === 'right' ? 20 : -20

  // 记录结果
  const result = direction === 'right' ? 'known' : 'forgotten'
  submitReview(currentWord.value.id, result)

  // 动画结束后移除当前词,推进队列
  setTimeout(() => {
    words.value.shift()
    flyX.value = 0
    flyRotation.value = 0
    dragX.value = 0
    // 队列快用完时预加载
    if (words.value.length < 5) fetchMoreWords()
  }, 350)
}

飞出期间新卡片已经在原来的第二张位置就位,视觉上无缝衔接。


四队列切换

高频词、大纲词、综合、错词巩固,四种模式对应不同 API 参数:

typescript 复制代码
type QueueMode = 'high' | 'outline' | 'mixed' | 'wrong'

const switchQueue = async (mode: QueueMode) => {
  currentMode.value = mode
  words.value = []
  await fetchWords(mode)
}

错词队列的每个词带有复习记录,后端根据复习次数和上次复习时间动态计算下次推送时机,实现间隔重复。


性能注意事项

  • 事件监听用 passive: false :Pointer Events 需要 preventDefault 防止页面滚动,所以要显式设置 { passive: false }
  • 动画用 transform 而非 left/top:transform 走合成层,不触发重排
  • 队列预加载:剩 5 张时自动拉取,滑动体验零等待

完整代码较长,核心逻辑即上面这些。这个交互方案可以直接复用到任何需要卡片滑动判断的场景------背单词、刷题、审阅、相亲......

在线体验:m.dobell.top,打开就能看到效果。注册即送 3 天会员,月卡 29 元。

相关推荐
IT规划师8 小时前
工业 AI 走向产线规模 - 20+ 伙伴的演示,不如一条 +20% 吞吐
ai编程
Agent世界8 小时前
用 CDP 做发布自动化:四道机器闸门与产物验收
前端·javascript
FungLeo8 小时前
成为全栈·Vue 管理后台篇·编辑器图片上传:从文件选择到文章附件关联
前端·vue.js·编辑器·markdown·文件上传·formdata·附件管理
某哈压力大8 小时前
SSH 突然 Connection reset?不是密钥坏了,是 OpenSSL 把 sshd 干趴了
前端·node.js·命令行
程序员大阳8 小时前
副队长ES6+教程(3)--解构赋值
前端·javascript·es6·赋值·解构
神膘护体小月半9 小时前
css - 元素背景图由 hover 触发,初次加载时闪动的解决办法
前端·javascript·css
程序员大阳9 小时前
副队长ES6+教程(2)--let 与 const
前端·javascript·es6·const·let·var
deli0079 小时前
写满 200GB 后 SSD 为什么掉到 95MB/s?SLC 缓存模型实测
前端
曦樂~10 小时前
【Web Development】03JavaScript 基础和事件
开发语言·前端·javascript
deli00710 小时前
HTTPS 的锁是怎么挂上的?TLS 1.3 握手 6 步实测
前端