基于vue2+elementUI年份范围选择器

基于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__;
    }
};

因为项目工期比较赶,功能可能会存在缺陷,码友们若有发现,请留言告知,感谢!