首页 前端知识 vue时间轴组件,支持交替展示

vue时间轴组件,支持交替展示

2025-03-13 15:03:20 前端知识 前端哥 413 588 我要收藏

首先是效果图

1. 组件概述

这个时间轴组件支持以下功能:

  • 支持展示多个时间轴项。
  • 每个时间轴项可以有不同的颜色、标题和描述。
  • 支持不同的排列模式(左对齐、右对齐、居中对齐)。
  • 自定义边框样式。
  • 通过插槽支持定制化的图标、内容等。
     

const ColorStyle = { 
  blue: '#1677ff',
  green: '#52c41a',
  red: '#ff4d4f',
  gray: '#00000040'
};

这里定义了默认颜色

2.computedwatchEffect

通过 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>

转载请注明出处或者链接地址:https://www.qianduange.cn//article/23496.html
标签
评论
会员中心 联系我 留言建议 回顶部
复制成功!