背景
前两天,一妹子紧皱着眉头,走到我面前,轻轻地说:"哥,这个功能我不会弄,能不能帮帮忙?"
就这么简单的一声"哥"
,让我心中涌起了一股难以抗拒的责任感。没有多想,心一横,我答应了。这一忙,注定无法拒绝。
需求
- 要做一个时间轴
00:00 ~ 24:00
,按照每15
分钟一个刻度,每四个刻度一个大刻度。 - 需要展示两个选中的值,一个是
接口默认的值
,一个用户选择的值
。 - 接口默认的值为默认颜色,用户选择的值为其他颜色,区分开。
- 该项目技术栈:
Vue3 + Element-Plus2 + Echarts5
设计图如下:
帮忙
看看妹子实现到什么程度了?总不至于一个 code 没写吧?😱
还好,看了妹子做出的效果,以及实现的代码,样式以及基础的能力都实现了,只是遇到一些解决不了的问题。还不错,"妹子可教也"
!😄
妹子已经实现上述需求的第一点。是用 Element Plus 的 Slider 修改的
。
html
<template>
<el-slider
class="time-slider"
ref="sliderRef"
range
:marks="props.marksData"
:min="props.min"
:max="props.max"
:format-tooltip="handleFormat" />
</template>
scss
.time-slider {
box-sizing: border-box;
width: 100%;
height: 32px;
margin-top: 0;
padding: 3px 20px 0;
align-items: flex-start;
// 修改 el-slider 默认样式
:deep(.el-slider__runway) {
width: 100%;
height: 4px;
border-bottom: 1px solid #01a59b;
background-color: transparent;
.el-slider__bar {
background: transparent;
}
// 选中的样式
.el-slider__button {
height: 12px;
width: 12px;
border-radius: 50%;
background-color: #01a59b;
border-color: #01a59b;
}
.el-slider__marks-stop {
box-sizing: border-box;
width: 7px;
height: 7px;
background: #f2fbff;
border: 1px solid #01a59b;
border-radius: 50%;
}
// 每四个一个大刻度的样式
.el-slider__marks-stop:nth-child(4n+1) {
width: 12px;
height: 12px;
bottom: -6px;
}
.el-slider__marks-text {
height: 20px;
font-size: 16px;
font-weight: 400;
text-align: CENTER;
color: #4f5566;
line-height: 20px;
margin-top: 14px;
}
}
}
js
// 时间格式 转 刻度的方法
import { timeToScale } from '@/utils'
const model = defineModel()
const emits = defineEmits(['change'])
const props = defineProps({
marksData: {
type: Object,
default() {
let timeRange = timeToScale()
let data = {}
timeRange.forEach((time, index) => {
if (index % 8 == 0 || index === 95) {
data[index] = time
} else {
data[index] = ''
}
})
return data
}
},
min: {
type: Number,
default: 0
},
max: {
type: Number,
default: 95
},
highlight: {
type: String,
default: ''
},
format: {
type: Boolean,
default: true
},
formatData: {
type: Object,
default() {
let timeRange = timeToScale()
let data = {}
timeRange.forEach((time, index) => {
data[index] = time
})
return data
}
}
})
const handleFormat = (value) => {
return props.format ? props.formatData[value] : value;
};
// 设置最小时刻
let sliderRef = ref()
watchPostEffect(() => {
if (!props.highlight) return
let index = null
for (const key in props.formatData) {
if (props.formatData[key] == props.highlight) {
index = Number(key)
break
}
}
if (!Number.isNaN(index) && index != null) {
let el = sliderRef.value.$el.children[0].children[2].children[index]
el.classList.add('active')
}
})
js
export function timeToScale(timer = 15, length = 96) {
const list = []
let hour = 0
let minutes = 0
for (let i = 0; i < length; i++) {
if (minutes >= 60) {
minutes = 0
hour += 1
}
list.push((hour > 9 ? hour : '0' + hour) + ':' + (minutes > 9 ? minutes : '0' + minutes))
minutes += timer
}
return list
}
实现效果,整体上把样式实现了:
难点 1
因为接口给的默认是时间格式,而组件其实是按照数值计算的,那么怎么把时间格式转成对应的数值,对应到图上呢?
js
export function timeToIndex(time, timer = 15) {
const [hour, minutes] = time.split(':').map(Number);
return (hour * 60 + minutes) / timer;
}
难点 2
妹子的问题就卡在了第二点需求:2. 需要展示两个选中的值,一个是接口默认的值
,一个用户选择的值
。
其实就是怎么在图上标记一个固定点,不能更改,另外一个点用户可以随意选择。
我去了看了 Element Plus 的 Slider 官方组件的 API,element-plus.org/zh-CN/compo...
没有直接能支持的 API,但是间接支持的有:
- range 是否开启选择范围,这样就能选择 2 个值了。
解题思路: 那么就需要想办法固定其中一个值,只让另外一个值变化就行。那么就是去干扰用户选择的值,重新赋值。那就 v-model 绑定一个计算属性的值。
经过几番调试,得出如下代码
js
import { timeToScale, timeToIndex } from '@/utils'
// 接口默认的值
const fixedValue = ref(timeToIndex(model.value));
// 用户自己选的值
// 首先默认为和 接口默认值 相同
// 因为添加 range 属性后,值是一个数组 [low, hight]
const userValue = ref(timeToIndex(model.value));
// 计算属性,重新赋值
const modelFormat = computed({
get() {
// 组件展示的值,需要排一下大小顺序
return [fixedValue.value, userValue.value].sort((a, b) => a - b);
},
set(val) {
// 用户手动操作的值,和我想要的结果有点出入
const [low, high] = val.sort((a, b) => a - b);
// 进一步加工一下数据,得到预期的数据
// 1. 如果固定值是小的,那么大的就是 用户选的
// 2. 如果固定值是大的,则小的就是 用户选的
// 本来以为到这就可以了,但是会有 bug,大家可以不加下边两句可以试试效果,
// 固定值的左右都选选,固定值丢失了,都被用户选的覆盖了,所以...
// 3. 上次用户值是小的,那么新选的用户值就是 大的
// 4. 上次用户值是大的,则新选的用户值就是 小的
if (fixedValue.value === low) {
userValue.value = high;
} else if (fixedValue.value === high) {
userValue.value = low;
} else if (userValue.value === low) {
userValue.value = high;
} else if (userValue.value === high) {
userValue.value = low;
}
// 返会用户选的值
emits("change", userValue.value);
},
});
效果就是如下图了,固定值是 08:00
, 另外一个值可以随意选择了,小于 08:00
,大于 08:00
,都可以!等于也可以,但是就没业务意义了。所以相同没必要了。
难点 3
选择的操作是可以了,但是选完了,看不出哪个是用户选的?哪个是原来固定值?那么就需要颜色区分开。
解题思路:打开浏览器控制台,看看怎么给他们加颜色,能不能固定的拿到对应的 Dom ? 给其动态增加颜色。
得出如下代码:
js
const modelFormat = computed({
get() {
return [fixedValue.value, userValue.value].sort((a, b) => a - b);
},
set(val) {
const [low, high] = val.sort((a, b) => a - b);
if (fixedValue.value === low) {
userValue.value = high;
} else if (fixedValue.value === high) {
userValue.value = low;
} else if (userValue.value === low) {
userValue.value = high;
} else if (userValue.value === high) {
userValue.value = low;
}
// 新加的代码
// 需要加载更新后才能获得新 dom
nextTick(() => {
// 找到用户点的那个元素
const parentElement = document.querySelector(`[aria-valuetext="${userValue.value}"]`);
console.log(1111, userValue.value, parentElement)
if (parentElement) {
// 查找子元素 .el-slider__button
const childElement = parentElement.querySelector('.el-slider__button');
console.log(22222, childElement)
if (childElement) {
// 修改子元素的背景颜色和边框颜色
childElement.style.backgroundColor = "#2D99FF"; // 示例背景颜色
childElement.style.borderColor = "#2D99FF"; // 示例边框颜色
}
}
});
emits("change", userValue.value);
},
});
这些代码其实是有 bug 的,点几下后,改完之后颜色都一起改了,两个点的颜色都是新颜色了。又不能区分了。
解题思路:固定的颜色跟着变了!不要让它跟着变,那就需要把它按在原地,或者变了再给它按回去。
刚才新加的代码删除掉,新增代码如下:
js
// 监听 modelFormat 变化
watch(
modelFormat,
() => {
// dom 更新后
nextTick(() => {
// 整理一个通用的方法
const applyStylesToSliderButton = (value, color) => {
// 找到对应值的父级 dom 元素,因为只有这个能区分他们,可以控制台看它们元素区别
const parentElement = document.querySelector(`[aria-valuetext="${value}"]`);
if (parentElement) {
const childElement = parentElement.querySelector('.el-slider__button');
if (childElement) {
childElement.style.backgroundColor = color; // 设置背景色
childElement.style.borderColor = color; // 边框色也跟上
childElement.style.width = "12px"; // 选中要变大
childElement.style.height = "12px"; // 选中要变大
}
}
};
// 按住固定值的颜色,或者叫做按回去
applyStylesToSliderButton(fixedValue.value, "#01a59b");
// 两个值一样了,就当用户没有选择
if (userValue.value === fixedValue.value) return;
// 用户选择的值,设置颜色
applyStylesToSliderButton(userValue.value, "#2D99FF");
});
},
{ immediate: true }
);
反反复复点了几次,测了一下,完美,至此终于解决了妹子的问题。一看表,时间过去了 2 个小时,该准备吃中午饭了...
效果
妹子凑过来看了一眼效果,眼睛顿时一亮,忍不住笑着夸道:"哥,你太棒了!"
她那一句赞美透着几分惊喜,我厚脸皮的也没忍住,脸上微微发热,挠了挠头,假装镇定地回她一个浅笑。
"哥,要不中午我请你吃饭吧?"
妹子歪着头,声音里透着期待和一丝俏皮。
我嘴角扬起,心想这顿饭是推不掉了,便笑着应了她的邀请。
中午,跟着她去了家小店,点了排骨铁锅焖面,热腾腾的香气扑鼻而来。妹子看我吃得津津有味,忍不住问:"怎么样?"
我抬头冲她一笑:"真不错!这面比你夸我还带劲儿!"
妹子噗嗤笑了,眼睛弯成了月牙。那一瞬间,心里觉得这顿饭,不光味道好,还透着点说不清的甜。
小结
上述大概介绍了就是我用了 2 个小时,用技术换了一顿排骨铁锅焖面的故事。顺带用 1 个小时写成文章分享给大家。
此文只为告诉大家,没事多帮妹子写写代码,说不定会有一顿大餐哦......
代码如有什么问题,或者优化空间,也请评论区告知我,别让我的得瑟影响了我的技术进步......
题外话
掘金2024年度人气创作者打榜中,快来帮我打榜吧,目标 200 名之前,能有个标志就行 😄~ activity.juejin.cn/rank/2024/w...