插件版本:
"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>