代码语言: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 浮动后,父元素高度塌陷?
- 清除浮动。
- dot 支持定制,当 dot 尺寸被定制时,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/