因为自身的项目比较简单,只有几个H5页面,用来嵌入app中,全部没有引入移动端的UI框架,可是介于能让用户在浏览H5页面时有下拉刷新和上拉加载,有更好的用户体验,本身写组件实现。css
1、下拉刷新
DropDownRefresh.vue
html
<template lang="html"> <div class="refresh-moudle" @touchstart="touchStart($event)" @touchmove="touchMove($event)" @touchend="touchEnd($event)" :style="{transform: 'translate3d(0,' + top + 'px, 0)'}"> <header class="pull-refresh"> <slot name="pull-refresh"> <div class="down-tip" v-if="dropDownState==1"> <img v-if="dropDownInfo.downImg" class="down-img" :src="require('../../assets/images/refreshAndReload/'+dropDownInfo.downImg)"> <span class="down-text">{{dropDownInfo.downText}}</span> </div> <div class="up-tip" v-if="dropDownState==2"> <img v-if="dropDownInfo.upImg" class="up-img" :src="require('../../assets/images/refreshAndReload/'+dropDownInfo.upImg)"> <span class="up-text">{{dropDownInfo.upText}}</span> </div> <div class="refresh-tip" v-if="dropDownState==3"> <img v-if="dropDownInfo.refreshImg" class="refresh-img" :src="require('../../assets/images/loading/'+dropDownInfo.refreshImg)"> <span class="refresh-text">{{dropDownInfo.refreshText}}</span> </div> </slot> </header> <slot></slot> </div> </template> <script> export default { props: { onRefresh: { type: Function, required: false } }, data () { return { defaultOffset: 50, // 默认高度, 相应的修改.releshMoudle的margin-top和.down-tip, .up-tip, .refresh-tip的height top: 0, scrollIsToTop: 0, startY: 0, isDropDown: false, // 是否下拉 isRefreshing: false, // 是否正在刷新 dropDownState: 1, // 显示1:下拉能够刷新, 2:松开当即刷新, 3:正在刷新数据中... dropDownInfo: { downText: '下拉能够刷新', downImg: 'arrow.png', upText: '松开当即刷新', upImg: 'arrow.png', refreshText: '正在刷新数据...', refreshImg: 'loading.png' } } }, created () { if (document.querySelector('.down-tip')) { // 获取不一样手机的物理像素(dpr),以便适配rem this.defaultOffset = document.querySelector('.down-tip').clientHeight || this.defaultOffset } }, methods: { /** * 触摸开始,手指点击屏幕时 * @param {object} e Touch 对象包含的属性 */ touchStart (e) { this.startY = e.targetTouches[0].pageY }, /** * 接触点改变,滑动时 * @param {object} e Touch 对象包含的属性 */ touchMove (e) { this.scrollIsToTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop // safari 获取scrollTop用window.pageYOffset if (e.targetTouches[0].pageY > this.startY) { // 下拉 this.isDropDown = true if (this.scrollIsToTop === 0 && !this.isRefreshing) { // 拉动的距离 let diff = e.targetTouches[0].pageY - this.startY - this.scrollIsToTop this.top = Math.pow(diff, 0.8) + (this.dropDownState === 3 ? this.defaultOffset : 0) if (this.top >= this.defaultOffset) { this.dropDownState = 2 e.preventDefault() } else { this.dropDownState = 1 // 去掉会致使ios没法刷新 e.preventDefault() } } } else { this.isDropDown = false this.dropDownState = 1 } }, /** * 触摸结束,手指离开屏幕时 * @param {object} e Touch 对象包含的属性 */ touchEnd (e) { if (this.isDropDown && !this.isRefreshing) { if (this.top >= this.defaultOffset) { // do refresh this.refresh() this.isRefreshing = true } else { // cancel refresh this.isRefreshing = false this.isDropDown = false this.dropDownState = 1 this.top = 0 } } }, /** * 刷新 */ refresh () { this.dropDownState = 3 this.top = this.defaultOffset // 这是全是静态数据,延时1200毫秒,给用户一个刷新的感受,若是是接口数据的话,直接调接口便可 setTimeout(() => { this.onRefresh(this.refreshDone) }, 1200) }, /** * 刷新完成 */ refreshDone () { this.isRefreshing = false this.isDropDown = false this.dropDownState = 1 this.top = 0 } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .refresh-moudle { width: 100%; margin-top: -100px; -webkit-overflow-scrolling: touch; /* ios5+ */ } .pull-refresh { width: 100%; color: #999; transition-duration: 200ms; font-size: 24px; } .refresh-moudle .down-tip, .up-tip, .refresh-tip { display: flex; align-items: center; justify-content: center; height: 100px; } .down-img { width: 35px; height: 35px; margin-right: 15px; transform: rotate(0deg); animation: anticlockwise 0.8s ease; } @keyframes anticlockwise { 0% { transform: rotate(-180deg); } 100% { transform: rotate(0deg); } } .up-img { width: 35px; height: 35px; margin-right: 15px; transform: rotate(180deg); animation: clockwise 0.8s ease; } @keyframes clockwise { 0% { transform: rotate(0deg); } 100% { transform: rotate(-180deg); } } .refresh-img { width: 35px; height: 35px; margin-right: 15px; animation: rotating 1.5s linear infinite; } @keyframes rotating { 0% { transform: rotate(0deg); } 100% { transform: rotate(1turn); } } </style>
2、上拉加载
PullUpReload.vue
vue
<template lang="html"> <div class="load-moudle" @touchstart="touchStart($event)" @touchmove="touchMove($event)" @touchend="touchend($event)"> <slot></slot> <footer class="load-more"> <slot name="load-more"> <div class="more-tip" v-if="pullUpState==1"> <span class="more-text">{{pullUpInfo.moreText}}</span> </div> <div class="loading-tip" v-if="pullUpState==2"> <span class="loading-icon"></span> <span class="loading-text">{{pullUpInfo.loadingText}}</span> </div> <div class="no-more-tip" v-if="pullUpState==3"> <span class="connecting-line"></span> <span class="no-more-text">{{pullUpInfo.noMoreText}}</span> <span class="connecting-line"></span> </div> </slot> </footer> </div> </template> <script> export default { props: { parentPullUpState: { default: 0 }, onInfiniteLoad: { type: Function, require: false } }, data () { return { top: 0, pullUpState: 0, // 1:上拉加载更多, 2:加载中……, 3:我是有底线的 isLoading: false, // 是否正在加载 pullUpInfo: { moreText: '上拉加载更多', loadingText: '数据加载中...', noMoreText: '我是有底线的' }, startX: 0, startY: 0, endX: 0, endY: 0 } }, methods: { /** * 触摸开始,手指点击屏幕时 * @param {object} e Touch 对象包含的属性 */ touchStart (e) { this.startX = e.touches[0].pageX this.startY = e.touches[0].pageY }, /** * 接触点改变,滑动时 * @param {object} e Touch 对象包含的属性 */ touchMove (e) { this.endX = e.changedTouches[0].pageX this.endY = e.changedTouches[0].pageY let direction = this.getSlideDirection(this.startX, this.startY, this.endX, this.endY) switch (direction) { case 0: // console.log('没滑动') break case 1: // console.log('向上') this.scrollToTheEnd() break case 2: // console.log('向下') break case 3: // console.log('向左') break case 4: // console.log('向右') break default: } }, /** * 触摸结束,手指离开屏幕时 * @param {object} e Touch 对象包含的属性 */ touchend (e) { this.isLoading = false }, /** * 判断滚动条是否到底 */ scrollToTheEnd () { let innerHeight = document.querySelector('.load-moudle').clientHeight // 变量scrollTop是滚动条滚动时,距离顶部的距离 let scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop // 变量scrollHeight是滚动条的总高度 let scrollHeight = document.documentElement.clientHeight || document.body.scrollHeight // 滚动条到底部的条件 if (scrollTop + scrollHeight >= innerHeight) { if (this.pullUpState !== 3 && !this.isLoading) { this.infiniteLoad() } // console.log('距顶部' + scrollTop + '滚动条总高度' + scrollHeight + '内容高度' + innerHeight) } }, /** * 上拉加载数据 */ infiniteLoad () { if (this.pullUpState !== 0) { this.pullUpState = 2 this.isLoading = true this.onInfiniteLoad(this.infiniteLoadDone) } }, /** * 加载数据完成 */ infiniteLoadDone () { this.pullUpState = 1 }, /** * 返回角度 */ getSlideAngle (dx, dy) { return Math.atan2(dy, dx) * 180 / Math.PI }, /** * 根据起点和终点返回方向 1:向上,2:向下,3:向左,4:向右,0:未滑动 * @param {number} startX X轴开始位置 * @param {number} startY X轴结束位置 * @param {number} endX Y轴开始位置 * @param {number} endY Y轴结束位置 */ getSlideDirection (startX, startY, endX, endY) { let dy = startY - endY let dx = endX - startX let result = 0 // 若是滑动距离过短 if (Math.abs(dx) < 2 && Math.abs(dy) < 2) { return result } let angle = this.getSlideAngle(dx, dy) if (angle >= -45 && angle < 45) { result = 4 } else if (angle >= 45 && angle < 135) { result = 1 } else if (angle >= -135 && angle < -45) { result = 2 } else if ((angle >= 135 && angle <= 180) || (angle >= -180 && angle < -135)) { result = 3 } return result } }, watch: { parentPullUpState (curVal, oldVal) { this.pullUpState = curVal } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .load-more { width: 100%; color: #c0c0c0; background: #fafafa; font-size: 24px; } .more-tip, .loading-tip, .no-more-tip { display: flex; align-items: center; justify-content: center; height: 150px; } .load-moudle .loading-icon { display: inline-flex; width: 35px; height: 35px; background: url(../../assets/images/refreshAndReload/loading.png) no-repeat; background-size: cover; margin-right: 5px; animation: rotating 2s linear infinite; } @keyframes rotating { 0% { transform: rotate(0deg); } 100% { transform: rotate(1turn); } } .connecting-line { display: inline-flex; width: 150px; height: 2px; background: #ddd; margin-left: 20px; margin-right: 20px; } </style>
3、对两个组件的使用ios
<template> <section class="container"> <v-refresh :on-refresh="onRefresh"> <v-reload :on-infinite-load="onInfiniteLoad" :parent-pull-up-state="infiniteLoadData.pullUpState" > <div class="bank-box"> <div class="bank-list" v-for="item in bankList" :key="item.id"> <div class="bank-icon" :style="{ 'background': 'url(' + require('../../assets/images/bankIcon/'+item.iconName) + ') no-repeat', 'background-size': '100%' }" ></div> <span class="bank-name">{{item.bankName}}</span> </div> </div> <div class="hot-box"> <div class="hot-header"> <span class="hot-name">热门推荐</span> <div class="more-box"> <span class="more-text">查看更多</span> <span class="more-icon"></span> </div> </div> <div class="hot-centenrt"> <div class="hot-left"> <span class="left-name">{{hotLeft.name}}</span> <span class="left-desc">{{hotLeft.desc}}</span> <div class="left-img" :style="{ 'background': 'url(' + require('../../assets/images/bank/'+hotLeft.imgName) + ') no-repeat', 'background-size': '100%' }" ></div> </div> <div class="hot-right"> <div class="right-top"> <div class="text-box"> <span class="right-name">{{centenrtOne.name}}</span> <span class="right-desc">{{centenrtOne.desc}}</span> </div> <div class="right-img" :style="{ 'background': 'url(' + require('../../assets/images/bank/'+centenrtOne.imgName) + ') no-repeat', 'background-size': '100%' }" ></div> </div> <div class="hot-right-bottom"> <div class="text-box2"> <span class="right-name2">{{centenrtTwo.name}}</span> <span class="right-desc2">{{centenrtTwo.desc}}</span> </div> <div class="right-img" :style="{ 'background': 'url(' + require('../../assets/images/bank/'+centenrtTwo.imgName) + ') no-repeat', 'background-size': '100%' }" ></div> </div> </div> </div> </div> <div class="card-state"> <div class="card-progress border-right"> <div class="progress-icon"></div> <div class="card-text"> <span class="card-name">办卡进度</span> <span class="card-desc">让等待随处可见</span> </div> </div> <div class="card-activation"> <div class="activation-icon"></div> <div class="card-text"> <span class="card-name">办卡激活</span> <span class="card-desc">让等待随处可见</span> </div> </div> </div> <div class="card-order"> <div class="border-bottom card-bottom"> <div class="hot-header"> <span class="hot-name">热卡排行</span> </div> </div> <div slot="load-more"> <li class="card-list" v-for="(item,index) in infiniteLoadData.pullUpList" :key="item.id" > <div class="card-content" :class="infiniteLoadData.pullUpList.length - 1 != index? 'card-bottom':''" > <div class="card-img" :style="{ 'background': 'url(' + require('../../assets/images/bank/'+item.imgName) + ') no-repeat', 'background-size': '100%' }" ></div> <div class="card-list-text"> <p class="card-name">{{item.cardName}}</p> <p class="card-title">{{item.cardTitle}}</p> <div class="words-lists"> <div class="card-words"> <p class="card-word">{{item.cardWordOne}}</p> </div> <div v-if="item.cardWordTwo" class="card-words words-two"> <p class="card-word">{{item.cardWordTwo}}</p> </div> </div> </div> </div> </li> </div> </div> </v-reload> </v-refresh> </section> </template> <script> import DropDownRefresh from '../common/DropDownRefresh' import PullUpReload from '../common/PullUpReload' export default { data () { return { bankList: [ { iconName: 'zhaoshang.png', bankName: '招商银行' }, { iconName: 'minsheng.png', bankName: '民生银行' }, { iconName: 'pingancar.png', bankName: '平安联名' }, { iconName: 'xingye.png', bankName: '兴业银行' }, { iconName: 'shanghai.png', bankName: '上海银行' }, { iconName: 'jiaotong.png', bankName: '交通银行' }, { iconName: 'guangda.png', bankName: '光大银行' }, { iconName: 'more.png', bankName: '所有银行' } ], hotLeft: { bankName: '交通银行', name: '交行Y-POWER黑卡', desc: '额度100%取现', imgName: 'jiaohangY-POWER.png' }, centenrtOne: { bankName: '招商银行', name: '招行YOUNG卡', desc: '生日月双倍积分', imgName: 'zhaohangYOUNG.png' }, centenrtTwo: { bankName: '光大银行', name: '光大淘票票公仔联名卡', desc: '电影达人必备', imgName: 'guangdalianming.png' }, cardList: [ { bankName: '平安联名', imgName: 'pinganqiche.png', cardName: '平安银行信用卡', cardTitle: '平安银行汽车之家联名单币卡', cardWordOne: '首年免年费', cardWordTwo: '加油88折' }, { bankName: '上海银行', imgName: 'shanghaitaobao.png', cardName: '上海银行信用卡', cardTitle: '淘宝金卡', cardWordOne: '积分抵现', cardWordTwo: '首刷有礼' }, { bankName: '华夏银行', imgName: 'huaxiaiqiyi.png', cardName: '华夏银行信用卡', cardTitle: '华夏爱奇艺悦看卡', cardWordOne: '送爱奇艺会员', cardWordTwo: '商城8折' }, { bankName: '浦发银行', imgName: 'pufajianyue.png', cardName: '浦发银行信用卡', cardTitle: '浦发银行简约白金卡', cardWordOne: '团购立减', cardWordTwo: '酒店优惠 免年费' }, { bankName: '中信银行', imgName: 'zhongxinbaijin.png', cardName: '中信银行信用卡', cardTitle: '中信银行i白金信用卡', cardWordOne: '首刷有礼', cardWordTwo: '双倍积分' } ], // 上拉加载的设置 infiniteLoadData: { initialShowNum: 3, // 初始显示多少条 everyLoadingNum: 3, // 每次加载的个数 pullUpState: 2, // 子组件的pullUpState状态 pullUpList: [], // 上拉加载更多数据的数组 showPullUpListLength: this.initialShowNum // 上拉加载后所展现的个数 } } }, mounted () { this.getStartPullUpState() this.getPullUpDefData() }, methods: { /** * 获取上拉加载的初始数据 */ getPullUpDefData () { this.infiniteLoadData.pullUpList = [] if (this.cardList.length < this.infiniteLoadData.initialShowNum) { for (let i = 0; i < this.cardList.length; i++) { this.infiniteLoadData.pullUpList.push(this.cardList[i]) } } else { for (let i = 0; i < this.infiniteLoadData.initialShowNum; i++) { this.infiniteLoadData.pullUpList.push(this.cardList[i]) } } this.getStartPullUpState() }, /** * 获取上拉加载的pullUpState状态 */ getStartPullUpState () { if (this.infiniteLoadData.pullUpList.length) { if (this.cardList.length <= this.infiniteLoadData.initialShowNum) { // 修改子组件的pullUpState状态 this.infiniteLoadData.pullUpState = 3 } else { this.infiniteLoadData.pullUpState = 1 } } else { this.infiniteLoadData.pullUpState = 0 } }, /** * 上拉一次加载更多的数据 */ getPullUpMoreData () { this.showPullUpListLength = this.infiniteLoadData.pullUpList.length if (this.infiniteLoadData.pullUpList.length + this.infiniteLoadData.everyLoadingNum > this.cardList.length) { for (let i = 0; i < this.cardList.length - this.showPullUpListLength; i++) { this.infiniteLoadData.pullUpList.push(this.cardList[i + this.showPullUpListLength]) } } else { for (let i = 0; i < this.infiniteLoadData.everyLoadingNum; i++) { this.infiniteLoadData.pullUpList.push(this.cardList[i + this.showPullUpListLength]) } } if (this.cardList.length === this.infiniteLoadData.pullUpList.length) { this.infiniteLoadData.pullUpState = 3 } else { this.infiniteLoadData.pullUpState = 1 } }, /** * 下拉刷新 */ onRefresh (done) { // 若是下拉刷新和上拉加载同时使用,下拉时初始化上拉的数据 this.getStartPullUpState() this.getPullUpDefData() done() // call done }, /** * 上拉加载 */ onInfiniteLoad (done) { if (this.infiniteLoadData.pullUpState === 1) { this.getPullUpMoreData() } done() } }, components: { 'v-refresh': DropDownRefresh, 'v-reload': PullUpReload } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> @import "../../assets/css/not2rem.css"; .container { display: flex; flex-direction: column; width: 750px; height: 1334px; background-color: #f7f7f7; } .bank-box { display: flex; flex-wrap: wrap; padding: 2px 7px 42px 7px; background-color: #fff; } .bank-list { display: flex; flex-direction: column; justify-content: space-between; width: 100px; height: 98px; margin: 40px 42px 0 42px; } .bank-icon { width: 56px; height: 56px; margin: 0 22px 18px; } .bank-name { display: flex; align-items: center; width: 110px; height: 24px; line-height: 24px; font-size: 24px; color: #333; } .hot-box { width: 100%; height: 420px; margin-top: 10px; background: #fff; } .hot-header { display: flex; justify-content: space-between; align-items: center; width: 674px; height: 80px; margin: 0 30px 0 46px; } .hot-name { display: flex; align-items: center; height: 28px; font-size: 28px; color: #333; } .more-text { display: flex; align-items: center; font-size: 24px; color: #999; } .more-box { display: flex; align-items: center; } .more-icon { margin-left: 20px; width: 11px; height: 20px; background: url("../../assets/images/bank/more.png") no-repeat; background-size: 100%; } .hot-centenrt { display: flex; align-items: center; width: 710px; height: 320px; margin: 0 20px 20px 20px; } .hot-left { display: flex; flex-direction: column; width: 350px; height: 320px; background: #f7f7f7; } .left-name { display: flex; align-items: center; width: 282px; height: 24px; margin: 50px 34px 0 34px; font-size: 24px; line-height: 24px; color: #333; } .left-desc { display: flex; align-items: center; width: 282px; height: 20px; margin: 12px 34px 0 34px; font-size: 20px; line-height: 20px; color: #999; } .left-img { width: 220px; height: 142px; margin-left: 34px; margin-top: 34px; } .hot-right { display: flex; flex-direction: column; width: 350px; height: 320px; margin-left: 10px; } .right-top { display: flex; flex-direction: row; width: 100%; height: 156px; background: #f7f7f7; } .text-box { display: flex; flex-direction: column; width: 180px; height: 58px; margin: 49px 20px 0 20px; } .right-name { display: flex; align-items: center; width: 100%; height: 24px; line-height: 24px; font-size: 24px; color: #333; } .right-desc { display: flex; align-items: center; margin-top: 10px; width: 100%; height: 24px; line-height: 24px; font-size: 24px; color: #999; } .right-img { width: 110px; height: 70px; margin-top: 43px; } .hot-right-bottom { display: flex; flex-wrap: wrap; width: 100%; height: 156px; margin-top: 8px; background: #f7f7f7; } .text-box2 { display: flex; flex-direction: column; width: 180px; margin: 31px 20px 0 20px; } .right-name2 { display: flex; align-items: center; width: 100%; height: 58px; line-height: 30px; font-size: 24px; color: #333; } .right-desc2 { display: flex; align-items: center; margin-top: 12px; width: 100%; height: 24px; line-height: 24px; font-size: 24px; color: #999; } .card-state { display: flex; flex-direction: row; width: 100%; height: 128px; margin-top: 10px; background-color: #fff; } .card-progress { display: flex; align-items: center; width: 327px; height: 88px; margin: 20px 0 20px 48px; } .progress-icon { width: 48px; height: 48px; margin: 20px 0; background: url("../../assets/images/bank/search.png") no-repeat; background-size: 100%; } .activation-icon { width: 48px; height: 48px; margin: 20px 0; background: url("../../assets/images/bank/activation.png") no-repeat; background-size: 100%; } .card-text { display: flex; flex-direction: column; align-items: center; width: 228px; height: 66px; margin: 11px 20px 11px 30px; } .card-name { display: flex; align-items: center; width: 100%; height: 28px; line-height: 28px; font-size: 28px; color: #333; } .card-desc { display: flex; align-items: center; width: 100%; height: 22px; line-height: 22px; font-size: 22px; margin-top: 16px; color: #999; } .card-activation { display: flex; align-items: center; width: 326px; height: 88px; margin: 20px 0 20px 48px; } .card-order { width: 100%; height: auto; margin-top: 10px; background-color: #fff; } .border-bottom { width: 100%; height: 80px; } .card-list { width: 100%; height: 228px; list-style-type: none; } .card-content { display: flex; flex-direction: row; width: 700px; height: 228px; margin-left: 50px; } .card-img { width: 186px; height: 120px; margin: 54px 0 54px 20px; } .card-list-text { display: flex; flex-direction: column; width: 386px; height: 124px; margin: 52px 34px 52px 74px; } .card-name { width: 100%; height: 28px; line-height: 28px; font-size: 28px; color: #333; } .card-title { width: 100%; height: 24px; margin-top: 20px; line-height: 24px; font-size: 24px; color: #666; } .words-lists { display: flex; flex-direction: row; } .card-words { height: 36px; margin-top: 16px; border-radius: 20px; background-color: #e8ca88; } .card-word { height: 20px; padding: 8px 18px; line-height: 20px; font-size: 20px; color: #4b4b4b; } .words-two { margin-left: 20px; } </style>
这里只是展现了一下效果,使用的全是静态数据,若是要用接口数据的话,this.getPullUpMoreData()
方法直接换成接口数据的方法,并将判断的逻辑移到接口方法里,固然根据实际状况,分页呀啥的作作适当的修改。web