跳至主要內容
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?

4563博客

全新的繁體中文 WordPress 網站
  • 首頁
  • Vue.js|Nuxt 模仿探探叠加滑动|vue 仿 Tinder 卡片效果
未分類
23 10 月 2020

Vue.js|Nuxt 模仿探探叠加滑动|vue 仿 Tinder 卡片效果

Vue.js|Nuxt 模仿探探叠加滑动|vue 仿 Tinder 卡片效果

資深大佬 : xiaoyan2017 6

前言

这段时间一直在捣鼓 Nuxt.js 项目,有个需求是实现类似探探卡片左右滑动切换功能。要求能实现左右手指拖动切换、点击按钮进行切换、拖拽回弹等功能。

基于 Vue|Nuxt.js 卡片式翻牌效果

Vue.js|Nuxt 模仿探探叠加滑动|vue 仿 Tinder 卡片效果

如上图:最终展示效果

okay,下面就来简单的讲解下实现过程。

布局

整体布局分为 顶部 headerbar 、卡片堆叠区域、底部 tabbar 三个部分。

Vue.js|Nuxt 模仿探探叠加滑动|vue 仿 Tinder 卡片效果

<!-- //卡片页面模板 --> <template>     <div>         <!-- >>顶部 -->         <header-bar :back="false" bgcolor="linear-gradient(to right, #00e0a1, #00a1ff)" fixed>             <div slot="title"><i class="iconfont icon-like c-red"></i> <em class="ff-gg">遇见 TA</em></div>             <div slot="right" class="ml-30" @click="showFilter = true"><i class="iconfont icon-filter"></i></div>         </header-bar>          <!-- >>主页面 -->         <div class="nuxt__scrollview scrolling flex1" ref="scrollview" style="background: linear-gradient(to right, #00e0a1, #00a1ff);">             <div class="nt__flipcard">                 <div class="nt__stack-wrapper">                     <flipcard ref="stack" :pages="stackList" @click="handleStackClicked"></flipcard>                 </div>                 <div class="nt__stack-control flexbox">                     <button class="btn-ctrl prev" @click="handleStackPrev"><i class="iconfont icon-unlike "></i></button>                     <button class="btn-ctrl next" @click="handleStackNext"><i class="iconfont icon-like "></i></button>                 </div>             </div>         </div>          <!-- >>底部 tabbar -->         <tab-bar bgcolor="linear-gradient(to right, #00e0a1, #00a1ff)" color="#fff" />     </div> </template>  

侧边筛选框

点击右上角筛选按钮,在侧边会出现弹窗。里面的范围滑块、switch 开关、Rate 评分等组件则是使用 Vant 组件库。

Vue.js|Nuxt 模仿探探叠加滑动|vue 仿 Tinder 卡片效果

<template>     <!-- ... -->          <!-- @@侧边栏弹框模板 -->     <v-popup v-model="showFilter" position="left" xclose xposition="left" title="高级筛选与设置">         <div class="flipcard-filter">             <div class="item nuxt-cell">                 <label class="lbl">范围</label>                 <div class="flex1">                     <van-slider v-model="distanceRange" bar-height="2px" button-size="12px" active-color="#00e0a1" min="1" @input="handleDistanceRange" />                 </div>                 <em class="val">{{distanceVal}}</em>             </div>             <div class="item nuxt-cell">                 <label class="lbl flex1">自动增加范围</label>                 <em class="val"><van-switch v-model="autoExpand" size="20px" active-color="#00e0a1" /></em>             </div>             <div class="item nuxt-cell">                 <label class="lbl flex1">性别</label>                 <em class="val">女生</em>             </div>             <div class="item nuxt-cell">                 <label class="lbl">好评度</label>                 <div class="flex1"><van-rate v-model="starVal" color="#00e0a1" icon="like" void-icon="like-o" @change="handleStar" /></div>                 <em class="val">{{starVal}}星</em>             </div>             <div class="item nuxt-cell">                 <label class="lbl flex1">优先在线用户</label>                 <em class="val"><van-switch v-model="firstOnline" size="20px" active-color="#00e0a1" /></em>             </div>             <div class="item nuxt-cell">                 <label class="lbl flex1">优先新用户</label>                 <em class="val"><van-switch v-model="firstNewUser" size="20px" active-color="#00e0a1" /></em>             </div>             <div class="item nuxt-cell mt-20">                 <div class="mt-30 nuxt__btn nuxt__btn-primary--gradient" style="height:38px;"><i class="iconfont icon-filter"></i> 更新</div>             </div>         </div>     </v-popup> </template>   <script>     export default {         // 用于配置应用默认的 meta 标签         head() {             return {                 title: `${this.title} - 翻一翻`,                 meta: [                     {name:'keywords',hid: 'keywords',content:`${this.title} | 翻一翻 | 翻动卡片`},                     {name:'description',hid:'description',content:`${this.title} | 仿探探卡片翻动`}                 ]             }         },         middleware: 'auth',         data () {             return {                 title: 'Nuxt',                 showFilter: false,                 distanceRange: 1,                 distanceVal: '<1km',                 autoExpand: true,                 starVal: 5,                 firstOnline: false,                 firstNewUser: true,                                  // ...             }         },         methods: {             /* @@左侧筛选函数 */             // 范围选择             handleDistanceRange(val) {                 if(val == 1) {                     this.distanceVal = '<1km';                 } else if (val == 100) {                     this.distanceVal = "100km+"                 }else {                     this.distanceVal = val+'km';                 }             },             // 好评度             handleStar(val) {                 this.starVal = val;             },                          // ...         },     } </script> 

Nuxt 仿 Tinder 堆叠卡片

其中卡片堆叠区单独封装了一个 flipcard.vue 组件,只需传入 pages 数据就可以。

<flipcard ref="stack" :pages="stackList"></flipcard>

Vue.js|Nuxt 模仿探探叠加滑动|vue 仿 Tinder 卡片效果

在卡片的四角拖拽卡片,会出现不同程度的斜切视角。

pages 支持传入的参数

module.exports = [     {         avatar: '/assets/img/avatar02.jpg',         name: '放荡不羁爱自由',         sex: 'female',         age: 23,         starsign: '天秤座',         distance: '艺术 /健身',         photos: [...],         sign: '交个朋友,非诚勿扰'     },          ... ] 

堆叠卡片模板

<template>     <ul class="stack">         <li class="stack-item" v-for="(item, index) in pages" :key="index" :style="[transformIndex(index),transform(index)]"             @touchmove.stop.capture="touchmove"             @touchstart.stop.capture="touchstart"             @touchend.stop.capture="touchend($event, index)"             @touchcancel.stop.capture="touchend($event, index)"             @mousedown.stop.capture.prevent="touchstart"             @mouseup.stop.capture.prevent="touchend($event, index)"             @mousemove.stop.capture.prevent="touchmove"             @mouseout.stop.capture.prevent="touchend($event, index)"             @webkit-transition-end="onTransitionEnd(index)"             @transitionend="onTransitionEnd(index)"         >             <img :src="item.avatar" />             <div class="stack-info">                 <h2 class="name">{{item.name}}</h2>                 <p class="tags">                     <span class="sex" :class="item.sex"><i class="iconfont" :class="'icon-'+item.sex"></i> {{item.age}}</span>                     <span class="xz">{{item.starsign}}</span>                 </p>                 <p class="distance">{{item.distance}}</p>             </div>         </li>     </ul> </template>  /**  * @Desc     Vue 仿探探|Tinder 卡片滑动 FlipCard  * @Time     andy by 2020-10-06  * @About    Q:282310962  wx:xy190310  */ <script>     export default {         props: {             pages: {                 type: Array,                 default: {}             }         },         data () {             return {                 basicdata: {                     start: {},                     end: {}                 },                 temporaryData: {                     isStackClick: true,                     offsetY: '',                     poswidth: 0,                     posheight: 0,                     lastPosWidth: '',                     lastPosHeight: '',                     lastZindex: '',                     rotate: 0,                     lastRotate: 0,                     visible: 3,                     tracking: false,                     animation: false,                     currentPage: 0,                     opacity: 1,                     lastOpacity: 0,                     swipe: false,                     zIndex: 10                 }             }         },         computed: {             // 划出面积比例             offsetRatio () {                 let width = this.$el.offsetWidth                 let height = this.$el.offsetHeight                 let offsetWidth = width - Math.abs(this.temporaryData.poswidth)                 let offsetHeight = height - Math.abs(this.temporaryData.posheight)                 let ratio = 1 - (offsetWidth * offsetHeight) / (width * height) || 0                 return ratio > 1 ? 1 : ratio             },             // 划出宽度比例             offsetWidthRatio () {                 let width = this.$el.offsetWidth                 let offsetWidth = width - Math.abs(this.temporaryData.poswidth)                 let ratio = 1 - offsetWidth / width || 0                 return ratio             }         },         methods: {             touchstart (e) {                 if (this.temporaryData.tracking) {                     return                 }                 // 是否为 touch                 if (e.type === 'touchstart') {                     if (e.touches.length > 1) {                         this.temporaryData.tracking = false                         return                     } else {                         // 记录起始位置                         this.basicdata.start.t = new Date().getTime()                         this.basicdata.start.x = e.targetTouches[0].clientX                         this.basicdata.start.y = e.targetTouches[0].clientY                         this.basicdata.end.x = e.targetTouches[0].clientX                         this.basicdata.end.y = e.targetTouches[0].clientY                         // offsetY 在 touch 事件中没有,只能自己计算                         this.temporaryData.offsetY = e.targetTouches[0].pageY - this.$el.offsetParent.offsetTop                     }                 // pc 操作                 } else {                     this.basicdata.start.t = new Date().getTime()                     this.basicdata.start.x = e.clientX                     this.basicdata.start.y = e.clientY                     this.basicdata.end.x = e.clientX                     this.basicdata.end.y = e.clientY                     this.temporaryData.offsetY = e.offsetY                 }                 this.temporaryData.isStackClick = true                 this.temporaryData.tracking = true                 this.temporaryData.animation = false             },             touchmove (e) {                 this.temporaryData.isStackClick = false                 // 记录滑动位置                 if (this.temporaryData.tracking && !this.temporaryData.animation) {                     if (e.type === 'touchmove') {                         e.preventDefault()                         this.basicdata.end.x = e.targetTouches[0].clientX                         this.basicdata.end.y = e.targetTouches[0].clientY                     } else {                         e.preventDefault()                         this.basicdata.end.x = e.clientX                         this.basicdata.end.y = e.clientY                     }                     // 计算滑动值                     this.temporaryData.poswidth = this.basicdata.end.x - this.basicdata.start.x                     this.temporaryData.posheight = this.basicdata.end.y - this.basicdata.start.y                     let rotateDirection = this.rotateDirection()                     let angleRatio = this.angleRatio()                     this.temporaryData.rotate = rotateDirection * this.offsetWidthRatio * 15 * angleRatio                 }             },             touchend (e, index) {                 if(this.temporaryData.isStackClick) {                     this.$emit('click', index)                     this.temporaryData.isStackClick = false                 }                 this.temporaryData.isStackClick = true                 this.temporaryData.tracking = false                 this.temporaryData.animation = true                 // 滑动结束,触发判断                 // 判断划出面积是否大于 0.4                 if (this.offsetRatio >= 0.4) {                     // 计算划出后最终位置                     let ratio = Math.abs(this.temporaryData.posheight / this.temporaryData.poswidth)                     this.temporaryData.poswidth = this.temporaryData.poswidth >= 0 ? this.temporaryData.poswidth + 200 : this.temporaryData.poswidth - 200                     this.temporaryData.posheight = this.temporaryData.posheight >= 0 ? Math.abs(this.temporaryData.poswidth * ratio) : -Math.abs(this.temporaryData.poswidth * ratio)                     this.temporaryData.opacity = 0                     this.temporaryData.swipe = true                     this.nextTick()                     // 不满足条件则滑入                 } else {                     this.temporaryData.poswidth = 0                     this.temporaryData.posheight = 0                     this.temporaryData.swipe = false                     this.temporaryData.rotate = 0                 }             },             nextTick () {                 // 记录最终滑动距离                 this.temporaryData.lastPosWidth = this.temporaryData.poswidth                 this.temporaryData.lastPosHeight = this.temporaryData.posheight                 this.temporaryData.lastRotate = this.temporaryData.rotate                 this.temporaryData.lastZindex = 20                 // 循环 currentPage                 this.temporaryData.currentPage = this.temporaryData.currentPage === this.pages.length - 1 ? 0 : this.temporaryData.currentPage + 1                 // currentPage 切换,整体 dom 进行变化,把第一层滑动置最低                 this.$nextTick(() => {                     this.temporaryData.poswidth = 0                     this.temporaryData.posheight = 0                     this.temporaryData.opacity = 1                     this.temporaryData.rotate = 0                 })             },             onTransitionEnd (index) {                 let lastPage = this.temporaryData.currentPage === 0 ? this.pages.length - 1 : this.temporaryData.currentPage - 1                 // dom 发生变化正在执行的动画滑动序列已经变为上一层                 if (this.temporaryData.swipe && index === lastPage) {                     this.temporaryData.animation = true                     this.temporaryData.lastPosWidth = 0                     this.temporaryData.lastPosHeight = 0                     this.temporaryData.lastOpacity = 0                     this.temporaryData.lastRotate = 0                     this.temporaryData.swipe = false                     this.temporaryData.lastZindex = -1                 }             },             prev () {                 this.temporaryData.tracking = false                 this.temporaryData.animation = true                 // 计算划出后最终位置                 let width = this.$el.offsetWidth                 this.temporaryData.poswidth = -width                 this.temporaryData.posheight = 0                 this.temporaryData.opacity = 0                 this.temporaryData.rotate = '-3'                 this.temporaryData.swipe = true                 this.nextTick()             },             next () {                 this.temporaryData.tracking = false                 this.temporaryData.animation = true                 // 计算划出后最终位置                 let width = this.$el.offsetWidth                 this.temporaryData.poswidth = width                 this.temporaryData.posheight = 0                 this.temporaryData.opacity = 0                 this.temporaryData.rotate = '3'                 this.temporaryData.swipe = true                 this.nextTick()             },             rotateDirection () {                 if (this.temporaryData.poswidth <= 0) {                     return -1                 } else {                     return 1                 }             },             angleRatio () {                 let height = this.$el.offsetHeight                 let offsetY = this.temporaryData.offsetY                 let ratio = -1 * (2 * offsetY / height - 1)                 return ratio || 0             },             inStack (index, currentPage) {                 let stack = []                 let visible = this.temporaryData.visible                 let length = this.pages.length                 for (let i = 0; i < visible; i++) {                     if (currentPage + i < length) {                         stack.push(currentPage + i)                     } else {                         stack.push(currentPage + i - length)                     }                 }                 return stack.indexOf(index) >= 0             },             // 非首页样式切换             transform (index) {                 let currentPage = this.temporaryData.currentPage                 let length = this.pages.length                 let lastPage = currentPage === 0 ? this.pages.length - 1 : currentPage - 1                 let style = {}                 let visible = this.temporaryData.visible                 if (index === this.temporaryData.currentPage) {                     return                 }                 if (this.inStack(index, currentPage)) {                     let perIndex = index - currentPage > 0 ? index - currentPage : index - currentPage + length                     style['opacity'] = '1'                     style['transform'] = 'translate3D(0,0,' + -1 * 60 * (perIndex - this.offsetRatio) + 'px' + ')'                     style['zIndex'] = visible - perIndex                     if (!this.temporaryData.tracking) {                         style['transitionTimingFunction'] = 'ease'                         style['transitionDuration'] = 300 + 'ms'                     }                 } else if (index === lastPage) {                     style['transform'] = 'translate3D(' + this.temporaryData.lastPosWidth + 'px' + ',' + this.temporaryData.lastPosHeight + 'px' + ',0px) ' + 'rotate(' + this.temporaryData.lastRotate + 'deg)'                     style['opacity'] = this.temporaryData.lastOpacity                     style['zIndex'] = this.temporaryData.lastZindex                     style['transitionTimingFunction'] = 'ease'                     style['transitionDuration'] = 300 + 'ms'                 } else {                     style['zIndex'] = '-1'                     style['transform'] = 'translate3D(0,0,' + -1 * visible * 60 + 'px' + ')'                 }                 return style             },             // 首页样式切换             transformIndex (index) {                 if (index === this.temporaryData.currentPage) {                     let style = {}                     style['transform'] = 'translate3D(' + this.temporaryData.poswidth + 'px' + ',' + this.temporaryData.posheight + 'px' + ',0px) ' + 'rotate(' + this.temporaryData.rotate + 'deg)'                     style['opacity'] = this.temporaryData.opacity                     style['zIndex'] = 10                     if (this.temporaryData.animation) {                         style['transitionTimingFunction'] = 'ease'                         style['transitionDuration'] = (this.temporaryData.animation ? 300 : 0) + 'ms'                     }                     return style                 }             },         }     } </script> 

点击卡片会直接跳转到详细页面。

Vue.js|Nuxt 模仿探探叠加滑动|vue 仿 Tinder 卡片效果

ok,基于 Vue.js|Nuxt.js 实现卡片拖拽切换效果就分享到这里。希望能喜欢~~

作者:xiaoyan2017
链接: https://segmentfault.com/a/1190000037446858
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Vue.js|Nuxt 模仿探探叠加滑动|vue 仿 Tinder 卡片效果

大佬有話說 (3)

  • 資深大佬 : Exia

    感谢分享,已经收藏,看着流畅性不错~

  • 主 資深大佬 : xiaoyan2017

    @Exia 感谢支持!滑动挺流畅的。

  • 資深大佬 : think2011

    那么多 this.xxx = xx, 就不能写成一个变量再赋值吗?

文章導覽

上一篇文章
下一篇文章

AD

其他操作

  • 登入
  • 訂閱網站內容的資訊提供
  • 訂閱留言的資訊提供
  • WordPress.org 台灣繁體中文

51la

4563博客

全新的繁體中文 WordPress 網站
返回頂端
本站採用 WordPress 建置 | 佈景主題採用 GretaThemes 所設計的 Memory
4563博客
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?
在這裡新增小工具