共计 7064 个字符,预计需要花费 18 分钟才能阅读完成。
第十五集: 从零开始实现一套 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 项目 (这个项目里面也有很多有趣的想法): 项目地址
相关文章: 链接描述