基于vue2+elementUI实现年份范围选择器,支持时间跨度设置。代码如下:
<template><el-popoverref="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"><divref="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><inputclass="_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><inputclass="_inner range_input"ref="inputRight"type="text"name="yearInput":placeholder="endPlaceholder"v-model="endShowYear"@focus="onFocus"@keyup="handleInput('end')" /><iv-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__;}
};
因为项目工期比较赶,功能可能会存在缺陷,码友们若有发现,请留言告知,感谢!