收场

技术宅男对探探/陌陌并不生疏,一款专一于陌生人的社交App。外面的左右滑动翻牌子成果更是让人眼前一亮,仿佛有一种古时君王选妃子的感觉。让人玩的爱不释手。

一睹风采

哈哈,成果还行吧。上面就简略的解说下具体的实现办法。

页面布局

页面整体分为 顶部Navbar、卡片区域、底部Tabbar 三个局部。

<!-- //翻一翻模板 --><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组件库。

侧边弹窗模板

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

仿探探翻牌子

卡片区独自封装了一个组件flipcard,只需传入pages数据就能够。
<flipcard ref="stack" :pages="stackList"></flipcard>

在周围拖拽卡片会呈现不同的斜切视角。

pages数据格式

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

flipcard组件模板

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

组件反对touchmouse事件,在挪动端和PC端均可滑动。

另外,点击卡片跳转到卡片具体页面。

好了,基于Vue实现探探卡片成果就分享到这里。心愿能喜爱~~ ✍