插件版本:
"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组件:
<template><div class="o-timeline-area" :style="`width: ${width}px;`"><div class="o-timeline"><divv-for="(item, index) in timelineDesc":key="index":class="['o-timeline-item', { last: index === timelineDesc.length - 1 }]"><div class="o-tail" /><divclass="o-finish":style="{'--rate': item.ratePercent,'--borderColor': item.rate == 100 ? GREEN_COLOR : BLUE_COLOR}"/><div class="o-dot"><spanclass="solid-dot":class="item.rate == 100 ? 'green' : item.rate == 0 ? 'gray' : 'blue'"></span></div><el-tooltippopper-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?: stringtitle2: stringtitle3?: stringtitle: stringtext: stringratePercent: stringrate: number
}
interface IProps {width?: numberdisplay: '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组件
<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>
调用该时间轴组件(描述内容只在右边显示):
<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>
调用该时间轴组件(描述内容左右边岔开显示):
<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>