vue 手写一个时间选择器

最近研究了 DatePicker 的实现原理后做了一个 vue 的 DatePicker 组件,今天带大家一步一步实现 DatePicker 的 vue 组件。

原理

DatePicker 的原理是——计算日历面板中当月或选中月份的总天数及前后月份相近的日子,根据点击事件计算日历面板显示内容,以及将所选值赋值给<input/>标签。

实现

  • CSS 代码于文章末尾处

1. 构思页面结构

DatePicker 组件由输入框和日历面板组成,写好页面主体结构。

<div class="date-picker"><input class="input" v-model="dateValue" @click="openPanel"/><transition name="fadeDownBig"><div class="date-panel" v-show="panelState"></div></transiton>
</div>

输入框<input>点击显示或隐藏日历面板,openPanel()方法改变 panelState 布尔值控制日历面板的显示隐藏。

日历面板由顶部条和面板两部分组成,而面板则由年份选择面板,月份选择面板,日期选择面板所组成,结构如下:

<div class="date-panel" v-show="panelState"><!-- 顶部按钮及年月显示条 --><div class="topbar"><span @click="leftBig">&lt;&lt;</span><span @click="left">&lt;</span><span class="year" @click="panelType = 'year'">{{tmpYear}}</span><span class="month" @click="panelType = 'month'">{{changeTmpMonth}}</span><span @click="right">&gt;</span><span @click="rightBig">&gt;&gt;</span></div><!-- 年面板 --><div class="type-year" v-show="panelType === 'year'"><ul class="year-list"><li v-for="(item, index) in yearList":key="index"@click="selectYear(item)"><span :class="{selected: item === tmpYear}" >{{item}}</span></li></ul></div><!-- 月面板 --><div class="type-year" v-show="panelType === 'month'"><ul class="year-list"><li v-for="(item, index) in monthList":key="index"@click="selectMonth(item)"><span :class="{selected: item.value === tmpMonth}" >{{item.label}}</span></li></ul></div><!-- 日期面板 --><div class="date-group" v-show="panelType === 'date'"><span v-for="(item, index) in weekList" :key="index" class="weekday">{{item.label}}</span><ul class="date-list"><li v-for="(item, index) in dateList"v-text="item.value":class="{preMonth: item.previousMonth, nextMonth: item.nextMonth,selected: date === item.value && month === tmpMonth && item.currentMonth, invalid: validateDate(item)}":key="index" @click="selectDate(item)"></li></ul></div>
</div>

2. 页面数据实现

DatePicker 所对应的 data 代码

data() {return {dateValue: "", // 输入框显示日期date: new Date().getDate(), // 当前日期panelState: false, // 初始值,默认panel关闭tmpMonth: new Date().getMonth(), // 临时月份,可修改month: new Date().getMonth(),tmpYear: new Date().getFullYear(), // 临时年份,可修改weekList: [{ label: "Sun", value: 0 },{ label: "Mon", value: 1 },{ label: "Tue", value: 2 },{ label: "Wed", value: 3 },{ label: "Thu", value: 4 },{ label: "Fri", value: 5 },{ label: "Sat", value: 6 }], // 周monthList: [{ label: "Jan", value: 0 },{ label: "Feb", value: 1 },{ label: "Mar", value: 2 },{ label: "Apr", value: 3 },{ label: "May", value: 4 },{ label: "Jun", value: 5 },{ label: "Jul", value: 6 },{ label: "Aug", value: 7 },{ label: "Sept", value: 8 },{ label: "Oct", value: 9 },{ label: "Nov", value: 10 },{ label: "Dec", value: 11 }], // 月nowValue: 0, // 当前选中日期值panelType: "date" // 面板状态};
},

DatePicker 的核心在于日期面板的数据。我们知道,一个月最多31天,最少28天。面板按周日至周六设计,最极端的情况如下:

最多的极端情况:

最少的极端情况:

根据上表我们可以得知一个月最多占六个星期,最少四个星期,所以日历面板必须设计为 6 行,剩余的用下个月的日期补上,最多补14天。因此日期数组可以这么设计:

computed: {dateList() {//获取当月的天数let currentMonthLength = new Date(this.tmpYear,this.tmpMonth + 1,0).getDate();//先将当月的日期塞入dateListlet dateList = Array.from({ length: currentMonthLength },(val, index) => {return {currentMonth: true,value: index + 1};});// 获取当月1号的星期是为了确定在1号前需要插多少天let startDay = new Date(this.tmpYear, this.tmpMonth, 1).getDay();// 确认上个月一共多少天let previousMongthLength = new Date(this.tmpYear,this.tmpMonth,0).getDate();// 在1号前插入上个月日期for (let i = 0, len = startDay; i < len; i++) {dateList = [{ previousMonth: true, value: previousMongthLength - i }].concat(dateList);}// 补全剩余位置,至少14天,则 i < 15for (let i = 1, item = 1; i < 15; i++, item++) {dateList[dateList.length] = { nextMonth: true, value: i };}return dateList;},
}

changeTmpMonth 为选择月份后显示的文案,yearList 为年份列表,为了与月份数量保持一致,我们也设长度为12.

computed: {changeTmpMonth() {return this.monthList[this.tmpMonth].label;},// 通过改变this.tmpYear则可以改变年份数组yearList() {return Array.from({ length: 12 }, (value, index) => this.tmpYear + index);}
}

3. 实现页面功能

(1)面板切换功能

- 点击输入框,除了打开日历面板,同时也默认为日期面板

openPanel() {this.panelState = !this.panelState;this.panelType = "date";
},

  • 点击 2018 年份进入年份面板,点击相对应年份显示该年份并进入月份选择面板
<span class="year" @click="panelType = 'year'">{{tmpYear}}</span>
selectYear(item) {this.tmpYear = item;this.panelType = "month";
},

  • 点击 Aug 月份进入月份面板,点击相对应月份显示该月份并进入日期选择面板
<span class="month" @click="panelType = 'month'">{{changeTmpMonth}}</span>
selectMonth(item) {this.tmpMonth = item.value;this.panelType = "date";
},

点击日期选择日期,关闭面板同时赋值给输入框

// methods
selectDate(item) {// 赋值 当前 nowValue,用于控制样式突出显示当前月份日期this.nowValue = item.value;// 选择了上个月if (item.previousMonth) this.tmpMonth--;// 选择了下个月if (item.nextMonth) this.tmpMonth++;// 获取选中日期的 datelet selectDay = new Date(this.tmpYear, this.tmpMonth, this.nowValue);// 格式日期为字符串后,赋值给 inputthis.dateValue = this.formatDate(selectDay.getTime());// 关闭面板this.panelState = !this.panelState;
},
// 日期格式方法
formatDate(date, fmt = this.format) {if (date === null || date === "null") {return "--";}date = new Date(Number(date));var o = {"M+": date.getMonth() + 1, // 月份"d+": date.getDate(), // 日"h+": date.getHours(), // 小时"m+": date.getMinutes(), // 分"s+": date.getSeconds(), // 秒"q+": Math.floor((date.getMonth() + 3) / 3), // 季度S: date.getMilliseconds() // 毫秒};if (/(y+)/.test(fmt))fmt = fmt.replace(RegExp.$1,(date.getFullYear() + "").substr(4 - RegExp.$1.length));for (var k in o) {if (new RegExp("(" + k + ")").test(fmt))fmt = fmt.replace(RegExp.$1,RegExp.$1.length === 1? o[k]: ("00" + o[k]).substr(("" + o[k]).length));}return fmt;
},
// 确认是否为当前月份
validateDate(item) {if (this.nowValue === item.value && item.currentMonth) return true;
},

(2)topbar 中左右箭头功能,具体详看下面方法

// <
left() {if (this.panelType === "year") this.tmpYear--;else {if (this.tmpMonth === 0) {this.tmpYear--;this.tmpMonth = 11;} else this.tmpMonth--;}
},
// <<
leftBig() {if (this.panelType === "year") this.tmpYear -= 12;else this.tmpYear--;
},
// >
right() {if (this.panelType === "year") this.tmpYear++;else {if (this.tmpMonth === 11) {this.tmpYear++;this.tmpMonth = 0;} else this.tmpMonth++;}
},
// >>
rightBig() {if (this.panelType === "year") this.tmpYear += 12;else this.tmpYear++;
},

(3) 实现输入框的双向绑定及格式规定

props

props: {value: {type: [Date, String],default: ""},format: {type: String,default: "yyyy-MM-dd"}
},

其中 value 支持日期格式和字符串,当设置了props时,则需在monted钩子函数中初始化input 值。format 默认值为 "yyyy-MM-dd", 当然你也可以设置为 "dd-MM-yyyy"等。

mounted() {if (this.value) {this.dateValue = this.formatDate(new Date(this.value).getTime());}
},

双向绑定父组件赋值 props 为 value, 子组件传递的事件为input, 因此需在 selectDate 方法中 emit 事件及数据给父组件

selectDate(item) {...this.$emit("input", selectDay);
},

这样,父组件便可以进行双向绑定了

<Datepicker v-model="time" format="dd-MM-yyyy"/>

(4)点击页面其他位置收起日历面板

原理
监听页面的点击事件,检测到有点击事件时关闭面板,但点击组件内容时也会触发点击事件,因此需要在组件内部阻止冒泡。同时,当组件销毁时,也要及时清除该监听器。

组件最外层阻止冒泡

<div class="date-picker" @click.stop></div>

页面创建设置监听

mounted() {...window.addEventListener("click", this.eventListener);
}

页面销毁清除监听

destroyed() {window.removeEventListener("click", this.eventListener);
}

公共方法

eventListener() {this.panelState = false;
},

项目Demo

项目源码

有用就点个赞呗~

最后,贴上 CSS 代码...

  • fadeDownBig 后面的样式为 vue <transiton> 的动画特效.
.topbar {padding-top: 8px;
}
.topbar span {display: inline-block;width: 20px;height: 30px;line-height: 30px;color: #515a6e;cursor: pointer;
}
.topbar span:hover {color: #2d8cf0;
}
.topbar .year,
.topbar .month {width: 60px;
}
.year-list {height: 200px;width: 210px;
}
.year-list .selected {background: #2d8cf0;border-radius: 4px;color: #fff;
}
.year-list li {display: inline-block;width: 70px;height: 50px;line-height: 50px;border-radius: 10px;cursor: pointer;
}
.year-list span {display: inline-block;line-height: 16px;padding: 8px;
}
.year-list span:hover {background: #e1f0fe;
}
.weekday {display: inline-block;font-size: 13px;width: 30px;color: #c5c8ce;text-align: center;
}
.date-picker {width: 210px;text-align: center;font-family: "Avenir", Helvetica, Arial, sans-serif;
}
.date-panel {width: 210px;box-shadow: 0 0 8px #ccc;background: #fff;
}
ul {list-style: none;padding: 0;margin: 0;
}
.date-list {width: 210px;text-align: left;height: 180px;overflow: hidden;margin-top: 4px;
}
.date-list li {display: inline-block;width: 28px;height: 28px;line-height: 30px;text-align: center;cursor: pointer;color: #000;border: 1px solid #fff;border-radius: 4px;
}
.date-list .selected {border: 1px solid #2d8cf0;
}
.date-list .invalid {background: #2d8cf0;color: #fff;
}
.date-list .preMonth,
.date-list .nextMonth {color: #c5c8ce;
}
.date-list li:hover {background: #e1f0fe;
}
input {display: inline-block;box-sizing: border-box;width: 100%;height: 32px;line-height: 1.5;padding: 4px 7px;font-size: 12px;border: 1px solid #dcdee2;border-radius: 4px;color: #515a6e;background-color: #fff;background-image: none;position: relative;cursor: text;transition: border 0.2s ease-in-out, background 0.2s ease-in-out,box-shadow 0.2s ease-in-out;margin-bottom: 6px;
}
.fadeDownBig-enter-active,
.fadeDownBig-leave-active,
.fadeInDownBig {-webkit-animation-duration: 0.5s;animation-duration: 0.5s;-webkit-animation-fill-mode: both;animation-fill-mode: both;
}
.fadeDownBig-enter-active {-webkit-animation-name: fadeInDownBig;animation-name: fadeInDownBig;
}
.fadeDownBig-leave-active {-webkit-animation-name: fadeOutDownBig;animation-name: fadeOutDownBig;
}
@-webkit-keyframes fadeInDownBig {from {opacity: 0.8;-webkit-transform: translate3d(0, -4px, 0);transform: translate3d(0, -4px, 0);}to {opacity: 1;-webkit-transform: none;transform: none;}
}
@keyframes fadeInDownBig {from {opacity: 0.8;-webkit-transform: translate3d(0, -4px, 0);transform: translate3d(0, -4px, 0);}to {opacity: 1;-webkit-transform: none;transform: none;}
}
@-webkit-keyframes fadeOutDownBig {from {opacity: 1;}to {opacity: 0.8;-webkit-transform: translate3d(0, -4px, 0);transform: translate3d(0, -4px, 0);}
}
@keyframes fadeOutDownBig {from {opacity: 1;}to {opacity: 0;}
}

vue @click 赋值_vue 手写一个时间选择器相关推荐

  1. vue 手写一个时间选择器

    最近研究了 DatePicker 的实现原理后做了一个 vue 的 DatePicker 组件,今天带大家一步一步实现 DatePicker 的 vue 组件. 原理 DatePicker 的原理是- ...

  2. 深入Vue底层,手写一个vuex

    深入底层,手把手教你写一个Vuex 1. Vuex是什么?什么场景下使用? 2. Vuex的基本使用 3. 手写一个vuex 1. Vuex是什么?什么场景下使用? Vuex是vue的一个插件,叫做状 ...

  3. Vue手写一个日历组件

    工作中遇到一个需求是根据日历查看某一天/某一周/某一月的睡眠报告,但是找了好多日历组件都不是很符合需求,只好自己手写一个日历组件,顺便记录一下. 先来看看设计图是什么样式, 跟其他日历有点不一样,这个 ...

  4. vue手写一个计算器

    计算器大家都不陌生 有计算器机器 有手机计算器 网页计算器! 那么好 今天我来给大家手写一个计算器 啥都不说上操作 请听题:vue手写计算器 一个个小方块拼成一个计算器 绿色比较好 可以缓解视力哦 i ...

  5. vue中手写一个放大镜功能

    vue中手写一个放大镜功能 有的时候需要对图片进行放大,类似于电商的商品放大功能,于是在这个想法上写了一个放大镜的功能,并且在放大镜的基础上新添加了一些小功能,下面开始吧! 放大镜是封装的组件的形式, ...

  6. vue 使用fs_模仿vue-cli,手写一个脚手架

    vue-cli 在vue的开发的过程中,经常会使用到vue-cli脚手架工具去生成一个项目.在终端运行命令vue create hello-world后,就会有许多自动的脚本运行. 为什么会这样运行呢 ...

  7. @cacheable 设置过期时间_缓存面试三连击——聊聊Redis过期策略?内存淘汰机制?再手写一个LRU 吧!...

    大家好,今天我和大家想聊一聊有关redis的过期策略的话题. 听到这里你也许会觉得:"我去,我只是个日常搬砖的,这种偏底层的知识点,我需要care吗?" 话虽如此·,但是兄die, ...

  8. 基于vue手写一个分屏器,通过鼠标控制屏幕宽度。

    基于vue手写一个分屏器,通过鼠标控制屏幕宽度. 先来看看实现效果: QQ录屏20220403095856 下面是实现代码: <template><section class=&qu ...

  9. Vue 源码之手写Vue Router

    Vue 源码之手写Vue Router 源码地址:https://github.com/CONOR007/Handwritten-routing 一.Vue Router的两种模式 hash模式实现原 ...

最新文章

  1. mysql schedule event,MySQL 定时器event
  2. ai怎么画循环曲线,怎么用 Adobe Illustrator 画出曲率连续的曲线?
  3. 百度黄埔学院学员开题 | ‘首席 AI 架构师’实战之旅开启!
  4. redis Hash槽
  5. java有 号_JAVA揭竿而起总要有名号
  6. java继承孙子类_Java:类与继承
  7. session中存放一个对象,只修改对象的属性,不将修改后的对象存放session,发现session中存放的对象也发生改变!
  8. java 程序输出 赵_编写一个完整的JAVA的程序
  9. gcc之UTF-8编码
  10. 路畅安卓最新固件升级_路畅车载导航系统刷机-路畅导航系统刷机固件大全下载最新完整版-《百度网盘下载》西西软件下载...
  11. 乐优商城(04)--商品规格
  12. 1、Django下载与搭建、配置环境变量
  13. 三级行政区划 mysql_全国三级行政区划数据 常用查询语句
  14. iOS后台唤醒实战:微信收款到账语音提醒技术总结
  15. mysql 允许局域网连接_设置Mysql允许局域网或外部连接
  16. 2、智慧变电站 - 地面、围墙绘制及动态贴图
  17. python selenium下载电子书
  18. 找一份好工作只需要这几招
  19. 教孩子学习乘法和除法,我算是绞尽脑汁了
  20. 用Tagxedo制作文字云

热门文章

  1. SpringBoot面向切面编程-用AOP方式管理日志
  2. python中fit什么意思_使用Logit()和fit()在python中进行逻辑回归
  3. java求一个数的阶乘_Java如何使用方法计算一个数字的阶乘值?
  4. string类有可以调换方向的函数吗_C++中的string类的用法小结
  5. pandas的str函数的一些功能
  6. 怎么制作升温曲线图_利用金字塔原理制作PPT,完整有逻辑
  7. php中address,address.php
  8. file 选择的文件胖多有多大_「HTML5 进阶」FileAPI 文件操作实战,内附详细案例,建议收藏...
  9. pythin怎么根据月份获取月初和月末_信息流是什么?到底应该怎么去做?
  10. android gridview 间隔线,Android开发之RecyclerView的间隔线处理