第十五集: 从零开始实现一套pc端vue的ui组件库( 日历组件 )
1. 本集定位
'日历组件'在后台管理系统里面是十分常见的, 在pc端的展示方式基本都为一个方方的表格, 别看功能单一, 这个组件做起来还是有点意思的, 本次我来实现的组件只包含最核心的功能,也就是日期的选择, Element-ui里面的日期组件功能很多有兴趣的同学可以去看看他的思想.
效果展示
2. 需求分析
- 一个输入框用来展示以及点击弹出'日历组件'.
- 展示日期选择使用6*7的矩形.
- 可以按年份与月份进行翻页.
- 当本月第一天不是周日的时候, 要显示上一个月的最后几天.
- 可以选择一个日期.
- 个人不太喜欢手动输入日期这个操作, 所以本次是禁止手动输入的.
3. 基础的搭建
vue-cc-ui/src/components/DatePicker/index.js
import DatePicker from './main/datePicker.vue'
DatePicker.install = function(Vue) {
Vue.component(DatePicker.name, DatePicker);
};
export default DatePicker
vue-cc-ui/src/components/DatePicker/main/datePicker.vue
<template>
<div class="cc-date" ref='popover'>
// 用来展示日期的那个输入框
<input readonly
type="text"
class="cc-date-input"
// 这是个很有用的指令, 接下来我讲一下他
v-clickoutside='hide'
:value='formatDare'
// 每次聚焦都会呼出日历
@focus='isShowPanel = true'>
// 接下来的'日历'就在它里面做了.
<div v-show='isShowPanel'
class="cc-date-pannel"
ref='content'
:style="{
top:top+'px',
left:left+'px'
}">
</div>
</div>
</template>
export default {
name: "ccDatePicker",
props: {
value: {
type: Date, // 指定类型不许是日期类型
default: () => new Date() // 你不传我就取当前时间呗
}
},
data() {
return {
top: 0,
left: 0,
isShowPanel: false,
};
},
//...
v-clickoutside : 判断点击的是不是自身
这个方法一定要挂在组件内部的指令上, 不要污染全局.
const Clickoutside = {
bind(el, bindings, vnode) {
// 单独抽出来是为了最后好把它移除
const handleClick = function(e) {
// 如果点击的元素不在目标元素的包裹内, 那就说明点击了与元素无关的位置.
if (!el.contains(e.target)) {
// 虚拟dom的context属性可以找到这个实例, 调用他的hide方法可以隐藏这个dom
vnode.context[bindings.expression]();
}
};
el.handleClick = handleClick;
document.addEventListener('click', handleClick);
},
unbind(el) {
document.removeEventListener('click', el.handleClick);
}
};
export default Clickoutside;
创给指令的hide方法
methods: {
hide() {
this.isShowPanel = false;
},
//...
给他定个位把, 具体出现在哪里
其实这个我们上一个组件已经封装好了方法
我们先观察这个isShowPanel, 如果他出现, 那我们就计算出现的位置
watch: {
isShowPanel(val) {
if (val) {
this.$nextTick(() => {
this.setPosion(); // 这个方法是真正获取位置的
});
}
}
},
setPosion
setPosion() {
let { popover, content } = this.$refs;
let { left, top } = getPopoverPosition( // 这个函数上一集有说明, 不赘述了.
popover,
content,
"bottom-start",
3
);
this.top = top;
this.left = left;
}
上面的步骤我们做到了点击input弹出日期选择, 点击其他地方让其消失
4.样式很重要
- 首先要有header展示具体的年月日以及前进与后退.
- 其次是一个title展示'周一''周二'...这种.
- 具体的显示框来显示具体的day.
展示一下结构代码
首先是第一排
<div class="pannel-nav">
<span><</span>
<span>←</span>
<div class="pannel-selected">
// 像这种结构有人用v-for生成...
// 其实有时候直接写出来更直观, 仁者见仁吧.
<span>{{formatDare.split('-')[0]}}年 </span>
<span>{{formatDare.split('-')[1]}}月 </span>
<span>{{formatDare.split('-')[2]}}日</span>
</div>
<span>→</span>
<span>></span>
</div>
formatDare: 是用来展示时间的 --> '年-月-日'
computed: {
formatDare() {
let { year, month, day } = getYMD(this.value),
result = `${year}-${month + 1}-${day}`;
return result;
},
// ...
展示星期
<div class="pannel-content">
<ul class="pannel-content__title">
<li v-for="i in weeksList"
:key="i">{{i}}</li>
</ul>
//...
data() {
return {
top: 0,
left: 0,
isShowPanel: false,
weeksList: ["日", "一", "二", "三", "四", "五", "六"]
};
},
重头戏: 展示day天
思路: 例如当前是' x年n月 ';
- 计算出x年n月有多少天.
- 计算出x年n月的第一天是星期几.
- 如果是星期日, 那就不用添加上一个月的日期, 直接开头就显示本月的1日.
- 如果不是星期日, 需要上个月的日期来补全.
- 求出x年n-1月有多少天, 这里要注意, 很可能-1导致跨年了, 所以要判断好边界.
- 在当前日期比如有31天展示完毕, 需要用下个月的日期来填补所有剩下来的格子.
template
<ul class="pannel-content__item"
v-for="i in 6"
:key="i">
<li v-for="j in 7"
:key="j">{{getVisibeDaysIndex(i,j).day}}</li>
</ul>
计算当前有多少天
getVisibeDaysIndex(i, j) {
i = i - 1;
j = j - 1;
let index = i * 7 + j; // 当前第几个格子
return this.visibeDays[index];
},
visibeDays: 它是比较核心的方法
visibeDays() {
let result = [],
{ year, month } = getYMD(this.value),
// 传入年,月,日,就会返回相应的date实例, 用getDay取得星期几;
dayOffset = new Date(year, month, 1).getDay(),
// 传入年月, 求出本月几天, 这个方法下面会讲.
dateCountOfMonth = getDayCountOfMonth(year, month),
// 这个是求得上一个月
previousMonth = month - 1;
// 没有0月, 所以需要变为12月, 年份-1;
if (previousMonth === 0) {
year--;
previousMonth = 12;
}
// 取得上个月有多少天, 这样才能知道现实上个月的最后一天是不是31;
let dateCountOfLastMonth = getDayCountOfMonth(year, previousMonth);
// 把取得完毕的数据传给专门把它们做成数组用于展示的函数;
this.getDayList(
dayOffset,
dateCountOfMonth,
dateCountOfLastMonth,
result
);
// 这个结果直接返回出去就行
return result;
}
vue-cc-ui/src/assets/js/handelDate.js
这里面就是对日期相关的处理
export function getYMD(date){
let day = date.getDate();
let month = date.getMonth();
let year = date.getFullYear();
return {
year, month, day
}
}
export const getDayCountOfMonth = function(year, month) {
if (month === 3 || month === 5 || month === 8 || month === 10) {
return 30;
}
if (month === 1) {
if (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0) {
return 29;
} else {
return 28;
}
}
return 31;
};
把日期整理为使用的数组
getDayList
- readOnly为真, 显示为灰色不可选, 为假就是正常的黑色可选
- activate为真, 则显示高亮, 表示被选中
getDayList(dayOffset, dateCountOfMonth, dateCountOfLastMonth, result) {
// 处理上个月的日期, 没有的话当然就不走这个循环
for (let i = 0; i < dayOffset; i++) {
result.unshift({ readOnly: true, day: dateCountOfLastMonth - i });
}
// 处理当前月的天数
let day = getYMD(this.value).day;
for (let i = 1; i <= dateCountOfMonth; i++) {
let obj = { day: i, activate: true };
if (day !== i) {
obj.activate = false;
}
result.push(obj);
}
// 总个数减去已使用的数, 把剩余空间填满
let len = 42 - result.length;
for (let i = 1; i <= len; i++) {
result.push({ readOnly: true, day: i });
}
// 这个函数处理好了也没必要有返回值
},
上面的步骤做完其实就已经可以正常显示当前月了
5.选中日期 与 切换月年
其实随着核心代码的完成, 周边的功能都是很好添加的, 这也就是为什么写代码一定要符合设计模式;
选中某一天
<li v-for="j in 7"
@click="handlerActiveDay(getVisibeDaysIndex(i,j,true))"
:class="{
'active-date': getVisibeDaysIndex(i,j).activate,
'read-only':getVisibeDaysIndex(i,j).readOnly
}"
:key="j">{{getVisibeDaysIndex(i,j).day}}</li>
handlerActiveDay: 这里我在getVisibeDaysIndex传了第三个参数
因为这里我只需要他返回给我具体的序号就行了, 而不是具体哪天.
getVisibeDaysIndex(i, j, type) {
i = i - 1;
j = j - 1;
let index = i * 7 + j;
return type ? index : this.visibeDays[index];
},
handlerActiveDay(index) {
let result = this.visibeDays[index],
{ year, month } = getYMD(this.value);
if (!result.readOnly) {
// 这一步其实是与用户的 v-model相结合的.
this.$emit("input", new Date(year, month, result.day));
}
},
前进与后退
<span @click="handlerChangeYear(-1)"><</span>
<span @click="handlerChangeMonth(-1)">←</span>
// ...
<span @click="handlerChangeMonth(1)">→</span>
<span @click="handlerChangeYear(1)">></span>
月份的
handlerChangeMonth
注意不要超出边界
handlerChangeMonth(n) {
let { year, month } = getYMD(this.value);
month += n;
if (month === 0) {
month = 12;
year += n;
} else if (month === 13) {
month = 1;
year += n;
}
this.$emit("input", new Date(year, month, 1));
},
年份
handlerChangeYear
没必要判断负数了, 毕竟选一个公元前的时间这种情况太极端了, 没必要浪费性能去判断了.
handlerChangeYear(n) {
let { year, month } = getYMD(this.value);
year += n;
this.$emit("input", new Date(year, month, 1));
},
6. 具体的scss样式
@import './common/var.scss';
@import './common/mixin.scss';
@import './common/extend.scss';
@include b(date) {
position: relative;
display: inline-block;
@include b(date-input){
border: 1px solid $--color-disabled;
// 输入框的outline根据需求来判断到底要不要清理吧.
outline: 0px;
padding: 8px;
font-size: 16px;
border-radius:7px;
}
@include b(date-pannel){
// 这种弹出框肯定是要针对视口定位的
position: fixed;
border: 1px solid $--color-disabled;
background-color: $--color-white;
width: 280px;
padding: 8px;
border-radius:7px;
.pannel-nav{
display: flex;
align-items: center;
// 整体有一个环绕效果
justify-content: space-around;
// 外圈的轮廓
box-shadow: 0px 2px 2px 2px $--color-difference;
padding: 6px 0;
margin-bottom: 10px;
.pannel-selected{
width: 160px;
text-align: center;
}
&>span{
&:hover{
cursor: pointer;
color: $--color-nomal
}
}
}
.pannel-content{
box-shadow: 0px 2px 2px 2px $--color-difference;
ul{
display: flex;
}
li{
text-align: center;
flex: 1;
height: 35px;
line-height: 35px;
}
.read-only{
color: $--color-disabled;
}
.active-date{
@extend .active-item;
}
.pannel-content__item{
cursor: pointer;
border: 1px solid $--color-difference;
// li标签中, 没有.read-only class的标签;
li:not(.read-only){
// 平时是处于缩小状态的
transition: all .2s;
transform: scale(.8);
&:hover{
transform: scale(1.3);
@extend .active-item;
}
}
}
}
}
}
end
大家都可以一起交流, 共同学习,共同进步, 早日实现自我价值!!
下一集聊聊'tree组件'
作者对tree组件有些不一样的理解, 所以做出来的组件也比较怪异吧,但是我挺喜欢我的想法, 下一期与大家分享一下另类的tree.
github:github
个人技术博客(组件的官网):博客
仿写Vue项目(这个项目里面也有很多有趣的想法): 项目地址
相关文章:链接描述
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。