vue3写一个简单的时间轴组件

插件版本:

"element-plus": "^2.3.12"

"vue": "^3.0.0"

代码示例:

样式文件style.less:

改变el-tooltip样式,可以复制代码到公共样式文件

复制代码
.el-popper.o-el-tooltip-popper-class {
    max-width: 300px;
    white-space: pre-wrap;
  }

MyTimeLineCol组件:

TypeScript 复制代码
<template>
  <div class="o-timeline-area" :style="`width: ${width}px;`">
    <div class="o-timeline">
      <div
        v-for="(item, index) in timelineDesc"
        :key="index"
        :class="['o-timeline-item', { last: index === timelineDesc.length - 1 }]"
      >
        <div class="o-tail" />
        <div
          class="o-finish"
          :style="{
            '--rate': item.ratePercent,
            '--borderColor': item.rate == 100 ? GREEN_COLOR : BLUE_COLOR
          }"
        />
        <div class="o-dot">
          <span
            class="solid-dot"
            :class="item.rate == 100 ? 'green' : item.rate == 0 ? 'gray' : 'blue'"
          ></span>
        </div>
        <el-tooltip
          popper-class="o-el-tooltip-popper-class"
          effect="dark"
          :content="`${item.title1 || ''} ${item.title2 || ''} ${item.title3 || '--'}${'\n'}${
            item.title
          } ${item.text}`"
          placement="top"
        >
          <div class="o-content">
            <div
              :class="[
                display === 'right-only'
                  ? 'o-content-right'
                  : index % 2 === 1
                  ? 'o-content-left'
                  : 'o-content-right',
                item.rate == 100 ? 'green' : item.rate == 0 ? 'gray' : 'blue'
              ]"
              :style="{
                '--width': display === 'right-only' ? '100%' : '50%'
              }"
            >
              <div
                :class="[
                  display === 'right-only'
                    ? 'o-ang-left'
                    : index % 2 === 0
                    ? 'o-ang-left'
                    : 'o-ang-right',
                  item.rate == 100 ? 'green' : item.rate == 0 ? 'gray' : 'blue'
                ]"
              ></div>
              <div class="time">
                <span class="time-title1">{{ item.title1 || '-' }}</span>
                <span class="time-title2">{{ item.title2 }}</span>
                <span class="time-title3">{{ item.title3 || '--' }}</span>
              </div>

              <div class="o-content-text">
                <span>
                  {{ item.title || '--' }}
                </span>
                <span>{{ item.text || '--' }}</span>
              </div>
            </div>
          </div>
        </el-tooltip>
      </div>
    </div>
  </div>
</template>
<script lang="ts" setup>
import { computed, defineProps, toRefs } from 'vue'
import { ElTooltip } from 'element-plus'

const BLUE_COLOR = '#1890ff'
const GREEN_COLOR = '#52c41a'
interface listItem {
  title1?: string
  title2: string
  title3?: string
  title: string
  text: string
  ratePercent: string
  rate: number
}
interface IProps {
  width?: number
  display: 'left-and-right' | 'right-only'
  timelineDesc: listItem[]
}
const props = defineProps<IProps>()
const { timelineDesc, display } = toRefs(props)
const dotLeft = computed(() => {
  if (display.value === 'left-and-right') {
    return '50%'
  }
  return '0%'
})
</script>
<style lang="less" scoped>
@blue: #1890ff;
@green: #52c41a;
@gray: #aaa;
@white: #fff;
@black: #333;
@dotWidth: 10px;
@dotHeight: 10px;
@dotLeft: v-bind(dotLeft);
@tailBorderWidth: 3px;
@itemHeight: 80px;
@angBorderWidth: 5px;
.o-timeline-area {
  margin: 0 auto;
  margin-top: @itemHeight / 2;
  .o-timeline {
    .o-timeline-item {
      position: relative;
      height: @itemHeight;
      .o-tail {
        position: absolute;
        top: @dotHeight;
        left: calc(@dotLeft - @tailBorderWidth / 2);
        height: calc(100% - @dotHeight);
        border-left: @tailBorderWidth solid @gray;
      }
      .o-finish {
        position: absolute;
        top: @dotHeight;
        left: calc(@dotLeft - @tailBorderWidth / 2);
        height: calc((100% - @dotHeight) * var(--rate));
        border-left: @tailBorderWidth solid var(--borderColor);
      }
      .o-dot {
        position: absolute;
        width: @dotWidth;
        height: @dotHeight;
        left: calc(@dotLeft - @dotWidth / 2);
        display: flex;
        justify-content: center;
        align-items: center;
      }
      .solid-dot {
        width: 100%;
        height: 100%;
        border-radius: 50%;
        background-color: @gray;
        &.green {
          background-color: @green;
        }
        &.blue {
          background-color: @blue;
        }
        &.gray {
          background-color: @gray;
        }
      }
      .o-content {
        height: 100%;
        display: flex;
        align-items: center;
      }
      .o-content-left,
      .o-content-right {
        position: relative;
        top: calc(@dotHeight / 2 - 50%);
        margin-left: 0px;
        word-break: break-all;
        word-wrap: break-word;
        font-size: 14px;
        font-weight: 400;
        background-color: rgba(@gray, 0.5);
        border-radius: 5px;
        padding: 5px 16px 10px;
        box-sizing: border-box;
        color: @gray;
        &.blue {
          background-color: rgba(@blue, 0.5);
          .time {
            color: @white;
            &-title1 {
              color: @black;
            }
          }
        }
        &.green {
          background-color: rgba(@green, 0.5);
          .time {
            color: @white;
            &-title1 {
              color: @black;
            }
          }
        }
        .time {
          display: inline-block;
          font-size: 14px;
          font-weight: 400;
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
          width: 100%;
          &-title1 {
            margin-right: 5px;
          }
          &-title2 {
            margin-right: 5px;
          }
          &-title3 {
            margin-right: 0px;
          }
        }
        &-text {
          display: flex;
          justify-content: space-between;
        }
      }
      .o-content-left {
        width: calc(var(--width) - @dotWidth - 10px);
        margin-right: 10px;
      }
      .o-content-right {
        width: calc(var(--width) - @dotWidth - 10px);
        margin-left: calc(@dotLeft + @dotWidth + 10px);
      }
      .o-ang-left,
      .o-ang-right {
        position: absolute;
        display: block;
        width: 0;
        height: 0;
        border-width: 5px;
        border-style: solid;
      }
      .o-ang-left {
        left: calc(0px - @angBorderWidth * 2);
        top: calc(50% - @angBorderWidth);
        border-color: transparent rgba(@gray, 0.5) transparent transparent;
        &.blue {
          border-color: transparent rgba(@blue, 0.5) transparent transparent;
        }
        &.green {
          border-color: transparent rgba(@green, 0.5) transparent transparent;
        }
      }
      .o-ang-right {
        right: calc(0px - @angBorderWidth * 2);
        top: calc(50% - @angBorderWidth);
        border-color: transparent transparent transparent rgba(@gray, 0.5);
        &.blue {
          border-color: transparent transparent transparent rgba(@blue, 0.5);
        }
        &.green {
          border-color: transparent transparent transparent rgba(@green, 0.5);
        }
      }
    }
    .last {
      .o-tail,
      .o-finish {
        display: none;
      }
    }
  }
}
</style>

TCard组件

TypeScript 复制代码
<template>
  <div class="t-card">
    <div class="t-card-view-header"
      ><span class="left-card-line"></span><span class="card-title">{{title}}</span
      ><button @click="gotoDetail">跳转详情</button>
    </div>
    <div class="t-card-box">
      <slot name="content"></slot>
    </div>
  </div>
</template>
<script lang="ts" setup>
defineProps<{
  title: string
}>()
const emit = defineEmits(['gotoDetail'])
const gotoDetail = () => {
  emit('gotoDetail')
}
</script>
<style lang="less" scoped>
.t-card {
  width: 100%;
  background-color: #fff;
  padding: 15px 16px 15px 16px;
  margin-bottom: 10px;
  border-radius: 10px;
  min-height: 215px;
}
.t-card-box {
  width: 100%;
  font-size: 14px;
}
.t-card-view-header {
  width: 100%;
  display: flex;
  align-items: center;
}
.left-card-line {
  display: inline-block;
  width: 5px;
  height: 30px;
  background-color: blue;
  margin-right: 10px;
}
.card-title {
  font-size: 20px;
  font-weight: 700;
  color: #333;
}
.card-title {
  flex: 1 1 0%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.right-arrow-class {
  font-size: 20px;
  color: #aaa;
  font-weight: 700;
}
</style>

调用该时间轴组件(描述内容只在右边显示):

TypeScript 复制代码
<template>
  <TCard title="xxx" @gotoDetail="gotoDetail">
    <template #content>
      <div class="o-main-box">
        <MyTimeLineCol :timelineDesc="timelineDesc" display="right-only" />
      </div>

      <div v-if="list.length > VISIBLE_NUM" class="operate" @click="handleClick">
        {{ flag == 'fold' ? '展开' : '收起' }}
        <van-icon v-if="flag == 'fold'" name="arrow-down" />
        <van-icon v-else-if="flag == 'open'" name="arrow-up" />
      </div>
    </template>
  </TCard>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import TCard from './TCard.vue'
import MyTimeLineCol from './MyTimeLineCol.vue'
const VISIBLE_NUM = 2;
const flag = ref('fold')
const visibleNum = ref<number>(VISIBLE_NUM)
  const list = ref([
  {
    title: '启动',
    title1: '标题1',
    title2: '2019-01-01~2019-12-30',
    title3: '已完成',
    rate: 100,
    currentRate: 0,
    text: '100%',
    ratePercent: 1,
  },
  {
    title: '需求确认',
    title1: '标题2',
    title2: '2020-01-01~2020-12-30',
    rate: 60,
    currentRate: 0,
    text: '60%',
    ratePercent: 0.6,
    title3: '需求确认中'
  },
  {
    title: '项目开发',
    title1: '标题3',
    title2: '2021-01-01~2021-12-30',
    rate: 0,
    currentRate: 0,
    text: '0%',
    ratePercent: 0.0,
    title3: '未开始'
  },
  {
    title: '功能测试',
    title1: '标题4',
    title2: '2022-01-01~2022-12-30', 
    rate: 0,
    currentRate: 0,
    text: '0%',
    ratePercent: 0,
    title3: '未开始'
  },
  {
    title: '上线',
    title1: '标题5',
    title2: '2023-01-01~2023-12-30', 
    rate: 0,
    currentRate: 0,
    text: '0%',
    ratePercent: 0,
    title3: '未开始'
  }
])
const timelineDesc = computed(() => {
  return list.value.slice(0, visibleNum.value)
})
const handleClick = () => {
  if (flag.value === 'fold') {
    flag.value = 'open'
    visibleNum.value = list.value.length
  } else {
    flag.value = 'fold'
    visibleNum.value = VISIBLE_NUM
  }
}
const gotoDetail = () => {
  alert('跳转详情')
}
</script>
<style lang="less" scoped>
.o-main-box {
  overflow-x: auto;
}
.operate {
  color: #9096a5;
  font-size: 12px;
  line-height: 20px;
  height: 20px;
  margin-top: 18px;
  text-align: center;
  cursor: pointer;
}
</style>

调用该时间轴组件(描述内容左右边岔开显示):

TypeScript 复制代码
<template>
  <TCard title="xxx" @gotoDetail="gotoDetail">
    <template #content>
      <div class="o-main-box">
        <MyTimeLineCol :timelineDesc="timelineDesc" display="left-and-right" />
      </div>

      <div v-if="list.length > VISIBLE_NUM" class="operate" @click="handleClick">
        {{ flag == 'fold' ? '展开' : '收起' }}
        <van-icon v-if="flag == 'fold'" name="arrow-down" />
        <van-icon v-else-if="flag == 'open'" name="arrow-up" />
      </div>
    </template>
  </TCard>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import TCard from './TCard.vue'
import MyTimeLineCol from './MyTimeLineCol.vue'
const VISIBLE_NUM = 2;
const flag = ref('fold')
const visibleNum = ref<number>(VISIBLE_NUM)
  const list = ref([
  {
    title: '启动',
    title1: '标题1',
    title2: '2019-01-01~2019-12-30',
    title3: '已完成',
    rate: 100,
    currentRate: 0,
    text: '100%',
    ratePercent: 1,
  },
  {
    title: '需求确认',
    title1: '标题2',
    title2: '2020-01-01~2020-12-30',
    rate: 60,
    currentRate: 0,
    text: '60%',
    ratePercent: 0.6,
    title3: '需求确认中'
  },
  {
    title: '项目开发',
    title1: '标题3',
    title2: '2021-01-01~2021-12-30',
    rate: 0,
    currentRate: 0,
    text: '0%',
    ratePercent: 0.0,
    title3: '未开始'
  },
  {
    title: '功能测试',
    title1: '标题4',
    title2: '2022-01-01~2022-12-30', 
    rate: 0,
    currentRate: 0,
    text: '0%',
    ratePercent: 0,
    title3: '未开始'
  },
  {
    title: '上线',
    title1: '标题5',
    title2: '2023-01-01~2023-12-30', 
    rate: 0,
    currentRate: 0,
    text: '0%',
    ratePercent: 0,
    title3: '未开始'
  }
])
const timelineDesc = computed(() => {
  return list.value.slice(0, visibleNum.value)
})
const handleClick = () => {
  if (flag.value === 'fold') {
    flag.value = 'open'
    visibleNum.value = list.value.length
  } else {
    flag.value = 'fold'
    visibleNum.value = VISIBLE_NUM
  }
}
const gotoDetail = () => {
  alert('跳转详情')
}
</script>
<style lang="less" scoped>
.o-main-box {
  overflow-x: auto;
}
.operate {
  color: #9096a5;
  font-size: 12px;
  line-height: 20px;
  height: 20px;
  margin-top: 18px;
  text-align: center;
  cursor: pointer;
}
</style>
相关推荐
计算机学姐40 分钟前
基于SpringBoot的社团管理系统【2026最新】
java·vue.js·spring boot·后端·mysql·spring·mybatis
烛阴1 小时前
解锁 TypeScript 的元编程魔法:从 `extends` 到 `infer` 的条件类型之旅
前端·javascript·typescript
前端开发爱好者2 小时前
弃用 ESLint + Prettier!快 35 倍的 AI 格式化神器!
前端·javascript·vue.js
wayhome在哪2 小时前
Cropper.js 轻松拿捏前端裁剪🤞
javascript·canvas·设计
&白帝&2 小时前
vue2和vue3的对比
javascript·vue.js·ecmascript
江东大都督周总2 小时前
rabbitmq集群
javascript·rabbitmq·ruby
vivi_and_qiao2 小时前
HTML的form表单
java·前端·html
一嘴一个橘子2 小时前
uniapp 顶部tab + 占满剩余高度的内容区域swiper
javascript·uni-app
wayhome在哪2 小时前
30KB 轻量王者!SortableJS 轻松搞定拖拽需求
javascript·设计·dom
骑驴看星星a3 小时前
Vue中的scoped属性
前端·javascript·vue.js