instructions
A recent Nuxt project required a similar toggle effect for probing card stacks. Required to support left and right drag sliding, click the button to switch, drag rebound and other functions. Vue/Nuxt js generic sound out | Tinder card slip
See the effect
The final result is like the picture above, does it feel ok?
The overall layout
The page layout is divided into three modules: the top navigation bar, the stack area, and the bottom TAB bar.
<! -- // Card page template --> <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"> Meet 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 "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> <! <tab-bar bgcolor="linear-gradient(to right, # 00e0A1, #00a1ff)" color="# FFF "/> </div> </template>Copy the code
Sidebar pop-ups
Click the page filter button to pop up the side window. The range slider, switch switch, Rate rating and other components are implemented using the Vant component library.
<template> <! -... -- > <! <v-popup v-model="showFilter" position="left" xclose xposition="left" title=" advanced Filter and Settings "> <div Class ="flipcard-filter"> <div class="item nuxt-cell"> <label class=" LBL "> range </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"> Gender </label> <em class="val"> Female </em> </div> <div class="item Nuxt-cell ">< label class=" LBL ">< div class="flex1"><van-rate v-model="starVal" color="# 00e0A1 "icon="like" Void -icon="like-o" @change="handleStar" /></div> <em class="val"> </em> </div> <div class="item nuxt-cell"> <label class=" LBL flex1"> priority online user </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"> Priority new user </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 > Update </div> </div> </v-popup> </template> <script> export default {// Head () {return {title: '${this.title} - 查 看 ', meta: [{name:'keywords',hid: 'keywords, content: ` ${enclosing the title} | | double flip CARDS `}, {name: 'description', hid: 'description, content: ` ${enclosing the title} | imitation of sound out CARDS flip `}]}}, 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>Copy the code
Vue mock probe card stack
For page code specification, the card stack area encapsulates a separate component and simply passes in the Pages data.
<flipcard ref="stack" :pages="stackList"></flipcard>
When you drag your finger around the card, a different Angle of view will appear.
Pages passed parameter
Module. Exports = [{avatar: '/assets/img/avatar02.jpg', name: 'female', sex: 'female', age: 23, starsign: 'libra, short:' art/fitness 'photos: [...], sign:' make friends, if you are the one '},...Copy the code
Card component template
<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>
Copy the code
Since the component handles touch and Mouse events, it can be swiped on both mobile and PC.
Clicking on the card takes you straight to the details page.
Okey, based on vue. js to explore the card effect to share here. I hope I like it! 😕 ✍
Finally, a recent example project is attached
Electron + example WeChat desktop client chat Vue | Electron – Vue chat rooms