Vue3时间轴(Timeline)

简介: 这是一个基于 Vue2 的时间轴(Timeline)组件,支持多种自定义属性,包括时间轴内容数组 `timelineData`、总宽度 `width`、线条样式 `lineStyle`、模式 `mode` 和位置 `position`。时间轴内容数组包含描述 `desc` 和圆圈颜色 `color`。组件提供了丰富的样式选项,如虚线、居中显示等,并支持内容交替展现。适用于多种场景下的时间轴展示需求。

可自定义设置以下属性:

  • 时间轴内容数组(timelineData),类型:Array<{desc: string, color?: string}>,默认 []

  • 时间轴区域总宽度(width),类型:number|string,单位 px,默认 '100%'

  • 时间线样式(lineStyle),类型: 'solid' | 'dashed' | 'dotted',默认 'solid'

  • 通过设置 mode 可以改变时间轴和内容的相对位置(mode),类型:'left' | 'center' | 'right',默认 'left

  • 当 mode 为 center 时,内容交替展现,内容从左边(left)开始或者右边(right)开始展现(position),类型:'left' | 'right',默认:'left'

时间轴内容数组(TimelineData):

  • desc:文字描述,类型:string | slot,默认 undefined

  • color?:圆圈颜色,类型:'blue' | 'green' | 'red' | 'gray' | string,默认 'blue'

效果如下图:在线预览

①创建时间轴组件Timeline.vue:

<script setup lang="ts">
import { ref, computed, watchEffect } from 'vue'
interface Data {
  desc: string // 文字描述 string | slot
  color?: 'blue' | 'green' | 'red' | 'gray' | string // 圆圈颜色,默认值 blue
}
interface Props {
  timelineData?: Data[] // 时间轴内容数组
  width?: number | string // 时间轴区域总宽度,单位 px
  lineStyle?: 'solid' | 'dashed' | 'dotted' // 时间线样式
  mode?: 'left' | 'center' | 'right' // 通过设置 mode 可以改变时间轴和内容的相对位置
  position?: 'left' | 'right' // 当 mode 为 center 时,内容交替展现,内容从左边(left)开始或者右边(right)开始展现
}
const props = withDefaults(defineProps<Props>(), {
  timelineData: () => [],
  width: '100%',
  lineStyle: 'solid',
  mode: 'left',
  position: 'left'
})
enum ColorStyle { // 颜色主题对象
  blue = '#1677ff',
  green = '#52c41a',
  red = '#ff4d4f',
  gray = '#00000040'
}
const desc = ref()
const dotsHeight = ref<string[]>([])
const totalWidth = computed(() => {
  if (typeof props.width === 'number') {
    return props.width + 'px'
  } else {
    return 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')
          }
        } 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>
            <span class="dot-item" v-else-if="data.color === 'gray'" :style="{ borderColor: ColorStyle.gray }"></span>
            <span class="dot-item" v-else-if="data.color === 'green'" :style="{ borderColor: ColorStyle.green }"></span>
            <span class="dot-item" v-else-if="data.color === 'blue'" :style="{ borderColor: ColorStyle.blue }"></span>
            <span class="dot-item" v-else :style="{ borderColor: data.color || ColorStyle.blue }"></span>
          </slot>
        </div>
        <div ref="desc" :class="`timeline-desc desc-${mode}`">
          <slot name="desc" :index="index">{
  
  { data.desc || '--' }}</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: #e8e8e8;
      }
      .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: 12px;
          height: 12px;
          border-width: 2px;
          border-style: solid;
          border-radius: 50%;
          background: #fff;
        }
      }
      .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;
      }
    }
  }
}
</style>

②在要使用的页面引入:

<script setup lang="ts">
import Timeline from './Timeline.vue'
import { ref } from 'vue'

const timelineData = ref([
  {
    desc: 'Create a services site 2023-05-24',
    color: 'green'
  },
  {
    desc: 'Solve initial network problems 1 Solve initial network problems 2 2023-05-24',
    color: 'red'
  },
  {
    desc: 'Technical testing 2023-05-24',
    color: 'blue'
  },
  {
    desc: 'Network problems being solved 2023-05-24'
  },
  {
    desc: 'Network problems being solved 2',
    color: 'gray'
  }
])
</script>
<template>
  <div>
    <h1>Timeline 时间轴</h1>
    <h2 class="mt30 mb10">基本使用</h2>
    <Timeline :timeline-data="timelineData" />
    <h2 class="mb10">自定义样式</h2>
    <Timeline :timeline-data="timelineData">
      <template #dot="{ index }">
        <span class="big-dot" v-if="index===2"></span>
        <svg focusable="false" v-if="index===3" class="u-icon" data-icon="clock-circle" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"></path><path d="M686.7 638.6L544.1 535.5V288c0-4.4-3.6-8-8-8H488c-4.4 0-8 3.6-8 8v275.4c0 2.6 1.2 5 3.3 6.5l165.4 120.6c3.6 2.6 8.6 1.8 11.2-1.7l28.6-39c2.6-3.7 1.8-8.7-1.8-11.2z"></path></svg>
      </template>
      <template #desc="{ index }">
        <p class="desc" v-if="index===2">Create a services site</p>
      </template>
    </Timeline>
    <h2 class="mb10">使用虚线</h2>
    <Timeline :timeline-data="timelineData" line-style="dashed" />
    <h2 class="mb10">右侧时间轴点</h2>
    <Timeline :timeline-data="timelineData" mode="right" :width="500" />
    <h2 class="mb10">中间时间轴点</h2>
    <h3 class="mb10">内容从左边开始交替展现</h3>
    <Timeline :timeline-data="timelineData" mode="center" :width="500">
      <template #dot="{ index }">
        <span class="big-dot" v-if="index===2"></span>
      </template>
    </Timeline>
    <h3 class="mb10">内容从右边开始交替展现</h3>
    <Timeline :timeline-data="timelineData" mode="center" position="right" :width="500">
      <template #dot="{ index }">
        <span class="big-dot" v-if="index===2"></span>
      </template>
    </Timeline>
  </div>
</template>
<style lang="less" scoped>
.big-dot {
  display: inline-block;
  width: 18px;
  height: 18px;
  border: 4px solid #1677ff;
  border-radius: 50%;
  background: #FFF;
}
.u-icon {
  fill: #1668dc;
  background: #fff;
  border-radius: 50%;
}
.desc {
  font-size: 16px;
  font-weight: 500;
}
</style>
相关文章
|
13天前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
117 64
|
13天前
|
JavaScript 前端开发 API
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
|
13天前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
24 8
|
13天前
|
存储 JavaScript 数据管理
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
17 1
|
13天前
|
JavaScript
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
25 1
|
13天前
|
JavaScript
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
|
1月前
|
存储 JavaScript 前端开发
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
【10月更文挑战第21天】 vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
|
22天前
|
JavaScript 索引
Vue 3.x 版本中双向数据绑定的底层实现有哪些变化
从Vue 2.x的`Object.defineProperty`到Vue 3.x的`Proxy`,实现了更高效的数据劫持与响应式处理。`Proxy`不仅能够代理整个对象,动态响应属性的增删,还优化了嵌套对象的处理和依赖追踪,减少了不必要的视图更新,提升了性能。同时,Vue 3.x对数组的响应式处理也更加灵活,简化了开发流程。
|
1月前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
65 7
|
1月前
|
前端开发 数据库
芋道框架审批流如何实现(Cloud+Vue3)
芋道框架审批流如何实现(Cloud+Vue3)
71 3