react| 封装TimeLine组件
功能
- 支持居中/局左/居右布局
- 可自定义线条颜色
- 默认情况下图标是圆形,可自定义圆形颜色和大小,同时也可以自定义图标
- 支持自定义内容
效果
const data=[
{
"title": "2022-12-05 12:03:40",
"des": "茶陵县实时广播防火宣传"
},
...
]
<TimeLine data={data}/>
实现思路
居左居右比较简单,这里讲一下居中的情况。居中使用的是三列的Grid
布局,接着根据它的排列规则给每一个空位填充内容,包括实际内容(content
)、对应的图标(icon
)以及空元素(empty
):
const curData = [];
let left = true;// 定义一个变量来判断当前的数据项在左侧还是在右侧,根据不同位置采取不同的填充方式
data.forEach((item) => {
if (left) {
curData.push({ ...item, type: 'content' });
curData.push({ ...item, type: 'icon' });
curData.push({ type: 'empty' });
curData.push({ type: 'empty' });
left = false;
} else {
curData.push({ ...item, type: 'icon' });
curData.push({ ...item, type: 'content' });
left = true;
}
});
接着根据类型将元素渲染出来。预想情况下,第一列的内容是居右对齐;第三列是局左对齐(默认)。而现在第一列是局左的,所以需要进一步给第一列加上居右的样式,只需根据index来判断元素是否属于第一列即可:
{curData.map((item, index) => {
const isLeft = index % 3 === 0;
switch (item.type) {
case 'content':
return (
<ContentBox
item={item}
boxStyle={{
...ContentBoxStyle,
textAlign: isLeft ? 'right' : 'left' // 如果是左侧的内容,则居右对齐
}}
options={contentOpts}
/>
);
case 'icon':
return <IconBox item={item} boxStyle={IconBoxStyle} options={iconOpts} />;
default:
return <div className="content-box" style={ContentBoxStyle}></div>;
}
})}
Api
属性 | 类型 | 默认值 | 描述 |
---|---|---|---|
data | Array<TimeLineInfo> |
[] |
数据项数组 |
mode | middle / left / right |
middle |
布局模式 |
lineColor | String |
#eee |
线条颜色 |
rowGap | Number |
0 |
每项的行间距,支持负数 |
verticalAlign | center / top / bottom |
center |
内容垂直对齐方式 |
titleStyle | Object |
{} |
标题样式(如果data中定义了,这里的会被覆盖) |
desStyle | Object |
{} |
描述样式(如果data中定义了,这里的会被覆盖) |
circleColor | String |
#00ccff |
圆形图标颜色(如果data中定义了,这里的会被覆盖) |
showInnerCircle | Boolean |
false |
是否显示圆形内环(如果data中定义了,这里的会被覆盖) |
innerCircleColor | String |
#fff |
圆形内环颜色(如果data中定义了,这里的会被覆盖) |
circleSize | Number |
12 |
圆形图标大小(如果data中定义了,这里的会被覆盖) |
getCustomContent | (TimeLineInfo) => jsx |
- |
自定义内容(此时title和des无效) |
getContentBoxStyle | (TimeLineInfo) => Object |
- |
自定义内容容器样式 |
getIconBoxStyle | (TimeLineInfo) => Object |
- |
自定义图标容器样式 |
TimeLineInfo
可以为单独的数据项自定义样式,在这里定义的样式优先级最高
属性 | 类型 | 默认值 | 描述 |
---|---|---|---|
title | String |
- | 标题 |
des | String |
- | 描述 |
titleStyle | Object |
- | 标题样式 |
desStyle | Object |
- | 描述样式 |
circleColor | String |
- | 圆形图标颜色 |
circleSize | Number |
- | 圆形图标大小 |
CustomContent | JSX对象 |
- | 自定义内容 |
CustomIcon | JSX对象 |
- | 自定义图标 |
源码
jsx
import React from 'react';
const getStyleObj = ({ rowGap, verticalAlign }) => {
let alignItems = 'center';
switch (verticalAlign) {
case 'top':
alignItems = 'baseline';
break;
case 'bottom':
alignItems = 'end';
break;
default:
break;
}
// 内容容器样式
const ContentBoxStyle = {
// 在middle布局下 每一列的item上下空余会很多,所以允许传入负数要缩小间距
...(rowGap > 0 ? { padding: `${rowGap}px 0` } : { margin: `${rowGap}px 0` })
};
// 图标容器样式
const IconBoxStyle = {
...ContentBoxStyle,
alignItems
};
return { ContentBoxStyle, IconBoxStyle };
};
const getClassifiedOpts = (options) => {
let {
lineColor,
circleColor,
circleSize,
titleStyle,
desStyle,
getCustomContent,
getContentBoxStyle,
getIconBoxStyle,
showInnerCircle,
innerCircleColor
} = options;
let iconOpts = {
lineColor,
circleColor,
circleSize,
getIconBoxStyle,
showInnerCircle,
innerCircleColor
};
let contentOpts = {
titleStyle,
desStyle,
getCustomContent,
getContentBoxStyle
};
return { iconOpts, contentOpts };
};
// 图标容器组件
const IconBox = ({ item, boxStyle, options }) => {
let { lineColor, circleColor, circleSize, getIconBoxStyle, showInnerCircle, innerCircleColor } = options; // 默认样式
let outerSize = item.circleSize || circleSize;
let innerSize = outerSize >= 18 ? parseInt(outerSize / 2 - 1) : 4;
let outerColor=item.circleColor || circleColor
let innerColor=item.innerCircleColor || innerCircleColor
return (
<div className="icon-box" style={{ ...boxStyle, ...getIconBoxStyle(item) }}>
<div className="line" style={{ background: lineColor }}></div>
{item.CustomIcon ? (
item.CustomIcon
) : (
<div
className="icon"
style={{
height: `${outerSize}px`,
width: `${outerSize}px`,
background: outerColor,
border: `2px solid ${lineColor}`
}}
>
{(item.showInnerCircle || showInnerCircle) && (
<div
className="inner"
style={{
height: `${innerSize}px`,
width: `${innerSize}px`,
background: innerColor
}}
></div>
)}
</div>
)}
</div>
);
};
// 内容容器组件
const ContentBox = ({ item, boxStyle, options }) => {
let { titleStyle, desStyle, getCustomContent, getContentBoxStyle } = options; // 默认样式
const getContent = () => {
if (getCustomContent) {
return getCustomContent(item);
} else {
return (
<React.Fragment>
<div style={item.titleStyle || titleStyle}>{item.title}</div>
<div style={item.desStyle || desStyle}>{item.des}</div>
</React.Fragment>
);
}
};
return (
<div className="content-box" style={{ ...boxStyle, ...getContentBoxStyle(item) }}>
{item.CustomContent ? item.CustomContent : getContent()}
</div>
);
};
// 居中布局
const MiddleDisplay = ({ data, options }) => {
const { ContentBoxStyle, IconBoxStyle } = getStyleObj(options);
const { iconOpts, contentOpts } = getClassifiedOpts(options);
const curData = [];
let left = true;
data.forEach((item) => {
if (left) {
curData.push({ ...item, type: 'content' });
curData.push({ ...item, type: 'icon' });
curData.push({ type: 'empty' });
curData.push({ type: 'empty' });
left = false;
} else {
curData.push({ ...item, type: 'icon' });
curData.push({ ...item, type: 'content' });
left = true;
}
});
return curData.length !== 0 ? (
<React.Fragment>
{curData.map((item, index) => {
const isLeft = index % 3 === 0;
switch (item.type) {
case 'content':
return (
<ContentBox
item={item}
boxStyle={{
...ContentBoxStyle,
textAlign: isLeft ? 'right' : 'left' // 如果是左侧的内容,则居右对齐
}}
options={contentOpts}
/>
);
case 'icon':
return <IconBox item={item} boxStyle={IconBoxStyle} options={iconOpts} />;
default:
return <div className="content-box" style={ContentBoxStyle}></div>;
}
})}
</React.Fragment>
) : null;
};
// 左/右布局
const NormalDisplay = ({ data, mode, options }) => {
const { ContentBoxStyle, IconBoxStyle } = getStyleObj(options);
const { iconOpts, contentOpts } = getClassifiedOpts(options);
if (data.length === 0) return null;
return mode === 'left' ? (
<React.Fragment>
{data.map((item) => (
<React.Fragment>
<IconBox item={item} boxStyle={IconBoxStyle} options={iconOpts} />
<ContentBox item={item} boxStyle={ContentBoxStyle} options={contentOpts} />
</React.Fragment>
))}
</React.Fragment>
) : (
<React.Fragment>
{data.map((item) => (
<React.Fragment>
<ContentBox
item={item}
boxStyle={{
...ContentBoxStyle,
textAlign: 'right'
}}
options={contentOpts}
/>
<IconBox item={item} boxStyle={IconBoxStyle} options={iconOpts} />
</React.Fragment>
))}
</React.Fragment>
);
};
const TimeLine = ({
data,
mode = 'middle', // 默认为左右两侧分布
lineColor = '#eee', // 线条颜色
rowGap = 0, // 行距
style = {},
className = '',
getCustomContent = null, // (item)=> jsx, 将会把data中的item作为参数
getContentBoxStyle = () => ({}), // (item)=> object, 将会把data中的item作为参数 自定义容器样式
getIconBoxStyle = () => ({}), // (item)=> object, 将会把data中的item作为参数 自定义容器样式
// data里面定义的以下样式 优先于 组件属性的样式
verticalAlign = 'center', // 对齐方式
circleColor = '#00ccff', // 圆形颜色
circleSize = 12, // 圆形大小 单位px
titleStyle = {}, // 标题样式
desStyle = {}, // 描述样式
showInnerCircle = false,
innerCircleColor = '#fff'
}) => {
let Content;
switch (mode) {
case 'middle':
Content = MiddleDisplay;
break;
case 'left':
case 'right':
Content = NormalDisplay;
break;
default:
break;
}
return (
<div className={`event_display grid ${mode} ${className}`} style={style}>
<Content
data={data}
mode={mode}
options={{
verticalAlign,
rowGap,
lineColor,
circleColor,
circleSize,
titleStyle,
desStyle,
getCustomContent,
getContentBoxStyle,
getIconBoxStyle,
showInnerCircle,
innerCircleColor
}}
/>
</div>
);
};
export default TimeLine;
css
.time_line {
&.grid{
display: grid;
grid-column-gap: 0px;
grid-row-gap: 0px;
}
&.middle {
grid-template-columns: 1fr 27px 1fr;
}
&.left {
grid-template-columns: 27px 1fr;
}
&.right {
grid-template-columns: 1fr 27px;
}
.content-box {
}
.icon-box {
display: flex;
justify-content: center;
align-items: center;
position: relative;
> .line {
position: absolute;
height: 100%;
content: '';
width: 1px;
background-color: rgba(5,5,5,.06);
z-index: -1;
}
> .icon {
border-radius: 50%;
background-color: #00ccff;
display: flex;
align-items: center;
justify-content: center;
>.inner{
border-radius: 50%;
background-color: #fff;
}
}
}
}