首先是效果图
1. 组件概述
这个时间轴组件支持以下功能:
- 支持展示多个时间轴项。
- 每个时间轴项可以有不同的颜色、标题和描述。
- 支持不同的排列模式(左对齐、右对齐、居中对齐)。
- 自定义边框样式。
- 通过插槽支持定制化的图标、内容等。
const ColorStyle = {
blue: '#1677ff',
green: '#52c41a',
red: '#ff4d4f',
gray: '#00000040'
};
这里定义了默认颜色
2.computed
和 watchEffect
通过 computed
来计算:
totalWidth
: 计算时间轴的宽度,支持百分比和像素值。len
: 获取timelineData
的长度,来确定时间轴项的数量
watchEffect(
() => {
getDotsHeight();
},
{ flush: 'post' }
);
watchEffect(
() => {
if (props.mode === 'center') {
// 根据时间轴项的索引,控制奇偶项的不同排列
for (let n = 0; n < len.value; n++) {
if ((n + 1) % 2) {
if (props.position === 'left') {
desc.value[n].classList.add('desc-alternate-left');
} else {
desc.value[n].classList.add('desc-alternate-right');
}
} else {
if (props.position === 'left') {
desc.value[n].classList.add('desc-alternate-right');
} else {
desc.value[n].classList.add('desc-alternate-left');
}
}
}
}
},
{ flush: 'post' }
);
3. 组件模板
在模板部分,我们使用 v-for
来渲染时间轴项,每个时间轴项包括一个圆点(timeline-dot
)和一个描述(timeline-desc
)。我们使用 :class
动态绑定类名来根据当前模式调整布局样式。
<div class="m-timeline-wrap" :style="`width: ${totalWidth};`">
<div class="m-timeline">
<div
:class="['timeline-item', { 'item-last': index === timelineData.length - 1 }]"
v-for="(data, index) in timelineData"
:key="index"
>
<span class="timeline-tail" :class="`tail-${mode}`" :style="`border-left-style: ${lineStyle};`"></span>
<div class="timeline-dot" :class="`dot-${mode}`" :style="`height: ${dotsHeight[index]}`">
<slot name="dot" :index="index">
<span class="dot-item" :style="{ borderColor: ColorStyle[data.color] }">
<span class="circle"></span>
</span>
</slot>
</div>
<div ref="desc" :class="`timeline-desc desc-${mode}`">
<slot name="desc" :index="index">
<div class="history_card">
<div class="font_style">
<div class="title">{{ data.title || '' }}</div>
<div class="content">{{ data.desc || '' }}</div>
</div>
<div class="img" v-if="data.img"></div>
</div>
</slot>
</div>
</div>
</div>
</div>
最后是整个组件的源代码 你也可以自己进行改写
<script setup>
import { ref, computed, watchEffect } from 'vue'
const props = defineProps({
timelineData: {
type: Array,
default: () => []
},
width: {
type: [Number, String],
default: '100%'
},
lineStyle: {
type: String,
default: 'solid'
},
mode: {
type: String,
default: 'left'
},
position: {
type: String,
default: 'left'
}
})
const ColorStyle = { // 颜色主题对象
blue: '#1677ff',
green: '#52c41a',
red: '#ff4d4f',
gray: '#00000040'
}
const desc = ref()
const dotsHeight = ref([])
const totalWidth = computed(() => {
return typeof props.width === 'number' ? `${props.width}px` : props.width
})
const len = computed(() => {
return props.timelineData.length
})
function getDotsHeight() {
for (let n = 0; n < len.value; n++) {
dotsHeight.value[n] = getComputedStyle(desc.value[n].firstElementChild || desc.value[n], null).getPropertyValue(
'line-height'
)
}
}
watchEffect(
() => {
getDotsHeight()
},
{ flush: 'post' }
)
watchEffect(
() => {
if (props.mode === 'center') {
for (let n = 0; n < len.value; n++) {
if ((n + 1) % 2) {
// odd
if (props.position === 'left') {
desc.value[n].classList.add('desc-alternate-left')
} else {
desc.value[n].classList.add('desc-alternate-right')
desc.value[n].classList.add('ygy')
}
} else {
// even
if (props.position === 'left') {
desc.value[n].classList.add('desc-alternate-right')
} else {
desc.value[n].classList.add('desc-alternate-left')
}
}
}
}
},
{ flush: 'post' }
)
</script>
<template>
<div class="m-timeline-wrap" :style="`width: ${totalWidth};`">
<div class="m-timeline">
<div
:class="['timeline-item', { 'item-last': index === timelineData.length - 1 }]"
v-for="(data, index) in timelineData"
:key="index"
>
<span class="timeline-tail" :class="`tail-${mode}`" :style="`border-left-style: ${lineStyle};`"></span>
<div class="timeline-dot" :class="`dot-${mode}`" :style="`height: ${dotsHeight[index]}`">
<slot name="dot" :index="index">
<span
class="dot-item"
v-if="data.color === 'red'"
:style="{ borderColor: ColorStyle.red }"
><span class="circle"></span></span
>
<span
class="dot-item"
v-else-if="data.color === 'gray'"
:style="{ borderColor: ColorStyle.gray }"
><span class="circle"></span></span
>
<span
class="dot-item"
v-else-if="data.color === 'green'"
:style="{ borderColor: ColorStyle.green }"
><span class="circle"></span></span
>
<span
class="dot-item"
v-else-if="data.color === 'blue'"
:style="{ borderColor: ColorStyle.blue }"
><span class="circle"></span></span
>
<span class="dot-item" v-else :style="{ borderColor: data.color || ColorStyle.blue }">
<span class="circle"></span>
</span>
</slot>
</div>
<div ref="desc" :class="`timeline-desc desc-${mode}`">
<slot name="desc" :index="index">
<div
class="history_card"
:style="{
paddingLeft: index % 2 === 0 && mode === 'center' ? '34px' : '',
paddingRight: index % 2 !== 0 && mode === 'center' ? '34px' : '',
flexDirection: index % 2 !== 0 && mode === 'center' ? 'row-reverse' : ''
}"
>
<div class="font_style">
<div
class="title"
:style="{
textAlign: index % 2 === 0 && mode === 'center' ? 'left' : 'right'
}"
>
{{ data.title || '' }}
</div>
<div
class="content"
:style="{
textAlign: index % 2 === 0 && mode === 'center' ? 'left' : 'right',
width: data.img ? '217px' : '500px',
paddingBottom: data.img ? '0px' : '50px'
}"
>
{{ data.desc || '' }}
</div>
</div>
<div class="img" v-if="data.img"></div>
</div>
</slot>
</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.m-timeline-wrap {
.m-timeline {
.timeline-item {
position: relative;
padding-bottom: 30px;
.timeline-tail {
position: absolute;
top: 12px;
width: 0;
height: 100%;
border-left-width: 2px;
border-left-color: #FF4600;
}
.tail-left {
left: 5px;
}
.tail-center {
left: 0;
right: 0;
margin: 0 auto;
}
.tail-right {
right: 5px;
}
.timeline-dot {
position: absolute;
display: flex;
align-items: center;
.dot-item {
display: inline-block;
width: 24px;
height: 24px;
border-width: 2px;
border-style: solid;
border-radius: 50%;
background: #fff;
display: flex;
justify-content: center;
align-items: center;
.circle {
width: 14px;
height: 14px;
background: #FF4600;
border-radius: 50%;
}
}
}
.dot-left {
left: 6px;
transform: translateX(-50%);
}
.dot-center {
left: 50%;
transform: translateX(-50%);
}
.dot-right {
right: 6px;
transform: translateX(50%);
}
.timeline-desc {
font-size: 14px;
line-height: 1.5714285714285714;
word-break: break-all;
}
.desc-left {
margin-left: 25px;
}
.desc-center {
width: calc(50% - 12px);
}
.desc-alternate-left {
text-align: end;
}
.desc-alternate-right {
margin-left: calc(50% + 12px);
}
.desc-right {
margin-right: 25px;
text-align: end;
}
}
.item-last {
.timeline-tail {
display: none;
}
}
}
}
.history_card {
width: 100%;
display: flex;
justify-content: space-between;
.img {
width: 143px;
height: 129px;
background: #D9D9D9;
border-radius: 0px 0px 0px 0px;
margin-top: -40px;
}
}
.font_style {
display: flex;
flex-direction: column;
width: 217px;
.title {
width: 100%;
font-weight: 500;
font-size: 32px;
color: #FF4600;
line-height: 38px;
text-align: left;
margin-top: -10px;
}
.content {
font-weight: 400;
font-size: 20px;
color: #000000;
line-height: 23px;
text-align: left;
margin-top: 10px;
}
}
</style>