6 changed files with 617 additions and 63 deletions
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 803 B |
@ -0,0 +1,274 @@ |
|||
<template> |
|||
<div :class='["TimeLine1", { "auto-size": autoSize }]' ref="TimeLine1Ref"> |
|||
<!-- 节点 --> |
|||
<div class="node" v-for="(item, index) in data" :key="index" ref="nodeRefs"> |
|||
<span class="top-label keep-ratio-bottom"> |
|||
<slot name="bottom-label" :data="item"> |
|||
{{ item.time }} |
|||
</slot> |
|||
</span> |
|||
<div class="center"> |
|||
<div class="line" v-if="!index" :style="{ width: `${nodeLinesWidth[-1]}px` }" /> |
|||
<div class="circle keep-ratio" :style="{ '--active-color': !item.isActive ? normalColor : activeColor }"> |
|||
</div> |
|||
<div class="line" :style="{ |
|||
width: `${nodeLinesWidth[index]}px`, |
|||
borderImage: getBorderImageStyle(item), |
|||
}" /> |
|||
</div> |
|||
<slot name="bottom-label" :data="item"> |
|||
<div class="bottom keep-ratio" @click="onClick(item)" |
|||
:style="{ backgroundImage: `url(${require(`./images/bg${item.isActive ? '-active' : ''}.svg`)})` }"> |
|||
{{ item.label }} |
|||
</div> |
|||
</slot> |
|||
</div> |
|||
|
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
|
|||
function getPositionAtCenter(element) { |
|||
const { top, left } = element.getBoundingClientRect(); |
|||
|
|||
return { |
|||
x: left, |
|||
y: top |
|||
}; |
|||
} |
|||
|
|||
function getDistanceBetweenElements(domA, domB) { |
|||
const domAPosition = getPositionAtCenter(domA); |
|||
const domBPosition = getPositionAtCenter(domB); |
|||
|
|||
return Math.hypot(domAPosition.x - domBPosition.x, domAPosition.y - domBPosition.y); |
|||
} |
|||
|
|||
export default { |
|||
name: 'TimeLine1', |
|||
props: { |
|||
data: { |
|||
type: Array, |
|||
default: () => Array.from({ length: 15 }).map(() => ({ |
|||
time: "16.36", |
|||
label: "阿发", |
|||
isActive: false |
|||
})) |
|||
}, |
|||
activeColor: { |
|||
type: String, |
|||
default: "#39D5BF" |
|||
}, |
|||
normalColor: { |
|||
type: String, |
|||
default: "#ccc" |
|||
}, |
|||
lineActiveColor: { |
|||
type: String, |
|||
default: "linear-gradient(90deg, rgba(59, 216, 188, 1), rgba(29, 171, 215, 1)}) 2 2" |
|||
}, |
|||
lineNormalColor: { |
|||
type: String, |
|||
default: null |
|||
}, |
|||
// 自动适配宽度 |
|||
autoSize: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
filterDistance: { |
|||
type: Function, |
|||
default: null |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
nodeLinesWidth: {} |
|||
} |
|||
}, |
|||
watch: { |
|||
data: { |
|||
handler(data) { |
|||
let needFilter = false |
|||
const nodeLinesWidth = []; |
|||
|
|||
const filterDistance = num => needFilter ? this.filterDistance(num) : num; |
|||
|
|||
const removeDistance = 20 + 4 * 2; |
|||
|
|||
function getDistance(index) { |
|||
return filterDistance(getDistanceBetweenElements( |
|||
this.$refs.nodeRefs[index].querySelector('.center'), |
|||
this.$refs.nodeRefs[index + 1].querySelector('.center')) |
|||
) - removeDistance |
|||
}; |
|||
|
|||
function getSpecialDistance(index) { |
|||
return filterDistance(getDistanceBetweenElements( |
|||
this.$refs.nodeRefs[index].parentElement, |
|||
this.$refs.nodeRefs[index].querySelector('.center')) |
|||
) - removeDistance |
|||
} |
|||
|
|||
const getLineWidths = () => { |
|||
nodeLinesWidth.length = 0; |
|||
|
|||
data.forEach((_, index) => { |
|||
if (index === data.length - 1) { |
|||
if (this.autoSize) { |
|||
nodeLinesWidth[-1] = getSpecialDistance.call(this, 0) |
|||
nodeLinesWidth[data.length - 1] = getSpecialDistance.call(this, data.length - 1) |
|||
} |
|||
|
|||
return |
|||
}; |
|||
|
|||
nodeLinesWidth[index] = getDistance.call(this, index); |
|||
}); |
|||
|
|||
this.nodeLinesWidth = nodeLinesWidth; |
|||
} |
|||
|
|||
this.$nextTick(() => { |
|||
const timeLine1RefDom = this.$refs.TimeLine1Ref; |
|||
if (timeLine1RefDom.clientWidth != timeLine1RefDom.getBoundingClientRect().width) { |
|||
needFilter = true |
|||
} |
|||
getLineWidths(); |
|||
}) |
|||
}, |
|||
immediate: true |
|||
} |
|||
}, |
|||
methods: { |
|||
getBorderImageStyle(item) { |
|||
const linearColor = item.isActive ? (this.lineActiveColor || `${this.activeColor}, ${this.activeColor}`) : (this.lineNormalColor || `${this.normalColor}, ${this.normalColor}`) |
|||
|
|||
return `linear-gradient(90deg, ${linearColor}) 2 2` |
|||
}, |
|||
onClick(item) { |
|||
this.data.forEach(it => { |
|||
if (it.id == item.id) { |
|||
it.isActive = true; |
|||
} else { |
|||
it.isActive = false; |
|||
} |
|||
}) |
|||
this.$emit('update:tableData', item.id); |
|||
// console.log('data', item) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang='scss' scoped> |
|||
div.auto-size { |
|||
justify-content: space-between; |
|||
overflow: hidden; |
|||
|
|||
.node { |
|||
flex: 1; |
|||
min-width: auto !important; |
|||
|
|||
&:first-child, |
|||
&:last-child { |
|||
.line { |
|||
width: auto; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.TimeLine1 { |
|||
color: #fff; |
|||
width: auto; |
|||
|
|||
font-size: 14px; |
|||
font-family: PingFang SC, PingFang SC; |
|||
font-weight: 400; |
|||
color: #FFFFFF; |
|||
display: flex; |
|||
flex-wrap: nowrap; |
|||
overflow: auto; |
|||
gap: 15px; |
|||
padding-left: 15px; |
|||
|
|||
.node { |
|||
display: flex; |
|||
align-items: center; |
|||
// justify-content: center; |
|||
flex-direction: column; |
|||
gap: 3px; |
|||
min-width: 90px; |
|||
|
|||
.top-label { |
|||
line-height: 16px; |
|||
height: 24px; |
|||
} |
|||
|
|||
.center { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
gap: 6px; |
|||
position: relative; |
|||
|
|||
.circle { |
|||
border: 1px solid var(--active-color, #39D5BF); |
|||
border-radius: 50%; |
|||
width: 15px; |
|||
height: 15px; |
|||
position: relative; |
|||
|
|||
&::before { |
|||
content: ""; |
|||
position: absolute; |
|||
width: 72%; |
|||
height: 72%; |
|||
border-radius: 50%; |
|||
left: 50%; |
|||
top: 50%; |
|||
transform: translate(-50%, -50%); |
|||
background-color: var(--active-color, #39D5BF); |
|||
} |
|||
} |
|||
|
|||
|
|||
.line { |
|||
position: absolute; |
|||
height: 0px; |
|||
opacity: 1; |
|||
border: 2px solid; |
|||
//border-image: linear-gradient(90deg, rgba(59, 216, 188, 1), rgba(29, 171, 215, 1)) 2 2; |
|||
border-image: linear-gradient(90deg, rgb(204, 204, 204), rgb(204, 204, 204)) 2 / 1 / 0 stretch; |
|||
|
|||
&:first-child { |
|||
right: calc(50% + 8px + 6px); |
|||
} |
|||
|
|||
&:last-child { |
|||
left: calc(50% + 8px + 6px); |
|||
} |
|||
} |
|||
} |
|||
|
|||
&:first-child, |
|||
&:last-child { |
|||
.line { |
|||
width: 60px; |
|||
} |
|||
} |
|||
|
|||
.bottom { |
|||
line-height: 16px; |
|||
background-repeat: no-repeat; |
|||
background-size: 100% 100%; |
|||
min-width: 90px; |
|||
text-align: center; |
|||
padding: 6px 12px; |
|||
height: 27px; |
|||
} |
|||
} |
|||
} |
|||
</style> |
Loading…
Reference in new issue