上一篇文章介绍了如何实现elementPlus季度选择器,本文将介绍下如何实现年份范围选择器。
演示效果
实现思路
实现方式基本与季度的差别不大,区别就是单元格的计算,季度的选择器只是显示当年年份的季度各自,年份需要根据范围来渲染。首先完成年份的切换,这里我按照element-plus的年份选择器一次切换10个年份,可以根据需求自行调整, 接下来就是按照切换的年份范围,渲染对应的单元格,给单元格设置样式。
与上一篇季度选择器代码有点不同,组件接受的参数名称发生了改变,defaultValue改为了modelValue,使用是可以通过v-model进行绑定,不需要在监听组件事件修改传入值。
完整代码
<div class="el-picker-panel el-date-picker year-panel">
<div class="el-picker-panel__body-wrapper">
<div class="el-picker-panel__body">
<div class="el-date-picker__header el-date-picker__header--bordered">
<span class="el-date-picker__prev-btn">
<button
type="button"
class="d-arrow-left el-picker-panel__icon-btn"
@click="moveByYear(false)"
>
<el-icon><d-arrow-left /></el-icon>
</button>
</span>
<span
role="button"
class="el-date-picker__header-label"
aria-live="polite"
tabindex="0"
>{{ yearLabel }}</span
>
<span class="el-date-picker__next-btn">
<button
type="button"
class="el-picker-panel__icon-btn d-arrow-right"
@click="moveByYear(true)"
>
<el-icon><d-arrow-right /></el-icon>
</button>
</span>
</div>
<div class="el-picker-panel__content">
<BasicYearTable
:min-date="minDate"
:max-date="maxDate"
:date="innerDate"
:range-state="rangeState"
:disabled-date="disabledDate"
@pick="handleRangePick"
@select="onSelect"
@changerange="handleChangeRange"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from 'vue'
import { useDatePicker } from '../../hooks/useDatePicker'
import { useDatePanel } from './hooks/useDatePanel'
import BasicYearTable from './src/BasicYearTable'
export default defineComponent({
name: 'YearPanel',
components: {
BasicYearTable
},
props: {
range: {
type: Array,
default() {
return null
}
},
modelValue: {
type: Array,
require: true,
default() {
return null
}
}
},
emits: ['change', 'update:modelValue'],
setup(props) {
const {
minDate,
maxDate,
disabledDate,
rangeState,
onSelect,
handleRangePick,
handleChangeRange
} = useDatePicker(props)
const { innerDate, moveByYear, yearLabel } = useDatePanel({
minDate
})
return {
minDate,
maxDate,
innerDate,
rangeState,
yearLabel,
moveByYear,
disabledDate,
handleRangePick,
onSelect,
handleChangeRange
}
}
})
</script>
<style>
.year-panel {
width: 100%;
}
.year-panel .el-picker-panel__content {
width: 100%;
margin: 0px;
}
</style>
useDatePanel.js
import { computed, ref } from 'vue'
import _ from 'lodash'
export const useDatePanel = ({ minDate }) => {
const innerDate = ref(_.cloneDeep(minDate.value))
const year = computed(() => {
return innerDate.value.year()
})
const yearLabel = computed(() => {
const startYear = Math.floor(year.value / 10) * 10
return `${startYear} - ${startYear + 9}`
})
/**
* 切换时间
* @param {*} forward
*/
const moveByYear = (forward) => {
const currentDate = innerDate.value
const action = forward ? 'add' : 'subtract'
innerDate.value = currentDate[action](10, 'year')
}
return {
innerDate,
yearLabel,
moveByYear
}
}
import _ from 'lodash'
import { getCurrentInstance, watch, ref, unref } from 'vue'
import dayjs from 'dayjs'
// 将计算时间的相关操作单独抽一个hooks 之后做年份的时候可以复用 季度,年份时间单位不同而已
export const useDatePicker = (props) => {
// 时间范围 超出该范围禁用
const { range } = props
const { emit } = getCurrentInstance()
// 默认值起始时间
const minDate = ref()
// 默认值结束时间
const maxDate = ref()
// 左侧面板时间 ps:面板时间与默认值时间无关 只是用于面板title显示 不需要过于纠结
const leftDate = ref()
// 右侧面板时间
const rightDate = ref()
const rangeState = ref({
endDate: null,
selecing: false
})
const disabledDate = (cell) => {
// range 不存在 不做禁用
if (!range) {
return false
}
const [start, end] = range.map((item) => dayjs(item))
return !(
dayjs(cell).isSameOrAfter(start) && dayjs(cell).isSameOrBefore(end)
)
}
const onSelect = (selecting) => {
rangeState.value.selecting = selecting
if (!selecting) {
rangeState.value.endDate = null
}
}
const handleChangeRange = (val) => {
rangeState.value = val
}
const isValidRange = (range) => {
if (!_.isArray(range)) return false
const [left, right] = range
return (
dayjs.isDayjs(left) && dayjs.isDayjs(right) && left.isSameOrBefore(right)
)
}
const handleRangeConfirm = () => {
const _minDate = unref(minDate)
const _maxDate = unref(maxDate)
if (isValidRange([_minDate, _maxDate])) {
emit(
'change',
[_minDate, _maxDate].map((_) => _.toDate())
)
emit(
'update:modelValue',
[_minDate, _maxDate].map((_) => _.toDate())
)
}
}
const handleRangePick = (val) => {
const minDate_ = val.minDate
const maxDate_ = val.maxDate
if (maxDate.value === maxDate_ && minDate.value === minDate_) {
return
}
maxDate.value = maxDate_
minDate.value = minDate_
handleRangeConfirm()
}
const restoreDefault = () => {
// 将默认转为dayjs时间对象
const [start, end] = props.modelValue.map((item) => dayjs(item))
minDate.value = start
maxDate.value = end
leftDate.value = start
rightDate.value = start.add(1, 'year')
}
watch(
() => props.modelValue,
(value) => {
if (value) {
restoreDefault()
}
},
{
immediate: true
}
)
return {
minDate,
maxDate,
leftDate,
rightDate,
rangeState,
disabledDate,
handleRangePick,
onSelect,
handleChangeRange
}
}
<template>
<table
role="grid"
class="el-year-table el-month-table year-table"
@click="handleYearTableClick"
@mousemove="handleMouseMove"
>
<tbody>
<tr v-for="(row, key) in rows" :key="key">
<td
v-for="(cell, key_) in row"
:key="key_"
:class="getCellStyle(cell)"
@click="handleMonthTableClick"
>
<div>
<span class="cell">
{{ cell.text }}
</span>
</div>
</td>
</tr>
</tbody>
</table>
</template>
<script>
import { computed, defineComponent, ref } from 'vue'
import dayjs from 'dayjs'
import { hasClass } from '@/date-picker-panel/utils'
export default defineComponent({
name: 'BasicYearTable',
props: {
minDate: {
type: Object,
default() {
return null
}
},
maxDate: {
type: Object,
default() {
return null
}
},
date: {
type: Object,
default() {
return null
}
},
disabledDate: {
type: Function,
default() {
return null
}
},
rangeState: {
type: Object,
default() {
return null
}
}
},
setup(props, { emit }) {
const lastRow = ref()
const lastColumn = ref()
const tableRows = ref([[], [], []])
const startYear = computed(() => {
return Math.floor(props.date.year() / 10) * 10
})
// 设置单元格样式
const getCellStyle = (cell) => {
const style = {}
const cellYear = cell.text
style.disabled = props.disabledDate
? props.disabledDate(dayjs().startOf('year').year(cellYear))
: false
style.today = cell.type === 'today'
if (cell.inRange) {
style['in-range'] = true
if (cell.start) {
style['start-date'] = true
}
if (cell.end) {
style['end-date'] = true
}
}
return style
}
const rows = computed(() => {
const rows = tableRows.value
const now = dayjs().startOf('year')
for (let i = 0; i < 3; i++) {
const row = rows[i]
for (let j = 0; j < 4; j++) {
const index = i * 4 + j
// 只显示10个单元格 超出跳出循环
if (index > 9) {
continue
}
const cell = (row[j] ||= {
row: i,
column: j,
type: 'normal',
inRange: false,
start: false,
end: false,
disabled: false
})
cell.type = 'normal'
const calTime = props.date.startOf('year').add(index, 'year')
const calEndDate =
props.rangeState.endDate ||
props.maxDate ||
(props.rangeState.selecting && props.minDate) ||
null
cell.inRange =
!!(
props.minDate &&
calTime.isSameOrAfter(props.minDate, 'year') &&
calEndDate &&
calTime.isSameOrBefore(calEndDate, 'year')
) ||
!!(
props.minDate &&
calTime.isSameOrBefore(props.minDate, 'year') &&
calEndDate &&
calTime.isSameOrAfter(calEndDate, 'year')
)
if (props.minDate?.isSameOrAfter(calEndDate)) {
cell.start = !!(calEndDate && calTime.isSame(calEndDate, 'year'))
cell.end = props.minDate && calTime.isSame(props.minDate, 'year')
} else {
cell.start = !!(
props.minDate && calTime.isSame(props.minDate, 'year')
)
cell.end = !!(calEndDate && calTime.isSame(calEndDate, 'year'))
}
const isToday = now.isSame(calTime)
if (isToday) {
cell.type = 'today'
}
cell.text = startYear.value + i * 4 + j
cell.disabled = props.disabledDate?.(calTime.toDate()) || false
}
}
return rows
})
const handleMouseMove = (event) => {
if (!props.rangeState.selecting) return
let target = event.target
if (target.tagName === 'A') {
target = target.parentNode?.parentNode
}
if (target.tagName === 'DIV') {
target = target.parentNode
}
if (target.tagName !== 'TD') return
const row = target.parentNode.rowIndex
const column = target.cellIndex
if (rows.value[row][column].disabled) return
if (row !== lastRow.value || column !== lastColumn.value) {
lastRow.value = row
lastColumn.value = column
emit('changerange', {
selecting: true,
endDate: props.date.startOf('year').add(row * 4 + column, 'year')
})
}
}
const handleMonthTableClick = (event) => {
const target = event.target.closest('td')
if (target?.tagName !== 'TD') return
if (hasClass(target, 'disabled')) return
const column = target.cellIndex
const row = target.parentNode.rowIndex
const year = row * 4 + column
const newDate = props.date.startOf('year').add(year, 'year')
if (!props.rangeState.selecting) {
emit('pick', { minDate: newDate, maxDate: null })
emit('select', true)
} else {
if (props.minDate && newDate >= props.minDate) {
emit('pick', { minDate: props.minDate, maxDate: newDate })
} else {
emit('pick', { minDate: newDate, maxDate: props.minDate })
}
emit('select', false)
}
}
return {
rows,
getCellStyle,
handleMouseMove,
handleMonthTableClick
}
}
})
</script>
<style>
.year-table td {
padding: 20px 0px;
}
</style>