概述
在日常工作中,比不可少会用到时间组件,我们的第一反应就是直接到组件库去找一下现成的来用下,毕竟,时间组件看起来还是很复杂的,对于没接触过的人来说,要自己去写一个这样的组件出来,还是有难度的,但是作为一名前端开发,这么常见的组件,我们还是值得取自己写一个这样的组件的,现在就手把手带你实现vue中的DatePicker组件。看完不会找我。
前置知识
在开始写代码之前,建议代价先看看时间组件的布局,目前主流的组件库iview ,element等提供的时间组件布局都基本类似,功能也差不多,因此在这里实现的组件库的布局也也element家的布局差不多,大家先看下布局的最终样子。 这是日的时间组件,当我们这个能实现的时候,像那些年,月的就更简单了,因此这里我们只实现一个,其他的可以自己扩展。
布局方面
- 我们可以看到最终时间组件可以拆分为两大部分,最上面可以拆分为一个时间切换的组件,下面为一个table,用于记录日期,下面的表格我们又可以拆分为上面的星期组件,和下面的日期具体实现组件。
- 然后,我们注意观察,表格是一个6行七列共计42个单元格的布局形式,包含上月的剩余天数,当前月份的全部天数,以及下月的开始天数,加起来组成42个单元格。其中上月和下月我们布局样式区别于当前月份的布局,还有就是当前天数的那个日期,我们需要高亮显示。
具体的实现思路
- 在清楚布局之后,我们需要根据用户传入的时间,生成一个6*7=42的天数的td单元格,在这42个单元格中,包含上月剩余的天数,当前月份的全部天数,下月的开始天数。
- 如果你清楚了步骤一,那么我们接下来就容易多了,我们要计算上月的天数,当前月份的全部天数,下月的开始天数,以及当前月份1号星期几。
- 由于头部是星期日,星期一,星期二,星期三,星期四,星期五,星期六的布局,因此我们需要计算当前月份1号星期几,这样我们就能找到上月的剩余天数了,下月的剩余天数就等于42-当前月份天数-上月剩余天数。
- 最后我们就需要提供计算月份天数,月份1号星期几,以及一个生成对应数据的工具函数了。
具体实现
目录结构
utils.js(工具函数(核心))
- .components/DatePicker/utils.js
export function getCurrentMonthCount([year, month]) {
// 当我们实例化Date函数的时候,传入第三个参数为0可以通过getDate获取到当前月份具体有多少天
return new Date(year, month, 0).getDate();
}
export function getFirstMonthDayWeek([year, month]) {
return new Date(year, month - 1, 1).getDay();
}
export function genarateDayData([year, month]) {
// 获取上月天数
let lastMonthCount = getCurrentMonthCount([year, month - 1]);
// 获取当月天数
let currentMonthCount = getCurrentMonthCount([year, month]);
// 获取当月1号星期
let currentMonthFirstDayWeek = getFirstMonthDayWeek([year, month]);
let dayList = [];
let lastMonthPointer = 1;
let currentMonthPoiner = 1;
let nextMonthPointer = 1;
// 根据日期组件的天数布局,共计42天,包含上月剩余天数+当月天数+下月初始天数
for (let i = 0; i < 42; i++) {
// 上个月需要渲染的td个数,以及对应的值
if (lastMonthPointer <= currentMonthFirstDayWeek) {
// 上月
dayList.unshift({
value: lastMonthCount--,
disbled: true,
date: year + "-" + (month - 1) + "-" + (lastMonthCount + 1),
index: i,
});
lastMonthPointer++;
} else if (currentMonthPoiner <= currentMonthCount) {
// 当月
dayList.push({
value: currentMonthPoiner++,
disbled: false,
active:
new Date().getFullYear() == year &&
new Date().getMonth() + 1 == month &&
currentMonthPoiner - 1 == new Date().getDate(),
date: year + "-" + month + "-" + (currentMonthPoiner - 1),
index: i,
});
} else {
// 下月
dayList.push({
value: nextMonthPointer++,
disbled: true,
date: year + "-" + (month + 1) + "-" + (nextMonthPointer - 1),
index: i,
});
}
}
// 当前天数高亮
// 最后将数据生成二维数组返回:对应的就是6*7的二维数组用于渲染天数表格列
let result = [];
let index = 1;
let i = 0;
while (index <= 6) {
let arr = [];
for (let j = 0; j < 7; j++) {
arr.push(dayList[i]);
i++;
}
result.push(arr);
index++;
}
return result;
}
constant.js
- .components/DatePicker/constant.js(常量文件)
//用于保存组建的常量,静态数据,比如表头的星期
export const weekList = ["日", "一", "二", "三", "四", "五", "六"];
DatePicker.vue
- .components/DatePicker/DatePicker.vue(整体包裹组件供外部使用)
<template>
<div class="date-picker-wrap">
<div class="date-eidtor">
<!-- 显示时间的input -->
<input
type="text"
:placeholder="placeholder"
class="date-edit-input"
v-model="currentDate"
@click.stop="showDatePannel = !showDatePannel"
/>
</div>
<!-- 面包通过过渡组件包裹,实现显示隐藏友好过渡 -->
<transition name="date-picker">
<div class="date-pocker-panel" v-show="showDatePannel">
<!-- 时间控件的头部,用户切换年月 -->
<date-picker-head
@dateRangeChange="dateRangeChange"
:date="curDate"
></date-picker-head>
<!-- 主要的时间显示列表组件,用于显示对应月份的时间 -->
<date-table :list="list" @dateChange="dateChange"></date-table>
</div>
</transition>
</div>
</template>
<script>
import { genarateDayData } from "./utils";
import DatePickerHead from "./DatePickerHead.vue";
import DateTable from "./DateTable.vue";
export default {
components: {
DatePickerHead,
DateTable,
},
props: {
// 输入框提示
placeholder: {
type: String,
default: "选择时间",
},
// 时间,为Date类型,默认为当前时间
date: {
type: Date,
default() {
return new Date();
},
},
},
data() {
return {
// 用于控制面包显示与隐藏
showDatePannel: false,
// 表格数据
list: [],
// 处理props时间为数组格式[年,月]
curDate: [this.date.getFullYear(), this.date.getMonth() + 1],
// 用户input显示时间
currentDate: "",
};
},
mounted() {
// 获取当前月份的时间数据
this.getDateList();
// 除开时间组件的其他地方点击,关闭时间面板
window.addEventListener("click", () => {
this.showDatePannel = false;
});
},
methods: {
// 监听每个td时间项点击
dateChange(date) {
this.$emit("dateChange", date);
this.showDatePannel = false;
this.currentDate = date;
},
// 头部年月切换
dateRangeChange(type) {
switch (type) {
// 上一年点击
case "lastYear":
this.curDate = [this.curDate[0] - 1, this.curDate[1]];
break;
// 上一月点击(月份<1,就要返回到上一年的12月份)
case "lastMonth":
this.curDate = [
this.curDate[1] - 1 <= 0 ? this.curDate[0] - 1 : this.curDate[0],
this.curDate[1] - 1 <= 0 ? 12 : this.curDate[1] - 1,
];
break;
// 下一年点击
case "nextYear":
this.curDate = [this.curDate[0] + 1, this.curDate[1]];
break;
case "nextMonth":
// 下一月点击(月份>12,就要到下一年的一月份)
this.curDate = [
this.curDate[1] + 1 > 12 ? this.curDate[0] + 1 : this.curDate[0],
this.curDate[1] + 1 > 12 ? 1 : this.curDate[1] + 1,
];
break;
}
this.getDateList();
},
// 通过props传递的时间,组装成长度为42的数组,具体看utils文件下下面的这个方法
getDateList() {
this.list = genarateDayData(this.curDate);
},
},
};
</script>
<style lang="less">
.date-picker-wrap {
position: relative;
.date-pocker-panel {
position: absolute;
left: 0;
top: 50px;
width: 324px;
height: 343px;
color: #606266;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
background: #fff;
border-radius: 4px;
line-height: 30px;
padding: 12px;
text-align: center;
}
.date-eidtor {
width: 220px;
.date-edit-input {
background-color: #fff;
background-image: none;
border-radius: 4px;
border: 1px solid #dcdfe6;
box-sizing: border-box;
color: #606266;
display: inline-block;
font-size: inherit;
height: 40px;
line-height: 40px;
outline: none;
padding: 0 15px;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
width: 100%;
}
}
.date-picker-enter-active,
.date-picker-leave-active {
transition: all 0.25s ease;
}
.date-picker-enter,
.date-picker-leave-to {
opacity: 0;
height: 0;
}
}
</style>
DatePickerHead.vue
- .components/DatePicker/DatePickerHead.vue(顶部时间切换组件)
<template>
<!-- 顶部的时间切换组件 -->
<div class="date-picker-head">
<div class="arrow-left">
<!-- 上一年点击 -->
<span class="last-year arrow" @click.stop="toogleDate('lastYear')"></span>
<!-- 上一月点击 -->
<span class="last-month arrow" @click.stop="toogleDate('lastMonth')"></span>
</div>
<!-- 显示当前的年和月 -->
<div class="date-content">{{ date[0] + "年" + date[1] + "月" }}</div>
<div class="arrow-right">
<!-- 下一月点击 -->
<span class="next-month arrow" @click.stop="toogleDate('nextMonth')"></span>
<!-- 下一年点击 -->
<span class="next-year arrow" @click.stop="toogleDate('nextYear')"></span>
</div>
</div>
</template>
<script>
export default {
props: {
// 时间
date: {
type: Array,
default() {
return [new Date().getFullYear(), new Date().getMonth() + 1];
},
},
},
methods: {
// 派发table事件处理逻辑,参数为当前td的时间,格式为2022-7-22
toogleDate(type) {
this.$emit("dateRangeChange", type);
},
},
};
</script>
<style lang="less">
.date-picker-head {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
.last-year,
.next-month {
margin-right: 15px;
}
.arrow {
cursor: pointer;
}
}
</style>
DateTable.vue(表格组件)
- .components/DatePicker/DateTable.vue
<template>
<div class="date-table">
<!-- 时间表格 -->
<table>
<!-- 顶部的星期组件 -->
<date-picker-week-bar></date-picker-week-bar>
<!-- 下方的6*7的月份天数组件 -->
<date-picker-day-content
:list="list"
@dateChange="dateChange"
></date-picker-day-content>
</table>
</div>
</template>
<script>
import DatePickerWeekBar from "./DatePickerWeekBar.vue";
import DatePickerDayContent from "./DatePickerDayContent.vue";
export default {
props: {
// 表格数据
list: {
type: Array,
default() {
return [];
},
},
},
components: {
DatePickerWeekBar,
DatePickerDayContent,
},
methods: {
// 派发td天数点击事件,参数为当前天数的时间格式为 2022-07-22
dateChange(date) {
this.$emit("dateChange", date);
},
},
};
</script>
<style lang="less">
.date-table {
font-size: 12px;
}
</style>
DatePickerWeekBar.vue(表头组件,渲染星期)
- .components/DatePicker/DatePickerWeekBar.vue
<template>
<thead class="date-picker-week-bar">
<tr>
<th v-for="item in weekList" :key="item">{{ item }}</th>
</tr>
</thead>
</template>
<script>
import { weekList } from "./constant.js";
export default {
data() {
return {
weekList,
};
},
};
</script>
<style lang="less">
.date-picker-week-bar {
th {
width: 42px;
height: 42px;
color: #606266;
font-weight: 400;
border-bottom: 1px solid #ebeef5;
}
}
</style>
DatePickerDayContent.vue
表格主题内容组件,用于渲染具体日期
- .components/DatePicker/DatePickerDayContent.vue
<template>
<tbody class="date-picker-day-content">
<tr v-for="(item, index) in list" :key="index">
<td
v-for="(subItem, index) in item"
:key="index"
:class="[
subItem.disbled ? 'disble-item' : 'day-item',
subItem.active ? 'active' : '',
subItem.index == currentDay ? 'active-click' : '',
]"
@click="handleDayClick(subItem)"
>
{{ subItem.value }}
</td>
</tr>
</tbody>
</template>
<script>
export default {
props: {
//表格数据
list: {
type: Array,
default() {
return [];
},
},
},
data() {
return {
//当前点击项活跃高亮
currentDay: -1,
};
},
methods: {
// 处理天的表格点击,触发关闭时间控件面板,设置时间input的值
handleDayClick(item) {
if (item.currentDay == item.index) return;
this.currentDay = item.index;
this.$emit("dateChange", item.date);
},
},
};
</script>
<style lang="less">
.date-picker-day-content {
td {
width: 40px;
height: 40px;
color: #606266;
font-weight: 400;
text-align: center;
cursor: pointer;
}
.disble-item {
cursor: not-allowed;
color: #c0c4cc;
}
.day-item.active {
color: #008c8c;
font-weight: bold;
}
.day-item.active-click {
border-radius: 50%;
width: 30px;
height: 30px;
line-height: 30px;
color: #fff;
background-color: #008c8c;
}
}
</style>
index.js(按需导出文件)
- .components/DatePicker/index.js
import DatePicker from "./DatePicker.vue";
export { DatePicker };
使用
- App.vue
<template>
<div id="app">
<div class="test-date-picker">
<date-picker :date="date" @dateChange="dateChange"></date-picker>
</div>
</div>
</template>
<script>
import { DatePicker } from "./components/DatePicker/index";
export default {
name: "App",
components: {
DatePicker,
},
data() {
return {
date: new Date(),
};
},
};
</script>
<style lang="less">
html,
body,
#app {
height: 100%;
width: 100%;
}
#app {
.test-date-picker {
width: 50%;
margin: 20px auto;
}
}
</style>
最终效果
总结
组件库很多看着很难的组件,只要我们认真斟酌,然后试着去是实现下,还是不难的,实现上面的这种类型的组件之后,其他的年和月类型的就更简答了,大家可以自己扩展,更多关于vue时间组件DatePicker组件的资料请关注编程网其它相关文章!