前言
图例不只能够帮助我们在查看地图的时候更加方便容易地分辨不同颜色代表的要素,本文要介绍的图例组件还可以按需求过滤掉不用显示的要素,使地图的更能清晰的显示我们需要显示的内容
技术核心
说到过滤要素,第一时间想到的就是滑块组件,这里我们选择了Element-plus的Slider 滑块组件,主要功能就是根据滑块背景的不同颜色,滑动滑块过滤掉滑块范围以外背景颜色的要素。这里遇到一个大麻烦,本来以为很容易解决的,后面发现没有这么容易完成,就是滑块的背景颜色添加,这里想了好几种方式,最后还是只能使用原生js通过操作dom实现背景颜色的添加和上下两个滑块遮罩的添加(将要过滤的颜色范围遮盖掉)。核心代码如下:
watchEffect(() => {
let [min, max] = sliderValue.value
//为了添加滑块的遮罩,达到想要的效果(重点)
nextTick(() => {
let runway = document.getElementsByClassName('sliderLegend')?.[0]?.querySelectorAll('.el-slider__runway')[0]
if (!runway.querySelector('.maxBar')) {
let div = document.createElement('div')
div.setAttribute('class', 'maxBar maskBar')
runway.appendChild(div)
}
if (!runway.querySelector('.minBar')) {
let div = document.createElement('div')
div.setAttribute('class', 'minBar maskBar')
runway.appendChild(div)
}
let maxMaskBar = document.querySelector('.maxBar')
let minMaskBar = document.querySelector('.minBar')
if (maxMaskBar) {
maxMaskBar.style.height = new BigNumber(100).minus(max).toNumber() + '%'
}
if (minMaskBar) {
minMaskBar.style.height = min + '%'
}
})
})
完整代码
/**
* sliderLegend.vue 可以拖动滑块过滤的图例
* @Author ZhangJun
* @Date 2024/1/19 9:36
**/
<template>
<el-card class='slider-block absolute right-10 bottom-10'
v-if='grades.values&&grades.values.length>0'
style='z-index: 1000;'>
<el-space direction='vertical' :size='20'>
<el-text size='large' type='info'>{{ grades.desc }}({{ grades.unit }})</el-text>
<div style='width: 60px;text-align: left;'>
<el-slider ref='legendRef'
class='sliderLegend'
v-model='sliderValue'
@change='onChange'
:range='true'
vertical
:marks='marks'
v-bind='options' />
</div>
</el-space>
</el-card>
</template>
<script setup>
import { ref, defineProps, computed, reactive, watchEffect, nextTick } from 'vue'
import BigNumber from 'bignumber.js'
const props = defineProps({
gradesValues: { type: Array, default: () => ([]) },//分级的value数值
gradesColors: { type: Array, default: () => ([]) },//分级的color数值(需要与gradesValues一一对应)
})
const emits = defineEmits(['update:modeValue'])
let legendRef = ref()
//当前滑块的值
let sliderValue = ref([0, 100])
//间隔数值
let interval = ref()
//为了获取真实数据和百分比之间的映射关系
let percentage2ValueDic = {}
let valueDic2Percentage = {}
/**
* @Description 生成滑块的背景颜色
* @Param colors 备选颜色集合
* @Author ZhangJun
* @Date 2024-01-19 01:43:56
* @return void
**/
let generationLinearGradient = computed(() => {
let { colors } = grades
let interval = new BigNumber(1).div(colors?.length)
let temp = colors.map((color, index) => {
let percentage = interval.times(index).times(100).toFixed(0) + '%'
let rgba = `rgba(${color[0]},${color[1]},${color[2]},${color.length > 3 ? color[3] / 255 : 1})`
if (index === 0) {
return [rgba]
} else {
return [percentage, rgba]
}
})
return `linear-gradient(to top,${temp.flat()})`
})
//刻度值
const marks = computed(() => {
//清空
percentage2ValueDic = {}
valueDic2Percentage = {}
let { values, unit } = grades
interval.value = new BigNumber(1).div(values?.length - 1)
let marks = {}
values.forEach((value, index) => {
let percentageVal = interval.value.times(index).times(100).toFixed(0)
marks = { ...marks, [percentageVal]: `${value}` }
//做映射使用
percentage2ValueDic = { ...percentage2ValueDic, [percentageVal]: value }
valueDic2Percentage = { ...valueDic2Percentage, [value]: percentageVal }
})
return marks
})
//分级数据
let grades = reactive({
values: props.gradesValues,
colors: props.gradesColors,
unit: undefined,
desc: '',
})
//默认的配置参数
let options = computed(() => {
let { values } = grades
let height = (values?.length - 1) * 40
height = height > 300 ? 300 : height
return {
min: 0,
max: 100,
height: `${height}px`,
step: interval.value.times(100).toFixed(0),
showTooltip: false,
}
})
/**
* @Description 初始化当前control
* @Param gradesData 分级参数
* @Author ZhangJun
* @Date 2024-01-22 09:38:17
* @return void
**/
function initControl(gradesData) {
sliderValue.value = [0, 100]
grades = Object.assign(grades, gradesData)
}
/**
* @Description 获取数组里面最接近这个值的数
* @Param arr 数组
* @Param target 目标值
* @Author ZhangJun
* @Date 2024-01-19 07:19:21
* @return void
**/
function getClosestNumber(arr = [], target) {
let minDistance = Math.abs(target - arr[0])
let [closestNumber] = arr
for (let i = 1; i < arr.length; i++) {
let distance = Math.abs(target - arr[i])
if (distance < minDistance) {
minDistance = distance
closestNumber = arr[i]
}
}
return closestNumber
}
/**
* @Description 滑块数值改变
* @Param e 事件回调参数
* @Author ZhangJun
* @Date 2024-01-22 09:38:59
* @return void
**/
function onChange(e) {
let [min, max] = e
let arr = Object.keys(percentage2ValueDic)
min = getClosestNumber(arr, min)
max = getClosestNumber(arr, max)
emits('update:modeValue', [percentage2ValueDic[min], percentage2ValueDic[max]])
}
watchEffect(() => {
let [min, max] = sliderValue.value
//为了添加滑块的遮罩,达到想要的效果(重点)
nextTick(() => {
let runway = document.getElementsByClassName('sliderLegend')?.[0]?.querySelectorAll('.el-slider__runway')[0]
if (!runway.querySelector('.maxBar')) {
let div = document.createElement('div')
div.setAttribute('class', 'maxBar maskBar')
runway.appendChild(div)
}
if (!runway.querySelector('.minBar')) {
let div = document.createElement('div')
div.setAttribute('class', 'minBar maskBar')
runway.appendChild(div)
}
let maxMaskBar = document.querySelector('.maxBar')
let minMaskBar = document.querySelector('.minBar')
if (maxMaskBar) {
maxMaskBar.style.height = new BigNumber(100).minus(max).toNumber() + '%'
}
if (minMaskBar) {
minMaskBar.style.height = min + '%'
}
})
})
defineExpose({ initControl })
</script>
<style scoped lang='scss'>
::v-deep .el-slider__runway {
background: v-bind(generationLinearGradient);
.maskBar {
width: 100%;
height: auto;
background: lightgrey;
position: absolute;
border-radius: 6px;
}
.maxBar {
top: -3px;
}
.minBar {
bottom: -3px;
}
}
::v-deep .el-slider__bar {
background: transparent;
}
</style>
效果
过滤后的效果
本来原理很简单,主要时间还是花费到了slider的背景颜色上,还要添加一个上下滑动的遮罩,才能将要过滤掉的颜色范围遮掉
进阶版
加入可横向/竖向选择
/**
* sliderLegend.vue 可以拖动滑块过滤的图例
* @Author ZhangJun
* @Date 2024/1/19 9:36
**/
<template>
<el-card
class="slider-block absolute right-10 bottom-10 text-center"
v-if="grades.values && grades.values.length > 0"
:body-style="getBodyStyle"
style="z-index: 600"
>
<el-space direction="vertical" :size="vertical ? 20 : 0">
<el-text type="info">{{ grades.desc }}({{ grades.unit }})</el-text>
<div style="text-align: left">
<el-slider ref="legendRef" class="sliderLegend" v-model="sliderValue" @change="onChange" :range="true" :marks="marks" v-bind="options" />
</div>
</el-space>
</el-card>
</template>
<script setup>
import { ref, defineProps, computed, reactive, watchEffect, nextTick } from 'vue'
import BigNumber from 'bignumber.js'
const props = defineProps({
gradesValues: { type: Array, default: () => [] }, //分级的value数值
gradesColors: { type: Array, default: () => [] }, //分级的color数值(需要与gradesValues一一对应)
intervalHeight: { type: Number, default: 40 }, //间隔的高的
maxHeight: { type: Number, default: 195 }, //图例最高高度限制
width: { type: Number, default: 130 }, //图例宽度
vertical: { type: Boolean, default: false }, //是否垂直模式
})
const emits = defineEmits(['update:modeValue'])
let legendRef = ref()
//当前滑块的值
let sliderValue = ref([0, 100])
//间隔数值
let interval = ref()
//为了获取真实数据和百分比之间的映射关系
let percentage2ValueDic = {}
let valueDic2Percentage = {}
//得到内容部分的样式
let getBodyStyle = computed(() => {
if (props?.vertical) {
return { padding: `10px 0 0`, width: `${props.width}px` }
} else {
return { padding: `10px`, height: `${props.width}px` }
}
})
/**
* @Description 生成滑块的背景颜色
* @Param colors 备选颜色集合
* @Author ZhangJun
* @Date 2024-01-19 01:43:56
* @return void
**/
let generationLinearGradient = computed(() => {
let { colors } = grades
let interval = new BigNumber(1).div(colors?.length - 1)
let temp = colors.map((color, index) => {
let percentage = interval.times(index).times(100).toFixed(0) + '%'
let rgba = `rgba(${color[0]},${color[1]},${color[2]},${color.length > 3 ? color[3] / 255 : 1})`
if (index === 0) {
return [rgba]
} else {
return [percentage, rgba]
}
})
if (props?.vertical) {
return `linear-gradient(to top,${temp.flat()})`
} else {
return `linear-gradient(to right,${temp.flat()})`
}
})
//刻度值
const marks = computed(() => {
//清空
percentage2ValueDic = {}
valueDic2Percentage = {}
let { values, unit } = grades
interval.value = new BigNumber(1).div(values?.length - 1)
let marks = {}
values.forEach((value, index) => {
let percentageVal = interval.value.times(index).times(100).toFixed(0)
marks = { ...marks, [percentageVal]: `${value}` }
//做映射使用
percentage2ValueDic = { ...percentage2ValueDic, [percentageVal]: value }
valueDic2Percentage = { ...valueDic2Percentage, [value]: percentageVal }
})
return marks
})
//分级数据
let grades = reactive({
values: props.gradesValues,
colors: props.gradesColors,
unit: undefined,
desc: '',
})
//默认的配置参数
let options = computed(() => {
let { values } = grades
let height = new BigNumber(props.intervalHeight).times(values?.length - 1).toNumber()
height = height > props.maxHeight ? props.maxHeight : height
let options_temp = {
min: 0,
max: 100,
step: interval.value.times(100).toFixed(0),
showTooltip: false,
vertical: props?.vertical,
size: 'small',
}
if (props?.vertical) {
options_temp = {
...options_temp,
height: `${height}px`,
}
} else {
options_temp = {
...options_temp,
style: {
margin: '0 20px 0',
width: `${height}px`,
},
}
}
return options_temp
})
/**
* @Description 初始化当前control
* @Param gradesData 分级参数
* @Author ZhangJun
* @Date 2024-01-22 09:38:17
* @return void
**/
function initControl(gradesData) {
sliderValue.value = [0, 100]
grades = Object.assign(grades, gradesData)
}
/**
* @Description 获取数组里面最接近这个值的数
* @Param arr 数组
* @Param target 目标值
* @Author ZhangJun
* @Date 2024-01-19 07:19:21
* @return void
**/
function getClosestNumber(arr = [], target) {
let minDistance = Math.abs(target - arr[0])
let [closestNumber] = arr
arr.forEach((value, index) => {
if (index > 0) {
let distance = Math.abs(target - value)
if (distance < minDistance) {
minDistance = distance
closestNumber = value
}
}
})
return closestNumber
}
/**
* @Description 滑块数值改变
* @Param e 事件回调参数
* @Author ZhangJun
* @Date 2024-01-22 09:38:59
* @return void
**/
function onChange(e) {
let [min, max] = e
let arr = Object.keys(percentage2ValueDic)
min = getClosestNumber(arr, min)
max = getClosestNumber(arr, max)
emits('update:modeValue', [percentage2ValueDic[min], percentage2ValueDic[max]])
}
watchEffect(() => {
let [min, max] = sliderValue.value
//为了添加滑块的遮罩,达到想要的效果(重点)
nextTick(() => {
let runway = document.getElementsByClassName('sliderLegend')?.[0]?.querySelectorAll('.el-slider__runway')[0]
if (!runway?.querySelector('.maxBar')) {
let div = document.createElement('div')
div.setAttribute('class', `maxBar maskBar ${props?.vertical ? 'verticalBar' : ''}`)
runway.appendChild(div)
}
if (!runway?.querySelector('.minBar')) {
let div = document.createElement('div')
div.setAttribute('class', `minBar maskBar ${props?.vertical ? 'verticalBar' : ''}`)
runway.appendChild(div)
}
let maxMaskBar = document.querySelector('.maxBar')
let minMaskBar = document.querySelector('.minBar')
if (maxMaskBar) {
if (props?.vertical) {
maxMaskBar.style.height = new BigNumber(100).minus(max).toNumber() + '%'
} else {
debugger
maxMaskBar.style.width = new BigNumber(100).minus(max).toNumber() + '%'
}
}
if (minMaskBar) {
if (props?.vertical) {
minMaskBar.style.height = min + '%'
} else {
minMaskBar.style.width = min + '%'
}
}
})
})
defineExpose({ initControl })
</script>
<style scoped lang="scss">
::v-deep .el-slider__runway {
background: v-bind(generationLinearGradient);
.maskBar {
height: 100%;
width: auto;
top: 0;
&.verticalBar {
width: 100%;
height: auto;
top: auto;
}
background: lightgrey;
position: absolute;
border-radius: 6px;
}
.maxBar {
right: -3px;
&.verticalBar {
top: -3px;
right: auto;
}
}
.minBar {
left: -3px;
&.verticalBar {
bottom: -3px;
left: auto;
}
}
}
::v-deep .el-slider__bar {
background: transparent;
}
</style>
本文为学习笔记,仅供参考