前端实践:打造高度可定制的Vue3时间线组件——图标、节点与连接线的个性化配置

发布于:2025-05-15 ⋅ 阅读:(20) ⋅ 点赞:(0)

前言
在项目开发中,我需要实现一个支持自定义图标、节点颜色和辅助线的时间轴组件。经过多方搜索,发现现有方案都无法完全满足需求,于是决定自行开发。现将实现过程记录下来,希望能为遇到类似需求的开发者提供参考。

效果预览(以工作经历&教育经历为例)
在这里插入图片描述
核心特性

  1. 完全可定制:图标、节点颜色、连接线独立配置
  2. 响应式布局:完美适配不同内容长度

组件使用指南

<template>
	<stepBar type="work" :stepList="handelData(infoDetail.resumeWorkExpList, 'work')" />
	<stepBar class="mt-8" type="education" :stepList="handelData(infoDetail.resumeEduExpList, 'education')" />
</template>
import educationIcon from '@/assets/img/ai/education_icon.png'
import companyIcon from '@/assets/img/ai/company_icon.png'
import { stepBar } from './Components'
const educationIconImg = educationIcon
const companyIconImg  = companyIcon
const handelData = (data, type) => {
  let stepList = []
  if (type === 'work') {
    data.forEach((item) => {
      const timeStr = [];
      if (item.workStartDate) timeStr.push(item.workStartDate);
      if (item.workEndDate) timeStr.push(item.workEndDate);
      const parts = [];
      if (item.workCompany) parts.push(item.workCompany);
      if (item.workJobName) parts.push(item.workJobName);
      stepList.push({
        icon: companyIconImg,
        time: timeStr.join(' - '),
        desc:  parts.join(' · '),
        linColor: '#7AC3FF',
        dotColor: '#006EF0'
      })
    })
  } else {
    data.forEach((item) => {
      const timeStr = [];
      if (item.eduStartDate) timeStr.push(item.eduStartDate);
      if (item.eduEndDate) timeStr.push(item.eduEndDate);
      const parts = [];
      if (item.schoolName) parts.push(item.schoolName);
      if (item.speciality) parts.push(item.speciality);
      const educationLabel = translateDict(item.education, dictStore.dicts['topEduDegree']);
      parts.push(educationLabel);
      stepList.push({
        icon: educationIconImg,
        time: timeStr.join(' - '),
        desc: parts.join(' · '),
        linColor: '#B9E4D6',
        dotColor: '#19B383'
      })
    })
  }
  return stepList
}

组件实现解析
核心代码结构

<template>
  <div class="timeline-container">
    <div v-for="(item, index) in stepList" :key="index" class="timeline-item"
      :class="{ 'last-item': index === stepList.length - 1 }">
      <img :src="item.icon" v-if="index === 0" class="current-icon"/>
      <div v-else class="timeline-dot" :style="{ background:item.dotColor}"></div>
      <div class="text-content ml-12">
        <span class="time-label">{{ item.time }}</span>
        <span class="desc-text">{{ item.desc }}</span>
      </div>
      <div v-if="index !== stepList.length - 1" class="timeline-line" :style="{ background:item.linColor}"></div>
    </div>
  </div>
</template>

<script setup>
const props = defineProps({
  type: {
    type: String,
    default: 'work'
  },
  stepList: {
    type: Array,
    default: () => []
  }
})

</script>

<style lang="scss" scoped>
.timeline-container {
  width: 100%;
  padding-left: 3px;
  box-sizing: border-box;
}

.timeline-item {
  position: relative;
  margin-bottom: 8px;
  min-height: 20px;
  /* background: red; */
}
.text-content{
  display: flex;
  
}
.time-label {
  font-size: 14px;
  font-weight: normal;
  line-height: 20px;
  letter-spacing: 0px;
  color: #606266;
  flex-shrink: 0;
  min-width: 118px;
}

.desc-text {
  font-size: 14px;
  font-weight: normal;
  line-height: 20px;
  letter-spacing: 0px;
  color: #303133;
}

.timeline-dot {
  position: absolute;
  left: -3px;
  top: 5px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  z-index: 2;
}
.current-icon{
  position: absolute;
  left: -8px;
  top: -2px;
  width: 16px;
  height: 15px;
}
.timeline-line {
  position: absolute;
  left: -1px;
  top: 11px;
  bottom: -15px;
  width: 2px;
}

.last-item {
  margin-bottom: 0;
}
</style>