引言
最近一直在研發Nuxt.js聊天專案,有個需求是實現類似探探卡片左右滑動效果。要求四個方向角拖拽出現不同的傾斜視角,左滑unlike|右滑like,並且點選按鈕也可以左右切換。
效果圖
如上圖所示,基本實現了需要的效果。下面就簡單分享下實現過程。
頁面佈局
頁面整體分為 頂部導航條、卡片滑動區域、底部標籤欄 三個部分。
<!-- //頁面模板 -->
<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開關、star評分 等元件則是使用Vant元件庫。
側邊欄模板
<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>
vue仿製探探卡片
卡片堆疊區單獨封裝了一個元件flipcard,只需傳入pages資料就可以。
pages引數格式
module.exports = [
{
avatar: '/assets/img/avatar02.jpg',
name: '放蕩不羈愛自由',
sex: 'female',
age: 23,
starsign: '天秤座',
distance: '藝術/健身',
photos: [...],
sign: '交個朋友,非誠勿擾'
},
...
]
flipcard.vue元件模板
<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>
由於元件中處理了mouse和touch兩種事件,所以支援移動端和pc端拖拽。
另外點選卡片後會跳轉到詳情頁。
Okay,基於vue.js模仿探探卡片滑動效果就分享到這裡。感興趣的同學可以試試哈!✍
最後分享個Electron例項專案
基於Electron+Vue仿微信桌面端介面聊天例項|electron-vue聊天室
本作品採用《CC 協議》,轉載必須註明作者和本文連結