使用VUE实现无缝滚动组件
最近这俩天被临时拉去写一个vue消息滚动组件,作为一个后端开发,只会css,js,html,没有学过vue,最近参考样例,官网文档,和开源的滚动消息组件,封装了一个可以复用的vue组件。直接在html引入该js,定义组件名称标签传递数据即可。鸣谢github开源项目,本消息滚动组件无需额外引用外部js,无第三方依赖。
2020.0107修改,增加填充父级元素属性设置
实现源码如下:
html部分:
<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <!--需要添加的属性配置是每页显示的条数 根据li的高度,边框宽度,li之间的margin来计算--> <div id="app"> <scroll-tony v-bind:data="data" :obj-val="objData"></scroll-tony> </div> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="scroll-tony.js"></script> <script> new Vue({ el: '#app', data: { data: [{ 'label': '无缝滚动第一行无缝滚动第一行第一行无缝滚动第一行第一行无缝滚动第一行第一行无缝滚动第一行', 'unit': '2017-12-16第一行无缝滚动第一行第一行无缝滚动第一行' }, { 'label': '无缝滚动第二行无缝滚动第二行', 'unit': '2017-12-16' }, { 'label': '无缝滚动第三行无缝滚动第三行', 'unit': '2017-12-16' }, { 'label': '无缝滚动第四行无缝滚动第四行', 'unit': '2017-12-16' }, { 'label': '无缝滚动第五行无缝滚动第五行', 'unit': '2017-12-16' }, { 'label': '无缝滚动第六行无缝滚动第六行', 'unit': '2017-12-16' }, { 'label': '无缝滚动第七行无缝滚动第七行', 'unit': '2017-12-16' }, { 'label': '无缝滚动第八行无缝滚动第八行', 'unit': '2017-12-16' }, { 'label': '无缝滚动第九行无缝滚动第九行', 'unit': '2017-12-16' }] }, computed: { objData: function (){ return { rollSpeed: 0.25, //数值越大速度滚动越快(Number) limitMoveNum: 5, //开启无缝滚动的数据量(Number) hoverStop:true, //是否启用鼠标hover控制(Boolean) direction:1, //方向 0 往下 1 往上(Number) singleHeight:3, //单步运动几个信息条后停止(默认值0是无缝不停止的滚动)(Number) rollInterval:2000, //单步停止等待时间(默认值1000ms,设置单步等待时长)(Number) openRoll:true, //是否开启轮播 默认true(开启) (Boolean) fwidth:360, fheight:320, // borderWidth:1, //取消默认有整个滚动区域边框 // borderColor:'#FFB42C', // borderCornerValue:5, liborderWidth:2, //信息条边框 liborderColor:'#FFFFFF', liborderCornerValue:5, liheight:60, //每条信息展示的高度 backgroudColor:'#E8EDF1',//'#282C34', //信息条背景颜色 labelFontSize:25, //标签字体大小 labelFontWeight:'bold', labelFontColor:'#282C34', unitFontSize:20, //内容字体大小 unitFontWeight:'normal', unitFontColor:'#1370FB', iconUrl:'img2.png',//'https://www.baidu.com/img/labadong_9f901c0eb1a677ad32efe1d024e12bac.gif', //图标路径 iconWidth:50, iconHeight:50, limargin:2, //每个信息条之间的间隔
isAbsolute:false,//当使用false时,开启填充父级元素,true使用组件默认设置的宽高/自定义设置的宽高
} } } }) </script> </body> </html>
js部分:
Vue.component("scroll-tony",{ template: `<div ref="wrap" :style="wrapStyle"> <div ref="realBox" :style="pos" @mouseenter="enter" @mouseleave="leave"> <div ref="slotList" style="overflow: 'hidden'"> <solt> <ul :style="wrapul"> <li ref="wrapli" :style="wrapli" v-for="item in data"> <div class="imgdiv"> <img :style="img" :src='options.iconUrl'/> </div> <div :style="content"> <div :style="inner"><span :style="label" v-text="item.label"></span></div> <div :style="inner"><span :style="unit" v-text="item.unit"></span></div> </div> </li> </ul> </slot> </div> <div v-html="copyHtml" style="overflow: 'hidden'"></div> </div> </div>`, props: { data: { type: Array, default: () => { return [] } }, objVal: { type: Object, default: () => { return {} } }, }, data () { return { xPos: 0, yPos: 0, delay: 0, copyHtml: '', height: 0, width: 0, // 外容器宽度 realBoxWidth: 0, // 内容实际宽度 reqFrame: null, // move动画的animationFrame定时器 singleWaitTime: null, // single 单步滚动的定时器 isHover: false // mouseenter mouseleave 控制this._move()的开关 } }, created() { if(this.options.isAbsolute){
this.options.fheight = this.$parent.$el.offsetHeight
this.options.fwidth = this.$parent.$el.offsetHeight;
} }, computed: { wrapStyle: { get (){ //可以根据其他需求作判断来按需求返回值 return{ height: this.options.fheight+`px`, width: this.options.fwidth+`px`, overflow: `hidden`, border: this.options.borderWidth+`px solid`, borderRadius:this.options.borderCornerValue+`px`, borderColor:this.options.borderColor+`` } } }, wrapul: { get(){ return{ listStyle: "none", padding: "0", margin: "0 auto" } } }, wrapli:{ get(){ return{ justifyContent:"center", alignItems:"Center", height: this.options.liheight+"px", margin: this.options.limargin+"px", lineHeight: (this.options.liheight/2)+"px", border: this.options.liborderWidth+`px solid`, borderRadius:this.options.liborderCornerValue+`px`, borderColor:this.options.liborderColor, display: "flex", verticalAlign: "middle", backgroundColor: this.options.backgroudColor } } }, img:{ get(){ return{ float: "left", width: this.options.iconWidth+"px", height: this.options.iconHeight+"px", marginLeft: (this.options.limargin*2)+"px", marginRight: (this.options.limargin*2)+"px", } } }, content:{ get(){ return{ overflow: "hidden", width: (this.options.fwidth*0.72)+"px", height: this.options.liheight+"px" } } }, inner:{ get(){ return{ overflow: "hidden", width: (this.options.fwidth*0.72)+"px", height: (this.options.liheight/2)+"px", } } }, label:{ get(){ return{ fontSize: this.options.labelFontSize+"px", fontWeight: this.options.labelFontWeight+"", color:this.options.labelFontColor+"" } } }, unit:{ get(){ return{ fontSize: this.options.unitFontSize+"px", fontWeight: this.options.unitFontWeight+"", color:this.options.unitFontColor+"" } } }, /* 滚动实现 */ pos () { return { transform: `translate(${this.xPos}px,${this.yPos}px)`, //x,y 轴坐标变化 transition: `all ${this.ease || 'ease-in'} ${this.delay}ms`, //动画过渡 overflow: 'hidden' } }, //一些默认属性,可通过外部传递变量更改 defaultOption () { return { rollSpeed: 2, //速度调节数字越大速度越快 limitMoveNum: 5, //启动无缝滚动最小数据数 hoverStop: true, //是否启用鼠标hover控制 direction: 1, // 0 往下 1 往上 singleHeight: 0, //单条数据高度有值hoverStop关闭 rollInterval: 1000, //单步停止等待时间 openRoll: true, switchDelay: 400, fwidth:360, fheight:320, //取消整个滚动区域的边框 // borderWidth:1, // borderColor:'#FFB42C', // borderCornerValue:5, liborderWidth:1, //信息条边框 liborderColor:'#FFB42C', liborderCornerValue:5, liheight:60, //每条信息展示的高度 backgroudColor:' #7B68EE', //信息条背景颜色 labelFontSize:15, //标签字体大小 labelFontWeight:'bold', labelFontColor:'red', unitFontSize:14, //内容字体大小 unitFontWeight:'bold', unitFontColor:'gray', // iconUrl:'img.png', //图标路径 iconWidth:50, iconHeight:50, limargin:2 //每个信息条之间的间隔 } }, options () { //我传递进来的值 return this.copyObj({}, this.defaultOption, this.objVal) }, openRoll () { //控制能否自动轮播 return this.options.openRoll }, scrollSwitch () { return this.data.length >= this.options.limitMoveNum }, hoverStopSwitch () { return this.options.hoverStop && this.openRoll && this.scrollSwitch }, realSingleStopHeight () { //传递进来的信息条的数目*每个信息条的高度 return (this.options.singleHeight)*(this.options.liheight+this.options.limargin+(this.options.liborderWidth)*2) }, rollSpeed () { let singleStep let rollSpeed = this.options.rollSpeed singleStep = this.realSingleStopHeight if (singleStep > 0 && singleStep % rollSpeed > 0) { console.error('如果设置了单步滚动,rollSpeed需是单步大小的约数,否则无法保证单步滚动结束的位置是否准确') } return rollSpeed } }, methods: { _typeof(){ return typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; }, copyObj() { if (!Array.isArray) { Array.isArray = function (arg) { return Object.prototype.toString.call(arg) === '[object Array]'; }; } var name = void 0, options = void 0, src = void 0, copy = void 0, copyIsArray = void 0, clone = void 0, i = 1, target = arguments[0] || {}, // 使用||运算符,排除隐式强制类型转换为false的数据类型 deep = false, len = arguments.length; if (typeof target === 'boolean') { deep = target; target = arguments[1] || {}; i++; } if ((typeof target === 'undefined' ? 'undefined' : this._typeof(target)) !== 'object' && typeof target !== 'function') { target = {}; } // 如果arguments.length === 1 或typeof arguments[0] === 'boolean',且存在arguments[1],则直接返回target对象 if (i === len) { return target; } for (; i < len; i++) { //所以如果源对象中数据类型为Undefined或Null那么就会跳过本次循环,接着循环下一个源对象 if ((options = arguments[i]) != null) { // 如果遇到源对象的数据类型为Boolean, Number for in循环会被跳过,不执行for in循环// src用于判断target对象是否存在name属性 for (name in options) { // src用于判断target对象是否存在name属性 src = target[name]; // 需要复制的属性当前源对象的name属性 copy = options[name]; // 判断copy是否是数组 copyIsArray = Array.isArray(copy); // 如果是深复制且copy是一个对象或数组则需要递归直到copy成为一个基本数据类型为止 if (deep && copy && ((typeof copy === 'undefined' ? 'undefined' : this._typeof(copy)) === 'object' || copyIsArray)) { if (copyIsArray) { copyIsArray = false; // 如果目标对象存在name属性且是一个数组 // 则使用目标对象的name属性,否则重新创建一个数组,用于复制 clone = src && Array.isArray(src) ? src : []; } else { // 如果目标对象存在name属性且是一个对象则使用目标对象的name属性,否则重新创建一个对象,用于复制 clone = src && (typeof src === 'undefined' ? 'undefined' : this._typeof(src)) === 'object' ? src : {}; } // 深复制,所以递归调用copyObject函数 // 返回值为target对象,即clone对象 // copy是一个源对象 target[name] = copyObj(deep, clone, copy); } else if (copy !== undefined) { // 浅复制,直接复制到target对象上 target[name] = copy; } } } } return target; }, arrayEqual(arr1, arr2) { if (arr1 === arr2) return true; if (arr1.length !== arr2.length) return false; for (var i = 0; i < arr1.length; ++i) { if (arr1[i] !== arr2[i]) return false; } return true; }, _cancle () { cancelAnimationFrame(this.reqFrame || '') }, //鼠标移入停止滚动 enter () { if (this.hoverStopSwitch) { this.isHover = true //关闭_move // 防止频频hover进出单步滚动,导致定时器乱掉 if (this.singleWaitTime) clearTimeout(this.singleWaitTime) this._cancle() } }, //鼠标离开开始滚动 leave () { if (this.hoverStopSwitch) { this.isHover = false //开启_move this._move() } }, _move () { // 鼠标移入时拦截_move() if (this.isHover) return this._cancle() //进入move立即先清除动画 防止频繁touchMove导致多动画同时进行 this.reqFrame = requestAnimationFrame( function () { const h = this.realBoxHeight / 2 //实际高度 const w = this.realBoxWidth / 2 //宽度 let { direction, rollInterval} = this.options let { rollSpeed } = this if (direction === 1) { // 上 if (Math.abs(this.yPos) >= h) { this.$emit('ScrollEnd') this.yPos = 0 } this.yPos -= rollSpeed } else if (direction === 0) { // 下 if (this.yPos >= 0) { this.$emit('ScrollEnd') this.yPos = h * -1 } this.yPos += rollSpeed } if (this.singleWaitTime) clearTimeout(this.singleWaitTime) if (!!this.realSingleStopHeight) { //是否启动了单行暂停配置 if (Math.abs(this.yPos) % this.realSingleStopHeight < rollSpeed) { // 符合条件暂停rollInterval this.singleWaitTime = setTimeout(() => { this._move() }, rollInterval) } else { this._move() } } else { this._move() } }.bind(this) ) }, _initMove () { this.$nextTick(() => { this.copyHtml = '' //清空copy this.height = this.$refs.wrap.offsetHeight this.width = this.$refs.wrap.offsetWidth let slotListWidth = this.$refs.slotList.offsetWidth const { switchDelay } = this.options const { openRoll} = this this.$refs.realBox.style.width = slotListWidth + 'px' this.realBoxWidth = slotListWidth if (!openRoll) { this.ease = 'linear' this.delay = switchDelay return } // 是否可以滚动判断 if (this.scrollSwitch) { let timer if (timer) clearTimeout(timer) this.copyHtml = this.$refs.slotList.innerHTML setTimeout(() => { this.realBoxHeight = this.$refs.realBox.offsetHeight + (this.$refs.wrapli.length>0?this.$refs.wrapli[0].offsetTop:0) this._move() }, 0); } else { this._cancle() this.yPos = this.xPos = 0 } }) }, changeStyle(){ } }, mounted () { this._initMove() }, watch: { data (newData, oldData) { //监听data是否有变更 if (!this.arrayEqual(newData, oldData)) { this._cancle() this._initMove() } } }, beforeDestroy () { this._cancle() } })
素材图片:
最终实现效果图如下:
isAbsloute