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