发布于 2026-01-06 7 阅读
0

在 Vue 上创建类似 Tinder 的滑动用户界面

在 Vue 上创建类似 Tinder 的滑动用户界面

你有没有想过那种类似 Tinder 的左右滑动式用户体验是如何实现的?几天前,我很好奇。我之前主要做后端开发,所以对于我这个外行来说,这种东西真的太神奇了。

我很好奇,像我这样水平一般的开发者,要做出那样酷炫的东西到底有多难?

侦察

在着手新项目时,收集信息始终是我的第一步。我不会贸然尝试任何代码,而是先上网搜索。毕竟,肯定有比我聪明的人早就想到过这一点了。

果不其然,在搜索“vue 刷卡”之后,谷歌给我的第一个结果就是这个(我真幸运)。

这是Mateusz Rybczonekcss-tricks 上发表的一篇关于使用.interact.js

这篇文章对可滑动组件的构建方式的解释比我好得多。更重要的是,他提取了相关功能并将其作为vue2-interact发布到了 npm 上(开源万岁!)。

虽然文章解释了所有功能的工作原理,但对我们来说,它本质上只是样板代码。我们需要的是实际使用提取出来的功能。这就是为什么这篇文章Vue2InteractDraggable如此宝贵,所有繁重的工作都已经完成,我们只需要弄清楚如何在自己的项目中使用它。

实验

现在,我只需要实际操作一下。文档写得很清楚。我们先从最简单的代码开始:



<template>
  <section class="container">
    <div class="fixed-center">
      <Vue2InteractDraggable
        :interact-out-of-sight-x-coordinate="500"
        :interact-max-rotation="15"
        :interact-x-threshold="200"
        :interact-y-threshold="200"
        class="rounded-borders shadow-10 card">
        <div class="card__main">    
        </div>
      </Vue2InteractDraggable>
    </div>
  </section>
</template>

<script>
import { Vue2InteractDraggable } from 'vue2-interact'

export default {
  name: 'SwipeableCards',
  components: { Vue2InteractDraggable }
}
</script>


Enter fullscreen mode Exit fullscreen mode

没什么特别的,它只是在屏幕中央显示一个粉色方框,我可以拖动它。

粉色盒子

好,好,好,好。一切正常。既然已经确认了这一点,现在是时候考虑一​​下我接下来想要完成的事情了。

为了展示我所期望的用户交互方式,我将其简化为以下要求。

  1. 检测卡片是否被拖出视野,如果是,则将其隐藏。
  2. 将可拖动的卡片堆叠在一起。
  3. 能够控制滑动操作(通过按钮以编程方式触发)。

问题一:检测和隐藏

问题 1 很简单,Vue2InteractDraggable组件在超出限制drag*时会发出事件interact-out-of-sight-*-coordinate,并且还会自动隐藏组件。
拖拽隐藏

问题二:堆叠卡片

问题二相当棘手。Vue2InteractDraggable从技术上讲,它只是一个可拖动的组件。从用户界面角度来看,堆叠这些组件可以很简单,只需使用 CSS 结合 `<div>` z-indexwidth`<span>` 和box-shadow`<span>` 来模拟深度即可。但是滑动组件还能正常工作吗?嗯,我可以停留pointer-events在最底部的卡片上,以避免任何副作用。

我们来试一试。我将使用一个数组,每次向右滑动时弹出数组的第一个元素。这听起来合理,对吧?

以下是目前为止的代码:



<template>
  <section class="container">
    <div>
        <Vue2InteractDraggable
          v-for="(card, index) in cards"
          :key="index"
          :interact-out-of-sight-x-coordinate="500"
          :interact-max-rotation="15"
          :interact-x-threshold="200"
          :interact-y-threshold="200"
          @draggedRight="right"
          class="rounded-borders card fixed fixed--center"
          :class="{
            'card--top': index === 0
          }">
          <div class="flex flex--center" style="height: 100%">
            <h1>{{card.text}}</h1>
          </div>
      </Vue2InteractDraggable>
    </div>
  </section>
</template>
<script>
import { Vue2InteractDraggable } from 'vue2-interact'

export default {
  name: 'SwipeableCards',
  components: { Vue2InteractDraggable },
  data() {
    return {
      cards: [
        { text: 'one' },
        { text: 'two' },
        { text: 'three' },
      ]
    }
  },
  methods: {
    right() {
      setTimeout(() => this.cards.shift(), 300);
    }
  }
}
</script>

<style lang="scss" scoped>
.container {
  background: #eceff1;
  width: 100%;
  height: 100vh;
}

.flex {
  display: flex;
  &--center {
    align-items: center;
    justify-content: center;
  }
}

.fixed {
  position: fixed;
  &--center {
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
  }
}
.rounded-borders {
  border-radius: 2rem
}
.card {
  pointer-events: none;
  width: 300px;
  height: 400px;
  &:nth-child(1) {
    background: pink;
    z-index: 3;
  }
  &:nth-child(2) {
    z-index: 2;
    background: red;
    top: 52%;
  }
  &:nth-child(3) {
    z-index: 1;
    background: green;
    top: 54%;
  }
  &--top {
    pointer-events: auto !important;
  }
}
</style>


Enter fullscreen mode Exit fullscreen mode

现在我的情况是这样的: 嗯,完全失败了。不知何故,当第一张卡片的事件触发时,第二张卡片的事件也触发了。您可以看到,在我第一次滑动之后,DOM 中只剩下两张卡片,但我们看不到第二张卡片,因为它旋转到屏幕外了。在开发者工具中,我们可以看到,在滑动第一张卡片之后,第二张卡片的 transform 动画样式被应用了(您可以通过开发者工具禁用该样式后,可以看到它又出现了)。
史诗级失败

dragfail2

即使我尝试简单地将卡片排列成行,问题仍然存在。我不确定这是为什么。我一定是遗漏了什么,或者这是组件Vue2InteractDraggable本身的问题。

目前我有两个选择:我可以继续调试,深入研究实际实现,或许可以追溯原作者是如何提取功能的,找出不同之处,查看 GitHub 仓库中是否有类似问题,并尝试从中寻找答案;或者思考另一种方法来实现同样的功能,以后再回头处理。

我选择后者。另一种方法或许也能达到同样的效果。现在没必要好高骛远。我也可以以后再来处理这件事。

我们继续吧。

之前的结果让我思考……如果每次使用多个Vue2InteractDraggable组件都会出错,为什么不干脆避免使用多个组件,只用一个呢?毕竟,我一次只拖拽一张卡片。为什么不直接使用同一张卡片,然后根据需要替换内容呢?结合其他一些 CSS 技巧,我觉得这或许可行。

让我们编写一段最简单的代码来验证我的假设:



<template>
  <section class="container">
    <div class="fixed fixed--center" style="z-index: 3">
      <Vue2InteractDraggable
        v-if="isVisible"
        :interact-out-of-sight-x-coordinate="500"
        :interact-max-rotation="15"
        :interact-x-threshold="200"
        :interact-y-threshold="200"
        @draggedRight="right"
        class="rounded-borders card card--one">
        <div class="flex flex--center" style="height: 100%">
          <h1>{{current.text}}</h1>
        </div>
      </Vue2InteractDraggable>
    </div>
    <div
      class="rounded-borders card card--two fixed fixed--center"
      style="z-index: 2">
      <div class="flex flex--center" style="height: 100%">
        <h1>test</h1>
      </div>
    </div>
    <div
      class="rounded-borders card card--three fixed fixed--center"
      style="z-index: 1">
      <div class="flex flex--center" style="height: 100%">
        <h1>test</h1>
      </div>
    </div>
  </section>
</template>
<script>
import { Vue2InteractDraggable } from 'vue2-interact'

export default {
  name: 'SwipeableCards',
  components: { Vue2InteractDraggable },
  data() {
    return {
      isVisible: true,
      index: 0,
      cards: [
        { text: 'one' },
        { text: 'two' },
        { text: 'three' },
      ]
    }
  },
  computed: {
    current() {
      return this.cards[this.index]
    }
  },
  methods: {
    right() {
      setTimeout(() => this.isVisible = false, 200)
      setTimeout(() => {
        this.index++
        this.isVisible = true
      }, 300)
    }
  }
}
</script>

<style lang="scss" scoped>
.container {
  background: #eceff1;
  width: 100%;
  height: 100vh;
}

.flex {
  display: flex;
  &--center {
    align-items: center;
    justify-items: center;
    justify-content: center;
  }
}

.fixed {
  position: fixed;
  &--center {
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
  }
}
.rounded-borders {
  border-radius: 12px;
}
.card {
  width: 300px;
  height: 400px;
  color: white;
  &--one {
    background-color: pink;
  }
  &--two {
    background-color: red;
    width: 280px;
    top: 51%;
  }
  &--three {
    background-color: orange;
    width: 260px;
    top: 51.8%;
  }
}
</style>



Enter fullscreen mode Exit fullscreen mode

成功了!
有用

这似乎比我最初的方法更简单。我重复使用同一个Vue2InteractDraggable实例,而不是为数组中的每个元素都实例化一个。反正我们也不需要把所有牌都叠起来,只需要维持这种错觉就行了。

也就是说,为了进一步增强视觉效果,我应该把下一个元素的内容显示在第一个元素后面的卡片上,像这样:



<template>
  <section class="container">
    <div class="fixed fixed--center" style="z-index: 3">
      <Vue2InteractDraggable
        v-if="isVisible"
        :interact-out-of-sight-x-coordinate="500"
        :interact-max-rotation="15"
        :interact-x-threshold="200"
        :interact-y-threshold="200"
        @draggedRight="right"
        class="rounded-borders card card--one">
        <div class="flex flex--center" style="height: 100%">
          <h1>{{current.text}}</h1>
        </div>
      </Vue2InteractDraggable>
    </div>
    <div
      v-if="next"
      class="rounded-borders card card--two fixed fixed--center"
      style="z-index: 2">
      <div class="flex flex--center" style="height: 100%">
        <h1>{{next.text}}</h1>
      </div>
    </div>
    <div
      v-if="index + 2 < cards.length"
      class="rounded-borders card card--three fixed fixed--center"
      style="z-index: 1">
      <div class="flex flex--center" style="height: 100%">
        <h1>test</h1>
      </div>
    </div>
  </section>
</template>
<script>
import { Vue2InteractDraggable } from 'vue2-interact'

export default {
  name: 'SwipeableCards',
  components: { Vue2InteractDraggable },
  data() {
    return {
      isVisible: true,
      index: 0,
      cards: [
        { text: 'one' },
        { text: 'two' },
        { text: 'three' },
      ]
    }
  },
  computed: {
    current() {
      return this.cards[this.index]
    },
    next() {
      return this.cards[this.index + 1]
    }
  },
  methods: {
    right() {
      setTimeout(() => this.isVisible = false, 200)
      setTimeout(() => {
        this.index++
        this.isVisible = true
      }, 300)
    }
  }
}
</script>


Enter fullscreen mode Exit fullscreen mode

当我更改最上面的卡片时,我也会相应地隐藏最下面的虚拟卡片,这是个经典的卡片替换技巧

角色互换

效果相当不错。向上移动卡片时隐藏虚拟卡片index也十分有效。如果我们用图片代替text彩色div卡片,效果可能会更好。我们还可以添加一些微妙的过渡动画,让最底部的卡片变成最顶部的卡片,从而进一步增强视觉效果。不过这些以后再说,我们先来完成最后一块拼图。

问题 3:通过按钮点击触发滑动操作

幸运的是,这也很简单。vue2-interact它暴露了一个EventBus可以用来触发拖拽/滑动操作的接口。根据文档,只需向该interact-event-bus-events属性提供一个包含所需事件的对象,然后使用该对象InteractEventBus来触发所需的操作即可。



<template>
 <Vue2InteractDraggable
  @draggedLeft="draggedLeft"
  :interact-event-bus-events="interactEventBusEvents"
  v-if="isShowing"
  class="card">
  <div>
    <h3 class="cardTitle">Drag me!</h3>
  </div>
 </Vue2InteractDraggable>

 <BaseButton @click="dragLeft" label="⇦" />
</template>
<script>
import { Vue2InteractDraggable, InteractEventBus } from 'vue2-interact'
const INTERACT_DRAGGED_LEFT = 'INTERACT_DRAGGED_LEFT';

export default {
  components: { Vue2InteractDraggable },
  data() {
    return {
      isShowing: true,
      interactEventBusEvents: {
        draggedLeft: INTERACT_DRAGGED_LEFT,
      },
    };
  },

  methods: {
    dragLeft() {
      InteractEventBus.$emit(INTERACT_DRAGGED_LEFT);
    },
  }
};
</script>


Enter fullscreen mode Exit fullscreen mode

本质上,我们只是告诉组件draggedLeft每次我们在某个地方时$emit触发该事件INTERACT_DRAGGED_LEFTInteractEventBus

这样一来,我认为我们已经具备了开始将所有东西整合起来所需的一切条件。

把所有东西整合起来

我从 Unsplash 下载了一些图片,并根据我的需求缩小了尺寸。我将这些图片作为数组的值,这样就可以替换文本并移除背景颜色。我还发现,如果改变卡片堆叠的方向,就能更轻松地增强视觉效果。我没有将它们向上堆叠,而是斜向堆叠。这样一来,我的过渡动画就可以非常简单,只需在切换时将第二张卡片的 x 和 y 坐标平移到第一张卡片上即可。我就不赘述所有步骤了,我想你已经明白我的意思了,剩下的就留给你自己想象吧。

加了一些 CSS 魔法,比如渐变、阴影之类的效果,再配上谷歌字体和 Material 图标,最后就成了这样: 瞧瞧,Kittynder!猫咪版的 Tinder。这名字听起来合理吗?我也不知道。不过这倒是个玩梗的好机会。如果这真的是个 app,我家猫估计会直接挠Katrina 的爪子,它们年纪差不多,我觉得它们肯定很合得来。
基廷德

你可以在这个 GitHub 仓库kittynder上查看完整代码。我在 Netlify 上发布了一个演示:kittynder.netlify.com。我强烈建议你在移动设备上查看。

结语

在这个简单的活动中,我意识到如今构建类似 Tinder 的滑动式用户界面是多么容易。我只用了不到两个小时就完成了。现在,互联网上的工具和资源比以往任何时候都多,足以让你构建很多东西,那些以前看起来遥不可及的东西。这就是开源社区的力量。这也是我开始写这类教程的原因之一。这是我回馈社区的方式。我可能只是一个平庸的开发者,但我的思考过程和解决问题的方法仍然可能对初学者(以及未来的我,因为一年后我可能会完全忘记)有所帮助。
诚实劳动

下一步?

当然,这离正式发布还差得远。我的 CSS 水平很差,你最好考虑使用像tailwind.css这样的工具,预缓存图片,检查浏览器兼容性等等。不过,这倒是个不错的练习。一步一步来,你最终会成功的。只要搜索、阅读、实践就好。

实际上,我正在使用Quasar 框架在一个稍大一些的个人项目中实现类似的功能,但这又是另一个故事了。

实用链接

本文最初发表于我的个人网站

文章来源:https://dev.to/vycoder/creating-a-tinder-like-swipe-ui-on-vue-4aa4