当前位置: 首页>前端>正文

elementplus走马灯改源码 vue走马灯组件

需求

  1. 背景:数据统计常需要实现内容定向移动的走马灯效果,便于用户实时观察数据变动。
  2. 目标:实现一个vue组件,可以上下左右实现走马灯效果,且走马灯元素可自定义。

技术选型

比较常用的方式是如下几种

  1. js定时移动元素
  2. 通过marquee标签实现
  3. css动画实现
    第一种方式生成的动画相比较而言比较消耗性能。第二种方式已经过时,marquee标签已被废弃,因此采用css动画来实现。

实现思路

  1. 为元素设置transform控制移动方向,再结合animation控制移动速度与循环次数。
  2. 初始化定时器以指定间隔时间添加移动元素,并删除“已过时”元素。

效果

效果视频

图中有些许抖动是生成gif并压缩导致的,真实效果不存在一点抖动情况。

主要代码

/**
 * <marquee-infinite v-model="${滑块数据项数组}" :totalInView="${跑马灯元素个数}" :elementSize="${元素高度|宽度}" :moveInMill="${元素切入切出总耗时ms}" direction="up(默认值)|down|left|right" :render="${跑马灯元素渲染函数}"></marquee-infinite>
 * example:<marquee-infinite v-model="datas" :totalInView="3" :elementSize="250" :moveInMill="15000" direction="down" :render="renderSlide"></marquee-infinite>
 */
Vue.component('marquee-infinite', {
    props: {
        dataArr: {//滑块数组,数据项,v-model=dataArr
          type: Array,
          require: true
        },
        totalInView: {//页面最多展示多少个完整元素
          type: Number,
          require: true
        },
        elementSize: {//每个滑动元素高度,横向移动则是宽度,单位px
            type: Number,
            require: true
        },
        moveInMill: {//单个元素移动完成所需时间,毫秒
            type: Number,
            require: true
        },
        render: {//滑块渲染函数,返回Html字符串
            type: Function,
            require: true
        },
        direction: {//移动方向 up|down|left|right,默认从下向上移动
            type: String,
            default: 'up'
        }
    },
    model: {
        prop: 'dataArr',
        event: 'change'
    },
    data(){
        return {
            slideRoadId: this.buildId(),//滑道id
            animationName: this.buildId(),//动画css id
            marqueeClsName: this.buildId(),//滑动元素class名
            runningElements: new Map(),//key: 在跑马灯的dom id,value: 已经移动了多少个元素高度
            intervals: null
        }
    },
    computed: {
        /** 滑道样式 */
        slideRoadStyle(){
            let size = this.totalInView * this.elementSize;
            let template = '';
            switch (this.direction) {
                case 'up':
                case 'down':
                    template = 'height: $size;position: relative;overflow: hidden;';
                    return template.replace('$size', size + 'px');
                case 'left':
                case 'right':
                    template = 'width: $size;min-height: $height;position: relative;overflow: hidden;';
                    return template.replace('$size', size + 'px')
                        .replace('$height', this.elementSize + 'px');
            }
            return '';
        }
    },
    methods: {
        /** 初始化跑马灯 */
        initMarquee(){
            //移动单位元素高度所需时间
            let periodInMill = (this.moveInMill / this.totalInView) * (this.totalInView + 1) / (this.totalInView + 2);
            this.intervals = window.setInterval(() => {
                //删除已过的
                //加入新元素
                //移动元素
            }, periodInMill);
        },
        /** 添加样式 */
        setStyle(){
            //动画样式
            let animationStyle = this.buildAnimationStyle();
            let marqueeStyle = this.buildMarqueeStyle();
            let totalStyle = animationStyle + '\n' + marqueeStyle;

            let style = document.createElement('style');
            style.type = 'text/css';
            style.rel = 'stylesheet';
            style.appendChild( document.createTextNode(totalStyle) );
            let head = document.getElementsByTagName('head')[0];
            head.appendChild(style);
        },
        /** 生成css移动动画 */
        buildAnimationStyle(){
            let template = '@keyframes $name {0% {transform: $begin;} 100% {transform: $end;}}';
            template = template.replace('$name', this.animationName);
            let beginPx = '';
            switch (this.direction) {
                case 'up':
                    beginPx = (this.totalInView + 1) * this.elementSize + 'px';
                    template = template.replace('$begin', 'translateY(' + beginPx + ')')
                        .replace('$end', 'translateY(-100%)');
                    break;
                case 'down':
                    beginPx = - (this.totalInView + 1) * this.elementSize + 'px';
                    template = template.replace('$begin', 'translateY(' + beginPx + ')')
                        .replace('$end', 'translateY(100%)');
                    break;
                case 'left':
                    beginPx = (this.totalInView + 1) * this.elementSize + 'px';
                    template = template.replace('$begin', 'translateX(' + beginPx + ')')
                        .replace('$end', 'translateX(-100%)');
                    break;
                case 'right':
                    beginPx = - (this.totalInView + 1) * this.elementSize + 'px';
                    template = template.replace('$begin', 'translateX(' + beginPx + ')')
                        .replace('$end', 'translateX(100%)');
                    break;
            }
            return template;
        },
        /** 滑动元素样式 */
        buildMarqueeStyle(){
            let shift = '';
            switch (this.direction) {
                case 'up':
                    shift = 'top:$top'.replace('$top', - this.elementSize + 'px');
                    break;
                case 'down':
                    shift = 'bottom:$bottom'.replace('$bottom', - this.elementSize + 'px');
                    break;
                case 'left':
                    shift = 'left:$left'.replace('$left', - this.elementSize + 'px');
                    break;
                case 'right':
                    shift = 'right:$right'.replace('$right', - this.elementSize + 'px');
                    break;
            }

            let totalTime = (this.moveInMill / this.totalInView) * (this.totalInView + 1);
            let style = '.$marqueeClsName{ animation: $animation $time linear 0s; position: absolute; $shift;}';
            style = style.replace('$marqueeClsName', this.marqueeClsName)
                .replace('$animation', this.animationName)
                .replace('$time', totalTime + 'ms')
                .replace('$shift', shift);
            return style;
        },
        //删除已过的
        removeOldMarquee(){
            if (this.runningElements.size > this.totalInView){
                var keys = this.runningElements.keys();
                var id = Array.from(keys)[0];
                if (id){
                    this.removeNode(id);
                    this.runningElements.delete(id);
                }
            }
        },
        //加入新元素
        addNewMarquee(){
            var temps = this.dataArr.splice(0, 1);
            //加入新元素
            if (temps.length > 0){
                //添加新元素
                var domId = this.buildId();
                var element = this.render(temps[0], domId);
                var div = document.createElement("div");
                div.id = domId;
                div.innerHTML = element;
                div.className = this.marqueeClsName;

                this.addChild(this.slideRoadId, div);

                this.runningElements.set(domId, 0);
                //更新到父组件
                this.$emit('change', this.dataArr);
            }
        },
        //移动元素
        moveMarqueeElement(){
            //上一位置有空位的上移
            var keyArr = Array.from( this.runningElements.keys() );
            var comparator = this.totalInView + 1;
            if (keyArr.length <= this.totalInView){
                comparator--;
            }
            for (let i = 0; i < keyArr.length; i++) {
                const key = keyArr[i];
                const offset = this.runningElements.get(key);
                const dom = document.getElementById(key);
                if (offset < comparator - i){
                    //移动
                    dom.style.setProperty('animation-play-state', 'running');
                    this.runningElements.set(keyArr[i], offset + 1);
                } else {
                    //停止移动
                    dom.style.setProperty('animation-play-state', 'paused');
                }
            }
        }
    },
    template: `<div :id="slideRoadId" :style="slideRoadStyle"></div>`
});

全部代码下载

使用demo

  1. 后端通过websocket定时往前端推送数据。
@ServerEndpoint("/ws/{sid}")
@Component
public class WebSocketServer {
    private static Map<String, Session> sessionMap = new ConcurrentHashMap<>(4);

    @OnOpen
    public void openSocket(Session session, @PathParam("sid") String sid){
        sessionMap.put(sid, session);
    }

    @OnClose
    public void closeSocket(@PathParam("sid") String sid){
        sessionMap.remove(sid);
    }

    public static void sendToAll(String msg){
        for (Session session : sessionMap.values()) {
            try {
                session.getBasicRemote().sendText(msg);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
public void sendMsg(){
        ScheduledExecutorService svr = Executors.newSingleThreadScheduledExecutor();
        svr.scheduleAtFixedRate(() -> {
            String msg = String.valueOf( System.currentTimeMillis() );
            WebSocketServer.sendToAll(msg);
        }, 0, 5, TimeUnit.SECONDS);
    }
  1. 前端与后端建立ws连接获取后端来的数据。
initWs: function () {
           this.ws = this.createConnection();
           //定义回调
           var that = this;
           this.ws.onmessage = function (evt) {
               that.datas.push( JSON.parse(evt.data) );
           };
           this.ws.onclose = function ( evt) {
               if (evt.code === 1000){//正常断开
                   return;
               }
               //重连
               window.setTimeout(function () {
                   that.initWs();
               }, 3000);
           };
       },
       createConnection: function(){
           var path = this.getWsPath() + '/' + this.wsId;
           return new WebSocket(path);
       },
       getWsPath: function () {
           var loc = window.location, new_uri;
           if (loc.protocol === "https:") {
               new_uri = "wss:";
           } else {
               new_uri = "ws:";
           }
           new_uri += "//" + loc.host + "/ws";
           return new_uri;
       }
  1. 引入组件,并传入数据。
<marquee-infinite v-model="datas" :totalInView="3" :elementSize="250" :moveInMill="15000" :render="renderSlide"></marquee-infinite>
renderSlide: function (data, domId) {
        var template = '<img class="marquee-img" src="$img" width="$size" height="$size"/>';
        
        return  template.replace('$img', data.img)
            .replaceAll('$size', this.elementSize);
    }



https://www.xamrdz.com/web/2h61938813.html

相关文章: