首先是效果图
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>
复制