背景
很久没写博客了,最近接到产品的新需求,需要按季度汇总数据,可是这么重要的组件,Element
官方居然没有提供。网上逛了一圈,发现功能都比较单一,无法满足需求。于是决定整一个并记录下来,供大家参考,欢迎留言共同学习交流。
实现方式
秉承自己动手丰衣足食的原则,通过查看并借鉴 Element
官方 DatePicker 日期选择器
源码,使用 Popover 弹出框
封装了一个 季度选择器
组件 QuarterPicker
,以期用法和功能尽量接近官方类似组件的用法,先来看下最终效果图,如下:
季度选择器 QuarterPicker 组件源码
在项目 components
路径下创建 quarter-picker
文件夹,并在其下创建 index.vue
文件,内容如下:
<!--
* @Descripttion: 季度选择器
* @version: 1.0
* @Author: https://www.lervor.com/
* @Date: 2021-12-06
* @LastEditTime: 2021-12-09
-->
<template>
<el-popover
trigger="focus"
v-model="pickerVisible"
popper-class="lervor-quarter-popover"
:disabled="disabled">
<el-input
ref="reference"
slot="reference"
class="el-date-editor"
readonly
:disabled="disabled"
:size="size"
:placeholder="placeholder"
:value="displayValue"
:validate-event="false"
:style="{ width }"
@mouseenter.native="handleMouseEnter"
@mouseleave.native="showClose = false">
<i slot="prefix"
class="el-input__icon"
:class="triggerClass">
</i>
<i slot="suffix"
class="el-input__icon"
:class="[showClose ? '' + clearIcon : '']"
@click="handleClickIcon"
@mousedown="handleMousedownIcon">
</i>
</el-input>
<div class="lervor-quarter-picker">
<div class="el-date-picker__header el-date-picker__header--bordered">
<button
type="button"
aria-label="前一年"
class="el-picker-panel__icon-btn el-date-picker__prev-btn el-icon-d-arrow-left"
@click="prevYear">
</button>
<span
role="button"
class="el-date-picker__header-label">{{ yearLabel }}</span>
<button
type="button"
aria-label="后一年"
class="el-picker-panel__icon-btn el-date-picker__next-btn el-icon-d-arrow-right"
@click="nextYear">
</button>
</div>
<div class="el-picker-panel__content" style="width: 200px; margin: 10px 15px;">
<table class="lervor-quarter-table" @click="handleTableClick">
<tbody>
<tr>
<td class="available" :class="getCellStyle(0)">
<a class="cell">第一季度</a>
</td>
<td class="available" :class="getCellStyle(1)">
<a class="cell">第二季度</a>
</td>
</tr>
<tr>
<td class="available" :class="getCellStyle(2)">
<a class="cell">第三季度</a>
</td>
<td class="available" :class="getCellStyle(3)">
<a class="cell">第四季度</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</el-popover>
</template>
\<script\>
import { formatDate, prevYear, nextYear, range, nextDate, isDateObject, parseDate } from 'element-ui/src/utils/date-util'
import { hasClass } from 'element-ui/src/utils/dom'
// 获取指定年份和季度的所有日期
const datesInYearAndQuarter = (year, quarter) =\> {
const numOfDays = getDayCountOfQuarter(year, quarter)
const firstDay = new Date(year, quarter \* 3, 1)
return range(numOfDays).map(n =\> nextDate(firstDay, n))
}
// 获取指定年份和季度总天数
const getDayCountOfQuarter = (year, quarter) =\> {
switch(quarter) {
case 0: // 第一季度包含二月,需要对是否闰年进行判断处理
if (year % 4 === 0 \&\& year % 100 !== 0 \|\| year % 400 === 0) {
return 91
} else {
return 90
}
case 1:
return 91
default:
return 92
}
}
export default {
name: 'QuarterPicker',
props: {
size: String,
format: String, // 显示在输入框中的格式,引入季度:q(阿拉伯数字)、Q(中文数字)
valueFormat: String,
placeholder: String,
prefixIcon: String,
clearIcon: {
type: String,
default: 'el-icon-circle-close'
},
disabled: Boolean,
clearable: {
type: Boolean,
default: true
},
width: { // 组件宽度
type: String,
default: ''
},
disabledDate: {}, // 不可用的日期
value: null
},
data() {
return {
showClose: false,
pickerVisible: false,
date: new Date(),
quarterText: \[ '一', '二', '三', '四' \]
}
},
computed: {
triggerClass() {
return this.prefixIcon \|\| 'el-icon-date'
},
displayValue() {
if (!this.value) return null
// 季度,从0开始
const quarter = parseInt(this.parsedValue.getMonth() / 3)
let fDate = formatDate(this.parsedValue, this.format)
fDate = fDate.replace(/q/, quarter + 1).replace(/Q/, this.quarterText\[quarter\])
return fDate
},
year() {
return this.date.getFullYear()
},
yearLabel() {
return this.year + ' 年'
},
parsedValue() {
if (!this.value) {
return this.value
}
if (isDateObject(this.value)) {
return this.value
}
// 非时间格式且设置了valueFormat,进行时间转换
if (this.valueFormat) {
return parseDate(this.value, this.valueFormat)
}
// 非时间格式且未设置valueFormat,再尝试转换时间
return new Date(this.value)
}
},
watch: {
value(value) {
this.date = value ? this.parsedValue : new Date()
}
},
methods: {
handleMouseEnter() {
if (this.disabled) return
if (this.value \&\& this.clearable) {
this.showClose = true
}
},
handleClickIcon(event) {
if (this.disabled) return
if (this.showClose) {
this.$emit('input', null)
this.$emit('change', null)
this.showClose = false
this.pickerVisible = false
this.$refs.reference.blur()
}
},
handleMousedownIcon(event) {
// 阻止鼠标按下清空按钮,防止清空数据时季度选择面板闪现
event.preventDefault()
},
handleTableClick(event) {
let target = event.target
if (target.tagName === 'A') {
target = target.parentNode
}
if (target.tagName !== 'TD') return
if (hasClass(target, 'disabled')) return
const column = target.cellIndex
const row = target.parentNode.rowIndex
// 季度,从0开始
const quarter = row \* 2 + column
// 季度开始月份,从0开始
const month = quarter \* 3
let newDate = new Date(this.year, month, 1)
if (this.valueFormat) {
newDate = formatDate(newDate, this.valueFormat)
}
this.pickerVisible = false
this.$emit('input', newDate)
this.$emit('change', newDate)
},
prevYear() {
this.date = prevYear(this.date)
},
nextYear() {
this.date = nextYear(this.date)
},
getCellStyle(quarter) {
const style = {}
const today = new Date()
const date = this.parsedValue ? this.parsedValue : today
style.disabled = typeof this.disabledDate === 'function'
? datesInYearAndQuarter(this.year, quarter).every(this.disabledDate) : false
// 当前选中的季度样式
style.current = date.getFullYear() === this.year \&\& parseInt(date.getMonth() / 3) === quarter
// 今日所在季度样式
style.quarter = today.getFullYear() === this.year \&\& parseInt(today.getMonth() / 3) === quarter
return style
}
}
}
\</script\>
\<style\>
.lervor-quarter-picker {
line-height: 30px;
}
.lervor-quarter-popover {
padding: 0;
}
.lervor-quarter-table {
font-size: 12px;
margin: -1px;
border-collapse: collapse;
width: 100%;
}
.lervor-quarter-table td {
text-align: center;
padding: 10px 3px;
cursor: pointer;
}
.lervor-quarter-table td .cell {
height: 32px;
display: block;
line-height: 32px;
color: #606266;
margin: 0 auto;
}
.lervor-quarter-table td .cell:hover {
color: #1890ff;
}
.lervor-quarter-table td.current:not(.disabled) .cell {
color: #409eff;
}
.lervor-quarter-table td.quarter .cell {
color: #409eff;
font-weight: 700;
}
`.lervor-quarter-table td.disabled .cell {
background-color: #F5F7FA;
cursor: not-allowed;
color: #C0C4CC;
}
</style>`
季度选择器 QuarterPicker 组件文档
Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | |:----------------|:---------------------------------|:---------|:-------------------|:---------------------| | value / v-model | 绑定值 | date | --- | --- | | disabled | 禁用 | boolean | --- | false | | clearable | 是否显示清除按钮 | boolean | --- | true | | size | 输入框尺寸 | string | large, small, mini | --- | | width | 组件宽度 | string | --- | --- | | placeholder | 占位内容 | string | --- | --- | | format | 显示在输入框中的格式,引入季度:q(阿拉伯数字)、Q(中文数字) | string | --- | --- | | value-format | 可选,绑定值的格式。不指定则绑定值为 Date 对象 | string | --- | --- | | prefix-icon | 自定义头部图标的类名 | string | --- | el-icon-date | | clear-icon | 自定义清空图标的类名 | string | --- | el-icon-circle-close | | disabled-date | 不可用的日期 | function | --- | --- |
Events
| 事件名称 | 说明 | 回调参数 | |:-------|:---------|:----------------------------------| | change | 用户选择值时触发 | 组件绑定值。格式与绑定值一致,可受 value-format 控制 |
注意:本组件获取到的值为所选季度的第一天日期,如 2021年第二季度
,其值为 2021-04-01 00:00:00
,类似官方 年/月选择器
。
季度选择器 QuarterPicker 组件的使用
简单使用如下:
<template>
<div>
<quarter-picker
width="150px"
format="yyyy年q季度"
value-format="yyyyMM"
placeholder="选择季度"
v-model="quarterDate"
:disabled-date="disabledQuarter"
@change="handleChangeQuarter" />
</div>
</template>
`<script>
import QuarterPicker from '@/components/quarter-picker'
export default {
components: {
QuarterPicker
},
data() {
return {
quarterDate: '202104',
// 禁用日期/季度示例,只允许选择本年度本季度(不含)之前的季度
disabledQuarter: time => {
const now = new Date()
return time.getFullYear() > now.getFullYear() || time.getFullYear() === now.getFullYear() && parseInt(time.getMonth() / 3) >= parseInt(now.getMonth() / 3)
}
}
},
methods: {
handleChangeQuarter(date) {
console.info(date)
}
}
}
</script>`
本例子的最终效果图如下:
友情提示:也可将组件进行全局注册,方便后续直接使用。