前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【React】【案例】:TimeLine 时间轴

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

作者头像
WEBJ2EE
发布2020-04-21 16:26:58
7.6K0
发布2020-04-21 16:26:58
举报
文章被收录于专栏:WebJ2EEWebJ2EE
代码语言: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/


本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-04-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WebJ2EE 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档