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