<template>
<div class="ruler-wrapper" ref="containerRef">
<a-tooltip v-for="(item, index) in markers" :key="item.value" placement="leftBottom">
<template #title>
值: {{ item.value }}<br>
序号: {{ index + 1 }}
</template>
<div class="marker lollipop" :class="{
'is-hover': hoveredValue === item.value, // 当前悬停的标记
'is-dimmed': hoveredValue !== null && hoveredValue !== item.value // 非悬停标记变暗
}" :style="{
left: `${calculatePosition(item.value)}px`, // 根据值计算水平位置
zIndex: calculateZIndex(item.value, hoveredValue), // 控制层级,确保悬停项在最上层
color: item.color // 添加color属性,用于继承
}" @mouseenter="handleMouseEnter(item.value)" @mouseleave="handleMouseLeave">
<div class="lollipop-stick" :style="{ backgroundColor: item.color }"></div>
<div class="lollipop-head">
<img :src="item.imageUrl" :alt="'image-' + item.value" class="lollipop-image" />
</div>
</div>
</a-tooltip>
<div class="rainbow-ruler" :style="{ width: `${width}px` }" ref="rulerRef"></div>
<div class="scale-container" :style="{ width: `${width}px` }">
<div v-for="value in scaleValues" :key="value" class="scale-item"
:style="{ left: `${calculatePosition(value)}px` }">
{{ value }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch, nextTick, onUnmounted } from 'vue'
import { Tooltip as ATooltip } from 'ant-design-vue'
const props = defineProps({
minValue: { type: Number, default: -90 },
maxValue: { type: Number, default: -30 },
step: { type: Number, default: 5 },
markerList: {
type: Array,
default: () => [
{ value: -85, imageUrl: 'https://picsum.photos/16?random=1' },
{ value: -80, imageUrl: 'https://picsum.photos/16?random=2' },
{ value: -75, imageUrl: 'https://picsum.photos/16?random=3' },
{ value: -75, imageUrl: 'https://picsum.photos/16?random=4' },
{ value: -70, imageUrl: 'https://picsum.photos/16?random=5' },
{ value: -65, imageUrl: 'https://picsum.photos/16?random=6' },
{ value: -61, imageUrl: 'https://picsum.photos/16?random=7' },
{ value: -60, imageUrl: 'https://picsum.photos/16?random=8' },
{ value: -55, imageUrl: 'https://picsum.photos/16?random=9' },
{ value: -50, imageUrl: 'https://picsum.photos/16?random=10' },
{ value: -45, imageUrl: 'https://picsum.photos/16?random=11' },
{ value: -40, imageUrl: 'https://picsum.photos/16?random=12' },
{ value: -35, imageUrl: 'https://picsum.photos/16?random=13' }
]
}
})
const containerRef = ref(null)
const rulerRef = ref(null)
const hoveredValue = ref(null)
const markers = ref([])
const width = ref(800)
const scaleValues = computed(() => {
const values = []
for (let i = props.minValue; i <= props.maxValue; i += props.step) {
values.push(i)
}
return values
})
const calculatePosition = (value) => {
const range = props.maxValue - props.minValue
return ((value - props.minValue) / range) * width.value
}
const calculateZIndex = (value, hoveredValue) => {
return value === hoveredValue ? 2 : 1
}
const updateMarkerColors = () => {
if (!rulerRef.value || width.value === 0) return
markers.value = props.markerList.map(item => ({
...item,
color: getColorAtPosition(rulerRef.value, calculatePosition(item.value))
}))
}
const handleMouseEnter = (value) => hoveredValue.value = value
const handleMouseLeave = () => hoveredValue.value = null
const updateWidth = () => {
if (containerRef.value) {
width.value = containerRef.value.offsetWidth
nextTick(() => {
updateMarkerColors()
})
}
}
onMounted(() => {
updateWidth()
window.addEventListener('resize', updateWidth)
})
onUnmounted(() => {
window.removeEventListener('resize', updateWidth)
})
watch(() => props.markerList, () => {
nextTick(() => {
updateMarkerColors()
})
}, { deep: true })
function createGradient(ctx) {
const gradient = ctx.createLinearGradient(0, 0, width.value, 0)
gradient.addColorStop(0, 'red')
gradient.addColorStop(0.1666, 'orange')
gradient.addColorStop(0.3333, 'yellow')
gradient.addColorStop(0.5, 'green')
gradient.addColorStop(0.6666, 'cyan')
gradient.addColorStop(0.8333, 'blue')
gradient.addColorStop(1, 'violet')
return gradient
}
function getColorAtPosition(element, x) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = width.value
canvas.height = 1
const gradient = createGradient(ctx)
ctx.fillStyle = gradient
ctx.fillRect(0, 0, width.value, 1)
const pixelData = ctx.getImageData(x, 0, 1, 1).data
return `rgb(${pixelData[0]}, ${pixelData[1]}, ${pixelData[2]})`
}
</script>
<style scoped>
.ruler-wrapper {
position: relative;
width: 100%;
}
.rainbow-ruler {
height: 2px;
background: linear-gradient(to right,
red 0%,
orange 16.66%,
yellow 33.33%,
green 50%,
cyan 66.66%,
blue 83.33%,
violet 100%);
}
.scale-container {
position: relative;
height: 16px;
}
.scale-item {
position: absolute;
top: 3px;
transform: translateX(-50%);
font-size: 10px;
}
.marker.lollipop {
position: absolute;
bottom: 18px;
transform: translateX(-50%);
transition: all 0.3s ease;
}
.lollipop-stick {
width: 2px;
height: 10px;
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
transition: background-color 0.3s ease, filter 0.3s ease;
}
.lollipop-head {
width: 24px;
height: 24px;
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
justify-content: center;
background-color: transparent !important;
}
.lollipop-head::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 2px solid currentColor;
border-radius: 50%;
}
.lollipop-image {
width: 16px;
height: 16px;
object-fit: contain;
z-index: 1;
transition: all 0.3s ease;
}
.marker.lollipop.is-hover {
transform: translateX(-50%) scale(1.1);
z-index: 100;
}
.marker.lollipop.is-dimmed {
opacity: 0.4;
filter: grayscale(0.8);
}
</style>