百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

Nuxt/Vue丝滑卡片式滑动|Vue仿探探卡片效果

bigegpt 2024-08-09 11:17 2 浏览

最近在开发Nuxt项目,有个需求是实现类似探探左右滑动切换卡片功能。经过反复调试,最终实现了这个效果。

页面整体布局分为 顶部导航条、卡片滑动区、底部Tabbar 三个模块。

为了规范页面代码,其中卡片滑动区域单独抽离了一个组件flipcard.vue。

下面就讲解下卡片滑动页面实现方法。

  • 整体模板
<!-- //翻一翻模板 -->
<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>

项目中顶部导航栏/底部Tabbar组件,这里不作过多介绍,之前有一篇分享文章,感兴趣的可以看下。

基于Nuxt/Vue自定义Topbar+Tabbar

  • 侧边栏弹出框

侧边弹出层使用的是VPopup组件实现。其中范围滑块、switch、star等组件使用的是Vant组件库。

<!-- //侧边栏弹窗模板 -->
<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" />
			</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>
  • 仿探探卡片滑块

滑动卡片封装成了一个flipcard.vue组件。只需传入pages数据即可。支持左右拖拽滑动、滑动回弹动画、点击按钮切换等功能。

<flipcard ref="stack" :pages="stackList" @click="handleStackClicked"></flipcard>

pages支持传入的JSON数据格式。

{
	avatar: 'assets/img/avatar.jpg',
	name: '甜甜圈',
	sex: 'female',
	age: 21,
	starsign: '双鱼座',
	distance: '800米',
	// 照片集
	photos: [...],
	// 签名
	sign: 'life is like a play 人生如戏。'
},

新建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>
<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
        }
    },
    mounted () {
        // 绑定事件
        this.$on('next', () => {
            this.next()
        })
        this.$on('prev', () => {
            this.prev()
        })
    },
    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端滑动。

另外,点击卡片可以跳转到卡片详情页面。

okay,代码中都有一些备注,这里不作详细讲解了。有兴趣的可以去试一试哈~~

今天就分享到这里。希望对大家有所帮助!

相关推荐

10w qps缓存数据库——Redis(redis缓存调优)

一、Redis数据库介绍:Redis:非关系型缓存数据库nosql:非关系型数据库没有表,没有表与表之间的关系,更不存在外键存储数据的形式为key:values的形式c语言写的服务(监听端口),用来存...

Redis系列专题4--Redis配置参数详解

本文基于windowsX64,3.2.100版本讲解,不同版本默认配置参数不同在Redis中,Redis的根目录中有一个配置文件(redis.conf,windows下为redis.windows....

开源一夏 | 23 张图,4500 字从入门到精通解释 Redis

redis是目前出场率最高的NoSQL数据库,同时也是一个开源的数据结构存储系统,在缓存、数据库、消息处理等场景使用的非常多,本文瑞哥就带着大家用一篇文章入门这个强大的开源数据库——Redis。...

redis的简单与集群搭建(redis建立集群)

Redis是什么?是开源免费用c语言编写的单线程高性能的(key-value形式)内存数据库,基于内存运行并支持持久化的nosql数据库作用主要用来做缓存,单不仅仅是做缓存,比如:redis的计数器生...

推荐几个好用Redis图形化客户端工具

RedisPlushttps://gitee.com/MaxBill/RedisPlusRedisPlus是为Redis可视化管理开发的一款开源免费的桌面客户端软件,支持Windows、Linux...

关于Redis在windows上运行及fork函数问题

Redis在将数据库进行持久化操作时,需要fork一个进程,但是windows并不支持fork,导致在持久化操作期间,Redis必须阻塞所有的客户端直至持久化操作完成。微软的一些工程师花费时间在解决在...

你必须懂的Redis十大应用场景(redis常见应用场景)

Redis作为一款高性能的键值存储数据库,在互联网业务中有着广泛的应用。今天,我们就来详细盘点一下Redis的十大常用业务场景,并附上Golang的示例代码和简图,帮助大家更好地理解和应用Redis。...

极简Redis配置(redis的配置)

一、概述Redis的配置文件位于Redis安装目录下,文件名为redis.conf(Windows名为redis.windows.conf,linux下的是redis.conf)你可以通过C...

什么是redis,怎么启动及如何压测

从今天起咱们一起来学习一下关于“redis监控与调优”的内容。一、Redis介绍Redis是一种高级key-value数据库。它跟memcached类似,不过数据可以持久化,而且支持的数据类型很丰富。...

一款全新Redis UI可视化管理工具,支持WebUI和桌面——P3X Redis UI

介绍P3XRedisUI这是一个非常实用的RedisGUI,提供响应式WebUI访问或作为桌面应用程序使用,桌面端是跨平台的,而且完美支持中文界面。Githubhttps://github....

windows系统的服务器快速部署java项目环境地址

1、mysql:https://dev.mysql.com/downloads/mysql/(msi安装包)2、redis:https://github.com/tporadowski/redis/r...

window11 下 redis 下载与安装(windows安装redis客户端)

#热爱编程是一种怎样的体验#window11下redis下载与安装1)各个版本redis下载(windows)https://github.com/MicrosoftArchive/r...

一款轻量级的Redis客户端工具,贼好用!

使用命令行来操作Redis是一件非常麻烦的事情,我们一般会选用客户端工具来操作Redis。今天给大家分享一款好用的Redis客户端工具TinyRDM,它的界面清新又优雅,希望对大家有所帮助!简介Ti...

一个.NET开发且功能强大的Windows远程控制系统

我们致力于探索、分享和推荐最新的实用技术栈、开源项目、框架和实用工具。每天都有新鲜的开源资讯等待你的发现!项目介绍SiMayRemoteMonitorOS是一个基于Windows的远程控制系统,完...

Redis客户端工具详解(4款主流工具)

大家好,我是mikechen。Redis是大型架构的基石,也是大厂最爱考察内容,今天就给大家重点详解4款Redis工具@mikechen本篇已收于mikechen原创超30万字《阿里架构师进阶专题合集...