Vue Js无限循环选框(横条)

Posted

技术标签:

【中文标题】Vue Js无限循环选框(横条)【英文标题】:Vue Js infinite loop marquee (horizontal bar) 【发布时间】:2020-11-06 21:22:39 【问题描述】:

我需要关于如何从我的 vue js 代码构建无限循环选取框的建议,或者至少如何为这个特定项目使用 Vue Js 和 jQuery。

我的项目Fiddle如下:https://jsfiddle.net/jackbauer/xz5wv617/7

它正在工作,它的作用是创建一个加密货币水平条,但现在我需要知道如何让它从右向左滑动,无限循环,甚至可能添加导航控件。我尝试在 The Progressive javascript Framework (https://vuejs.org/) 中搜索类似的内容,但找不到与水平动画相关的任何内容。

我想要达到的结果和这个 jQuery 插件中的一样:https://www.jqueryscript.net/demo/jQuery-Plugin-For-Horizontal-Text-Scrolling-Simple-Marquee/

我什至尝试过使用 Vue js 和 jQuery,但没有成功:https://jsfiddle.net/jackbauer/xz5wv617/14

也许我应该写一段代码让 vue 完全执行,然后 jQuery 代码才会执行​​?

我所有的代码都在我刚刚发布的两个小提琴中可用,但以防万一,这是我的 javascript:

    // common number filters
Vue.filter( 'toFixed', ( num, asset ) => 
  if ( typeof asset === 'number' ) return Number( num ).toFixed( asset );
  return Number( num ).toFixed( ( asset === 'USDT' ) ? 3 : 8 );
);
Vue.filter( 'toMoney', num => 
  return Number( num ).toFixed( 0 ).replace( /./g, ( c, i, a ) => 
    return i && c !== "." && ( ( a.length - i ) % 3 === 0 ) ? ',' + c : c;
  );
);

// component for creating line chart
Vue.component( 'linechart', 
  props: 
    width:  type: Number, default: 400, required: true ,
    height:  type: Number, default: 40, required: true ,
    values:  type: Array, default: [], required: true ,
  ,
  data() 
    return  cx: 0, cy: 0 ;
  ,
  computed: 
    viewBox() 
      return '0 0 '+ this.width +' '+ this.height;
    ,
    chartPoints() 
      let data = this.getPoints();
      let last = data.length ? data[ data.length - 1 ] :  x: 0, y: 0 ;
      let list = data.map( d => ( d.x - 10 ) +','+ d.y );
      this.cx  = last.x - 5;
      this.cy  = last.y;
      return list.join( ' ' );
    ,
  ,
  methods: 
    getPoints() 
      this.width  = parseFloat( this.width ) || 0;
      this.height = parseFloat( this.height ) || 0;
      let min     = this.values.reduce( ( min, val ) => val < min ? val : min, this.values[ 0 ] );
      let max     = this.values.reduce( ( max, val ) => val > max ? val : max, this.values[ 0 ] );
      let len     = this.values.length;
      let half    = this.height / 2;
      let range   = ( max > min ) ? ( max - min ) : this.height;
      let gap     = ( len > 1 ) ? ( this.width / ( len - 1 ) ) : 1;
      let points  = [];

      for ( let i = 0; i < len; ++i ) 
        let d = this.values[ i ];
        let val = 2 * ( ( d - min ) / range - 0.5 );
        let x = i * gap;
        let y = -val * half * 0.8 + half;
        points.push(  x, y  );
      
      return points;
    
  ,
  template: `
  <svg :viewBox="viewBox" xmlns="http://www.w3.org/2000/svg">
    <polyline class="cryptocolor" fill="none" stroke="#fff" stroke- stroke-linecap="round" :points="chartPoints" />
    <circle class="cryptocolor" :cx="cx" :cy="cy" r="4" fill="#fff" stroke="none" />
  </svg>`,
);

// vue instance
new Vue(
  // mount point
  el: '#app',

  // app data
  data: 
    endpoint : 'wss://stream.binance.com:9443/ws/!ticker@arr',
    iconbase : 'https://raw.githubusercontent.com/rainner/binance-watch/master/public/images/icons/',
    cache    : ,             // coins data cache
    coins    : [],             // live coin list from api
    asset    : 'USDT',          // filter by base asset pair
    search   : '',             // filter by search string
    sort     : 'Price',  // sort by param
    order    : 'desc',         // sort order ( asc, desc )
    limit    : 50,             // limit list 
    status   : 0,              // socket status ( 0: closed, 1: open, 2: active, -1: error )
    sock     : null,           // socket inst
    cx       : 0,
    cy       : 0,
  ,

  // computed methods
  computed: 

    // process coins list
    coinsList() 
      let list = this.coins.slice();
      let search = this.search.replace( /[^\s\w\-\.]+/g, '' ).replace( /[\r\s\t\n]+/g, ' ' ).trim();

      if ( this.asset ) 
        list = list.filter( i => i.asset === this.asset );
      
      if ( search && search.length > 1 ) 
        let reg = new RegExp( '^('+ search +')', 'i' );
        list = list.filter( i => reg.test( i.token ) );
      
      if ( this.sort ) 
        list = this.sortList( list, this.sort, this.order );
      
      if ( this.limit ) 
        list = list.slice( 0, this.limit );
      
      return list;
    ,

    // show socket connection loader
    loaderVisible() 
      return ( this.status === 2 ) ? false : true;
    ,

    // sort-by label for buttons, etc
    sortLabel() 
      switch ( this.sort ) 

        case 'token'       :  return 'Token';
        case 'percent'     :  return 'Percent';
        case 'close'       :  return 'Price';
        case 'change'      :  return 'Change';
        case 'assetVolume' :  return 'Volume';
        case 'tokenVolume' :  return 'Volume';
        case 'trades'      :  return 'Trades';
        default            :  return 'Default';
      
    ,
  ,

  // custom methods
  methods: 

    // apply sorting and toggle order
    sortBy( key, order ) 
      if ( this.sort !== key )  this.order = order || 'asc'; 
      else  this.order = ( this.order === 'asc' ) ? 'desc' : 'asc'; 
      this.sort = key;
    ,

    // filter by asset
    filterAsset( asset ) 
      this.asset = String( asset || 'BTC' );
    ,

    // set list limit
    setLimit( limit ) 
      this.limit = parseInt( limit ) || 0;
    ,

    // on socket connected
    onSockOpen( e ) 
      this.status = 1; // open
      console.info( 'WebSocketInfo:', 'Connection open ('+ this.endpoint +').' );
    ,

    // on socket closed
    onSockClose( e ) 
      this.status = 0; // closed
      console.info( 'WebSocketInfo:', 'Connection closed ('+ this.endpoint +').' );
      setTimeout( this.sockInit, 10000 ); // try again
    ,

    // on socket error
    onSockError( err ) 
      this.status = -1; // error
      console.error( 'WebSocketError:', err.message || err );
      setTimeout( this.sockInit, 10000 ); // try again
    ,

    // process data from socket
    onSockData( e ) 
      let list = JSON.parse( e.data ) || [];

      for ( let item of list ) 
        // cleanup data for each coin
        let c = this.getCoinData( item );
        // keep to up 100 previous close prices in hostiry for each coin
        c.history = this.cache.hasOwnProperty( c.symbol ) ? this.cache[ c.symbol ].history : this.fakeHistory( c.close );
        if ( c.history.length > 100 ) c.history = c.history.slice( c.history.length - 100 );
        c.history.push( c.close );
        // add coin data to cache
        this.cache[ c.symbol ] = c;
      
      // convert cache object to final prices list for each symbol
      this.coins = Object.keys( this.cache ).map( s => this.cache[ s ] );
      this.status = 2; // active
    ,

    // start socket connection
    sockInit() 
      if ( this.status > 0 ) return;
      try 
        this.status = 0; // closed
        this.sock = new WebSocket( this.endpoint );
        this.sock.addEventListener( 'open', this.onSockOpen );
        this.sock.addEventListener( 'close', this.onSockClose );
        this.sock.addEventListener( 'error', this.onSockError );
        this.sock.addEventListener( 'message', this.onSockData );
      
      catch( err ) 
        console.error( 'WebSocketError:', err.message || err );
        this.status = -1; // error
        this.sock = null;
      
    ,

    // start socket connection
    sockClose() 
      if ( this.sock ) 
        this.sock.close();
      
    ,

    // come up with some fake history prices to fill in the initial line chart
    fakeHistory( close ) 
      let num = close * 0.0001; // faction of current price
      let min = -Math.abs( num );
      let max = Math.abs( num );
      let out = [];

      for ( let i = 0; i < 50; ++i ) 
        let rand = Math.random() * ( max - min ) + min;
        out.push( close + rand );
      
      return out;
    ,

    // finalize data for each coin from socket
    getCoinData( item ) 
      let reg         = /^([A-Z]+)(BTC|ETH|BNB|USDT|TUSD)$/;
      let symbol      = String( item.s ).replace( /[^\w\-]+/g, '' ).toUpperCase();
      let token       = symbol.replace( reg, '$1' );
      let asset       = symbol.replace( reg, '$2' );
      let name        = token;
      let pair        = token +'/'+ asset;
      let icon        = this.iconbase + token.toLowerCase() + '_.png';
      let open        = parseFloat( item.o );
      let high        = parseFloat( item.h );
      let low         = parseFloat( item.l );
      let close       = parseFloat( item.c );
      let change      = parseFloat( item.p );
      let percent     = parseFloat( item.P );
      let trades      = parseInt( item.n );
      let tokenVolume = Math.round( item.v );
      let assetVolume = Math.round( item.q );
      let sign        = ( percent >= 0 ) ? '+' : '';
      let arrow       = ( percent >= 0 ) ? '▲' : '▼';
      let info        = [ pair, close.toFixed( 8 ), '(', arrow, sign + percent.toFixed( 2 ) +'%', '|', sign + change.toFixed( 8 ), ')' ].join( ' ' );
      let style       = '';

      if ( percent > 0 ) style = 'cryptogain';
      if ( percent < 0 ) style = 'cryptoloss';

      return  symbol, token, asset, name, pair, icon, open, high, low, close, change, percent, trades, tokenVolume, assetVolume, sign, arrow, style, info ;
    ,
    // sort an array by key and order
    sortList( list, key, order ) 
      return list.sort( ( a, b ) => 
        let _a = a[ key ];
        let _b = b[ key ];

        if ( _a && _b ) 
          _a = ( typeof _a === 'string' ) ? _a.toUpperCase() : _a;
          _b = ( typeof _b === 'string' ) ? _b.toUpperCase() : _b;

          if ( order === 'asc' ) 
            if ( _a < _b ) return -1;
            if ( _a > _b ) return 1;
          
          if ( order === 'desc' ) 
            if ( _a > _b ) return -1;
            if ( _a < _b ) return 1;
          
        
        return 0;
      );
    ,
  ,
    // app mounted
  mounted() 
    this.sockInit();
  ,

  // app destroyed
  destroyed() 
    this.sockClose();
  
);



// BEGIN SLIDER

// ---------------------------------
// ---------- SimpleMarquee ----------
// ---------------------------------
//Copyright (C) 2016  F*** Valle 
//An easy to implement marquee plugin. I know its easy because even I can use it.
//Forked from: https://github.com/conradfeyt/Simple-Marquee
//Re-Written by: F*** Valle (www.f***-valle.com) (www.obliviocompany.com)
// 
// ------------------------
// Structure //
//
//  *********************************** - marque-container - *************************************
//  *                                                                                            *
//  *   ******************************* ******************************************************   *
//  *   *                             * *                                                    *   *
//  *   * - marquee-content-sibling - * *                 - marquee-content -                *   *
//  *   *                             * *                                                    *   *
//  *   ******************************* ******************************************************   *
//  *                                                                                            *
//  **********************************************************************************************
//
//// Usage //
//  
//    Only need to call the createMarquee() function,
//    if desired, pass through the following paramaters:
//
//    $1 duration:                   controls the speed at which the marquee moves
//
//    $2 padding:                    right margin between consecutive marquees. 
//
//    $3 marquee_class:             the actual div or span that will be used to create the marquee - 
//                                   multiple marquee items may be created using this item's content. 
//                                   This item will be removed from the dom
//
//    $4 container_class:           the container div in which the marquee content will animate. 
//
//    $5 marquee-content-sibling :   (optional argument) a sibling item to the marqueed item  that 
//                                   affects the end point position and available space inside the 
//                                   container. 
//
//    $6 hover:                     Boolean to indicate whether pause on hover should is required. 
;(function ($, window, document, undefined)
    var pluginName = 'SimpleMarquee';

    function Plugin (element, options) 
        this.element = element;
        this._name = pluginName;
        this._defaults = $.fn.SimpleMarquee.defaults;
        this.settings = $.extend( , this._defaults, options );
        this.marqueeSpawned = [];
        this.marqueeHovered = false;
        this.documentHasFocus = false;        
        //
        this.counter = 0;

        this.timeLeft = 0;
        this.currentPos = 0;
        this.distanceLeft = 0;
        this.totalDistance = 0;
        this.contentWidth = 0;
        this.endPoint = 0;
        this.duration = 0;
        this.hovered = false;
        this.padding = 0;
        
        
        this.init();
    
    function marqueeObj(newElement)
        this.el=newElement;
        this.counter=0;
        this.name="";
        this.timeTop=0;
        this.currentPos=0;
        this.distanceTop=0;
        this.totalDistance=0;
        this.contentWidth=0;
        this.endPoint=0;
        this.duration=0;
        this.hovered=false;
        this.padding=0;
    
    //methods for plugin
    $.extend(Plugin.prototype, 

        // Initialization logic
        init: function () 
            this.buildCache();
            this.bindEvents();
            var config = this.settings;
            //init marquee
            if($(config.marquee_class).width() == 0)
                console.error('FATAL: marquee css or children css not correct. Width is either set to 0 or the element is collapsing. Make sure overflow is set on the marquee, and the children are postitioned relatively');
                return;
            
    
            if(typeof $(config.marquee_class) === 'undefined')
                console.error('FATAL: marquee class not valid');
                return;
            
    
            if(typeof $(config.container_class) === 'undefined')
                console.error('FATAL: marquee container class not valid');
                return;
            
    
            if(config.sibling_class != 0 && typeof $(config.sibling_class) === 'undefined')
                console.error('FATAL: sibling class container class not valid');
                return;
            
            
                if (config.autostart)
                
                    this.documentHasFocus = true;
                
            //create the Marquee
            this.createMarquee();
        ,

        // Remove plugin instance completely
        destroy: function() 
            this.unbindEvents();
            this.$element.removeData();
        ,

        // Cache DOM nodes for performance
        buildCache: function () 
            this.$element = $(this.element);
        ,

        // Bind events that trigger methods
        bindEvents: function() 
            var plugin = this;
            $(window).on('focus',function()
                plugin.documentHasFocus = true;
                for (var key in plugin.marqueeSpawned)
                  plugin.marqueeManager(plugin.marqueeSpawned[key]);   
                 
            );
            $(window).on('blur',function()
                plugin.documentHasFocus = false;
                for (var key in plugin.marqueeSpawned)
                    plugin.marqueeSpawned[key].el.clearQueue().stop(); 
                    plugin.marqueeSpawned[key].hovered = true;
                
            );

        ,

        // Unbind events that trigger methods
        unbindEvents: function() 
            $(window).off('blur focus');
        ,
        getPosition: function(elName)
            this.currentPos = parseInt($(elName).css('left'));
            return this.currentPos;
        ,
        createMarquee: function()
            var plugin = this;
            var config = plugin.settings;
            var marqueeContent =  $(config.marquee_class).html();
            var containerWidth = $(config.container_class).width();
            var contentWidth = $(config.marquee_class).width();
            
            var widthToIgnore = 0;
            if (config.sibling_class != 0) 
                widthToIgnore = $(config.sibling_class).width();
             
            
            var spawnAmount = Math.ceil(containerWidth / contentWidth);
            
            $(config.marquee_class).remove();

            if(spawnAmount<=2)
                spawnAmount = 3;
             else 
              spawnAmount++;
            

            var totalContentWidth = (contentWidth + config.padding)*spawnAmount;

            var endPoint = -(totalContentWidth - containerWidth);

            var totalDistance =  containerWidth - endPoint;
            
            
            
            
            for (var i = 0; i < spawnAmount; i++) 
                
                var newElement = false;
                
                if(config.hover == true)

                  
                  newElement = $('<div class="marquee-' + (i+1) + '">' + marqueeContent + '</div>')        
                  .mouseenter(function() 


                    if ((plugin.documentHasFocus == true) && (plugin.marqueeHovered == false))
                      plugin.marqueeHovered = true;

                      for (var key in plugin.marqueeSpawned)
                        plugin.marqueeSpawned[key].el.clearQueue().stop(); 
                        plugin.marqueeSpawned[key].hovered = true;
                      
                      

                    

                  )
                  .mouseleave(function() 


                      if ((plugin.documentHasFocus == true) && (plugin.marqueeHovered == true))

                        for (var key in plugin.marqueeSpawned)
                          plugin.marqueeManager(plugin.marqueeSpawned[key]);   
                         

                        plugin.marqueeHovered = false;
                       
                  );

                 else 

                  newElement = $('<div class="marquee-' + (i+1) + '">' + marqueeContent + '</div>') ;   

                

                plugin.marqueeSpawned[i] = new marqueeObj(newElement);

                $(config.container_class).append(newElement);

                plugin.marqueeSpawned[i].currentPos = (widthToIgnore + (contentWidth*i))+(config.padding*i);  //initial positioning
                plugin.marqueeSpawned[i].name = '.marquee-'+(i+1); 

                plugin.marqueeSpawned[i].totalDistance = totalDistance;  
                plugin.marqueeSpawned[i].containerWidth = containerWidth;  
                plugin.marqueeSpawned[i].contentWidth = contentWidth;  
                plugin.marqueeSpawned[i].endPoint = endPoint;  
                plugin.marqueeSpawned[i].duration = config.duration;  
                plugin.marqueeSpawned[i].padding = config.padding;  

                plugin.marqueeSpawned[i].el.css('left', plugin.marqueeSpawned[i].currentPos+config.padding +'px'); //setting left according to postition

                 if (plugin.documentHasFocus == true)
                  plugin.marqueeManager(plugin.marqueeSpawned[i]);
                

            
            //end for
            
            if(document.hasFocus())
                 plugin.documentHasFocus = true;
            else
                plugin.documentHasFocus = false;
            
            
        ,
        marqueeManager: function(marqueed_el)
            var plugin = this;
            var elName = marqueed_el.name;
            if (marqueed_el.hovered == false)  

                if (marqueed_el.counter > 0)   //this is not the first loop
                  
                      marqueed_el.timeLeft = marqueed_el.duration;
                      marqueed_el.el.css('left', marqueed_el.containerWidth +'px'); //setting margin 
                      marqueed_el.currentPos = marqueed_el.containerWidth; 
                      marqueed_el.distanceLeft = marqueed_el.totalDistance - (marqueed_el.containerWidth - plugin.getPosition(elName));

                 else     // this is the first loop
                  
                  marqueed_el.timeLeft = (((marqueed_el.totalDistance - (marqueed_el.containerWidth - plugin.getPosition(elName)))/ marqueed_el.totalDistance)) * marqueed_el.duration;
                

             else 
                  marqueed_el.hovered = false;
                  marqueed_el.currentPos = parseInt(marqueed_el.el.css('left'));
                  marqueed_el.distanceLeft = marqueed_el.totalDistance - (marqueed_el.containerWidth - plugin.getPosition(elName));
                  marqueed_el.timeLeft = (((marqueed_el.totalDistance - (marqueed_el.containerWidth - marqueed_el.currentPos))/ marqueed_el.totalDistance)) * marqueed_el.duration;
            

            plugin.marqueeAnim(marqueed_el);
        ,
        marqueeAnim: function(marqueeObject)
            var plugin = this;
            marqueeObject.counter++;
            marqueeObject.el.clearQueue().animate(
                    'left': marqueeObject.endPoint+'px', 
                    marqueeObject.timeLeft, 
                    'linear', 
                    function()
                        plugin.marqueeManager(marqueeObject);
                    );
        ,
        callback: function() 
            // Cache onComplete option
            var onComplete = this.settings.onComplete;

            if ( typeof onComplete === 'function' ) 
                onComplete.call(this.element);
            
        

    );
    //end methods for plugin
    
    $.fn.SimpleMarquee = function (options) 
        this.each(function() 
            if ( !$.data( this, "plugin_" + pluginName ) ) 
                $.data( this, "plugin_" + pluginName, new Plugin( this, options ) );
            
        );
        return this;
    ;
    $.fn.SimpleMarquee.defaults = 
        autostart: true,
            property: 'value',
            onComplete: null,
            duration: 20000,
            padding: 10,
            marquee_class: '.marquee',
            container_class: '.simple-marquee-container',
            sibling_class: 0,
            hover: true
    ;
    
)( jQuery, window, document );

如果有人能在这里给我指点方向,我将不胜感激。提前致谢。

【问题讨论】:

【参考方案1】:

这是我能想到的最好的:

Vue.filter('toFixed', (num, asset) => 
  if (typeof asset === 'number') return Number(num).toFixed(asset);
  return Number(num).toFixed((asset === 'USDT') ? 3 : 8);
);
Vue.filter('toMoney', num => 
  return Number(num).toFixed(0).replace(/./g, (c, i, a) => 
    return i && c !== "." && ((a.length - i) % 3 === 0) ? ',' + c : c;
  );
);

Vue.component('linechart', 
  props: 
    width: 
      type: Number,
      default: 400,
      required: true
    ,
    height: 
      type: Number,
      default: 40,
      required: true
    ,
    values: 
      type: Array,
      default: [],
      required: true
    ,
  ,
  data() 
    return 
      cx: 0,
      cy: 0
    ;
  ,
  computed: 
    viewBox() 
      return '0 0 ' + this.width + ' ' + this.height;
    ,
    chartPoints() 
      let data = this.getPoints();
      let last = data.length ? data[data.length - 1] : 
        x: 0,
        y: 0
      ;
      let list = data.map(d => (d.x - 10) + ',' + d.y);
      this.cx = last.x - 5;
      this.cy = last.y;
      return list.join(' ');
    ,
  ,
  methods: 
    getPoints() 
      this.width = parseFloat(this.width) || 0;
      this.height = parseFloat(this.height) || 0;
      let min = this.values.reduce((min, val) => val < min ? val : min, this.values[0]);
      let max = this.values.reduce((max, val) => val > max ? val : max, this.values[0]);
      let len = this.values.length;
      let half = this.height / 2;
      let range = (max > min) ? (max - min) : this.height;
      let gap = (len > 1) ? (this.width / (len - 1)) : 1;
      let points = [];

      for (let i = 0; i < len; ++i) 
        let d = this.values[i];
        let val = 2 * ((d - min) / range - 0.5);
        let x = i * gap;
        let y = -val * half * 0.8 + half;
        points.push(
          x,
          y
        );
      
      return points;
    
  ,
  template: `
  <svg :viewBox="viewBox" xmlns="http://www.w3.org/2000/svg">
    <polyline class="cryptocolor" fill="none" stroke="#fff" stroke- stroke-linecap="round" :points="chartPoints" />
    <circle class="cryptocolor" :cx="cx" :cy="cy" r="4" fill="#fff" stroke="none" />
  </svg>`,
);

new Vue(
  el: '#app',

  data: 
    endpoint: 'wss://stream.binance.com:9443/ws/!ticker@arr',
    iconbase: 'https://raw.githubusercontent.com/rainner/binance-watch/master/public/images/icons/',
    cache: , // coins data cache
    coins: [], // live coin list from api
    asset: 'USDT', // filter by base asset pair
    search: '', // filter by search string
    sort: 'Price', // sort by param
    order: 'desc', // sort order ( asc, desc )
    limit: 50, // limit list 
    status: 0, // socket status ( 0: closed, 1: open, 2: active, -1: error )
    sock: null, // socket inst
    cx: 0,
    cy: 0,
  ,

  computed: 

    coinsList() 
      let list = this.coins.slice();
      let search = this.search.replace(/[^\s\w\-\.]+/g, '').replace(/[\r\s\t\n]+/g, ' ').trim();

      if (this.asset) 
        list = list.filter(i => i.asset === this.asset);
      
      if (search && search.length > 1) 
        let reg = new RegExp('^(' + search + ')', 'i');
        list = list.filter(i => reg.test(i.token));
      
      if (this.sort) 
        list = this.sortList(list, this.sort, this.order);
      
      if (this.limit) 
        list = list.slice(0, this.limit);
      
      return list;
    ,

    loaderVisible() 
      return (this.status === 2) ? false : true;
    ,

    sortLabel() 
      switch (this.sort) 

        case 'token':
          return 'Token';
        case 'percent':
          return 'Percent';
        case 'close':
          return 'Price';
        case 'change':
          return 'Change';
        case 'assetVolume':
          return 'Volume';
        case 'tokenVolume':
          return 'Volume';
        case 'trades':
          return 'Trades';
        default:
          return 'Default';
      
    ,
  ,

  methods: 

    sortBy(key, order) 
      if (this.sort !== key) 
        this.order = order || 'asc';
       else 
        this.order = (this.order === 'asc') ? 'desc' : 'asc';
      
      this.sort = key;
    ,
    filterAsset(asset) 
      this.asset = String(asset || 'BTC');
    ,

    setLimit(limit) 
      this.limit = parseInt(limit) || 0;
    ,
    onSockOpen(e) 
      this.status = 1; // open
      console.info('WebSocketInfo:', 'Connection open (' + this.endpoint + ').');
    ,
    onSockClose(e) 
      this.status = 0; // closed
      console.info('WebSocketInfo:', 'Connection closed (' + this.endpoint + ').');
      setTimeout(this.sockInit, 10000); // try again
    ,

    onSockError(err) 
      this.status = -1; // error
      console.error('WebSocketError:', err.message || err);
      setTimeout(this.sockInit, 10000); // try again
    ,

    onSockData(e) 
      let list = JSON.parse(e.data) || [];

      for (let item of list) 
        let c = this.getCoinData(item);
        c.history = this.cache.hasOwnProperty(c.symbol) ? this.cache[c.symbol].history : this.fakeHistory(c.close);
        if (c.history.length > 100) c.history = c.history.slice(c.history.length - 100);
        c.history.push(c.close);
        this.cache[c.symbol] = c;
      
      this.coins = Object.keys(this.cache).map(s => this.cache[s]);
      this.status = 2; // active
    ,

    sockInit() 
      if (this.status > 0) return;
      try 
        this.status = 0; // closed
        this.sock = new WebSocket(this.endpoint);
        this.sock.addEventListener('open', this.onSockOpen);
        this.sock.addEventListener('close', this.onSockClose);
        this.sock.addEventListener('error', this.onSockError);
        this.sock.addEventListener('message', this.onSockData);
       catch (err) 
        console.error('WebSocketError:', err.message || err);
        this.status = -1; // error
        this.sock = null;
      
    ,
    sockClose() 
      if (this.sock) 
        this.sock.close();
      
    ,

    fakeHistory(close) 
      let num = close * 0.0001; // faction of current price
      let min = -Math.abs(num);
      let max = Math.abs(num);
      let out = [];

      for (let i = 0; i < 50; ++i) 
        let rand = Math.random() * (max - min) + min;
        out.push(close + rand);
      
      return out;
    ,

    getCoinData(item) 
      let reg = /^([A-Z]+)(BTC|ETH|BNB|USDT|TUSD)$/;
      let symbol = String(item.s).replace(/[^\w\-]+/g, '').toUpperCase();
      let token = symbol.replace(reg, '$1');
      let asset = symbol.replace(reg, '$2');
      let name = token;
      let pair = token + '/' + asset;
      let icon = this.iconbase + token.toLowerCase() + '_.png';
      let open = parseFloat(item.o);
      let high = parseFloat(item.h);
      let low = parseFloat(item.l);
      let close = parseFloat(item.c);
      let change = parseFloat(item.p);
      let percent = parseFloat(item.P);
      let trades = parseInt(item.n);
      let tokenVolume = Math.round(item.v);
      let assetVolume = Math.round(item.q);
      let sign = (percent >= 0) ? '+' : '';
      let arrow = (percent >= 0) ? '▲' : '▼';
      let info = [pair, close.toFixed(8), '(', arrow, sign + percent.toFixed(2) + '%', '|', sign + change.toFixed(8), ')'].join(' ');
      let style = '';

      if (percent > 0) style = 'cryptogain';
      if (percent < 0) style = 'cryptoloss';

      return 
        symbol,
        token,
        asset,
        name,
        pair,
        icon,
        open,
        high,
        low,
        close,
        change,
        percent,
        trades,
        tokenVolume,
        assetVolume,
        sign,
        arrow,
        style,
        info
      ;
    ,

    sortList(list, key, order) 
      return list.sort((a, b) => 
        let _a = a[key];
        let _b = b[key];

        if (_a && _b) 
          _a = (typeof _a === 'string') ? _a.toUpperCase() : _a;
          _b = (typeof _b === 'string') ? _b.toUpperCase() : _b;

          if (order === 'asc') 
            if (_a < _b) return -1;
            if (_a > _b) return 1;
          
          if (order === 'desc') 
            if (_a > _b) return -1;
            if (_a < _b) return 1;
          
        
        return 0;
      );
    ,
  ,

  mounted() 
    this.sockInit();

    const gridList = document.querySelector('.cryptomain-grid-list');
    let gridListWidth = 0;
    const gridListInterval = setInterval(() => 

      if (gridList.children.length > 0) 

        for (let i of gridList.children) 
          gridListWidth += 311.2;
        

        const cssAnimation = document.createElement('style');
        cssAnimation.type = 'text/css';
        const rules = document.createTextNode(`@keyframes marquee 
    0%  transform: translateX(100%); 
    100%  transform: translateX($-Math.abs(gridListWidth)px); 
`);
        cssAnimation.appendChild(rules);
        document.getElementsByTagName("head")[0].appendChild(cssAnimation);
        clearInterval(gridListInterval);
      
    , 1)
  ,

  destroyed() 
    this.sockClose();
  
);
.vw-sirat-search-icon i 
  color: #fff;


.placeholdercrypto::placeholder 
  /* Chrome, Firefox, Opera, Safari 10.1+ */
  color: #1567d8;
  opacity: 1;
  /* Firefox */


.placeholdercrypto:-ms-input-placeholder 
  /* Internet Explorer 10-11 */
  color: #1567d8;


.placeholdercrypto::-ms-input-placeholder 
  /* Microsoft Edge */
  color: #1567d8;



/* CSS Document */

.cryptoif-small 
  display: none;


@media only screen and (min-width: 420px) 
  .cryptoif-small 
    display: initial;
  


.cryptoif-medium 
  display: none;


@media only screen and (min-width: 720px) 
  .cryptoif-medium 
    display: initial;
  


.cryptoif-large 
  display: none;


@media only screen and (min-width: 1200px) 
  .cryptoif-large 
    display: initial;
  


.cryptohidden,
[hidden],
[v-cloak] 
  display: none;


.cryptodisabled,
[disabled] 
  pointer-events: none;
  opacity: 0.5;


.cryptocard 
  padding: 1em;
  background-color: #1e2126;
  border-radius: 4px;
  -moz-border-radius: 4px;
  -webkit-border-radius: 4px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);


.cryptopush-top 
  margin-top: 1em;


.cryptopush-right 
  margin-right: 1em;


.cryptopush-bottom 
  margin-bottom: 1em;


.cryptopush-left 
  margin-left: 1em;


.cryptopush-all 
  margin: 1em;


.cryptopad-top 
  padding-top: 1em;


.cryptopad-right 
  padding-right: 1em;


.cryptopad-bottom 
  padding-bottom: 1em;


.cryptopad-left 
  padding-left: 1em;


.cryptopad-all 
  padding: 1em;


.cryptoborder-top 
  border-top: 2px solid rgba(255, 255, 255, 0.04);


.cryptoborder-right 
  border-right: 2px solid rgba(255, 255, 255, 0.04);


.cryptoborder-bottom 
  border-bottom: 2px solid rgba(255, 255, 255, 0.04);


.cryptoborder-left 
  border-left: 2px solid rgba(255, 255, 255, 0.04);


.cryptoflex-row 
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;


.cryptoflex-wrap 
  flex-wrap: wrap;


.cryptoflex-left 
  justify-content: flex-start;


.cryptoflex-center 
  justify-content: center;


.cryptoflex-right 
  justify-content: flex-end;


.cryptoflex-space 
  justify-content: space-between;


.cryptoflex-around 
  justify-content: space-around;


.cryptoflex-top 
  align-items: flex-start;


.cryptoflex-middle 
  align-items: center;


.cryptoflex-bottom 
  align-items: flex-end;


.cryptoflex-1 
  flex: 1;


.cryptoflex-2 
  flex: 2;


.cryptoflex-3 
  flex: 3;


.cryptoflex-4 
  flex: 4;


.cryptoflex-5 
  flex: 5;


.cryptotext-left 
  text-align: left;


.cryptotext-right 
  text-align: right;


.cryptotext-center 
  text-align: center;


.cryptotext-justify 
  text-align: justify;


.cryptotext-uppercase 
  text-transform: uppercase;


.cryptotext-lowercase 
  text-transform: lowercase;


.cryptotext-capitalize 
  text-transform: capitalize;


.cryptotext-underline 
  text-decoration: underline;


.cryptotext-striked 
  text-decoration: line-through;


.cryptotext-italic 
  font-style: italic;


.cryptotext-bold 
  font-weight: bold;


@media only screen and (max-width: 719px) 
  .cryptoif-mediumtopo 
    display: none;
  
  .cryptoif-mediumtopo 
    display: block;
  
  .cryptodropdown>ul 
    left: 0;
  
  .cryptodropdown>ul 
    right: inherit;
  
  .cryptotext-nowrap.cryptotext-condense.cryptoshadow-text 
    font-size: 5vw;
  


@media only screen and (min-width: 720px) 
  .cryptoif-mediumtopo 
    display: block;
  
  .cryptoif-mediumtopo 
    display: none;
  
  .cryptodropdown>ul 
    right: 0;
  
  .cryptodropdown>ul 
    left: inherit;
  
  .cryptotext-nowrap.cryptotext-condense.cryptoshadow-text 
    font-size: 3vw;
  


.cryptotext-nowrap.cryptotext-condense.cryptoshadow-text 
  font-family: fantasy;
  text-transform: uppercase;
  white-space: normal;
  text-align: center;
  color: #f3f3f3;
  text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.2), 1px 0px 2px rgba(0, 0, 0, 0.2), -1px 0px 2px rgba(0, 0, 0, 0.2), -1px -1px 2px rgba(0, 0, 0, 0.2), 1px 1px 2px rgba(0, 0, 0, 0.2), 0px 1px 2px rgba(0, 0, 0, 0.2);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;



/* Manter o nowrap, por causa dos botões */

.cryptotext-nowrap 
  white-space: nowrap;


.cryptotext-clip 
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: #34b5eb;
  text-shadow: none;
  font-weight: bolder;


.cryptotext-primary 
  color: orange;
  font-size: 18px;
  margin-block-start: 0.1em;
  margin-block-end: 0.1em;
  text-shadow: none;


.cryptotext-secondary 
  color: #20acea;


.cryptotext-grey 
  color: #5c6776;


.cryptotext-bright 
  color: #34b5eb;
  /*color: #f0f0f0;
     color: #1567d8; */
  margin-block-start: 0.1em;
  margin-block-end: 0.1em;
  text-shadow: none;


.cryptotext-faded 
  color: #004eb9;
  opacity: 0.5;


.cryptotext-big 
  font-size: 120%;
  line-height: 1.212em;


.cryptotext-small 
  font-size: 70%;
  line-height: 1.14em;


.cryptotext-condense 
  letter-spacing: -1px;


.cryptoshadow-box 
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);


.cryptoshadow-text 
  text-shadow: 0px 1px 0px rgba(0, 0, 0, 0.3), 1px 0px 0px rgba(0, 0, 0, 0.3);


.cryptoform-input 
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  align-items: center;
  padding: 0.7em 1em;
  color: #f0f0f0;
  background-color: #1567d8;
  border-radius: 100px;
  -moz-border-radius: 100px;
  -webkit-border-radius: 100px;


.cryptoform-input.dark 
  background-color: #000;


.cryptoform-input>input 
  width: auto;
  border-radius: 15px;
  -moz-border-radius: 15px;
  -webkit-border-radius: 15px;
  border: 0;
  padding: 5px;
  color: #1567d8;


@keyframes dropdownShow 
  0% 
    transform: translateY(30px);
    opacity: 0;
  
  100% 
    transform: translateY(0);
    opacity: 1;
  


.cryptodropdown 
  display: block;
  position: relative;
  cursor: pointer;


.cryptodropdown>ul 
  z-index: 999;
  display: none;
  list-style: none;
  position: absolute;
  transition: none;
  animation: dropdownShow 300ms cubic-bezier(0.215, 0.61, 0.355, 1) forwards;
  top: 50%;
  min-width: 200px;
  max-width: 400px;
  padding: 0.5em 0;
  background-color: rgba(21, 103, 216, 0.19);
  border-radius: 4px;
  -moz-border-radius: 4px;
  -webkit-border-radius: 4px;
  -webkit-backdrop-filter: blur(0.18em);
  backdrop-filter: blur(0.18em);


.cryptodropdown>ul>li 
  display: block;
  padding: 0.5em 1em;
  background-color: rgba(0, 0, 0, 0);
  font-weight: bolder;
  color: #fff;
  text-shadow: #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px, #004eb9 0px 0px 1px;
  -webkit-font-smoothing: antialiased.;
  cursor: pointer;


.cryptodropdown .cryptotext-faded 
  opacity: 1;
  color: #fff;
  font-weight: bolder


.cryptodropdown>ul>li+li 
  border-top: 2px solid rgba(255, 255, 255, 0.50);


.cryptodropdown>ul>li:hover 
  background-color: rgba(0, 0, 0, 0.1);


.cryptodropdown:hover>ul,
.cryptodropdown:active>ul 
  display: block;


.cryptoheader-wrap 
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  background-color: #1e2126;
  background-image: radial-gradient(ellipse at top, rgba(255, 255, 255, 0.1) 0%, transparent 60%);
  box-shadow: 0 1px 8px rgba(0, 0, 0, 0.6);
  z-index: 999;


.cryptoheader-wrap .cryptoheader-row 
  height: 4em;
  padding: 1em;


.cryptoheader-wrap .cryptoheader-row .cryptodropdown 
  margin-left: 0.4em;


.cryptomain-wrap 
  position: relative;
  padding: 1px;
  overflow-x: hidden;


.cryptomain-wrap .cryptomain-grid-list 
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  grid-gap: 0.7em;
  grid-auto-flow: column;
  padding-bottom: 4px;
  transform: translateX(100%);
  animation: marquee 210s infinite linear;


.cryptomain-grid-list:hover 
  animation-play-state: paused;


.cryptomain-wrap .cryptomain-grid-list::-webkit-scrollbar 
  width: 12px;


.cryptomain-wrap .cryptomain-grid-list::-webkit-scrollbar-track 
  -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
  border-radius: 10px;


.cryptomain-wrap .cryptomain-grid-list::-webkit-scrollbar-thumb 
  border-radius: 10px;
  -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5);


@media only screen and (min-width: 420px) 
  .cryptomain-wrap .cryptomain-grid-list 
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  


.cryptomain-wrap .cryptomain-grid-list .cryptomain-grid-item .cryptomain-grid-chart 
  background-color: #f3f3f3;
  position: absolute;
  width: 100%;
  height: 100%;
  z-index: -2;


.cryptomain-wrap .cryptomain-grid-list .cryptomain-grid-item 
  background-color: transparent;
  position: relative;
  border-radius: 4px;
  -moz-border-radius: 4px;
  -webkit-border-radius: 4px;
  border-left: solid 7px #1567d8;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);


.cryptomain-wrap .cryptomain-grid-list .cryptomain-grid-item.cryptogain 
  background-color: transparent;



/* Cor do linha de ganho */

.cryptomain-wrap .cryptomain-grid-list .cryptomain-grid-item.cryptogain polyline.cryptocolor 
  stroke: rgba(50, 205, 50, 0.07);


.cryptomain-wrap .cryptomain-grid-list .cryptomain-grid-item.cryptogain circle.cryptocolor 
  fill: #32cd32;


.cryptomain-wrap .cryptomain-grid-list .cryptomain-grid-item.cryptogain .cryptocolor 
  color: #32cd32;


.cryptomain-wrap .cryptomain-grid-list .cryptomain-grid-item.cryptoloss 
  background-color: transparent;



/* Cor do linha de perda */

.cryptomain-wrap .cryptomain-grid-list .cryptomain-grid-item.cryptoloss polyline.cryptocolor 
  stroke: rgb(220, 20, 60, 0.04);


.cryptomain-wrap .cryptomain-grid-list .cryptomain-grid-item.cryptoloss circle.cryptocolor 
  fill: #dc143c;



/* Cor do texto de perda */

.cryptomain-wrap .cryptomain-grid-list .cryptomain-grid-item.cryptoloss .cryptocolor 
  color: #dc143c;


.cryptomain-wrap .cryptomain-grid-list .cryptomain-grid-item .cryptomain-grid-info 
  padding: 0.7em 1em;


.cryptomain-wrap .cryptomain-grid-list .cryptomain-grid-item .cryptomain-grid-info img 
  width: auto;
  height: 16px;


@media only screen and (min-width: 420px) 
  .cryptomain-wrap .cryptomain-grid-list .cryptomain-grid-item .cryptomain-grid-info img 
    height: 20px;
  


@media only screen and (min-width: 720px) 
  .cryptomain-wrap .cryptomain-grid-list .cryptomain-grid-item .cryptomain-grid-info img 
    height: 32px;
  


.cryptoloader-wrap 
  display: none;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  position: relative;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.8);
  text-align: center;
  z-index: 9999;


.cryptoloader-wrap.cryptovisible 
  display: flex;


.cryptoloader-wrap .cryptoloader-content 
  padding: 1em 2em;
  background-color: #1e2126;
  border-radius: 4px;
  -moz-border-radius: 4px;
  -webkit-border-radius: 4px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);


.cryptoloader-wrap .cryptoloader-content i 
  font-style: normal;
  font-size: 600%;
  line-height: normal;
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<!-- vue root -->
<div id="app" v-cloak>
  <!-- price list grid -->
  <main class="cryptomain-wrap">
    <div class="cryptomain-grid-list">
      <div class="cryptomain-grid-item marquee-text-text" v-for="c in coinsList" :key="c.symbol" :class="c.style">
        <div class="cryptomain-grid-chart">
          <linechart : : :values="c.history"></linechart>
        </div>
        <div class="cryptomain-grid-info cryptoflex-row cryptoflex-top cryptoflex-stretch">
          <div class="cryptopush-right">
            <img :src="c.icon" : onerror="this.onerror=null;this.src='/123/wp-content/imagenspersonalizadas/lb-crypto-nopic.png';" />
          </div>
          <div class="cryptoflex-1 cryptoshadow-text">
            <div class="cryptoflex-row cryptoflex-top cryptoflex-space">
              <div class="cryptotext-left cryptotext-clip cryptopush-right">
                <h1 class="cryptotext-primary cryptotext-clip"> c.token <small class="cryptotext-faded cryptotext-small text-condense">/ c.asset </small></h1>
                <h2 class="cryptotext-bright cryptotext-clip"> c.close | toFixed( asset ) </h2>
              </div>
              <div class="cryptotext-right">
                <div class="cryptocolor cryptotext-big cryptotext-clip"> c.arrow   c.sign  c.percent | toFixed( 2 ) %</div>
                <div class="cryptotext-clip"> c.sign  c.change | toFixed( asset )  <small class="cryptotext-faded">24h</small></div>
                <div class="cryptotext-clip"> c.assetVolume | toMoney  <small class="cryptotext-faded">Vol</small></div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </main>
  <!-- socket loader -->
  <div class="cryptoloader-wrap" :class=" 'visible': loaderVisible ">
    <div class="cryptoloader-content">
      <div v-if="status === 0"><i>?</i> <br /> Connecting to Socket API ...</div>
      <div v-else-if="status === 1"><i>?</i> <br /> Waiting for data from Socket API ...</div>
      <div v-else-if="status === 2"><i>?</i> <br /> Connected to the Socket API</div>
      <div v-else-if="status === -1"><i>?</i> <br /> Error connecting to the Socket API</div>
    </div>
  </div>
</div>

有点紧张,但我认为这可能是套接字更新或其他问题,不确定。

分解其工作原理:

将动画添加到.cryptomain-grid-listanimation: marquee 210s infinite linear,无限循环,因此它是线性的,所以从右到左运动是恒定的。

然后在挂载的钩子里:

    const gridList = document.querySelector('.cryptomain-grid-list');
    let gridListWidth = 0;
    const gridListInterval = setInterval(() => 

      if (gridList.children.length > 0) 

        for (let i of gridList.children) 
          console.log(i.offsetWidth)
          gridListWidth += 311.2;
        

        const cssAnimation = document.createElement('style');
        cssAnimation.type = 'text/css';
        const rules = document.createTextNode(
          `@keyframes marquee 
            0%  transform: translateX(100%); 
            100%  transform: translateX($-Math.abs(gridListWidth)px); 
          `);
        cssAnimation.appendChild(rules);
        document.getElementsByTagName("head")[0].appendChild(cssAnimation);
        clearInterval(gridListInterval);
      
    , 1)

设置一个变量来计算每个网格项的宽度,添加setInterval 以持续运行直到套接字加载数据,然后添加每个项的宽度(300px)加上间隙的宽度( 0.7em 是 11.2px) 到计数变量gridListWidth

获得网格宽度后,创建一个定义动画的style 元素并将其附加到文档头部。

希望这是有道理的,至少可以为您指明正确的方向!

【讨论】:

这都是vue创建的,没有jQuery? 我没有添加或删除任何 jQuery,所以除了已经存在的,它只是 Vue 和 CSS 我对 Vue Js 还是很陌生,直到最近我才意识到它对 DOM 有多么渴望。您的代码按我的需要工作。你能告诉我如何创建play/pausepreviousnext 按钮吗?还有,有什么办法可以把bar的两端连接起来,这样动画结束的时候就不会有很大的差距了? 悬停时也暂停。 悬停暂停很简单:.cryptomain-grid-list:hover animation-play-state: paused; ,已添加到我的帖子中

以上是关于Vue Js无限循环选框(横条)的主要内容,如果未能解决你的问题,请参考以下文章

如何在渲染时计算数组 [Vue.js] - 无限循环错误

vue 错误:组件渲染函数中的无限更新循环

控制台上的 Vue 警告:组件渲染函数中可能存在无限更新循环

如何可视化vue无限更新循环的无限循环

理解 vue-router的beforeEach无限循环的问题

为啥计算内部的异步会产生无限循环(vue)?