需求:带滑动的图例

91 阅读4分钟

用户想要左右可以支持手动滑动的图例

实现方法一:

<template>
  <div class="color_slider">
    <div class="line"></div>
    <div
      v-if="!disabled"
      class="dot dot_left"
      :style="{ left: positionArr[0] + '%' }"
      @mousedown="mouseDown('left', $event)"
    />
    <div
      v-if="!disabled"
      class="dot"
      :style="{ left: positionArr[1] + '%' }"
      @mousedown="mouseDown('right', $event)"
    />
    <div
      v-for="(color, index) in getImageDataArray(colorConfig.imageData)"
      :key="color"
      class="color-item"
      :style="{ background: color }"
    >
      <!-- 合并框颠簸特殊处理 -->
      <template
        v-if="colorConfig.specialLabels && colorConfig.specialLabels.length"
      >
        <div
          v-if="colorConfig.specialLabels.some((item: any) => item.startIndex === index)"
          class="item-special"
          :style="{
            width: colorConfig.specialLabels.filter((item: any) => item.startIndex === index)[0].cols * 100 + '%'
          }"
        >
          {{
            colorConfig.specialLabels.filter(
              (item: any) => item.startIndex === index
            )[0].name
          }}
        </div>
      </template>
      <!-- label下标显示 -->
      <div
        v-else-if="colorConfig.labels && colorConfig.labels.length"
        class="item-label"
      >
        {{ colorConfig.labels[index - 1] }}
      </div>
      <!-- 显示成替换内容 -->
      <div
        v-else-if="colorConfig.otherLevels && colorConfig.otherLevels.length"
        class="item-value"
      >
        {{ colorConfig.otherLevels[index - 1] }}
      </div>
      <!-- 自定义显示哪些数字 -->
      <div
        v-else-if="colorConfig.defineLevels && colorConfig.defineLevels.length"
        class="item-value"
      >
        {{ showDefineLevels(index) }}
      </div>
      <div v-else class="item-value">
        {{ ifShowColorLabel(index) ? colorConfig.levels[index - 1] : "" }}
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { getImageDataArray } from "@/utils/common";
import { ref, computed, onMounted, reactive } from "vue";
const direction = ref<"left" | "right">("left");
const leftStartX = ref(0); // 开始 clientY
const rightStartX = ref(0); // 开始 clientY
const leftStartPosition = ref(0);
const rightStartPosition = ref(0);
const positionArr = reactive([0, 100]);
const stepArr = ref<Array<number>>([]);

const colorConfig = reactive({
  imageData: [
    170, 0, 170, 255, 130, 0, 122, 255, 77, 1, 115, 255, 2, 36, 116, 255, 2, 76,
    165, 255, 64, 111, 209, 255, 2, 166, 203, 255, 109, 225, 248, 255, 173, 229,
    243, 255, 254, 255, 196, 255, 213, 255, 172, 255, 171, 254, 89, 255, 255,
    253, 1, 255, 246, 202, 64, 255, 254, 126, 67, 255, 250, 63, 97, 255, 253, 0,
    2, 255, 175, 0, 0, 255,
  ],
  levels: [
    -32, -28, -24, -20, -16, -12, -8, -4, 0, 4, 8, 12, 16, 20, 24, 28, 32,
  ],
  colorStep: 2,
});

const emit = defineEmits(["change"]);
const positionIndex = computed(() => {
  if (stepArr.value.length) {
    return [
      stepArr.value.findIndex((i) => i === positionArr[0]),
      stepArr.value.findIndex((i) => i === positionArr[1]),
    ];
  } else {
    return [0, getImageDataArray(colorConfig.imageData).length - 1];
  }
});
const defineLevelsArray = computed(() => {
  if (colorConfig.defineLevels && colorConfig.defineLevels.length) {
    const array: Array<{ index: number; value: number }> = [];
    colorConfig.defineLevels!.forEach((num) => {
      const arr = colorConfig.levels.map((n) => Math.abs(n - num));
      const index = arr.findIndex((n) => n === Math.min(...arr));
      const obj = {
        index,
        value: num,
      };
      array.push(obj);
    });
    return array;
  } else {
    return [];
  }
});

onMounted(() => {
  const step = 100 / getImageDataArray(colorConfig.imageData).length;
  for (let i = 0; i <= getImageDataArray(colorConfig.imageData).length; i++) {
    stepArr.value.push(i * step);
  }

  setTimeout(() => {
    console.log("1113", colorConfig);
  }, 1000);
});
const ifShowColorLabel = (index: number) => {
  let step;
  if (colorConfig.colorStep) {
    step = colorConfig.colorStep;
  } else {
    const showLen = 10;
    const len = colorConfig.levels.length;
    step = Math.ceil(len / showLen);
  }
  if (step === 1) {
    return index !== 0;
  } else {
    return index % step === 1;
  }
};
const showDefineLevels = (index: number) => {
  const newIndex = index - 1;
  const findIndex = defineLevelsArray.value.findIndex(
    (obj) => obj.index === newIndex
  );
  if (findIndex === -1) {
    return "";
  } else {
    return defineLevelsArray.value[findIndex].value;
  }
};
const mouseDown = (d: "left" | "right", event: MouseEvent) => {
  if (d === "left") {
    leftStartX.value = event.clientX;
    leftStartPosition.value = positionArr[0];
  } else {
    rightStartX.value = event.clientX;
    rightStartPosition.value = positionArr[1];
  }
  direction.value = d;
  window.addEventListener("mousemove", onDragging);
  window.addEventListener("mouseup", () => {
    window.removeEventListener("mousemove", onDragging);
  });
};

const onDragging = (event: MouseEvent) => {
  const d = direction.value;
  const startX = d === "left" ? leftStartX.value : rightStartX.value;
  const startPosition =
    d === "left" ? leftStartPosition.value : rightStartPosition.value;
  const currentX = event.clientX;
  const clientWidth =
    document.getElementsByClassName("color_slider")[0].clientWidth;
  const diff = ((currentX - startX) / clientWidth) * 100;
  const newPosition = startPosition + diff;
  setPosition(d, newPosition);
};
const setPosition = (d: "left" | "right", position: number) => {
  let newPosition = 0;
  if (d === "left") {
    if (position < 0) {
      newPosition = 0;
    } else if (position >= stepArr.value[positionIndex.value[1] - 1]) {
      newPosition = stepArr.value[positionIndex.value[1] - 1];
    } else {
      newPosition = position;
    }
  } else {
    if (position > 100) {
      newPosition = 100;
    } else if (position <= stepArr.value[positionIndex.value[0] + 1]) {
      newPosition = stepArr.value[positionIndex.value[0] + 1];
    } else {
      newPosition = position;
    }
  }
  const arr = stepArr.value.map((num) => Math.abs(num - newPosition));
  const index = arr.findIndex((num) => num === Math.min(...arr));
  if (d === "left") {
    if (positionArr[0] !== stepArr.value[index]) {
      positionArr[0] = stepArr.value[index];
      emit("change", positionIndex.value);
    }
  } else {
    if (positionArr[1] !== stepArr.value[index]) {
      positionArr[1] = stepArr.value[index];
      emit("change", positionIndex.value);
    }
  }
};
</script>

<style scoped lang="scss">
.color_slider {
  height: 10px;
  width: 99%;
  margin-left: 2px;
  position: relative;
  display: flex;
  align-items: center;
  .color-item {
    flex: 1;
    height: 100%;
    position: relative;
    .item-label {
      position: absolute;
      bottom: -20px;
      width: 100%;
      // color: #fff;
      font-size: 12px;
      text-align: center;
      white-space: nowrap;
    }
    .item-value {
      position: absolute;
      // color: #fff;
      font-size: 12px;
      bottom: -20px;
      transform: translateX(-50%);
      z-index: 9999;
    }
    .item-special {
      position: absolute;
      // color: #fff;
      font-size: 12px;
      bottom: -20px;
      z-index: 9999;
      text-align: center;
      border-right: 1px dashed #ccc;
    }
    &:nth-child(5) {
      .item-special {
        border-left: 1px dashed #ccc;
      }
    }
  }
  .line {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    height: 1px;
    background-color: #fff;
    width: 100%;
  }
  .dot {
    position: absolute;
    z-index: 99;
    top: 50%;
    transform: translate(-95%, -50%);
    cursor: pointer;
    border-width: 7px;
    border-style: dashed solid dashed dashed;
    border-color: transparent #b3b3b3 transparent transparent;
    &_left {
      transform: translate(0, -50%);
      border-style: dashed dashed dashed solid;
      border-color: transparent transparent transparent #b3b3b3;
    }
  }
}
</style>

getImageDataArray方法:

// 将 imgaData 分成颜色数组 可以加透明度 0-1 之间
export function getImageDataArray(arr: number[], alpha = 1) {
  const newArr: string[] = [];
  let data: number[] = [];
  arr.forEach((num, i) => {
    const result = i % 4;
    if (result != 3) {
      data.push(num);
      if (result == 2) {
        const str =
          alpha != 1
            ? `rgba(${data.join(",")},${alpha})`
            : `rgb(${data.join(",")})`;
        newArr.push(str);
      }
    } else {
      data = [];
    }
  });
  if (arr[3] == 0) {
    // 如果是0,第一个就是透明色
    newArr[0] = "rgba(0,0,0,0)";
  }
  if (arr[arr.length - 1] == 0) {
    // 如果是0,第一个就是透明色
    newArr[newArr.length - 1] = "rgba(0,0,0,0)";
  }
  return newArr;
}

实现方法二:【这个还需要完善】

<template>
  <el-card
    class="slider-block"
    v-if="grades.values.length > 0"
    :body-style="getBodyStyle"
    style="z-index: 600"
  >
    <el-space direction="vertical" :size="vertical ? 20 : 8" alignment="center">
      <!-- <el-text type="info">{{ grades.desc }}({{ grades.unit }})</el-text> -->

      <!-- 实时显示当前范围 -->
      <el-text type="primary" size="small">{{ currentRangeLabel }}</el-text>

      <div class="slider-wrapper" :style="wrapperStyle">
        <!-- 纯色块色带 -->
        <div class="color-bar">
          <div
            v-for="(color, index) in grades.colors"
            :key="index"
            class="color-segment"
            :style="getSegmentStyle(index)"
          />
        </div>

        <!-- 滑动条 -->
        <el-slider
          v-model="sliderValue"
          :range="true"
          :marks="marks"
          v-bind="options"
          @change="onChange"
          @input="updateCurrentRange"
          class="sliderLegend"
        />
      </div>
    </el-space>
  </el-card>
</template>
<script setup>
import { ref, computed, reactive, defineProps, defineExpose } from "vue";

const props = defineProps({
  gradesValues: { type: Array, default: () => [0, 0.1, 1.5, 7, 15, 40, 50] },
  gradesColors: {
    type: Array,
    default: () => [
      "#dfe8ff",
      "#a6f28f",
      "#3dba3d",
      "#61b8ff",
      "#0000e1",
      "#fa00fa",
      "#800040",
    ],
  },
  vertical: { type: Boolean, default: false },
  width: { type: Number, default: 300 },
});

const emits = defineEmits(["update:modeValue"]);

const sliderValue = ref([0, 100]);
const currentRangeLabel = ref("");
const grades = reactive({
  values: props.gradesValues,
  colors: props.gradesColors,
  desc: "",
  unit: "",
});

const getBodyStyle = computed(() => {
  return props.vertical
    ? { padding: "10px 0 0", width: `${props.width}px` }
    : { padding: "10px", height: `${props.width}px` };
});

const wrapperStyle = computed(() => ({
  position: "relative",
  width: props.vertical ? "10px" : `${props.width}px`,
  height: props.vertical ? `${props.width}px` : "10px",
}));

const marks = computed(() => {
  const result = {};
  const step = 100 / (grades.values.length - 1);
  grades.values.forEach((v, i) => {
    result[Math.round(step * i)] = `${v}`;
  });
  return result;
});

const options = computed(() => ({
  min: 0,
  max: 100,
  step: 100 / (grades.values.length - 1),
  showTooltip: false,
  vertical: props.vertical,
  size: "small",
  style: {
    margin: props.vertical ? "0" : "10px 0 0",
    width: props.vertical ? undefined : `${props.width}px`,
    height: props.vertical ? `${props.width}px` : undefined,
  },
}));

function updateCurrentRange([min, max]) {
  const step = 100 / (grades.values.length - 1);
  const indexMin = Math.round(min / step);
  const indexMax = Math.round(max / step);
  const v1 = grades.values[indexMin];
  const v2 = grades.values[indexMax];
  currentRangeLabel.value = `${v1} ~ ${v2} ${grades.unit}`;
}

function onChange([min, max]) {
  const step = 100 / (grades.values.length - 1);
  const indexMin = Math.round(min / step);
  const indexMax = Math.round(max / step);
  emits("update:modeValue", [grades.values[indexMin], grades.values[indexMax]]);
}

function initControl(data) {
  Object.assign(grades, data);
  sliderValue.value = [0, 100];
  updateCurrentRange([0, 100]);
}

defineExpose({ initControl });

// 计算每一段的样式
const getSegmentStyle = (index) => {
  const step = 100 / grades.colors.length;
  const left = step * index;
  const width = step;

  const [start, end] = sliderValue.value;
  const isSelected = left + width <= start;

  return {
    position: "absolute",
    top: 0,
    bottom: 0,
    left: `${left}%`,
    width: `${width}%`,
    backgroundColor: isSelected ? "#ccc" : grades.colors[index],
    transition: "background-color 0.3s",
  };
};
</script>
<style scoped lang="scss">
.sliderLegend {
  ::v-deep(.el-slider__runway) {
    background: transparent;
  }
  ::v-deep(.el-slider__bar) {
    background: transparent;
  }
}

.color-bar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  border-radius: 4px;
  overflow: hidden;
  pointer-events: none;
  z-index: 1;
}

.color-segment {
  position: absolute;
  height: 100%;
}

.el-slider--small {
  margin-top: -6px;
}
</style>