不升级Element-UI 版本为时间选择器增加标记功能
Element-UI里的date-picker是个优秀的时间选择器,支持的选项很多,定制型很强。不过date-picker在2.12版本之前并不支持自定义单元格样式,也就是2.12的cellClassName功能。所以如果使用了2.12之前的版本,那么你就无法直接去更改单元格的样式了,因此在日历上就无法标记出重要日期(比如放假安排)。
公司项目里用的Element-UI版本是2.3.9,但是需要使用2.12版本的那个cellClassName功能。如果你要问为什么不升级到最新版,那我只能说如果升级到了最新版就没有这篇文章了。
目的
- 传入一个数组里面存储
YYYY-MM-DD格式的时间,在面板上为符合的数据加上对应的class - 切换panel时已经标记的数据不会丢失
- 不能升级到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的日期。
解决方案
上面我们分析完了,实现需求我们需要完成下面的工作:
- 获取到
tableRows,找出我们需要的值(通过当前日期判断) - 修改
tableRows的值,并且不能触发计算属性。 - 封装成单独的组件
获取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 版本为时间选择器增加标记功能的更多相关文章
- element ui 中的时间选择器怎么设置默认值/el-date-picker区间选择器怎么这是默认值
template代码 <el-date-picker value-format="yyyy-MM-dd" v-model="search.date" ty ...
- element UI中的select选择器的change方法需要传递多个值
如果直接调用change事件,不传任何参数,则可以获取到当前选中的值(因为默认会将event参数传递过去) 场景: 你需要将select选择器 ”选中的当前元素“ 和 ”其他你需要的值“ 一起传递过去 ...
- vue + element ui table表格二次封装 常用功能
因为在做后台管理项目的时候用到了大量的表格, 且功能大多相同,因此封装了一些常用的功能, 方便多次复用. 组件封装代码: <template> <el-table :data=&qu ...
- element UI Cascader 级联选择器 编辑 修改 数组 路径 问题(转载)
来源:https://segmentfault.com/a/1190000014827485 element UI的Cascader级联选择器编辑时 vue.js element-ui 2 eleme ...
- vue+element ui项目总结点(一)select、Cascader级联选择器、encodeURI、decodeURI转码解码、mockjs用法、路由懒加载三种方式
不多说上代码: <template> <div class="hello"> <h1>{{ msg }}</h1> <p> ...
- 解决模糊查询问题 element UI 从服务器搜索数据,输入关键字进行查找
做项目是遇见下拉框的形式,后台返回来3万多条,用element UI中的select选择器中的搜索还是会造成页面卡顿和系统崩溃,因此用了它的远程搜索功能,发现还不错,解决了这个问题. 代码1 < ...
- 【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 状态:解决(相关资料的方法对我没什么用) 解决 ...
- 使用element ui 日期选择器获取值后的格式问题
一般情况下,我们需要给后台的时间格式是: "yyyy-MM-dd" 但是使用Element ui日期选择器获取的值是这样的: Fri Sep :: GMT+ (中国标准时间) 在官 ...
- element ui 1.4 升级到 2.0.11
公司的框架 选取的是 花裤衩大神开源的 基于 element ui + Vue 的后台管理项目, 项目源码就不公开了,记录 分享下 步骤 1. 卸载 element ui 1.4的依赖包 2. 卸载完 ...
随机推荐
- Flink初探wordCout
知识点 Flink介绍 1.无界数据-->数据不断产生 2.有界数据-->最终不再改变的数据 3.有界数据集是无界数据集的一个特例 4.有界数据集在flink内部是以一种终态数据集进行处理 ...
- Servlet 异常处理( 配置错误页面)
当一个 Servlet 抛出一个异常时,Web 容器在使用了 exception-type 元素的 web.xml 中搜索与抛出异常类型相匹配的配置. 您必须在 web.xml 中使用 error-p ...
- 几句简单的python代码完成周公解梦功能
<周公解梦>是靠人的梦来卜吉凶的一本于民间流传的解梦书籍,共有七类梦境的解述.这是非常传统的中国文化体系的一部分,但是如何用代码来获取并搜索周公解梦的数据呢?一般情况下,要通过爬虫获取数据 ...
- Delphi 10.2.3 精简版自动激活Embarcadero Delphi 10.2.3 v25.0.29899.2631 Lite v14.4
下载:https://maxwoods.ctfile.com/u/758954/28516301 Embarcadero.Delphi.10.2.RTM.v25.0.26309.314.Lite.v1 ...
- flask(1.0)
目录 一. 关于KeyError和IndexError 二. Python三大主流框架对比 三. flask基础 1.安装flask 2.用flask写出的第一个页面 3.Flask的Response ...
- Python multiprocess模块(上)
multiprocess模块 一. Process模块介绍 1. 直接使用Process模块创建进程 (1)主进程和子进程 (2)if __name__ == "__main__" ...
- C#对IQueryable<T>、IEnumerable<T>的扩展方法
#region IQueryable<T>的扩展方法 #region 根据第三方条件是否为真是否执行指定条件的查询 /// <summary> /// 根据第三方条件是否为真是 ...
- Go语言中的打包和工具链
包 所有Go语言的程序都会组织成若干组文件,每组文件被称为一个包.这样每个包的代码都可以作为很小的复用单元,被其他项目引用. 包名惯例 给包命名的惯例是使用包所在目录的名字.并不需要所有包的名字都与别 ...
- Oracle-DQL 3- 单行函数
单行函数: --使用函数对表中的数据进行运算和处理,针对每行数据返回一个结果,叫做单行函数--包括数字函数,字符函数,日期函数,转换函数,其他函数 1.数字函数 --round(m,n) 将数字m精确 ...
- ARC100E. Or Plus Max
题目 好题.没想出解法. 官方题解: 这个解法和 Small Multiple 那道题的解法有异曲同工之妙. 扩展 若把 $\mathsf{or}$ 改成 $\mathsf{and}$ 或者 $\ma ...