背景
最近公司要求新增一个大屏页面,需要封装一个时间信息收集组件。通过双向绑定响应式数据,父组件监听子组件对数据的操作。最终将信息作为接口参数,请求相应的数据,渲染至页面上。
技术栈
- Vue3(3.2.45)
-
Element-Plus(2.2.27)
-
dayjs(^1.11.7)
效果
其中时间轴是对Element Plus组件库中的Slider滑块组件的二次封装,使用样式穿透自定义样式,同时大家可以把onMounted生命周期中的代码取消注释,可以做更多的操作。我的另一篇文章也是时间轴组件的封装,不过两者略有区别,所以又单独记录一下。
代码
父组件关键代码
<!-- 时间选择 表单 -->
<h1 class="titleH2">
{{ titleTime[0] }} - {{ titleTime[1] }} <br />
</h1>
<div class="contentCenter_bottom">
<TimeForm v-model:up="objInfo" v-model:tt="titleTime"></TimeForm>
</div>
<script setup>
// 双向绑定 obj
let objInfo = ref(null);
// 双向绑定 标题时间
let titleTime = ref(null);
// 监听时间表单变化 - 针对左右两侧的数据
watch(objInfo, (newVal) => {
console.log(newVal, "父组件监听到变化");
});
</script>
时间收集组件代码
关键变量介绍
这些变量实际可以对外暴露出去,也就是让父组件去定义,让组件更具灵活性。
- optionsHour :每日小时节点配置项(可自行灵活配置)
- optionsDay :每周天数配置项(可自行灵活配置)
- LastDay :时间轴根据当前时间和该变量往前推多少天,之后for循环会利用该变量为marks赋值,并求出步进值stepValue 。
<template>
<div id="TimeForm">
<div class="top">
<div class="topContent">
<div class="topContent_left">
<el-button
v-for="(item, index) in optionsHour"
:key="index"
size="small"
:type="indHour == index ? 'primary' : ''"
style="
font-family: 'DIGIB';
font-size: 15px;
border-radius: 6px 6px 0px 0px;
"
@click="handleHour(index)"
>{{ item }}</el-button
>
</div>
<el-divider direction="vertical" style="height: 80%" />
<div class="topContent_right">
<el-button
v-for="(item, index) in optionsDay"
:key="index"
size="small"
:type="indDay == index ? 'primary' : ''"
style="
font-family: 'DIGIB';
letter-spacing: 1px;
font-size: 15px;
border-radius: 6px 6px 0px 0px;
padding-left: 7px;
padding-right: 7px;
"
@click="handleDay(index)"
>{{ item }}</el-button
>
</div>
</div>
</div>
<div class="bottom">
<el-slider
v-model="value"
:marks="marks"
:step="stepValue"
:show-tooltip="true"
:format-tooltip="handleFormatTooltip"
/>
</div>
</div>
</template>
<script setup>
// vue
import {
ref,
reactive,
watch,
watchEffect,
computed,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
defineProps,
defineEmits,
} from "vue";
// 时间处理
import dayjs from "dayjs";
// 宏
let props = defineProps(["up", "tt"]);
const emits = defineEmits(["update:up", "update:tt"]);
// 定义变量
let optionsHour = reactive(["2点", "8点", "14点", "20点"]);
let optionsDay = reactive([
"过去1天",
"过去2天",
"过去3天",
"过去4天",
"过去5天",
"过去6天",
"过去7天",
]);
// 高亮
let indHour = ref(0);
let indDay = ref(0);
// 步长
let stepValue = ref(0);
// slider 日期往前推多少天
let LastDay = ref(7);
// 当前时间
let NowTime = new Date().getTime();
// slider 值
const value = ref(0);
// 标记
const marks = reactive({});
// 方法
function GetOneWeek() {
// 为marks赋值
for (let i = LastDay.value, j = 0; i >= 0; i--, j++) {
let time = new Date(NowTime - i * 24 * 60 * 60 * 1000).getTime();
let t = dayjs(time).format("YYYY-MM-DD");
let num = Number(((100 * j) / LastDay.value).toFixed(1));
// console.log(num);
marks[num] = `${t}`;
// console.log(i, j, "ij");
}
// 步进值
stepValue.value = 100 / LastDay.value;
}
GetOneWeek();
// 函数
// 高亮
function handleHour(val) {
indHour.value = val;
}
function handleDay(val) {
indDay.value = val;
}
// 自定义 提示信息
function handleFormatTooltip(val) {
let num = Number(val.toFixed(1));
return marks[num];
}
// 监听
// watch([indHour, indDay, value],([newHour,newDay,newValue],[oldHour,oldDay,oldValue])=>{
// console.log(newHour,newDay,newValue);
// emits("update:objInfo", !props.objInfo);
// })
watchEffect(() => {
let hour, endTime, hourMinuteSecond;
let num = Number(value.value.toFixed(1));
switch (indHour.value) {
case 0:
hourMinuteSecond = 2;
break;
case 1:
hourMinuteSecond = 8;
break;
case 2:
hourMinuteSecond = 14;
break;
case 3:
hourMinuteSecond = 20;
break;
}
switch (indDay.value) {
case 0:
hour = (indDay.value + 1) * 24;
break;
case 1:
hour = (indDay.value + 1) * 24;
break;
case 2:
hour = (indDay.value + 1) * 24;
break;
case 3:
hour = (indDay.value + 1) * 24;
break;
case 4:
hour = (indDay.value + 1) * 24;
break;
case 5:
hour = (indDay.value + 1) * 24;
break;
case 6:
hour = (indDay.value + 1) * 24;
break;
}
// console.log(marks,value.value);
endTime = marks[num];
let obj = {
hour: hour,
hourMinuteSecond: hourMinuteSecond,
endTime: endTime,
};
// console.log(marks[0],marks[100]);
let timeStart = marks[0].replace(/-/, '年').replace(/-/, '月') + '日';
let timeEnd = marks[100].replace(/-/, '年').replace(/-/, '月') + '日';
console.log(timeStart);
let timeObj = [timeStart,timeEnd]
emits("update:up", obj);
emits("update:tt", timeObj);
});
// 生命周期
onBeforeMount(() => {});
onMounted(() => {
// 无需删除这段注释 或许日后有用
// document.querySelector(".el-slider__bar").innerHTML = `
// <div
// style="width:50px; height:24px;background:#ccc; position: absolute;right: 0px;top: -34px;transform: translateX(50%); display: flex; justify-content: space-between;"
// >
// <span id='span1'>1</span><span id='span2'>2</span>
// </div>
// `;
// document.querySelector(".bottom").addEventListener("click", (e) => {
// if (e.target.id == "span1") {
// console.log("实况");
// } else if (e.target.id == "span2") {
// console.log("预报");
// }
// });
});
</script>
<style lang="scss" scoped>
// 变量
$timeform-height: 110px;
$top-height: 30px;
#TimeForm {
width: 100%;
min-height: $timeform-height;
max-height: $timeform-height;
box-sizing: border-box;
// border: 1px dashed rgba(255, 255, 255, 0.5);
// background-color: white;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
user-select: none;
.top {
width: 100%;
height: $top-height;
padding: 0px 20px;
.topContent {
width: auto;
height: 100%;
border-radius: 3px 3px 0px 0px;
backdrop-filter: blur(10px); /* 背景模糊效果 */
background-color: rgba(255, 255, 255, 0.5);
display: flex;
justify-content: flex-start;
align-items: end;
padding: 0px 10px;
.topContent_left {
width: 260px;
height: 80%;
display: flex;
justify-content: space-around;
align-items: end;
}
.topContent_right {
width: auto;
height: 80%;
display: flex;
justify-content: space-around;
align-items: end;
flex-wrap: wrap;
}
}
}
.bottom {
width: 100%;
height: calc($timeform-height - $top-height);
background-color: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px); /* 背景模糊效果 */
display: flex;
justify-content: center;
align-items: center;
padding: 0px 45px;
}
:deep(.bottom .el-slider__stop) {
width: 2px;
height: 14px;
background-color: #005edc;
border-radius: 0px;
}
:deep(.bottom .el-slider__bar) {
background-color: transparent;
// position: relative;
}
:deep(.bottom .el-slider__marks-text) {
background-color: #e0eaf8;
color: #626f80;
padding-left: 2px;
padding-right: 2px;
}
// :deep(.bottom .el-slider__button-wrapper::before){
// position: absolute;
// right: 0px;
// top: -17px;
// transform: translateX(calc(50% - 18px));
// display: inline-block;
// content: '123';
// width: 60px;
// height: 20px;
// background: #005edc;
// z-index: 9999999;
// }
}
</style>
所遇问题
1.由于marks(取值范围在闭区间0-100)的赋值是根据LastDay 变量计算出来的,具体来说是这段代码:
Number(((100 * j) / LastDay.value).toFixed(1));(最初是没有.toFixed(1))
这导致计算有可能存在很长的小数,由于小数的存在,又使得watchEffect当中的
let num = Number(value.value.toFixed(1));endTime = marks[num];(最初没有.toFixed(1))
有可能匹配不上返回 undefined。
也有尝试使用Math中的四舍五入或向上/下取整解决,但这又导致滑块对应不上步进值,最终采取了.toFixed(1)解决该问题。(.toFixed()返回值是字符串)
2.代码中的v-model绑定组件必须采用别名方式,否则无效。