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 元。

相关推荐
烽火戏诸诸诸侯3 小时前
AI 让我的独门小工具焕发第二春
后端·ai编程·vibecoding
毕业设计7034 小时前
(免费领源码) 基于微信小程序的预制菜商城的设计与实现25172-java、PHP、python、C#、小程序、大数据、单片机、网络工程等)
vue.js·python·mysql·微信小程序·pycharm·微信开发者工具·推荐算法
xifangge20254 小时前
AGENTS.md 怎么写?涵盖 Java、Python、Vue、Go 的 8 套开箱即用模板
java·vue.js·python
几何心凉4 小时前
没有万能模型:我用 AiiOnly Token Plan,把多款大模型的长处拼进同一个项目
前端
kyriewen4 小时前
Claude Code 的额度今天缩水了17%——官方公告上写的是"永久提高25%"
前端·ai编程·claude
雪芽蓝域zzs5 小时前
vue解构平铺VS对象包裹
前端·javascript·vue.js
风骏时光牛马6 小时前
从零到实战,完整AI学习路线
前端
三十而立洋6 小时前
深入理解 Monorepo:子包安装依赖
前端·前端工程化
IT_陈寒6 小时前
Java Stream处理大集合,我的内存怎么就炸了
前端·人工智能·后端
Captaincc7 小时前
AI 用量桌面端-桌面宠物自定义指南
前端·人工智能