基于vue2+elementUI实现年份范围选择器,支持时间跨度设置。代码如下:
<template>
<el-popover
ref="popover"
placement="bottom"
v-model="showPanel"
popper-class="custom_year_range"
trigger="manual"
@hide="onHide()"
v-clickoutside="
() => {
showPanel = false;
}
">
<div class="_inner floatPanel">
<div class="_inner leftPanel">
<div class="_inner panelHead">
<i class="_inner el-icon-d-arrow-left" @click="onClickLeft"></i>
<span>
{{ leftYearList[0] + '年 ' + '- ' + leftYearList[9] + '年' }}
</span>
</div>
<div class="_inner panelContent">
<div
:class="{
oneSelected: item === startYear && oneSelected,
startSelected: item === startYear,
endSelected: item === endYear,
betweenSelected: item > startYear && item < endYear,
disabledSelect: compareNum && (compareNum - maxRange > item || compareNum + maxRange < item)
}"
v-for="item in leftYearList"
:key="item">
<a
:class="{
cell: true,
_inner: true,
selected: item === startYear || item === endYear,
isDisabled: compareNum && (compareNum - maxRange > item || compareNum + maxRange < item)
}"
@click="onClickItem(item)"
@mouseover="onHoverItem(item)">
{{ item }}
</a>
</div>
</div>
</div>
<div class="_inner rightPanel">
<div class="_inner panelHead">
<i class="_inner el-icon-d-arrow-right" @click="onClickRight"></i>
<span>{{ rightYearList[0] + '年 ' + '- ' + rightYearList[9] + '年' }}</span>
</div>
<div class="_inner panelContent">
<div
:class="{
startSelected: item === startYear,
endSelected: item === endYear,
betweenSelected: item > startYear && item < endYear,
disabledSelect: compareNum && (compareNum - maxRange > item || compareNum + maxRange < item)
}"
v-for="item in rightYearList"
:key="item">
<a
:class="{
cell: true,
_inner: true,
isDisabled:
compareNum && (compareNum - maxRange > item || compareNum + maxRange < item),
selected: item === endYear || item === startYear
}"
@click="onClickItem(item)"
@mouseover="onHoverItem(item)">
{{ item }}
</a>
</div>
</div>
</div>
</div>
<div slot="reference">
<div
ref="yearPicker"
@mouseenter="startShowYear && endShowYear && (showClearIcon = true)"
@mouseleave="showClearIcon = false"
class="yearPicker el-date-editor el-range-editor el-input__inner el-date-editor--daterange el-range-editor--small">
<i class="el-input__icon el-range__icon el-icon-date"></i>
<input
class="_inner range_input"
ref="inputLeft"
type="text"
name="yearInput"
:placeholder="startPlaceholder"
v-model="startShowYear"
@focus="onFocus"
@keyup="handleInput('start')" />
<span class="el-range-separator">{{ sp }}</span>
<input
class="_inner range_input"
ref="inputRight"
type="text"
name="yearInput"
:placeholder="endPlaceholder"
v-model="endShowYear"
@focus="onFocus"
@keyup="handleInput('end')" />
<i
v-if="showClearIcon && clearable"
class="el-icon-circle-close clearIcon"
@click.prevent="onClickClear()"></i>
</div>
</div>
</el-popover>
</template>
<script>
import moment from 'moment';
import {clickoutside} from '../directives/directives.m.js';
const SELECT_STATE = {
unselect: 0,
selecting: 1,
selected: 2
};
export default {
name: 'yearPicker',
directives: {clickoutside},
computed: {
oneSelected() {
return (
this.curState === SELECT_STATE.selecting && (this.startYear === this.endYear || this.endYear == null)
);
},
leftYearList() {
return this.yearList.slice(0, 10);
},
rightYearList() {
return this.yearList.slice(10, 20);
}
},
props: {
sp: {
default: '至'
},
value: {
default: null
},
startPlaceholder: {
type: String,
default: '开始年份'
},
endPlaceholder: {
type: String,
default: '结束年份'
},
clearable: {
type: Boolean,
default: false
},
// 是否需要限制时间跨度 配合maxRange使用
hasDisabled: {
type: Boolean,
default: false
},
// 时间跨度 必须与hasDisabled配合使用才生效
maxRange: {
type: Number,
default: 0
}
},
data() {
return {
itemBg: {},
startShowYear: null,
endShowYear: null,
yearList: [],
showPanel: false,
startYear: null,
endYear: null,
curYear: 0,
curSelectedYear: 0,
curState: SELECT_STATE.unselect,
showClearIcon: false,
compareNum: 0
};
},
methods: {
handleInput(type) {
switch (type) {
case 'start':
if (isNaN(this.startShowYear)) {
this.startShowYear = this.startYear;
return;
}
this.startYear = this.startShowYear * 1;
break;
case 'end':
if (isNaN(this.endShowYear)) {
this.endShowYear = this.endYear;
return;
}
this.endYear = this.endShowYear * 1;
break;
}
[this.startYear, this.endYear] = [this.endYear, this.startYear];
this.startShowYear = this.startYear;
this.endShowYear = this.endYear;
},
onHoverItem(iYear) {
if (this.hasDisabled) {
if (iYear > this.compareNum + this.maxRange || iYear < this.compareNum - this.maxRange) {
return;
}
}
if (this.curState === SELECT_STATE.selecting) {
const tmpStart = this.curSelectedYear;
this.endYear = Math.max(tmpStart, iYear);
this.startYear = Math.min(tmpStart, iYear);
}
},
async onClickItem(selectYear) {
if (this.hasDisabled) {
if (
this.compareNum &&
(selectYear > this.compareNum + this.maxRange || selectYear < this.compareNum - this.maxRange)
) {
return;
}
}
if (this.curState === SELECT_STATE.unselect || this.curState === SELECT_STATE.selected) {
this.startYear = selectYear;
this.curSelectedYear = selectYear;
this.endYear = null;
this.curState = SELECT_STATE.selecting;
if (this.hasDisabled) {
this.compareNum = selectYear;
}
} else if (this.curState === SELECT_STATE.selecting) {
this.endShowYear = this.endYear || this.startYear;
this.startShowYear = this.startYear;
this.curState = SELECT_STATE.selected;
await this.$nextTick();
this.showPanel = false;
this.$emit('picker-change', [this.startShowYear, this.endShowYear]);
// this?.$parent?.$parent?.$parent?.$parent?.$parent.clearValidate?.();
}
},
async onFocus() {
if (this.value?.length) {
const [first, end] = this.value || [];
this.startYear = first || null;
this.endYear = end || null;
this.curState = SELECT_STATE.selected;
this.startShowYear = first;
this.endShowYear = end;
this.updateYearList();
this.compareNum = 0;
}
await this.$nextTick();
this.showPanel = true;
},
updateYearList() {
const startYear = ~~(this.curYear / 10) * 10 - 10;
this.yearList = [];
for (let index = 0; index < 20; index++) {
this.yearList.push(startYear + index);
}
},
onClickLeft() {
this.curYear = this.curYear * 1 - 10;
this.updateYearList();
},
onClickRight() {
this.curYear = this.curYear * 1 + 10;
this.updateYearList();
},
onClickClear() {
this.startYear = null;
this.endYear = null;
this.startShowYear = null;
this.endShowYear = null;
this.curState = SELECT_STATE.selected;
this.curYear = moment().format('yyyy');
this.updateYearList();
this.$emit('picker-change', []);
},
onHide() {
this.startYear = null;
this.endYear = null;
this.curState = SELECT_STATE.selected;
this.curYear = moment().format('yyyy');
this.updateYearList();
}
},
watch: {
value: {
handler(val) {
if (val?.length === 0) {
return;
}
const [first, end] = val || [];
this.startShowYear = first;
this.endShowYear = end;
},
immediate: true,
deep: true
},
startShowYear: {
handler(val) {
this.$emit('input', [val, this.endShowYear || '']);
},
immediate: true,
deep: true
},
endShowYear: {
handler(val) {
this.$emit('input', [this.startShowYear || '', val]);
},
immediate: true,
deep: true
}
},
created() {
const [startYear, endYear] = this.value || [];
if (startYear) {
this.startYear = Number(startYear);
this.endYear = Number(endYear);
this.curState = SELECT_STATE.selected;
this.curYear = startYear;
} else {
this.curYear = moment().format('yyyy');
}
this.updateYearList();
},
mounted() {
window.Vue = this;
}
};
</script>
<style lang="scss">
.custom_year_range {
.floatPanel {
> div {
width: 50%;
}
padding: 0 16px;
display: flex;
background-color: #fff;
z-index: 2000;
border-radius: 4px;
width: 650px;
height: 250px;
top: 40px;
left: -50px;
.panelContent {
display: flex;
flex-wrap: wrap;
width: 100%;
height: calc(100% - 70px);
.oneSelected {
border-top-right-radius: 24px;
border-bottom-right-radius: 24px;
}
.startSelected {
background-color: #f2f6fc;
border-top-left-radius: 24px;
border-bottom-left-radius: 24px;
}
.endSelected {
background-color: #f2f6fc;
border-top-right-radius: 24px;
border-bottom-right-radius: 24px;
}
.betweenSelected {
background-color: #f2f6fc;
}
.disabledSelect {
background-color: #f5f5f5;
cursor: not-allowed;
}
> div {
width: 75px;
height: 48px;
line-height: 48px;
margin: 3px 0;
text-align: center;
a {
display: inline-block;
cursor: pointer;
width: 60px;
height: 36px;
line-height: 36px;
border-radius: 18px;
&:hover {
// color: #4088fe;
text-decoration: none;
cursor: pointer;
}
}
a.isDisabled:hover {
color: #4088fe;
text-decoration: none;
cursor: not-allowed !important;
}
.selected {
background-color: #4088fe;
color: #fff;
&:hover {
color: #fff !important;
}
}
}
}
.panelHead {
position: relative;
height: 46px;
line-height: 46px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
span {
font-size: 16px;
font-weight: 500;
padding: 0 5px;
line-height: 22px;
text-align: center;
cursor: pointer;
color: #606266;
&:hover {
color: #4088fe;
}
}
i {
position: absolute;
cursor: pointer;
&:hover {
color: #3e77fc;
}
}
}
.rightPanel {
padding-left: 8px;
}
.leftPanel .panelHead i {
left: 20px;
}
.rightPanel .panelHead i {
right: 20px;
}
}
.floatPanel::before {
content: '';
height: 100%;
top: 0;
position: absolute;
left: 50%;
width: 1px;
border-left: 1px solid #e4e4e4;
}
.el-range-separator {
min-width: 24px;
border: 1px solid #e4e4e4;
}
}
</style>
<style lang="scss" scoped>
.yearPicker {
width: 290px;
.range_input {
appearance: none;
border: none;
outline: 0;
padding: 0;
width: 40%;
color: #606266;
line-height: 1;
height: 100%;
margin: 0;
text-align: center;
display: inline-block;
}
.clearIcon {
height: 32px;
line-height: 32px;
position: absolute;
right: 8px;
color: #c0c4cc;
transition: all 0.3;
}
.el-range-separator {
min-width: 24px;
}
}
// 自定义指令
export const clickoutside = {
bind(el, binding, vnode) {
function documentHandler(e) {
// 这里判断点击的元素是否是本身,是本身,则返回
if (el.contains(e.target)) {
return false;
}
// 判断指令中是否绑定了函数
if (binding && binding.expression) {
// 如果绑定了函数 则调用那个函数,此处binding.value就是handleClose方法
if (binding.value && binding.value(e)) {
binding.value(e);
}
}
}
// 给当前元素绑定个私有变量,方便在unbind中可以解除事件监听
el.__vueClickOutside__ = documentHandler;
document.addEventListener('click', documentHandler);
},
unbind(el, binding) {
// 解除事件监听
document.removeEventListener('click', el.__vueClickOutside__);
delete el.__vueClickOutside__;
}
};
效果图如下:
因为项目工期比较赶,功能可能会存在缺陷,码友们若有发现,请留言告知,感谢!