类似qq空间的网站模板python做博客网站
信号条
任务目标
需要参考 https://noc.ruijie.com.cn/design/#/bench/2446188 (诺克云底部的信号条)进行实现,主要需求为:
- 样式为一个轴,轴的范围为(-85至-30)。轴上存在三个滑块,滑块(从左至右)颜色分别为红色#F49573、黄色#F8D75A、绿色#65EF6A【分别代表信号效果差、中等。良好】,同时滑块下需要显示其对应的值。三个滑块将轴分为四段,轴上从左至右的颜色分别为#FFD4D4, #F49573, #F8D75A, #65EF6A。三个滑块可以在轴上被拖动以更改不同信号强度所对应的效果,但是需要保持其顺序,红色滑块始终在黄色滑块左侧,黄色滑块始终在绿色滑块左侧。
- 由于三个滑块可以拖动选择信号强度范围所对应的效果,因此滑块的改变会对图中的颜色区域也进行更改(因为信号效果变化直接影响信号颜色渲染区域)
- 区域鼠标悬停会得到一个信号值,此时需要在轴上进行标注(为一个圆点,底部显示对应的值)
- 不同颜色的信号值在区域内会计算成为百分比,需在轴上方得以显示,并使用与轴一致的颜色以区分不同信号百分比。
- 如果轴底部的信号数字由于距离过小导致重叠需要做重叠处理(轴上方的信号百分比也需要做同样的处理)
思路
-
使用定位的方法先渲染四段不同颜色的轴,四段轴的父元素使用flex布局(水平方向),四段不同颜色的轴使用width: xxx% 进行渲染以实现四段不同颜色的轴。对于轴上百分比使用子绝父相的定位方式使其定位在每段轴中间上方20px的位置。
-
width的计算通过滑块的底部标识数据进行计算【注意是flex布局!是连接在一起的】
//<div class="slider strong-signal" :style="{ left: strongSignalPosition + '%' }" const segment1Width = computed(() => {return weakSignalPosition.value; });const segment2Width = computed(() => {return mediumSignalPosition.value - weakSignalPosition.value; });const segment3Width = computed(() => {return strongSignalPosition.value - mediumSignalPosition.value; });const segment4Width = computed(() => {return 100 - strongSignalPosition.value; });
-
-
轴上的滑块(连同滑块下的标识内容)使用百分比进行定位(子绝父相+left:xxx%+transform:translateX(-50%)),其中滑块本身使用flex布局使其轴为竖直方向,将下方的标识内容与滑块绑定。
-
left的计算通过滑块的底部标识数据进行计算
const weakSignalPosition = computed(() => {return ((weakSignal.value - MIN_DBM) / RANGE_SIZE) * 100; });const mediumSignalPosition = computed(() => {return ((mediumSignal.value - MIN_DBM) / RANGE_SIZE) * 100; });const strongSignalPosition = computed(() => {return ((strongSignal.value - MIN_DBM) / RANGE_SIZE) * 100; });
-
-
左右侧的 差/ 好 使用子绝父相的方式定位至合适位置。
-
滑块底部文字重叠处理
-
需要碰撞检测函数以判断两个dom元素是否重叠碰撞
-
滑块底部文字重叠碰撞后,应将一个数据置为null,置为null的数据添加到与其碰撞的滑块身上。例如-44与-45碰撞,会将值置为 -44,-45 并添加到(与其碰撞的)一个滑块身上。因此,碰撞发生后需要调整滑块的样式以及其内部的innerHTML 。
-
若为三个文字碰撞,逻辑类似,将数据放置于第一个滑块上,其余置空,调整第一个滑块的样式以及内容
-
-
百分比重叠处理(逻辑类似,但是由于百分比有四个,需要使用循环进行处理)
- 循环判断其中一个滑块与后方滑块是否冲突,若冲突则需判断是否有连续冲突滑块,记录最后一个冲突的百分比下标并将 第一个百分比到最后一个冲突的百分比下标使用数组存储,而后调整第一个百分比的样式以及其内容,并将其余百分比内容置空,而后跳过已处理的百分比【至冲突的后一个百分比】。
代码
<template><div class="signal-strength-selector"><div class="axis-container"><div class="axis-text axis-text-left">差</div><!-- 信号轴 --><div class="signal-axis" ref="signalAxis"><!-- 轴上的颜色区域 --><div class="axis-colors"><div class="color-segment segment-1" :style="{ width: segment1Width + '%' }"><div class="segment-percentage" :style="{ left: '50%', color: '#FFD4F6' }">{{ props.percentages.segment1 }}%</div></div><div class="color-segment segment-2" :style="{ width: segment2Width + '%' }"><div class="segment-percentage" :style="{ left: '50%', color: '#F49573' }">{{ props.percentages.segment2 }}%</div></div><div class="color-segment segment-3" :style="{ width: segment3Width + '%' }"><div class="segment-percentage" :style="{ left: '50%', color: '#F8D75A' }">{{ props.percentages.segment3 }}%</div></div><div class="color-segment segment-4" :style="{ width: segment4Width + '%' }"><div class="segment-percentage" :style="{ left: '50%', color: '#65EF6A' }">{{ props.percentages.segment4 }}%</div></div></div><!-- 条件显示滑块 --><div class="sliders" v-if="props.pointDBM === null"><div class="slider weak-signal" :style="{ left: weakSignalPosition + '%' }"@mousedown="startDrag('weakSignal', $event)"><div class="slider-handle"></div><div class="slider-value">{{ weakSignal }}</div></div><div class="slider medium-signal" :style="{ left: mediumSignalPosition + '%' }"@mousedown="startDrag('mediumSignal', $event)"><div class="slider-handle"></div><div class="slider-value">{{ mediumSignal }}</div></div><div class="slider strong-signal" :style="{ left: strongSignalPosition + '%' }"@mousedown="startDrag('strongSignal', $event)"><div class="slider-handle"></div><div class="slider-value">{{ strongSignal }}</div></div></div><!-- 圆形滑块 --><div class="point-slider" v-else :style="{ left: pointDBMPosition + '%' }"><div class="point-slider-handle"></div><div class="point-slider-value">{{ props.pointDBM }}</div></div><!-- 鼠标悬停指示器 --><div v-if="showHoverIndicator" class="hover-indicator" :style="{ left: hoverPosition + '%' }"><div class="indicator-dot"></div><div class="indicator-value">{{ hoverValue }}</div></div></div><div class="axis-text axis-text-right"><div style="display: inline-block;">好</div><div style="font-size: 10px;">dBm</div></div><div class="axis-text axis-refresh"><svg style="cursor: pointer;" @click="refreshDbm" t="1753773113019" class="icon" viewBox="0 0 1025 1024"version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4490" width="14" height="14"><pathd="M914.17946 324.34283C854.308387 324.325508 750.895846 324.317788 750.895846 324.317788 732.045471 324.317788 716.764213 339.599801 716.764213 358.451121 716.764213 377.30244 732.045471 392.584453 750.895846 392.584453L955.787864 392.584453C993.448095 392.584453 1024 362.040424 1024 324.368908L1024 119.466667C1024 100.615347 1008.718742 85.333333 989.868367 85.333333 971.017993 85.333333 955.736735 100.615347 955.736735 119.466667L955.736735 256.497996C933.314348 217.628194 905.827487 181.795372 873.995034 149.961328 778.623011 54.584531 649.577119 0 511.974435 0 229.218763 0 0 229.230209 0 512 0 794.769791 229.218763 1024 511.974435 1024 794.730125 1024 1023.948888 794.769791 1023.948888 512 1023.948888 493.148681 1008.66763 477.866667 989.817256 477.866667 970.966881 477.866667 955.685623 493.148681 955.685623 512 955.685623 757.067153 757.029358 955.733333 511.974435 955.733333 266.91953 955.733333 68.263265 757.067153 68.263265 512 68.263265 266.932847 266.91953 68.266667 511.974435 68.266667 631.286484 68.266667 743.028524 115.531923 825.725634 198.233152 862.329644 234.839003 892.298522 277.528256 914.17946 324.34283L914.17946 324.34283Z"fill="#389BFF" p-id="4491"></path></svg></div></div></div>
</template><script setup>
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';// 定义props
const props = defineProps({modelValue: {type: Object,default: () => ({weakSignal: -75,mediumSignal: -60,strongSignal: -45})},// 新增百分比propspercentages: {type: Object,default: () => ({segment1: 25,segment2: 25,segment3: 25,segment4: 25})},// 新增pointDBM propspointDBM: {type: Number,default: null}
});// 定义事件
const emit = defineEmits(['update:modelValue']);// 范围定义
const MIN_DBM = -85;
const MAX_DBM = -30;
const RANGE_SIZE = MAX_DBM - MIN_DBM;// 信号强度值
const weakSignal = ref(props.modelValue.weakSignal);
const mediumSignal = ref(props.modelValue.mediumSignal);
const strongSignal = ref(props.modelValue.strongSignal);// 滑块位置计算
const weakSignalPosition = computed(() => {return ((weakSignal.value - MIN_DBM) / RANGE_SIZE) * 100;
});const mediumSignalPosition = computed(() => {return ((mediumSignal.value - MIN_DBM) / RANGE_SIZE) * 100;
});const strongSignalPosition = computed(() => {return ((strongSignal.value - MIN_DBM) / RANGE_SIZE) * 100;
});// 段宽度计算
const segment1Width = computed(() => {return weakSignalPosition.value;
});const segment2Width = computed(() => {return mediumSignalPosition.value - weakSignalPosition.value;
});const segment3Width = computed(() => {return strongSignalPosition.value - mediumSignalPosition.value;
});const segment4Width = computed(() => {return 100 - strongSignalPosition.value;
});// 计算pointDBM在轴上的位置
const pointDBMPosition = computed(() => {if (props.pointDBM === null) return 0;return Math.max(0, Math.min(100, ((props.pointDBM - MIN_DBM) / RANGE_SIZE) * 100));
});// 拖拽相关变量
let isDragging = false;
let dragTarget = null;// 鼠标悬停相关
const showHoverIndicator = ref(false);
const hoverPosition = ref(0);
const hoverValue = ref(0);// 开始拖拽
const startDrag = (target, event) => {isDragging = true;dragTarget = target;event.preventDefault();
};const signalAxis = ref(null);
// 处理鼠标移动
const handleMouseMove = (event) => {if (!isDragging) return;const axis = signalAxis.value;if (!axis) return;const rect = axis.getBoundingClientRect();const position = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width));const value = Math.round(MIN_DBM + position * RANGE_SIZE);// 更新对应的信号值if (dragTarget === 'weakSignal') {// 确保弱信号小于中信号weakSignal.value = Math.min(value, mediumSignal.value - 1);} else if (dragTarget === 'mediumSignal') {// 确保中信号在弱信号和强信号之间mediumSignal.value = Math.max(weakSignal.value + 1, Math.min(value, strongSignal.value - 1));} else if (dragTarget === 'strongSignal') {// 确保强信号大于中信号strongSignal.value = Math.max(mediumSignal.value + 1, value);}// 更新模型值updateModelValue();
};// 停止拖拽
const stopDrag = () => {isDragging = false;dragTarget = null;
};// 更新模型值
const updateModelValue = () => {emit('update:modelValue', {weakSignal: weakSignal.value,mediumSignal: mediumSignal.value,strongSignal: strongSignal.value});
};const sliderValue = ref([]);
onMounted(() => {console.log('获取到的元素:', sliderValue.value);
})
// 处理滑块数字显示冲突
const handleSliderValueDisplay = () => {const sliderValueList = document.querySelectorAll('.slider-value');if (sliderValueList.length !== 3) return;const values = [weakSignal.value, mediumSignal.value, strongSignal.value];// 重置所有显示sliderValueList.forEach((slider, index) => {slider.style.width = '';slider.innerHTML = values[index];slider.style.display = 'block';});// 检测所有冲突情况,如果没有冲突,所有滑块都保持正常显示const collision01 = isColliding(sliderValueList[0], sliderValueList[1]);const collision12 = isColliding(sliderValueList[1], sliderValueList[2]);if (collision01 && collision12) {// 三个滑块都冲突:在第一个滑块显示所有值,隐藏其他两个sliderValueList[0].style.width = '72px';sliderValueList[0].innerHTML = `${values[0]}, ${values[1]}, ${values[2]}`;sliderValueList[1].style.display = 'none';sliderValueList[2].style.display = 'none';} else if (collision01) {// 只有第1个和第2个冲突sliderValueList[0].style.width = '48px';sliderValueList[0].innerHTML = `${values[0]}, ${values[1]}`;sliderValueList[1].style.display = 'none';// 第3个保持正常显示} else if (collision12) {// 只有第2个和第3个冲突sliderValueList[1].style.width = '48px';sliderValueList[1].innerHTML = `${values[1]}, ${values[2]}`;sliderValueList[2].style.display = 'none';// 第1个保持正常显示}};// 处理百分比显示冲突
const handlePercentageDisplay = () => {const percentageList = document.querySelectorAll('.segment-percentage');if (percentageList.length !== 4) return;const percentageValues = [props.percentages.segment1,props.percentages.segment2,props.percentages.segment3,props.percentages.segment4];// 重置所有显示percentageList.forEach((percentage, index) => {percentage.style.width = '';percentage.innerHTML = `${percentageValues[index]}%`;percentage.style.display = 'block';});// 检测相邻百分比的冲突for (let i = 0; i < percentageList.length - 1; i++) {const current = percentageList[i];const next = percentageList[i + 1];if (isColliding(current, next)) {// 检查是否存在连续冲突let mergeEnd = i + 1;while (mergeEnd < percentageList.length - 1 &&isColliding(percentageList[mergeEnd], percentageList[mergeEnd + 1])) {mergeEnd++;}// 合并显示从 i 到 mergeEnd 的所有百分比const mergedValues = [];for (let j = i; j <= mergeEnd; j++) {mergedValues.push(`${percentageValues[j]}%`);}current.style.width = `${(mergeEnd - i + 1) * 16}px`;// current.style.width = null;current.innerHTML = mergedValues.join(', ');// 隐藏被合并的元素for (let j = i + 1; j <= mergeEnd; j++) {percentageList[j].style.display = 'none';}// 跳过已处理的元素i = mergeEnd;}}
};// 监听props变化
watch(() => props.modelValue, (newValue) => {weakSignal.value = newValue.weakSignal;mediumSignal.value = newValue.mediumSignal;strongSignal.value = newValue.strongSignal;
}, { deep: true, immediate: true });
watch(() => [weakSignal, mediumSignal, strongSignal], (newValue) => {// 等待dom更新nextTick(() => {handleSliderValueDisplay();handlePercentageDisplay();})
}, { deep: true });
watch(() => props.pointDBM, (newVal) => {nextTick(() => {// 判空处理const pointDbmElement = document.querySelector(".point-slider-handle")const pointDbmValueElement = document.querySelector(".point-slider-value")if (!pointDbmElement) return;console.log("watch", props.pointDBM, newVal, pointDbmElement, weakSignal.value, mediumSignal.value, strongSignal.value)if (newVal < MIN_DBM) {pointDbmElement.style.backgroundColor = '#FFD4D4';pointDbmValueElement.style.color = '#FFD4D4';}else if (newVal >= MIN_DBM && newVal <= weakSignal.value) {pointDbmElement.style.backgroundColor = '#FFD4D4';pointDbmValueElement.style.color = '#FFD4D4';}else if (newVal > weakSignal.value && newVal <= mediumSignal.value) {pointDbmElement.style.backgroundColor = '#F49574';pointDbmValueElement.style.color = '#F49574';}else if (newVal > mediumSignal.value && newVal <= strongSignal.value) {pointDbmElement.style.backgroundColor = '#F8D75A';pointDbmValueElement.style.color = '#F8D75A';}else if (newVal > strongSignal.value && newVal <= MAX_DBM) {// console.log("??!")pointDbmElement.style.backgroundColor = '#65EF6A';pointDbmValueElement.style.color = '#65EF6A';}else if (newVal > MAX_DBM) {pointDbmElement.style.backgroundColor = '#65EF6A';pointDbmValueElement.style.color = '#65EF6A';}});
}, { deep: true, immediate: true })// 添加全局事件监听器
onMounted(() => {document.addEventListener('mousemove', handleMouseMove);document.addEventListener('mouseup', stopDrag);
});// 移除全局事件监听器
onUnmounted(() => {document.removeEventListener('mousemove', handleMouseMove);document.removeEventListener('mouseup', stopDrag);
});// 点击刷新按钮
const refreshDbm = () => {console.log('refreshDbm', document.getElementsByClassName('slider-value'));
}// 碰撞检测
const isColliding = (element1, element2) => {const rect1 = element1.getBoundingClientRect();const rect2 = element2.getBoundingClientRect();return !(rect1.right < rect2.left ||rect1.left > rect2.right ||rect1.bottom < rect2.top ||rect1.top > rect2.bottom);
};</script><style scoped lang="scss">
.signal-strength-selector {background: hsla(0, 0%, 100%, 0.9);border-radius: 8px;padding: 6px;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);min-width: 300px;
}.axis-container {position: relative;
}.signal-axis {height: 60px;position: relative;margin: 0 32px;margin-right: 40px;cursor: pointer;
}.axis-text {width: 16px;position: absolute;top: 16px;font-size: 16px;color: #2C3E50;&.axis-text-right {right: 6%;}&.axis-refresh {right: 0px;top: 18px;}&.axis-text-left {left: 3%;}}.signal-line {position: absolute;top: 0;bottom: 0;left: 0;width: 2px;background-color: #333;
}.signal-line.weak {background-color: #FFD4D4;
}.axis-colors {position: absolute;top: 25px;left: 0;right: 0;display: flex;align-items: center;height: 3px;.axis-text-right {color: #65EF6A;right: 0;}
}.signal-line {position: absolute;top: 0;bottom: 0;left: 0;width: 2px;background-color: #333;
}.signal-line.weak {background-color: #FFD4D4;
}.color-segment {height: 100%;position: relative;
}.segment-1 {background-color: #FFD4D4;
}.color-segment {height: 100%;position: relative;
}.segment-1 {background-color: #FFD4D4;
}.segment-2 {background-color: #F49573;
}.segment-3 {background-color: #F8D75A;
}.segment-4 {background-color: #65EF6A;
}.segment-percentage {position: absolute;top: -20px;transform: translateX(-50%);font-size: 10px;color: #666;border-radius: 2px;white-space: nowrap;z-index: 4;
}.axis-ticks {position: absolute;top: 35px;left: 0;right: 0;height: 20px;
}.tick {position: absolute;transform: translateX(-50%);
}.tick-label {font-size: 10px;color: #666;text-align: center;margin-top: 2px;
}.sliders {position: absolute;top: 18px;left: 0;right: 0;height: 30px;
}.slider {position: absolute;transform: translateX(-50%);width: 4px;/* height: 45px; */z-index: 2;cursor: pointer;display: flex;flex-direction: column;align-items: center;// border: 1px solid #ffffff;
}.slider-handle {width: 100%;height: 15px;background: white;
}.weak-signal .slider-handle {border: 1px solid white;background-color: #F49573;
}.medium-signal .slider-handle {border: 1px solid white;background-color: #F8D75A;
}.strong-signal .slider-handle {border: 1px solid white;background-color: #65EF6A;
}.slider-value {font-size: 10px;font-weight: bold;margin-top: 2px;color: #808080;text-align: center;min-width: 20px;white-space: nowrap;transition: width 0.2s ease;}/* 圆形滑块样式 */
.point-slider {position: absolute;top: 20px;transform: translateX(-50%);z-index: 3;display: flex;flex-direction: column;align-items: center;
}.point-slider-handle {width: 10px;height: 10px;border-radius: 50%;background: #4A90E2;border: 2px solid white;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);cursor: pointer;
}.point-slider-value {font-size: 10px;font-weight: bold;margin-top: 2px;color: #4A90E2;
}.hover-indicator {position: absolute;top: 5px;transform: translateX(-50%);display: flex;flex-direction: column;align-items: center;z-index: 3;pointer-events: none;
}.indicator-dot {width: 8px;height: 8px;border-radius: 50%;background-color: #333;
}.indicator-value {font-size: 10px;color: #333;margin-top: 2px;background-color: rgba(255, 255, 255, 0.8);padding: 0 2px;border-radius: 2px;
}.signal-preview {margin-top: 20px;padding-top: 16px;border-top: 1px solid #eee;
}.preview-item {display: flex;align-items: center;gap: 8px;margin-bottom: 8px;
}.preview-item:last-child {margin-bottom: 0;
}.color-box {width: 16px;height: 16px;border-radius: 2px;
}.preview-item.weak .color-box {background-color: #FFD4D4;
}.preview-item.medium .color-box {background-color: #F49573;
}.preview-item.good .color-box {background-color: #F8D75A;
}.preview-item.excellent .color-box {background-color: #65EF6A;
}.preview-text {font-size: 12px;color: #666;
}
</style>