不升級Element-UI 版本爲時間選擇器增長標記功能

Element-UI裏的date-picker是個優秀的時間選擇器,支持的選項不少,定製型很強。不過date-picker在2.12版本以前並不支持自定義單元格樣式,也就是2.12的cellClassName功能。因此若是使用了2.12以前的版本,那麼你就沒法直接去更改單元格的樣式了,所以在日曆上就沒法標記出重要日期(好比放假安排)。vue

公司項目裏用的Element-UI版本是2.3.9,可是須要使用2.12版本的那個cellClassName功能。若是你要問爲何不升級到最新版,那我只能說若是升級到了最新版就沒有這篇文章了。數組

目的

  1. 傳入一個數組裏面存儲YYYY-MM-DD格式的時間,在面板上爲符合的數據加上對應的class
  2. 切換panel時已經標記的數據不會丟失
  3. 不能升級到2.12版本

源碼解析

先直接看源碼的結構。 image.pngthis

date-picker的核心是picker.vue,用來操做整個picker的初始化、隱藏、顯示等功能。具體天天的展現是date-table.vue來控制的。 image.png date-table的HTML源碼以下,咱們能夠看出爲每一個TD,也就是單元格增長class是使用了getCellClasses這個方法。遍歷數據使用了rowsspa

<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的。code

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了。orm

固然這裏有一個很坑的地方,那就是不能觸發計算屬性的更新。這是由於計算屬性觸發以後會設置type爲normal,這樣就會讓數據從新渲染,從而覆蓋掉以前的type。因此這裏給tableRows直接賦值,不能用Vue.$set()對象

另外一個問題是,每一個cell裏存的text只是day,而不是一個完整的日期,所以還須要獲取到當前date-table的日期。blog

解決方案

上面咱們分析完了,實現需求咱們須要完成下面的工做:事件

  1. 獲取到tableRows,找出咱們須要的值(經過當前日期判斷)
  2. 修改tableRows的值,而且不能觸發計算屬性。
  3. 封裝成單獨的組件

獲取tableRows咱們須要使用$refs來獲取到組件的數據。代碼以下:ip

//獲取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。

相關文章
相關標籤/搜索