彩虹滚动条3

72 阅读2分钟
<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'

// Props 定义:组件的可配置参数
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()
    // 添加resize监听
    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) {
    // 创建临时canvas来获取渐变色
    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;
    /* 使用 all 确保所有变化都有过渡效果 */
}

.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>