最近在做一个类似课程表的需求, 需要自制一个日历来支持功能及展现, 就顺便研究一下应该怎么开发日历组件
本文主要涉及以下内容:
怎么开发一套日历皮肤?
怎么计算年月日?
怎么开发日历相关的功能?
总结 & DEMO 源码
怎么开发一套日历皮肤?
层层分离, 块块独立
在梳理日历逻辑之前我想先记录一下日历样式相关的问题:
下面是借鉴 px2rem 模式, 写的基于 vw 为主单位的自适应转化简单来说, 就是在我们的设计稿是 iPhone8 一倍图的情况下, 计算出某元素宽度与 375(iPhone8 最大宽度)的比例再与 100vw 相乘就得到了, 该元素的 vw 值因为 vw 是相对于屏幕的百分比单位, 所以就能达到我们想要的自适应效果啦, 不同的屏幕里, 同一元素的展现比例是一致的
- // 借鉴了 Rem 布局
- @function pxWithVw($n) {
- @return 100vw * $n / 375;
- }
- // 规定极限宽度, 避免 PC 上观感太差
- @function pxWithVwMax($n) {
- @return 480px * $n / 375;
- }
有了上面这段 SCSS 的函数, 我们就基本可以不用考虑屏幕适配的问题了, 可以尽情的敲样式啦关于日历的样式, 其实说复杂也还好, 我们只需要在做之前好好的分一下层级就好了
如同上图, 每一个框表示一层元素, 最后会有这样的布局 --
- <!-- 最外层的 div 限定整个日历的宽度以及一些圆角阴影等样式 -->
- <div class="calendar">
- <!--header 则为上图中绿色框的内容, 包含上下月切换以及日历 title-->
- <div class="calendar__header"></div>
- <!-- 顾名思义 main 则是整个日历的核心内容, 也就是日期的展示区域 -->
- <div class="calendar__main">
- <!-- 星期一~ 星期日的展示头, 列表渲染固定的 7 个 block-->
- <div class="main__block-head"></div>
- <!-- 相应月份的日期展示区域, 列表渲染 -->
- <div class="main__block"></div>
- </div>
- </div>
也许大家看完之后比较奇怪 calendar__main 里面的布局, 为什么没有把固定的展示头分离开来, 当你实际写到这里的时候会发现其实没有这个必要
因为我们用了 pxWithVw 去规定 calendar__main 的宽度以及每个 block 的宽度, 也就确保了每 7 块元素必定会占满我们一行, 再利用 justify-content: space-around 确保我们每块元素的间隙一致即可
好了, 层层分离说完了, 什么是块块独立呢?
主要指的是日期的展示块, 我们是每块独立的, 这样在我们渲染的时候可以很方便的决定应该以什么样式去展示他, 或是应该给他绑定怎么样的事件, 给我们精密控制每个日期的展示提供了便利
怎么计算年月日?
本小节的内容总结起来其实就一句话 --
我们只需要知道, 某个月的 1 号是星期几, 就能把整个日历渲染出来
关于年月日的计算, 我这边有两种模式, 一种是只计算当月日期, 另一种则是将整年的日期都计算出来在本篇文章里我想着重记录第一种写法, 大家想了解第二种的话可以到我的 github 里看看这个日历的 demo
我们先来个看图说话, 这个二月份有 28 天, 1 号是星期四那是不是说, 我们只要从周四开始, 按顺序渲染出 28 个'main__block'就好了呢? 其实就是这样, 关键是怎么把我们的 1 号定位到周四, 只要这个能够准确定位到, 我们的日历自然就出来了
- // 定义每个月的天数, 如果是闰年第二月改为 29 天
- // year=2018;month=1(js--month=0~11)
- let daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
- if ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) {
- daysInMonth[1] = 29;
- }
- // 获得指定年月的 1 号是星期几
- let targetDay = new Date(year, month, 1).getDay();
- // 将要在 calendar__main 中渲染的列表
- let total_calendar_list = [];
- let preNum = targetDay;
- // 首先先说一下, 我们的日期是 (日 -- 六) 这个顺序也就是(0--6)
- // 有了上述的前提我们可以认为 targetDay 为多少, 我们就只需要在 total_calendar_list 的数组中 push 几个 content 为''的 obj 作为占位
- if (targetDay > 0) {
- for (let i = 0; i < preNum; i++) {
- let obj = {
- type: "pre",
- content: ""
- };
- total_calendar_list.push(obj);
- }
- }
这样一来, 1 号的位置自然而然就到了我们需要的星期四了, 接下来就只需要按顺序渲染就 ok 啦下面是剩下日期数组填充, 填充完毕之后 return 出来供我们 view 层使用
- for (let i = 0; i < daysInMonth[month]; i++) {
- let obj = {
- type: "normal",
- content: i + 1
- };
- total_calendar_list.push(obj);
- }
- if (total_calendar_list.length > 35) {
- nextNum = 42 - total_calendar_list.length;
- } else {
- nextNum = 35 - total_calendar_list.length;
- }
- // 与上面的 type=pre 同理
- for (let i = 0; i < nextNum; i++) {
- let obj = {
- type: "next",
- content: ""
- };
- total_calendar_list.push(obj);
- }
- return total_calendar_list;
怎么开发日历相关的功能?
如何选择上一个月或下一个月?
- data() {
- return {
- // ...
- selectedYear: new Date().getFullYear(),
- selectedMonth: new Date().getMonth(),
- selectedDate: new Date().getDate()
- };
- }
- handlePreMonth() {
- if (this.selectedMonth === 0) {
- this.selectedYear = this.selectedYear - 1
- this.selectedMonth = 11
- this.selectedDate = 1
- } else {
- this.selectedMonth = this.selectedMonth - 1
- this.selectedDate = 1
- }
- }
- handleNextMonth() {
- if (this.selectedMonth === 11) {
- this.selectedYear = this.selectedYear + 1
- this.selectedMonth = 0
- this.selectedDate = 1
- } else {
- this.selectedMonth = this.selectedMonth + 1
- this.selectedDate = 1
- }
- }
就是这么简单, 需要注意的点是跨年的时间转换, 我们需要在变更月份的同时把年份也改变, 这样才能渲染出正确的日期
也许大家会有疑问, 怎么变更了月份或年份之后不需要重新计算一次日期呢? 其实是有计算的, 不知大家是否还记得, vue 可是数据驱动变更的, 我们只需要关注数据的变更即可, 其他东西 vue 都会帮我们解决
如果选中某一天?
- handleDayClick(item) {
- if (item.type === 'normal') {
- // do anything...
- this.selectedDate = Number(item.content)
- }
- }
在渲染列表的时候我就给每一个 block 绑定了 click 事件, 这样做的好处就是调用十分方便, 点击每一个 block 的时候, 可以获取该 block 的内容然后 do anything you like
当然我们也可以给外层的父级元素绑定事件监听, 通过事件流来解决每个 block 的点击事件, 这里看个人习惯~ 毕竟元素数量不是特别多
总结
一个移动端日历貌似也有惊无险的完成啦, 总体来说日历这活还是偏样式方面的, 对逻辑的要求不是特别高, 对样式的要求倒是挺高的需要对 flexbox 布局有一定理解, 才能迅速的吧日历的骨架搭起来, 虽然也不一定说必须用 flex, 不过个人认为用 flex 的效率会稍高一些
基于 Vue 写的日历 DEMO--Github
啰嗦一下, 为什么想起来写日历? 当然是业务需求啦, 所以说这个日历组件一开始是 react 写的, 后面想在 vue 里也尝试一下就改成了 vue 其实在 react 里面写也是大同小异啦, 只不过我会把日期的 block 抽离成无状态组件, 也不为啥就感觉比较好看:)
来源: https://juejin.im/post/5a8e9f925188257a5e575755