4
万事开头难,一直都想写博客,分享下自己的东西,科室无奈于工作比较忙,也不知道写点啥好,太浅了吧,觉得没意思,太深吧,我也不会,刚好公司最近要我们封装一批组件,就借花献佛拿出来分享一下吧!之前也没写过凑合看吧
哈哈。。。

首先来看下这个组件的样子, 它长这样:
图片描述

主要功能包括:

  1. 组件内部展示内容随意编辑,以便组件更好的复用(timeLineHTML);
  2. 点击左右箭头可以使时间轴左右滑动,每次移动量以单元格数量为准(moveCount),不能点击时鼠标指针给与相应的提示,鼠标按住时间轴中间任意位置拖动可实现相同效果(mouseSliding);
  3. 组件可以自动适应父元素宽高并且屏幕宽度变化时做到响应(height);

接下来看下代码和思路吧!
首先观察一下组件的样式左右两个按钮且宽度固定,响应式改变的主要是中间部分的宽度,
antd组价选择使用Layout布局来写,代码如下:
图片描述

布局定下来了估计剩下的都会写我就不啰嗦了,需要注意的是我们在componentDidMount生命周期中记录下页面初次更新时,每一个单元格的位置作为初始位置,
图片描述
下面主要说下鼠标触发的滚动是怎么是实现的;
废话不多说直接上代码:

// 时间轴滚动事件
  timeLineClick = (direction) => {
    const {moveCount} = this.props;
    let self = this;

    // 移动过程中禁止点击
    if (!this.allowClick) return;

    // 因使用scrollLeft 未达到最大值时scrollLeft已经达到最大 临界判断
    let lastPositionLeft = $('.diagnosis_box').eq(this.timeLineHTMLArr.length - 1).position().left;
    let lastWidth = $('.diagnosis_box').eq(this.timeLineHTMLArr.length - 1).width();
    let timeLinBoxWidth = $('#time_line_box').width() + _padding;

    if (direction === 'left' && $('.diagnosis_box').eq(0).position().left >= _padding) {
      return;
    }
    else if (direction === 'right') {
      if (Math.floor(lastPositionLeft + lastWidth) <= timeLinBoxWidth) {
        return
      }
    }

    this.allowClick = false;
    // 因拖拽屏幕会导致scrollLeft 有所变动 所这里重新定位下 this.timeLineCounter
    let currentPosition = $('#time_line_box').scrollLeft() + _padding;
    if (this.isResize || (direction === 'left' && Math.floor(lastPositionLeft + lastWidth) <= timeLinBoxWidth)) {
      this.timeLineHTMLArr.some((item, index) => {
        if (index < this.timeLineHTMLArr.length - 1
          && item <= currentPosition
          && currentPosition <= this.timeLineHTMLArr[index + 1]
        ) {
          this.timeLineCounter = this.isResize ? index + 1 : index
        }
      });
    }

    let count = 0;
    // 根据操作移动timeline
    if (direction === 'left') {
      if (this.timeLineCounter >= 0) {

        this.timeLineCounter = this.timeLineCounter - moveCount >= 0
          ? this.timeLineCounter - moveCount
          : 0;
        let leftCount = Math.floor((currentPosition - self.timeLineHTMLArr[self.timeLineCounter]) / 5);
        let displacementTime = 200 / leftCount;

        function moveLeft(count) {
          setTimeout(() => {
            self.isResize = false;
            let counts = count + 1;
            let _left = currentPosition - _padding - counts * 5;
            $('#time_line_box').scrollLeft(_left);
            if (leftCount > count) {
              moveLeft(counts);
            }
            else {
              self.allowClick = true;

              let newLastPositionLeft = $('.diagnosis_box').eq(self.timeLineHTMLArr.length - 1).position().left;
              self.setState({
                left: self.timeLineCounter === 0 ? false : true,
                right: Math.floor(newLastPositionLeft + lastWidth) <= timeLinBoxWidth ? false : true
              });
            }
          }, displacementTime)
        }

        moveLeft(count);
      }
    }
    else if (direction === 'right') {
      if (this.timeLineCounter <= this.timeLineHTMLArr.length - 1) {

        this.timeLineCounter = this.timeLineCounter + moveCount < this.timeLineHTMLArr.length - 1
          ? this.timeLineCounter + moveCount
          : this.timeLineHTMLArr.length - 1;

        let leftCount = Math.floor((self.timeLineHTMLArr[self.timeLineCounter] - currentPosition) / 5);
        let displacementTime = 200 / leftCount;

        function moveRight(count) {
          setTimeout(() => {
            self.isResize = false;
            let counts = count + 1;
            let _left = currentPosition - _padding + counts * 5;
            $('#time_line_box').scrollLeft(_left);

            if (leftCount > count) {
              moveRight(counts);
            }
            else {
              self.allowClick = true;

              let newLastPositionLeft = $('.diagnosis_box').eq(self.timeLineHTMLArr.length - 1).position().left;
              self.setState({
                left: self.timeLineCounter === 0 ? false : true,
                right: Math.floor(newLastPositionLeft + lastWidth) <= timeLinBoxWidth ? false : true
              });
            }
          }, displacementTime)
        }

        moveRight(count);
      }
    }
  };

这里的逻辑主要是鼠标点击触发的过程,需要注意的几点:

  1. 第一位移需要时间所以我们需要在唯一过程中,做一个节流,在位移结束前,不接受点击事件;
  2. 由于每一个位移单位的距离不同,就会出现单元格长的位移时间也会过长,视觉效果也不是很好,所以里面我们固定每次位移时间为200,用setTimeout递归调用;
  3. 在封装组件的时候遇到一个比较尴尬的问题,第一个单元格不在最左端的时候鼠标拖拽浏览器晃动改变大小,会使页面发生改变,导致计算好的位置出现偏差(使用鼠标拖拽发生位移时也需要这样处理),所以代码中会出现这样一部分处理:

图片描述

目的是为了修正偏移量;剩下的就不多说了直接看代码吧!

import React, {PureComponent} from 'react';
import styles from './TimeLine.less';
import {Icon, Layout} from 'antd';
import $ from "jquery";
import PropTypes from 'prop-types';
/*
* 公共时间轴 组件 接受定制显示模块 此组件提供模拟 获取TimeLineHTML函数
* 必须给定特性的 className = diagnosis_box; 每次点击保证diagnosis_box到达最左侧
* 自己定制部分的样式在 父组件内定义
* _padding 为time_line_box两边的padding 因纳入原生计算 所以统一赋值
* */
const _padding = 24;
const {Sider, Content} = Layout;

let eleDrag = false;

class TimeLine extends PureComponent {
  static propTypes = {
    timeLineHTML: PropTypes.array.isRequired, // 默认选项
    moveCount: PropTypes.number, // 每次点击移动个数
    height: PropTypes.number,
    mouseSliding: PropTypes.bool // 是否支持鼠标滑动事件
  };

  static defaultProps = {
    moveCount: 3,
    height: 65,
    mouseSliding: true
  };

  constructor(props) {
    super(props);
    this.state = {
      left: false,
      right: true
    };
    this.timeLineHTMLArr = [];
    this.timeLineCounter = 0;
    this.allowClick = true;
    this.isResize = false;
    this.initClientX = null;
  }

  componentDidMount() {
    let self = this;
    // 首次进入加载所有模块的positionleft 作为移动距离判定
    if (!this.timeLineHTMLArr.length) {
      $('.diagnosis_box').each(function () {
        self.timeLineHTMLArr.push($(this).position().left);
      });
    }
    window.onresize = () => {
      let target = this;
      if (target.resizeFlag) {
        clearTimeout(target.resizeFlag);
      }
      target.resizeFlag = setTimeout(function () {
        self.isResize = true;
        target.resizeFlag = null;
      }, 100);
    };
  }

  // 时间轴滚动事件
  timeLineClick = (direction) => {
    const {moveCount} = this.props;
    let self = this;

    // 移动过程中禁止点击
    if (!this.allowClick) return;

    // 因使用scrollLeft 未达到最大值时scrollLeft已经达到最大 临界判断
    let lastPositionLeft = $('.diagnosis_box').eq(this.timeLineHTMLArr.length - 1).position().left;
    let lastWidth = $('.diagnosis_box').eq(this.timeLineHTMLArr.length - 1).width();
    let timeLinBoxWidth = $('#time_line_box').width() + _padding;

    if (direction === 'left' && $('.diagnosis_box').eq(0).position().left >= _padding) {
      return;
    }
    else if (direction === 'right') {
      if (Math.floor(lastPositionLeft + lastWidth) <= timeLinBoxWidth) {
        return
      }
    }

    this.allowClick = false;
    // 因拖拽屏幕会导致scrollLeft 有所变动 所这里重新定位下 this.timeLineCounter
    let currentPosition = $('#time_line_box').scrollLeft() + _padding;
    if (this.isResize || (direction === 'left'
        && Math.floor(lastPositionLeft + lastWidth) <= timeLinBoxWidth)) {
      this.timeLineHTMLArr.some((item, index) => {
        if (index < this.timeLineHTMLArr.length - 1
          && item <= currentPosition
          && currentPosition <= this.timeLineHTMLArr[index + 1]
        ) {
          this.timeLineCounter = this.isResize ? index + 1 : index
        }
      });
    }

    let count = 0;
    // 根据操作移动timeline
    if (direction === 'left') {
      if (this.timeLineCounter >= 0) {

        this.timeLineCounter = this.timeLineCounter - moveCount >= 0
          ? this.timeLineCounter - moveCount
          : 0;
        let leftCount = Math.floor((currentPosition - self.timeLineHTMLArr[self.timeLineCounter]) / 5);
        let displacementTime = 200 / leftCount;

        function moveLeft(count) {
          setTimeout(() => {
            self.isResize = false;
            let counts = count + 1;
            let _left = currentPosition - _padding - counts * 5;
            $('#time_line_box').scrollLeft(_left);
            if (leftCount > count) {
              moveLeft(counts);
            }
            else {
              self.allowClick = true;

              let newLastPositionLeft = $('.diagnosis_box').eq(self.timeLineHTMLArr.length - 1).position().left;
              self.setState({
                left: self.timeLineCounter === 0 ? false : true,
                right: Math.floor(newLastPositionLeft + lastWidth) <= timeLinBoxWidth ? false : true
              });
            }
          }, displacementTime)
        }

        moveLeft(count);
      }
    }
    else if (direction === 'right') {
      if (this.timeLineCounter <= this.timeLineHTMLArr.length - 1) {

        this.timeLineCounter = this.timeLineCounter + moveCount < this.timeLineHTMLArr.length - 1
          ? this.timeLineCounter + moveCount
          : this.timeLineHTMLArr.length - 1;

        let leftCount = Math.floor((self.timeLineHTMLArr[self.timeLineCounter] - currentPosition) / 5);
        let displacementTime = 200 / leftCount;

        function moveRight(count) {
          setTimeout(() => {
            self.isResize = false;
            let counts = count + 1;
            let _left = currentPosition - _padding + counts * 5;
            $('#time_line_box').scrollLeft(_left);

            if (leftCount > count) {
              moveRight(counts);
            }
            else {
              self.allowClick = true;

              let newLastPositionLeft = $('.diagnosis_box').eq(self.timeLineHTMLArr.length - 1).position().left;
              self.setState({
                left: self.timeLineCounter === 0 ? false : true,
                right: Math.floor(newLastPositionLeft + lastWidth) <= timeLinBoxWidth ? false : true
              });
            }
          }, displacementTime)
        }

        moveRight(count);
      }
    }
  };

  // 拖拽鼠标使用时间轴滑动
  onMouseDown = (e) => {
    e.stopPropagation();
    e.preventDefault();
    eleDrag = true;
    this.initClientX = e.clientX;
  };

  onMouseMoveCapture = (e) => {
    e.stopPropagation();
    e.preventDefault();
    if (!this.allowClick) return;
    if (eleDrag) {
      let currentPosition = $('#time_line_box').scrollLeft();

      if (this.initClientX > e.clientX) { // 向右拖动鼠标
        let lastPositionLeft = $('.diagnosis_box').eq(this.timeLineHTMLArr.length - 1).position().left;
        let lastWidth = $('.diagnosis_box').eq(this.timeLineHTMLArr.length - 1).width();
        let timeLinBoxWidth = $('#time_line_box').width() + _padding;
        if (Math.floor(lastPositionLeft + lastWidth) <= timeLinBoxWidth) {
          return
        }
        let displacement = this.initClientX - e.clientX;
        $('#time_line_box').scrollLeft(currentPosition + displacement);
      }
      else if (this.initClientX < e.clientX) {// 向左拖动鼠标
        if ($('.diagnosis_box').eq(0).position().left >= _padding) {
          return;
        }
        let displacement = currentPosition - (e.clientX - this.initClientX);
        $('#time_line_box').scrollLeft(displacement);
      }
      this.initClientX = e.clientX;
    }
  };

  noDragging = (e) => {
    e.stopPropagation();
    e.preventDefault();
    eleDrag = false;
    this.initClientX = null;
  };

  render() {
    const {
      props: {
        timeLineHTML,
        height,
        mouseSliding
      },
      state: {
        left,
        right,
      }
    } = this;
    return (
      <div
        className={styles.TimeLine}
        style={{
          height,
          lineHeight: `${height}px`
        }}>
        <Layout>
          <Sider
            width={20}
            style={{
              position: 'relative',
              background: 'rgba(0, 0, 0, .25)',
              textAlign: 'center',
              cursor: left ? 'pointer' : 'not-allowed'
            }}>
            <span
              style={{
                position: 'absolute',
                left: '-1px',
                width: '100%',
                height: '100%',
                textAlign: 'center',
                display: 'inline-block',
                cursor: left ? 'pointer' : 'not-allowed'
              }}
              onMouseEnter={() => {
                if ($('.diagnosis_box').eq(0).position().left >= _padding - 0.01) {
                  this.setState({left: false});
                }
                else {
                  this.setState({left: true});
                }
              }}
              onClick={() => {
                this.timeLineClick('left')
              }}
            />
            <Icon type="left"/>
          </Sider>
          <Content
            style={{
              height: '100%',
              overflow: 'hidden',
              position: 'relative'
            }}>
            {mouseSliding
              ? <div
                onMouseDown={this.onMouseDown}
                onMouseMoveCapture={this.onMouseMoveCapture}
                onMouseUp={this.noDragging}
                onMouseLeave={this.noDragging}
                id='time_line_box'
                className={styles.time_line_box}>
                {timeLineHTML}
              </div>
              : <div
                id='time_line_box'
                className={styles.time_line_box}>
                {timeLineHTML}
              </div>}
          </Content>
          <Sider
            width={20}
            style={{
              position: 'relative',
              background: 'rgba(0, 0, 0, .25)',
              textAlign: 'center',
              cursor: right ? 'pointer' : 'not-allowed'
            }}>
             <span
               style={{
                 position: 'absolute',
                 left: '-1px',
                 width: '100%',
                 height: '100%',
                 textAlign: 'center',
                 display: 'inline-block',
                 cursor: right ? 'pointer' : 'not-allowed'
               }}
               onMouseEnter={() => {
                 let lastPositionLeft = $('.diagnosis_box').eq(this.timeLineHTMLArr.length - 1).position().left;
                 let lastWidth = $('.diagnosis_box').eq(this.timeLineHTMLArr.length - 1).width();
                 let timeLinBoxWidth = $('#time_line_box').width() + _padding;
                 if (Math.floor(lastPositionLeft + lastWidth) <= timeLinBoxWidth) {
                   this.setState({right: false});
                 }
                 else {
                   this.setState({right: true});
                 }
               }}
               onClick={() => {
                 this.timeLineClick('right')
               }}
             />
            <Icon type="right"/>
          </Sider>
        </Layout>
      </div>
    )
  }
}

export default TimeLine;

下面是对应的less:

.TimeLine{
  height: 100%;
  :global{
    .ant-layout.ant-layout-has-sider{
      height: 100%;
    }
  }

  .time_line_box{
    position: relative;
    padding: 0 24px;
    height: 100%;
    overflow: hidden;
    white-space: nowrap;
  }
}

下面是技术总结,主要说下这个组件需要注意的地方:

  1. 鼠标点击事件需要做判断,在唯一过程中应禁止下一次点击事件;
  2. 其次鼠标拖拽以后,再次点击移动或者屏幕宽度发生变化后在次点击时,需要校正一下偏移位置;
  3. 注意鼠标不可点击时的临界判断;

machinist
460 声望33 粉丝

javaScript、typescript、 react全家桶、vue全家桶、 echarts、node、webpack