Element Plus 组件库实现:12. Timeline组件

577 阅读5分钟

前言

时间是人类最忠实的记录者,而在数字世界中,Timeline(时间轴) 则是将抽象的时间流转转化为可视化叙事的重要工具。无论是项目里程碑的追溯、用户动态的串联,还是历史事件的回放,Timeline 组件都能以清晰的逻辑与优雅的交互,让用户直观感知时间的脉络。

本文将简单介绍时间轴组件的基本实现。

开始

需求分析

回顾日常开发场景,Timeline 组件的核心结构往往由三个元素构成:

  • 时间节点
    • 节点要和内容一一对应
  • 轴线
    • 贯穿所有节点
  • 内容容器
    • 展示内容的盒子

普通时间轴一般没有和用户交互的功能,所以这里先不做讨论,仅仅只介绍实现时间轴的方案以及属性,所以接下来要做的就是:

  • 确定结构
  • 确定属性

确定方案

属性

TimelineItemProps

// type.ts
import TimelineItem from './TimeLineItem.vue'

type TimelineItemType = 'primary' | 'success' | 'warning' | 'danger' | 'info'

export interface TimelineItemProps {
  // 时间戳
  timestamp?: string
  // 是否隐藏时间戳
  hideTimestamp?: boolean
  // 是否垂直居中
  center?: boolean
  // 时间戳位置
  placement?: 'top' | 'bottom'
  // 节点类型
  type?: TimelineItemType | string
  // 节点颜色
  color?: string
  // 节点大小
  size?: 'normal' | 'large' | string
  // 图标
  icon?: string
  // 是否为空心圆点
  hollow?: boolean
}

export type TimelineItemInstance = InstanceType<typeof TimelineItem>

时间轴组件由两部分组成,即TimelineTimelineItem,在使用的时候向TimelineItem传递props即可,所以只需要定义TimelineItemProps的接口。

组件

  • Timeline

我看了下Element Plus官方Timeline组件是通过render函数的方式来定义的,并且Elemen UI的时候还有反转节点的功能,但是Element Plus的时候就取消了这个功能,使用渲染函数的时候是为了方便节点的reverse,即slots.reverse() (这里是我的个人见解,如有错误还请大佬评论区批评指正),具体细节可以阅读Element两个组件库的源码,这里不做详细介绍。

我们这里的组件同样也没有反转的功能,也就不使用render函数的方式,直接使用模版来定义组件:

// Timeline.vue
<template>
  <ul class="yv-timeline">
    <slot />
  </ul>
</template>

然后是TimelineItem

// TimelineItem.vue
<template>
  <li class="yv-timeline-item" :class="{ 'yv-timeline-item-center': center }">
   <!-- 时间线 -->
    <div class="yv-timeline-item-tail"></div>
    <div
      v-if="!$slots.dot"
      class="yv-timeline-item-node"
      :class="{
        [`yv-timeline-item-node--${size}`]: size,
        [`yv-timeline-item-node--${type}`]: type,
        ['yv-timeline-item-node--hollow']: hollow
      }"
      :style="{
        backgroundColor: color
      }"
    >
      <!-- 时间轴节点icon -->
      <Icon v-if="icon" class="yv-timeline-item-icon" :icon="icon"></Icon>
    </div>
    <!-- 时间轴节点内容 -->
    <div v-if="$slots.dot" class="yv-timeline-dot">
      <slot name="dot" />
    </div>
    <!-- 时间轴内容 -->
    <div class="yv-timeline-item-wrapper">
      <!-- 时间戳下方展示 -->
      <div
        v-if="!hideTimestamp && placement === 'top'"
        class="yv-timeline-item-timestamp yv-timeline-item-timestamp-top"
      >
        {{ timestamp }}
      </div>
      <!-- 文本内容 -->
      <div class="yv-timeline-item-content">
        <slot />
      </div>
      <!-- 时间戳下方展示-->
      <div
        v-if="!hideTimestamp && placement === 'bottom'"
        class="yv-timeline-item-timestamp yv-timeline-item-timestamp-bottom"
      >
        {{ timestamp }}
      </div>
    </div>
  </li>
</template>

代码实现

<script setup lang="ts">
import { defineOptions, withDefaults, defineProps } from 'vue'
import Icon from '../Icon/Icon.vue'
import type { TimelineItemProps } from './types'
defineOptions({
  name: 'YvTimelineItem'
})

withDefaults(defineProps<TimelineItemProps>(), {
  timestamp: '',
  hideTimestamp: false,
  center: false,
  placement: 'bottom',
  size: 'normal',
  hollow: false
})
</script>

由于本组件仅有展示功能, 所以不需要其他逻辑代码,仅定义props默认属性即可。

css样式可以直接参考Element官方的样式,这里同样不做讨论。

测试

deepseek真的超级好用,所以这里就尝试用deepseek来写测试用例:

import { describe, test, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import YvTimelineItem from '../TimelineItem.vue'
import Icon from '../../Icon/Icon.vue'

describe('时间轴条目组件测试', () => {
  test('应该渲染组件', () => {
    const wrapper = mount(YvTimelineItem)
    expect(wrapper.find('.yv-timeline-item')).toBeTruthy()
  })

  test('应该包含默认属性', () => {
    const wrapper = mount(YvTimelineItem)
    expect(wrapper.props('timestamp')).toEqual('')
    expect(wrapper.props('hideTimestamp')).toEqual(false)
    expect(wrapper.props('center')).toEqual(false)
    expect(wrapper.props('placement')).toEqual('bottom')
    expect(wrapper.props('size')).toEqual('normal')
    expect(wrapper.props('hollow')).toEqual(false)
  })

  test('当不隐藏时间戳且位置在顶部时应该显示时间戳', () => {
    const wrapper = mount(YvTimelineItem, {
      propsData: {
        timestamp: '测试时间',
        placement: 'top'
      }
    })
    expect(wrapper.find('.yv-timeline-item-timestamp-top').text()).toEqual(
      '测试时间'
    )
  })

  test('当不隐藏时间戳且位置在底部时应该显示时间戳', () => {
    const wrapper = mount(YvTimelineItem, {
      propsData: {
        timestamp: '测试时间',
        placement: 'bottom'
      }
    })
    expect(wrapper.find('.yv-timeline-item-timestamp-bottom').text()).toEqual(
      '测试时间'
    )
  })

  test('当提供dot插槽时应该渲染自定义内容', () => {
    const wrapper = mount(YvTimelineItem, {
      slots: {
        dot: '<div>测试圆点</div>'
      }
    })
    expect(wrapper.find('.yv-timeline-dot').html()).toContain(
      '<div>测试圆点</div>'
    )
  })

  test('当提供图标属性时应该显示图标', () => {
    const wrapper = mount(YvTimelineItem, {
      propsData: {
        icon: '测试图标'
      },
      global: {
        stubs: ['Icon']
      }
    })
    const iconElement = wrapper.findComponent(Icon)
    expect(iconElement.exists()).toBeTruthy()

    expect(wrapper.find('.yv-timeline-item-icon')).toBeTruthy()
  })
})

测试结果:

image.png ai真的好用啊,用的越舒服越感觉自己找不到工作了,越感觉自己要被替代了😭😭😭。

总结

本文主要介绍了Timeline组件的实现,算是组件库里边一个比较简单实现的组件了,写完结构直接套用Element Plus官方大大的样式就行了。

题外话:已经好久没有更新写组件库的文章了,一直也没时间,从去年6月开始第一段实习,9月回学校开始秋招,秋招差点就有收获了,结果因为逆天的,,,,,,,没法出去,详情见这里吧 ,所以秋招等于颗粒无收,后边寒假又找了一个实习。

今天有时间更新这个组件是因为上周的一个需求需要时间轴,我当时没想到自己实现,直接用图片代替(也是服了自己,被自己蠢到了),后边还是mt帮我写的,之后移动端我就会写了,今天是实习lastday,实现一下Timeline组件也算是对mt教给我的东西的一个总结了。

后边就要继续春招了,各位大佬们有没有内推啊,bg双非本+非科班,buff叠满,一段中厂实习+一段小厂实习,求捞捞,然后组件库后边会继续更新的(速度应该会很慢),只不过不知道更新哪一个了,大家可以在评论区留下想看的组件库让俺参考参考参考。