需求

前几天公司要求做一个动态表格:大概就是可配置的(颜色,字体,行数等等),用作屏幕展示。因此就要求表格文字在超出宽度时候可以有动画循环滚动展示内容。大概这个样子:
请添加图片描述
项目使用vue框架,动画因为涉及到预览(用js写的)不能调用vue,因此使用jquery类库的animate方法实现。这里表格颜色、字体大小等等动态适配很简单,双向绑定就可以,在此不做赘述。

思路

实现思路:找到所有的span标签(内容)进行遍历,如果标签宽度超过父元素宽度,就给span添加滚动效果。(表格元素不会很多,因此性能问题不明显,如果有大佬有更好的思路,欢迎给出优化建议
动画实现:通过递归调用的方式,配合定时器,实现两段动画的循环调用。
接下来让我们来具体实现这个效果。

实现

确定了思路,开搞!

<template>
  <div style="width: 100%; height: 100%">
    <div :id="content.id" class="table-container">
      <table
        :id="`table_${content.id}`"
        :style="{
          'table-layout': 'fixed',
          fontSize: `30px`,
          'border-spacing': '0',
          'border-collapse': 'collapse'
        }"
      >
        <!-- 表头行 -->
        <tr
          :style="{
            width: '100%',
            height: `100px`,
            fontSize: `30px`
          }"
        >
          <!-- 表头行第一个【序号】可配置是否显示 thFlag支持配置 -->
          <th
            class="th0"
            :style="{
              display: thFlag ? 'table-cell' : 'none',
              width: '100px',
              whiteSpace: 'nowrap',
              overflow: 'hidden',
              'text-align': 'center',
              'border-style': 'solid'
            }"
          >
            <div
              :style="{
                width: '100px',
                height: `50px`,
                lineHeight: `50px`,
                overflow: 'hidden'
              }"
            >
              <span>序号</span>
            </div>
          </th>
          <!-- 表头行数据 -->
          <th
            v-for="(ite, inde) in content.columns"
            :key="inde"
            :class="`th${inde + 1}`"
            :style="{
              width: `${ite.width}px`,
              whiteSpace: 'nowrap',
              overflow: 'hidden',
              'text-align': ite.textAlign,
              'border-style': 'solid',
            }"
          >
            <div
              :style="{
                width: `${ite.realw}px`,
                height: `30px`,
                lineHeight: `30px`,
                overflow: 'hidden',
                'text-align': ite.textAlign
              }"
            >
              <span>{{ ite.headerName }}</span>
            </div>
          </th>
        </tr>
        <!-- 遍历实际数据 -->
        <!-- color那里可以配置基偶数行不同颜色 -->
        <tr
          v-for="(item, index) in content.rowStyle.rowCount"
          :key="index"
          :style="{
            width: '100%',
            height: `${content.rowStyle.realLineHeight}px`,
            color: index % 2 === 0 ? content.rowStyle.oddRowfontColor : content.rowStyle.evenRowfontColor,
            'background-color': index % 2 === 0 ? content.rowStyle.oddRowBgcolor : content.rowStyle.evenRowBgcolor
          }"
        >
          <!-- 第一列,序号:和上面序号的th对应 -->
          <td
            :style="{
              display: thFlag ? 'table-cell' : 'none',
              width: '100px',
              whiteSpace: 'nowrap',
              overflow: 'hidden',
              'text-align': 'center',
              'border-style': 'solid',
            }"
          >
            <div
              :style="{
                width: '100px',
                height: `30px`,
                lineHeight: `30px`,
                overflow: 'hidden'
              }"
            >
              {{ index + 1 }}
            </div>
          </td>
          <!-- 数据列,真实数据 -->
          <td
            v-for="(ite, inde) in content.columns"
            :key="inde"
            :style="{
              width: `${ite.realw}px`,
              height: '100%',
              whiteSpace: 'nowrap',
              overflow: 'hidden',
              'border-style': 'solid'
            }"
          >
            <div
              :style="{
                width: `${ite.realw}px`,
                height: `30px`,
                lineHeight: `30px`,
                overflow: 'hidden',
                'text-align': ite.textAlign
              }"
            >
              <span v-show="ite.type !== 'img'" v-html="ite.keyValue[index]">{{ ite.keyValue[index] }}</span>
              <span v-show="ite.type === 'img'" v-html="ite.keyValue[index]">{{ ite.keyValue[index] }}</span>
            </div>
          </td>
        </tr>
      </table>
    </div>
  </div>
</template>

html部分大概如上所示:很多动态配置数据我删掉了,或者换成了实际数据,使用时候自行修改就好了。

接下来实现js部分逻辑。
<script>
	export default {
		data() {
			return {
      		  	// 判断循环定时器是否要继续(退出组件时候设为false,防止继续调用。)
		      	intervalFlag: false,
      		  	// 保存定时器序列,当销毁组件或者修改表格数据需要重置表格时候,要把所有定时器清除掉。--定时器是在Windows对象上的,不会跟随组建销毁而销毁。(这里还涉及到一个疑问,最后说……)
		        timerList: [],
			}
		},
		methods:{
		    initAnimate() {
		      // 清空动画和定时器
		      this.stopAll();
		      this.$nextTick(() => {
		        this.intervalFlag = true;
		        // $_collection: th和td的集合
		        const $_collection = $(`#${this.content.id} th, #${this.content.id} td`);
		        // 遍历dom节点序列
		        [].forEach.call($_collection, (ele) => {
		          const span = $(ele).find('span')[0];
		          const div = $(ele).find('div')[0];
		          // divWidth : th或td下的div的宽度
		          const divWidth = $(div).width();
		          // spanWidth : 内部span的宽度
		          const spanWidth = $(span).width();
		          const flag = divWidth < spanWidth;
		          if (flag) {
		            // 文字超出表格宽度加动画
		            this.calculateAnimate($(span), $(div), divWidth, spanWidth, 1200);
		          }
		        });
		      });
		    },
		    // 单个span赋动画  divWidth : th或td里面的div | spanWidth : 内部span的宽度 | loopTime: 两次循环间隔时间(ms)
		    calculateAnimate($_span, $_div, divWidth, spanWidth, loopTime) {
		      let animate_loop = null;
		      // newMargin:每次需要向左偏移的距离
		      let newMargin;
		      // 这里有一个坑:居中的文字想向左偏移到消失要margin-left为   : -(自身+父元素宽度),左对齐文字只需要偏移-自身宽度即可。
		      // 动画效果即: 从margin-left:0  ->  margin-left:-newMargin ,然后第二段: margin-left: divWidth -> margin-left: 0;  等待loopTime   在进行第二轮动画。
		      const isCenter = $_div.css('text-align');
		      if (isCenter) {
		      // 判断是否居中,来判断需要偏移的距离(其实还需要计算右对齐需要偏移的距离,在这里偷个懒,大家自己算吧!)
		        newMargin = -spanWidth - divWidth;
		      } else {
		        newMargin = -spanWidth;
		      }
		      // 移动时间 【60ms移动1px】-计算时间。
		      // 第二个坑,当居中时候,移动距离变远了,但是视觉上移动距离是一样的,所以duration动画完成时间还是只计算spanWidth的。   ---   duration是第一段动画需要的时间。
		      const duration = spanWidth * 1 * 30;
		      // 动画函数(递归调用)--- 循环滚动
		      animate_loop = () => {
		      	// 每次先置为0的位置。
		        $_span.css({ marginLeft: '0px' });
		        // 一定要先调用 .stop() 如果你不想焦头烂额的找bug的话…………
		        $_span.stop().animate(
		          {
		            marginLeft: `${newMargin}px`
		          },
		          {
		            duration,
		            easing: 'linear',
		            complete: () => {
		            //complete: 动画完成时的执行函数: 此时执行第二段动画。
		              $_span.css({ marginLeft: `${divWidth}px` });
		              // stop()不要忘记,不要让它存到动画序列中和你捣乱…… 
		              $_span.stop().animate(
		                {
		                  marginLeft: '0px'
		                },
		                {
		                  duration: divWidth * 30, // 还是时间计算。
		                  easing: 'linear',
		                  complete: () => {
		                  	// 第二次执行结束后,调用自身函数,定时器中递归调用。
		                    // intervalFlag:全局判断是否还要继续循环(当组件销毁时使用)
		                    if (this.intervalFlag) {
		                      // 将定时器放入数组,方便清空定时器  --  不这么做的话,定时器肆意捣乱,是第三个坑吧!
		                      this.timerList.push(
		                        setTimeout(() => {
		                        // 循环调用自身。
		                          animate_loop();
		                          // 清出第一个定时器标识,防止数组冗余 -存一个清一个。
		                          this.timerList.shift();
		                        }, loopTime)
		                      );
		                    }
		                  }
		                }
		              );
		            }
		          },
		          
		        );
		      };
		      $_span.css('margin-left', '0px');
		      animate_loop();
		    },
		    // 停止所有动画
		    stopAll() {
		      // 清空残留定时器
		      this.timerList.forEach((item) => {
		        clearTimeout(item);
		      });
		      this.timerList = [];
		      // 关闭所有定时器,初始化dom位置
		      const $_collection = $(`#${this.content.id} th, #${this.content.id} td`);
		      this.intervalFlag = false;
		      [].forEach.call($_collection, (ele) => {
		        const span = $(ele).find('span')[0];
		        $(span).stop();
		        $(span).css('margin-left', 0);
		      });
		    },
		},
	  	beforeDestroy() {
	  	  this.intervalFlag = false;
	 	   this.stopAll();
	 	},
	}
</script>

就这样,文字应该就动起来了。至此表格文字超出动画实现完成。

代码写的比较随意,大佬可以给提优化建议,扩充下思路。功能简单但是坑不少的一个小功能。使用场景还是挺多的。

question:
其实虽然实现了,还是有遇到一个问题的(解决了但不知其所以然),万分希望有大佬可以帮忙解惑

在定时器循环那里,一开始我是没有把定时器放到数组并及时清空的。因为认为定时器在执行结束就自动销毁了。
但是项目中需要重置动画的操作很多。之后我就发现,如果改了表格宽度或文字宽度后重置动画:(按说所有逻辑都是重新执行的,包括计算宽度,时间的逻辑。)应该可以正常执行新的动画。

但是执行后发现经常出现动画效果不对,经过各种debug和打印发现执行的是上一次的数据。
我怀疑是没有拿到dom的真实数据,又尝试在this.$nextTick()中执行。结果发现并没什么卵用……
经过多次尝试(animate的api我用了一个遍……但没卵用),我把注意力从jquery的animate动画的api转向了这个万恶的定时器。
几次debug之后我发现,每次重置动画,第一次执行的数据都是对的,从第二次开始是错的。 — 问题很明显的指向了定时器 - 它污染了数据 - 新数据被定时器里的那次函数调用(闭包)里的数据覆盖掉了。。

我又将定时器存到数组,在每次重置都及时清除冗余的定时器,果然,问题解决了。!

但是问题就是:我不理解为什么残留的那一次定时器的数据执行会覆盖掉新的数据……

有问题、解答或需要补充的地方可以留言或私我。

静等思路……

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐