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. 白盒测试 && 接口测试 && 自动化测试

    一.什么是白盒测试 白盒测试是一种测试策略,这种策略允许我们检查程序的内部结构,对程序的逻辑结构进行检查,从中获取测试数据.白盒测试的对象基本是源程序,所以它又称为结构测试或逻辑驱动测试,白盒测试方法 ...

  2. Pytorch-tensor的维度变化

    引言 本篇介绍tensor的维度变化. 维度变化改变的是数据的理解方式! view/reshape:大小不变的条件下,转变shape squeeze/unsqueeze:减少/增加维度 transpo ...

  3. 数组中存放model去重

    在这个项目中出现"添加model数据"数组重复的情况,这就涉及到数组去重的问题了...... 1. 一开始使用的最笨的方法: 依次循环两个数组(原有的数组,选择的数组),双重for ...

  4. Java泛型(7):无界通配符<?>

    无界通配符<?>很容易和原生类型混淆. 以List为例: List表示持有任何Object类型的原生List,其实就等价于List<Object> List<?>表 ...

  5. kubernetes/dashboard Creating sample user

    Creating sample user In this guide, we will find out how to create a new user using Service Account ...

  6. 【ABAP系列】SAP MAC GUI750安装过程

    公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[ABAP系列]SAP MAC GUI750安装 ...

  7. 二分查找算法C++实现

    /************************************************************************* > File Name: binary_se ...

  8. Java中简单测试FastDFS的文件上传

    pom.xml文件内容如下: <dependencies> <!-- fastdfs --> <dependency> <groupId>org.cso ...

  9. 德州扑克AK打法攻略

    AK是所有德扑网游中最受争议的底牌,也是一副令人又爱又恨的底牌.<德州扑克培训大师>根据国内德州扑克网游特性,为大家制作了第一套AK打法攻略,希望所有玩家从今天开始能正确认识AK,发挥AK ...

  10. Apollo分布式部署总结

    环境 操作系统为centOS7 Apollo服务端为Java版本为1.8 MySQL5.8.x 环境DEV 注意事项 按文档安装config与admin的数据库,并修改相关配置 在修改Apollo P ...