Element-UI里的date-picker是个优秀的时间选择器,支持的选项很多,定制型很强。不过date-picker在2.12版本之前并不支持自定义单元格样式,也就是2.12的cellClassName功能。所以如果使用了2.12之前的版本,那么你就无法直接去更改单元格的样式了,因此在日历上就无法标记出重要日期(比如放假安排)。

公司项目里用的Element-UI版本是2.3.9,但是需要使用2.12版本的那个cellClassName功能。如果你要问为什么不升级到最新版,那我只能说如果升级到了最新版就没有这篇文章了。

目的

  1. 传入一个数组里面存储YYYY-MM-DD格式的时间,在面板上为符合的数据加上对应的class
  2. 切换panel时已经标记的数据不会丢失
  3. 不能升级到2.12版本

源码解析

先直接看源码的结构。

date-picker的核心是picker.vue,用来操作整个picker的初始化、隐藏、显示等功能。具体每天的展示是date-table.vue来控制的。



date-table的HTML源码如下,我们可以看出为每个TD,也就是单元格增加class是使用了getCellClasses这个方法。遍历数据使用了rows

<template>
<table cellspacing="0" cellpadding="0" class="el-date-table" @click="handleClick" @mousemove="handleMouseMove" :class="{ 'is-week-mode': selectionMode === 'week' }">
<tbody>
<tr>
<th v-if="showWeekNumber">{{ t('el.datepicker.week') }}</th>
<th v-for="(week, key) in WEEKS" :key="key">{{ t('el.datepicker.weeks.' + week) }}</th>
</tr>
<tr class="el-date-table__row" v-for="(row, key) in rows" :class="{ current: isWeekActive(row[1]) }" :key="key">
<td v-for="(cell, key) in row" :class="getCellClasses(cell)" :key="key">
<div>
<span>
{{ cell.text }}
</span>
</div>
</td>
</tr>
</tbody>
</table>
</template>
<script>
methods: {
getCellClasses(cell) {
const selectionMode = this.selectionMode;
const defaultValue = this.defaultValue
? Array.isArray(this.defaultValue)
? this.defaultValue
: [this.defaultValue]
: []; let classes = [];
if (
(cell.type === 'normal' || cell.type === 'today') &&
!cell.disabled
) {
classes.push('available');
if (cell.type === 'today') {
classes.push('today');
}
} else {
classes.push(cell.type);
} if (
cell.type === 'normal' &&
defaultValue.some((date) => this.cellMatchesDate(cell, date))
) {
classes.push('default');
} if (
selectionMode === 'day' &&
(cell.type === 'normal' || cell.type === 'today') &&
this.cellMatchesDate(cell, this.value)
) {
classes.push('current');
} if (
cell.inRange &&
(cell.type === 'normal' ||
cell.type === 'today' ||
this.selectionMode === 'week')
) {
classes.push('in-range'); if (cell.start) {
classes.push('start-date');
} if (cell.end) {
classes.push('end-date');
}
} if (cell.disabled) {
classes.push('disabled');
} if (cell.selected) {
classes.push('selected');
}
console.log(classes);
return classes.join(' ');
}
}
</script>

我们看看这个方法有没有办法可以趁虚而入的机会。反复观察之后(差不多观察了一个小时),可以看出在第一个if语句里面,只要type的值不是"normal""today"并且不是disabled时,就会走到else里面,此时就会把type作为class。因此,我们是有机会去更改class的。

rows() {
// TODO: refactory rows / getCellClasses
const date = new Date(this.year, this.month, 1);
let day = getFirstDayOfMonth(date); // day of first day
const dateCountOfMonth = getDayCountOfMonth(
date.getFullYear(),
date.getMonth()
);
const dateCountOfLastMonth = getDayCountOfMonth(
date.getFullYear(),
date.getMonth() === 0 ? 11 : date.getMonth() - 1
); day = day === 0 ? 7 : day; const offset = this.offsetDay;
const rows = this.tableRows;
let count = 1;
let firstDayPosition; const startDate = this.startDate;
const disabledDate = this.disabledDate;
const selectedDate = this.selectedDate || this.value;
const now = clearHours(new Date()); for (let i = 0; i < 6; i++) {
const row = rows[i]; if (this.showWeekNumber) {
if (!row[0]) {
row[0] = {
type: 'week',
text: getWeekNumber(nextDate(startDate, i * 7 + 1))
};
}
} for (let j = 0; j < 7; j++) {
let cell = row[this.showWeekNumber ? j + 1 : j];
if (!cell) {
cell = {
row: i,
column: j,
type: 'normal',
inRange: false,
start: false,
end: false
};
} cell.type = 'normal'; const index = i * 7 + j;
const time = nextDate(startDate, index - offset).getTime();
cell.inRange =
time >= clearHours(this.minDate) &&
time <= clearHours(this.maxDate);
cell.start =
this.minDate && time === clearHours(this.minDate);
cell.end =
this.maxDate && time === clearHours(this.maxDate);
const isToday = time === now; if (isToday) {
cell.type = 'today';
} if (i >= 0 && i <= 1) {
if (j + i * 7 >= day + offset) {
cell.text = count++;
if (count === 2) {
firstDayPosition = i * 7 + j;
}
} else {
cell.text =
dateCountOfLastMonth -
(day + offset - (j % 7)) +
1 +
i * 7;
cell.type = 'prev-month';
}
} else {
if (count <= dateCountOfMonth) {
cell.text = count++;
if (count === 2) {
firstDayPosition = i * 7 + j;
}
} else {
cell.text = count++ - dateCountOfMonth;
cell.type = 'next-month';
}
} let newDate = new Date(time);
cell.disabled =
typeof disabledDate === 'function' &&
disabledDate(newDate);
cell.selected =
Array.isArray(selectedDate) &&
selectedDate.filter(
(date) => date.toString() === newDate.toString()
)[0]; this.$set(row, this.showWeekNumber ? j + 1 : j, cell);
} if (this.selectionMode === 'week') {
const start = this.showWeekNumber ? 1 : 0;
const end = this.showWeekNumber ? 7 : 6;
const isWeekActive = this.isWeekActive(row[start + 1]); row[start].inRange = isWeekActive;
row[start].start = isWeekActive;
row[end].inRange = isWeekActive;
row[end].end = isWeekActive;
}
} rows.firstDayPosition = firstDayPosition; return rows;
}

再看遍历的数据,我们可以看到是一个计算属性rows,这个计算属性使用了tableRows的数据。假如这里每次都需要重新new新的cell对象,那我们的路就走不通了。可惜这里恰好是cell为空时才会创建,所以我们只要可以更改tableRows的值就可以更改class了。

当然这里有一个很坑的地方,那就是不能触发计算属性的更新。这是因为计算属性触发之后会设置type为normal,这样就会让数据重新渲染,从而覆盖掉之前的type。所以这里给tableRows直接赋值,不能用Vue.$set()

另一个问题是,每个cell里存的text只是day,而不是一个完整的日期,因此还需要获取到当前date-table的日期。

解决方案

上面我们分析完了,实现需求我们需要完成下面的工作:

  1. 获取到tableRows,找出我们需要的值(通过当前日期判断)
  2. 修改tableRows的值,并且不能触发计算属性。
  3. 封装成单独的组件

获取tableRows我们需要使用$refs来获取到组件的数据。代码如下:

//获取tableRows
this.$refs.datePicker.picker.$children[0].tableRows;
//获取到panel的当前日期
this.$refs.datePicker.picker.$children[0].date;

datePicker是原生组件的ref,picker是组件内部的一个子组件。picker的内部分成了panel和input,$children[0]就是panel组件。

然后根据这两个我们可以写出一个修改tableRows的方法,代码如下:

	    /**
* 根据datePicker的当前时间获取YYYY-MM-DD格式的时间
* date-table是6*7的表格,因此最多会显示三个月份的数据
* 此处是根据单元格的type计算所属月份
*/
getFormatDate(val) {
const date = this.$refs.datePicker.picker.$children[0].date;
let formatDate = moment(date);
formatDate.set('date', val.text);
if (val.type == 'prev-month') {
formatDate.subtract(1, 'M');
} else if (val.type == 'next-month') {
formatDate.add(1, 'M');
}
return formatDate.format('YYYY-MM-DD');
},
//检查单元格日期是否需要标记
checkMarked(cell) {
return this.mark.indexOf(this.getFormatDate(cell)) != -1;
},
//标记单元格
markDate() {
//获取到el-date-picker内部的数组
const rows = this.$refs.datePicker.picker.$children[0].tableRows;
//遍历修改数据为
for (let i = 0; i < rows.length; i++) {
for (let j = 0; j < rows[i].length; j++) {
let cell = rows[i][j];
if (this.checkMarked(cell)) {
cell.type = this.cellClassName;
}
}
}
//el-date-picker内部使用了计算属性,如果此处使用Vue.$set将会调用计算属性从而覆盖掉设置的class
this.$refs.datePicker.picker.$children[0].tableRows = rows;
}

方法的作用我在代码的注释里写的很清楚了,其实里面重点在于不要让组件的计算属性触发,所以不要使用Vue.$set

在封装的组件内部,我还使用了定时器来保证切换页码的时候也能实时修改到class。这个解决方法不优雅,但是我在源码里没有看到翻页的回调事件。理论上我应该捕捉鼠标的行为,鼠标点击之后触发markDate()方法,但是暂时没法实现。如果你有更好的实现方案,可以在评论区留言。

组件源码

下面给出完整的组件源码:

<template>
<el-date-picker v-model="bindingDate" :align="align" :default-value="defaultDate" :type="type" :placeholder="placeholder" :picker-options="pickerOptions" ref='datePicker' @focus="handleFocus">
</el-date-picker>
</template> <script>
import moment from 'moment';
export default {
props: {
value: {
default: Date.now()
},
//type
type: {
default: () => {
return 'date';
}
},
placeholder: {
default: () => {
return '请选择日期';
}
},
//是否可编辑
editable: {
type: Boolean,
default: true
},
//需要标记的数组(YYYY-MM-DD格式)
mark: {
type: Array
},
//默认时间
defaultDate: {
default: () => {
return new Date();
}
},
//自定义的单元格标记
cellClassName: {
type: String,
default: 'marked'
},
align: {
type: String,
default: 'left'
},
pickerOptions: {
default: {}
},
//是否可筛选
filterable: {
default: () => {
return true;
}
}
},
data() {
return {
//定时器
timer: ''
};
}, mounted() {
let _this = this;
//强制datePicker初始化
this.$refs.datePicker.mountPicker();
//使用定时器刷新单元格
this.timer = window.setInterval(() => {
_this.markDate();
}, 1000);
},
//销毁timer
beforeDestroy() {
clearInterval(this.timer);
},
computed: {
bindingDate: {
get: function() {
return this.value;
},
set: function(value) {
this.$emit('input', value);
}
}
},
watch: {
mark: function(val) {
if (val && val.length > 0) {
this.markDate();
}
}
},
methods: {
/**
* 根据datePicker的当前时间获取YYYY-MM-DD格式的时间
* date-table是6*7的表格,因此最多会显示三个月份的数据
* 此处是根据单元格的type计算所属月份
*/
getFormatDate(val) {
const date = this.$refs.datePicker.picker.$children[0].date;
let formatDate = moment(date);
formatDate.set('date', val.text);
if (val.type == 'prev-month') {
formatDate.subtract(1, 'M');
} else if (val.type == 'next-month') {
formatDate.add(1, 'M');
}
return formatDate.format('YYYY-MM-DD');
},
//检查单元格日期是否需要标记
checkMarked(cell) {
return this.mark.indexOf(this.getFormatDate(cell)) != -1;
},
//focus事件
handleFocus() {
this.markDate();
},
//标记单元格
markDate() {
//获取到el-date-picker内部的数组
const rows = this.$refs.datePicker.picker.$children[0].tableRows;
//遍历修改数据为
for (let i = 0; i < rows.length; i++) {
for (let j = 0; j < rows[i].length; j++) {
let cell = rows[i][j];
if (this.checkMarked(cell)) {
cell.type = this.cellClassName;
}
}
}
//el-date-picker内部使用了计算属性,如果此处使用Vue.$set将会调用计算属性从而覆盖掉设置的class
//故此处为直接赋值
this.$refs.datePicker.picker.$children[0].tableRows = rows;
}
}
};
</script>

总结

总结一下,本篇的目的是在不升级Element-UI版本的前提下,为DatePicker增加标记重要日期的功能(这里再次建议你,能升级的前提下优先考虑升级)。主要利用了date-table内部获取class的一个判断语句的漏洞以及直接给对象赋值不会触发计算属性这个特性。封装的组件内部使用了定时器来保证翻页的时候也能修改class。

不升级Element-UI 版本为时间选择器增加标记功能的更多相关文章

  1. element ui 中的时间选择器怎么设置默认值/el-date-picker区间选择器怎么这是默认值

    template代码 <el-date-picker value-format="yyyy-MM-dd" v-model="search.date" ty ...

  2. element UI中的select选择器的change方法需要传递多个值

    如果直接调用change事件,不传任何参数,则可以获取到当前选中的值(因为默认会将event参数传递过去) 场景: 你需要将select选择器 ”选中的当前元素“ 和 ”其他你需要的值“ 一起传递过去 ...

  3. vue + element ui table表格二次封装 常用功能

    因为在做后台管理项目的时候用到了大量的表格, 且功能大多相同,因此封装了一些常用的功能, 方便多次复用. 组件封装代码: <template> <el-table :data=&qu ...

  4. element UI Cascader 级联选择器 编辑 修改 数组 路径 问题(转载)

    来源:https://segmentfault.com/a/1190000014827485 element UI的Cascader级联选择器编辑时 vue.js element-ui 2 eleme ...

  5. vue+element ui项目总结点(一)select、Cascader级联选择器、encodeURI、decodeURI转码解码、mockjs用法、路由懒加载三种方式

    不多说上代码: <template> <div class="hello"> <h1>{{ msg }}</h1> <p> ...

  6. 解决模糊查询问题 element UI 从服务器搜索数据,输入关键字进行查找

    做项目是遇见下拉框的形式,后台返回来3万多条,用element UI中的select选择器中的搜索还是会造成页面卡顿和系统崩溃,因此用了它的远程搜索功能,发现还不错,解决了这个问题. 代码1 < ...

  7. 【vue-waring】element UI 由版本1.4.12 升级到element-ui@2.0.10

    遇到的问题:element UI   由版本1.4.12 升级到element-ui@2.0.10    cnpm run dev 运行后的waring 状态:解决(相关资料的方法对我没什么用) 解决 ...

  8. 使用element ui 日期选择器获取值后的格式问题

    一般情况下,我们需要给后台的时间格式是: "yyyy-MM-dd" 但是使用Element ui日期选择器获取的值是这样的: Fri Sep :: GMT+ (中国标准时间) 在官 ...

  9. element ui 1.4 升级到 2.0.11

    公司的框架 选取的是 花裤衩大神开源的 基于 element ui + Vue 的后台管理项目, 项目源码就不公开了,记录 分享下 步骤 1. 卸载 element ui 1.4的依赖包 2. 卸载完 ...

随机推荐

  1. PCL点云库(Point Cloud Library)简介

    博客转载自:http://www.pclcn.org/study/shownews.php?lang=cn&id=29 什么是PCL PCL(Point Cloud Library)是在吸收了 ...

  2. linux的dos开关机命令

    常用:halt.reboot CentOS关机命令: 重启命令 reboot shutdown -r now 立刻重启 shutdown -r 10 过10分钟自动重启 shutdown -r 20: ...

  3. Kafka管理与监控——查看和重设消费者组位移

    kafka 0.11.0.0版本丰富了kafka-consumer-groups脚本的功能,用户可以直接使用该脚本很方便地为已有的consumer group重新设置位移. 前提必须consumer ...

  4. CRM总结大纲

    目录 一. CRM客户关系管理系统 1. CRM是什么? 里面都有哪些功能(业务)? 2. 什么是公户?什么是私户?为什么要做这个区分? 3. 请列举出CRM系统中的表 4. 通过ORM操作对数据库的 ...

  5. Windows Server 2019安装OpenSSH Server简明教程

    Windows Server 2019安装OpenSSH Server简明教程   Windows Server 2019内置OpenSSH Server组件了.只不过OpenSSH Server默认 ...

  6. Rectangle

    题目描述 在 x 轴上有相互挨着的矩形, 这些矩形有一个边紧贴着 x 轴,现在给出每个矩形的长宽, 所有的矩形看作整体当作一个画布, 则可以在这个画布上画出的最大的矩形的面积是多少.(画出的矩形长和高 ...

  7. 安装部署FastDFS

    安装部署FastDFS 此篇博文是在安装好虚拟机和CentOS7的前提和转自以下几篇博客得来: 1.开启CentOS的网络连接:   1.1.网址:http://blog.csdn.net/white ...

  8. 吉首大学2019年程序设计竞赛(重现赛)- A SARS病毒 (矩阵,欧拉降幂)

    题目链接:https://ac.nowcoder.com/acm/contest/992/A 题意:求出长度为n的字符串个数,字符串由A.C.G.T组成,其中A和C必须成对出现. 思路:我们规定:   ...

  9. 01-Hadoop概述及基础环境搭建

    1 hadoop概述 1.1 为什么会有大数据处理 传统模式已经满足不了大数据的增长 1)存储问题 传统数据库:存储亿级别的数据,需要高性能的服务器:并且解决不了本质问题:只能存结构化数据 大数据存储 ...

  10. 查看Oracle索引是否被使用或者有效

    第一步: explain plan for select * from T_USER where OPEN_ID='12345'; 第二步: select * from table(dbms_xpla ...