【React】【案例】:TimeLine 时间轴

2020-04-21 16:26:58 浏览数 (1)

代码语言:javascript复制
目录
1. 组件基础
2. 需求分析
3. 关键技术
4. 代码实现
5. 形态展示

1. 组件基础

可视化地呈现时间流信息。

2. 需求分析

3. 关键技术

  • 为什么不直接用 antd、elementui、iview 等开源组件?
    • antd 很优秀,但是....
    • antd 不支持 label、content 按指定比例分布;
    • antd 在dot定制时,难以控制UI界面呈现;
    • elementui 不能将 label 放在左边;
    • .... 但是以 antd 为基础改造,会快很多;
  • 主体采用什么html结构实现?
    • ul、li(list-style:none)
  • TimelineItem 的 label、content、dot、dotline 布局结构如何实现?
    • label、content 采用 div float 实现左右布局;
    • div 通过 padding 留出视觉上 dot、dotline 的空隙;
    • dot、dotline 通过 div position:absolute 布局实现;
    • dot、dotline 的水平居中通过 left:50% transform:translate(-50%) 实现;
  • TimelineItem 的布局实现,有哪些问题需要考虑?
    • dot 支持定制,当 dot 尺寸被定制时,label、content 的位置如何处置?
      • Timeline 通过 dotsize 属性,获取定制后的 dot 尺寸,并控制 label、content 的padding 让出合适视觉空隙。
    • label、content 浮动后,父元素高度塌陷?
      • 清除浮动。
  • 开发辅助工具选择
    • Typescript Less

4. 代码实现

Timeline.tsx

代码语言:javascript复制
import * as React from 'react';
import classNames from 'classnames';

// eslint-disable-next-line no-unused-vars
import TimelineItem, {TimeLineItemProps, TimelineItemPosition} from './TimelineItem';

export interface TimelineProps {
    className?: string;
    style?: React.CSSProperties;
    reverse?: boolean;
    mode?: 'left' | 'alternate' | 'right';
    dotSize?: number;
    labelCol: 8 | 12
}

export default class Timeline extends React.Component<TimelineProps, any> {
    static Item: React.FunctionComponent<TimeLineItemProps> = TimelineItem;

    static defaultProps = {
        reverse: false,
        mode: 'left',
        labelCol: 12,
    };

    render() {
        const {
            children,
            className,
            reverse,
            mode,
            dotSize,
            labelCol,
            ...restProps
        } = this.props;

        const suffixCls = 'timeline';
        const prefixCls = `mousex-${suffixCls}`;

        const timeLineItems = reverse
            ? [...React.Children.toArray(children).reverse()]
            : [...React.Children.toArray(children)];

        const getPosition = (ele: React.ReactElement<any>, idx: number): TimelineItemPosition => {
            if (mode === 'alternate') {
                if (ele.props.position === 'right') return `right`;
                if (ele.props.position === 'left') return `left`;
                return idx % 2 === 0 ? `left` : `right`;
            }
            if (mode === 'left') return `left`;
            if (mode === 'right') return `right`;
            throw new Error(`mode [${mode}] error! should be one of 'left'、'right'、'alternate'!`);
        };

        // Remove falsy items
        const truthyItems = timeLineItems.filter(item => !!item);
        const itemsCount = React.Children.count(truthyItems);
        const lastCls = `${prefixCls}-item-last`;
        const items = React.Children.map(truthyItems, (ele: React.ReactElement<any>, idx) => {
            const readyClass = idx === itemsCount - 1 ? lastCls : '';
            const position: TimelineItemPosition = getPosition(ele, idx);
            return React.cloneElement(ele, {
                className: classNames([
                    ele.props.className,
                    readyClass,
                    `${prefixCls}-item-${position}`,
                ]),
                position: getPosition(ele, idx),
                dotSize
            });
        });

        const hasLabelItem = timeLineItems.some(
            (item: React.ReactElement<any>) => !!item?.props?.label,
        );

        const classString = classNames(
            prefixCls,
            {
                [`${prefixCls}-reverse`]: !!reverse,
                [`${prefixCls}-${mode}`]: !!mode && !hasLabelItem,
                [`${prefixCls}-label`]: hasLabelItem,
                [`${prefixCls}-label-col-12`]: hasLabelItem && labelCol == 12,
                [`${prefixCls}-label-col-8`]: hasLabelItem && labelCol == 8,
            },
            className,
        );

        return (
            <ul {...restProps} className={classString}>
                {items}
            </ul>
        );
    }
}

TimelineItem.tsx

代码语言:javascript复制
import * as React from 'react';
import classNames from 'classnames';

export type TimelineItemPosition = "left" | "right";

export interface TimeLineItemProps {
    className?: string;
    color?: string;
    dot?: React.ReactNode;
    dotSize?: number;
    position?: TimelineItemPosition;
    style?: React.CSSProperties;
    label?: React.ReactNode;
}

const TimelineItem: React.FunctionComponent<TimeLineItemProps> = (props: TimeLineItemProps & { children?: React.ReactNode }) => {
    const {
        className,
        style = {},
        color = '',
        children,
        dot,
        dotSize,
        position,
        label,
        ...restProps
    } = props;

    const suffixCls = 'timeline';
    const prefixCls = `mousex-${suffixCls}`;
    const itemClassName = classNames(
        {
            [`${prefixCls}-item`]: true,
        },
        className,
        "clearfix",
    );

    const dotClassName = classNames({
        [`${prefixCls}-item-head`]: true,
        [`${prefixCls}-item-head-custom`]: dot,
        [`${prefixCls}-item-head-${color}`]: true,
    });

    let itemStyle = {};
    const labelStyle = {};
    const contentStyle = {};
    if (dotSize) {
        itemStyle["minHeight"] = dotSize;
        if (position == "left") {
            labelStyle["paddingRight"] = Math.ceil(dotSize / 2)   14;
            contentStyle["paddingLeft"] = Math.ceil(dotSize / 2)   18;
        } else {
            labelStyle["paddingLeft"] = Math.ceil(dotSize / 2)   14;
            contentStyle["paddingRight"] = Math.ceil(dotSize / 2)   18;
        }
    }

    itemStyle = {
        ...itemStyle,
        ...style
    }

    return (
        <li {...restProps} className={itemClassName} style={itemStyle}>
            {label && <div className={`${prefixCls}-item-label`} style={labelStyle}>{label}</div>}
            <div className={`${prefixCls}-item-tail`}/>
            <div
                className={dotClassName}
                style={{borderColor: /blue/.test(color) ? undefined : color}}
            >
                {dot}
            </div>
            <div className={`${prefixCls}-item-content`} style={contentStyle}>{children}</div>
        </li>
    );
};

TimelineItem.defaultProps = {
    color: 'blue'
};

export default TimelineItem;

index.less

代码语言:javascript复制
@import '../../style/themes/index';
@import '../../style/mixins/index';

@timeline-prefix-cls: ~'@{mousex-prefix}-timeline';

.@{timeline-prefix-cls} {
  .reset-component();

  margin: 0;
  padding: 0;
  list-style: none;

  > .@{timeline-prefix-cls}-item {
    box-sizing: content-box;
    position: relative;
    margin: 0;
    padding-bottom: @timeline-item-padding-bottom;
    font-size: @font-size-base;

    > .@{timeline-prefix-cls}-item-head {
      position: absolute;
      width: 10px;
      height: 10px;
      background-color: @timeline-dot-bg;
      border: @timeline-dot-border-width solid transparent;
      border-radius: 100px;

      &-blue {
        color: @primary-color;
        border-color: @primary-color;
      }

      &-custom {
        position: absolute;
        top: 0;
        left: 5px;
        width: auto;
        height: auto;
        margin-top: 0;
        padding: 0;
        line-height: 1;
        text-align: center;
        border: 0;
        border-radius: 0;
        transform: translate(-50%, -50%);
      }
    }
    
    > .@{timeline-prefix-cls}-item-tail {
      position: absolute;
      top: 0;
      height: 100%;
      border-left: @timeline-width solid @timeline-color;
    }

    > .@{timeline-prefix-cls}-item-content {
      position: relative;
      top: -(@font-size-base * @line-height-base - @font-size-base)   1px;
      word-break: break-word;
    }
  }

  > .@{timeline-prefix-cls}-item.@{timeline-prefix-cls}-item-last {
    > .@{timeline-prefix-cls}-item-tail {
      display: none;
    }

    > .@{timeline-prefix-cls}-item-content {
      min-height: 48px;
    }
  }

  &&-left {
    > .@{timeline-prefix-cls}-item {
      > .@{timeline-prefix-cls}-item-head {
        left: 0;
      }

      > .@{timeline-prefix-cls}-item-tail {
        left: 4px;
      }

      > .@{timeline-prefix-cls}-item-content {
        padding-left: 18px;
        float: left;
      }
    }
  }

  &&-right {
    > .@{timeline-prefix-cls}-item {
      > .@{timeline-prefix-cls}-item-head {
        right: 0;
      }

      > .@{timeline-prefix-cls}-item-tail {
        right: 4px;
      }

      > .@{timeline-prefix-cls}-item-content {
        padding-right: 18px;
        float: right;
      }
    }
  }

  &&-alternate {
    > .@{timeline-prefix-cls}-item {
      > .@{timeline-prefix-cls}-item-head,
      > .@{timeline-prefix-cls}-item-tail {
        left: 50%;
        transform: translate(-50%, 0);
      }

      &-left {
        > .@{timeline-prefix-cls}-item-label {
          position: relative;
          top: -(@font-size-base * @line-height-base - @font-size-base)   1px;
          width: 50%;
          padding-right: 12px;
          text-align: right;
          float: right;
        }

        > .@{timeline-prefix-cls}-item-content {
          text-align: right;
          float: left;
          width: 50%;
          padding-right: 18px;
        }
      }

      &-right {
        > .@{timeline-prefix-cls}-item-label {
          padding-left: 14px;
          text-align: left;
          float: right;
          width: 50%;
        }

        > .@{timeline-prefix-cls}-item-content {
          left: 50%;
          padding-left: 18px;
          text-align: left;
          float: left;
          width: 50%;
        }
      }
    }
  }

  &&-label {
    > .@{timeline-prefix-cls}-item {
      > .@{timeline-prefix-cls}-item-head,
      > .@{timeline-prefix-cls}-item-tail {
        left: 50%;
        transform: translate(-50%, 0);
      }
    }

    > .@{timeline-prefix-cls}-item.@{timeline-prefix-cls}-item-left {
      > .@{timeline-prefix-cls}-item-label {
        position: relative;
        //top: -(@font-size-base * @line-height-base - @font-size-base)   1px;
        width: 50%;
        padding-right: 12px;
        text-align: right;
        float: left;
      }

      > .@{timeline-prefix-cls}-item-content {
        text-align: left;
        float: right;
        padding-left: 18px;
      }
    }

    > .@{timeline-prefix-cls}-item.@{timeline-prefix-cls}-item-right {
      > .@{timeline-prefix-cls}-item-label {
        padding-left: 14px;
        text-align: left;
        float: right;
      }

      > .@{timeline-prefix-cls}-item-content {
        padding-right: 18px;
        text-align: right;
        float: left;
      }
    }
  }

  &&-label&-label-col-8 {
    > .@{timeline-prefix-cls}-item {
      > .@{timeline-prefix-cls}-item-head,
      > .@{timeline-prefix-cls}-item-tail {
        left: 30%;
      }

      > .@{timeline-prefix-cls}-item-label {
        width: 30%
      }

      > .@{timeline-prefix-cls}-item-content {
        width: 70%;
      }
    }
  }

  &&-label&-label-col-12 {
    > .@{timeline-prefix-cls}-item {
      > .@{timeline-prefix-cls}-item-head,
      > .@{timeline-prefix-cls}-item-tail {
        left: 50%;
      }

      > .@{timeline-prefix-cls}-item-label {
        width: 50%
      }

      > .@{timeline-prefix-cls}-item-content {
        width: 50%;
      }
    }
  }
}

5. 形态展示

参考:

react: https://react.docschina.org/ antd-timeline: https://ant.design/components/timeline-cn/


0 人点赞