el-time-picker设置分钟间隔

1,681 阅读1分钟

需求的目标是分钟列表的间隔为5分钟

先看效果

屏幕录制2023-02-09-11.14.42.gif

但是饿了么是不支持设置分钟间隔的,想要设置间隔,就得换组件,比如下方这个

image.png

可是我口才不行,说服不了ui,那就手写一个呗

懒得废话了,直接上代码和使用方式

<template>
    <div class="zm-time-picker">
        <input
            ref="inputRef"
            id="zm-input-0208"
            type="text"
            class="input"
            :class="{ 'focus': isFocus, 'disabled': disabled }"
            readonly
            :placeholder="placeholder"
            :value="displayValue"
            :disabled="disabled"
            @input="$emit('input', $event.target.value)"
            @focus="handleFocus"
            @blur="handleBlur"
            @mouseenter="hanleMouseenter"
            @mouseleave="showCloseIcon = false">
        
        <!-- 输入框内前缀图标 -->
        <span
            slot="prefix"
            class="input-prefix">
            <i class="el-icon-time"></i>
        </span>

        <!-- 一键清空图标 -->
        <span
            v-if="clearable"
            @mouseenter="hanleMouseenter"
            @mouseleave="showCloseIcon = false"
            @click="hanldeCloseIconClick"
            slot="suffix"
            class="input-suffix">
            <i v-if="showCloseIcon" class="el-icon-circle-close"></i>
        </span>

        <!-- 选择面板 -->
        <div
            v-if="showPanel"
            @click="panelClick"
            id="zm-panel-0208"
            class="time-panel">
            <div class="time-panel-content">
                <!-- 小时列表 -->
                <ul
                    ref="housListRef"
                    @scroll.stop="bindHoursScroll"
                    class="time-spinner-list">
                    <li
                        @mousedown="choseHour(index, $event)"
                        class="time-spinner-list-item"
                        :class="{ 'active': index === hoursActiveIndex }"
                        v-for="(h, index) in getHoursList"
                        :key="index">
                        {{ h > 9 ? h : '0' + h }}
                    </li>
                </ul>

                <!-- 分钟列表 -->
                <ul
                    ref="minutesListRef"
                    @scroll.stop="bindMinutesScroll"
                    class="time-spinner-list">
                    <li
                        @mousedown="choseMinute(index, $event)"
                        class="time-spinner-list-item"
                        :class="{ 'active': index === minutesActiveIndex }"
                        v-for="(m, index) in getMinutesList"
                        :key="index">
                        {{ m > 9 ? m : '0' + m }}
                    </li>
                </ul>

                <!-- 当前选中范围标记 -->
                <div class="active-flag"></div>
            </div>

            <!-- 底部按钮 -->
            <div class="time-panel-footer">
                <div
                    @mousedown="cancelBtnClick"
                    class="cancel-btn">Cancel</div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        name: 'TimePicker',
        props: {
            value: String | Number, // v-model 日期对象 | 时间戳
            placeholder: {
                type: String,
                default: ''
            },
            defaultValue: { // 默认显示时间
                default() {
                    return Date.now()
                }
            },
            minutesStep: { // 设置分钟跨度
               validator: function (value) {
                    return typeof value === 'number' && value > 0
                }
            },
            disabled: {
                type: Boolean,
                default: false
            },
            clearable: {
                type: Boolean,
                default: true
            }
        },
        mounted() {
            document.addEventListener('click', this.hidePanel)
        },
        beforeDestroy() {
            document.removeEventListener('click', this.hidePanel)
        },
        data() {
            return {
                showCloseIcon: false,
                showPanel: false,
                timer: null,
                hoursActiveIndex: 0,
                minutesActiveIndex: 0,
                isFocus: false,
                oldVal: null // 记录value, 用于cancel还原
            }
        },
        computed: {
            valueIsEmpty() {
                const val = this.value
                if (val) {
                    return false
                }
                return true
            },
            // 输入框内展示的值
            displayValue() {
                const val = this.value
                if (val) {
                    const date = new Date(val)
                    let hours = date.getHours()
                    let minutes = date.getMinutes()

                    hours = hours > 9 ? hours : '0' + hours
                    minutes = minutes > 9 ? minutes : '0' + minutes

                    return hours + ':' + minutes
                }
                return ''
            },
            // 分钟列表
            getMinutesList() {
                const res = []
                for (let i = 0; i < 60; i ++ ) {
                    if (this.minutesStep ) {
                        if (i % this.minutesStep === 0) res.push(i)
                    } else {
                        res.push(i)
                    }
                }
                return res
            },
            // 小时列表
            getHoursList() {
                const res = []
                for (let i = 0; i < 24; i ++ ) {
                    res.push(i)
                }
                return res
            }
        },
        methods: {
            // 更新面板中时间所在位置
            updatePosition() {
                const hoursMenuEl = this.$refs.housListRef
                const minutesMenuEl = this.$refs.minutesListRef

                const value = this.value || this.defaultValue

                const date = new Date(value)

                const h = date.getHours()
                const m = date.getMinutes()

                this.hoursActiveIndex = this.getHoursList.indexOf(h)
                this.minutesActiveIndex = this.getMinutesList.indexOf(m)

                hoursMenuEl.scrollTo({
                    top: this.hoursActiveIndex * 32,
                })

                minutesMenuEl.scrollTo({
                    top: this.minutesActiveIndex * 32,
                })
            },
            // 隐藏面板
            hidePanel(e) {
                const id = e.target.id || ''
                const targetIdList = ['zm-input-0208', 'zm-panel-0208']

                if (targetIdList.indexOf(id) === -1) {
                    this.showPanel = false
                    this.isFocus = false
                }
            },
            panelClick(e) {
                e.stopPropagation() // 阻止冒泡 点击面板不消失
                this.$refs.inputRef.focus() // 点击面板时,input输入框默认失焦,手动聚焦,保证下一次点击触发input的blur事件
            },
            handleFocus(e) {
                this.showPanel = true
                this.isFocus = true

                // value为空,聚焦显示默认时间
                if (!this.value) {
                    this.$emit('input', this.defaultValue)
                }

                // 记录当前值
                this.oldVal = this.value

                this.$nextTick(() => {
                    this.updatePosition()
                })
            },
            handleBlur(e) {
                // this.showPanel = false
                this.isFocus = false

                // 值无变动,不触发事件
                if (this.value === this.oldVal) return

                this.$emit('change')
            },
            hanleMouseenter() {
                // 有值才显示一键清空icon
                if (!this.valueIsEmpty) {
                    this.showCloseIcon = true
                }
            },
            hanldeCloseIconClick() {
                this.showCloseIcon = false
                this.$emit('input', '')
            },
            // 选择小时
            choseHour(index, e) {
                const parent = e.target.parentElement

                // 滚动到中心点
                parent.scrollTo({
                    top: index * 32,
                    behavior: 'smooth'
                })

                const h = this.getHoursList[index]
                const date = new Date(this.value).setHours(h)
                this.$emit('input', date)
            },
            choseMinute(index, e) {
                const parent = e.target.parentElement
                parent.scrollTo({
                    top: index * 32,
                    behavior: 'smooth'
                })
                const m = this.getMinutesList[index]
                const date = new Date(this.value).setMinutes(m)
                this.$emit('input', date)
            },
            // 监听小时列表滚动
            bindHoursScroll(e) {
                this.bindScroll(e.target, 'hours')
            },
            // 监听分钟列表滚动
            bindMinutesScroll(e) {
                this.bindScroll(e.target, 'minutes')
            },
            bindScroll(el, type) {
                // 每一个li写死的高度为32px 判断(scrollTop/32)得到的 index 就是几个li的高度
                const scrollTop = el.scrollTop;
                const index = Math.round(scrollTop / 32)

                if (this.timer) {
                    clearTimeout(this.timer)
                }

                this.timer = setTimeout(() => {
                    el.scrollTo({
                        top: index * 32,
                        behavior: 'smooth'
                    })

                    if (type === 'hours') {
                        this.hoursActiveIndex = index

                        const h = this.getHoursList[index]
                        const date = new Date(this.value).setHours(h)
                        this.$emit('input', date)
                    }

                    if (type === 'minutes') {
                        this.minutesActiveIndex = index

                        const m = this.getMinutesList[index]
                        const date = new Date(this.value).setMinutes(m)
                        this.$emit('input', date)
                    }
                }, 200)
            },
            cancelBtnClick() {
                this.showPanel = false
                this.isFocus = false
                this.$emit('input', this.oldVal)
            }
        }
    }
</script>

<style lang="less" scoped>
.zm-time-picker {
    position: relative;
    display: inline-block;
    .input {
        box-sizing: border-box;
        width: 100%;
        height: 34px;
        padding: 0 30px;
        border-radius: 4px;
        border: 1px solid #dcdfe6;
        color: #606266;
        font-weight: inherit;
        font-size: 14px;
        transition: border-color .2s;
        outline: none;
    }

    .input::placeholder {
        opacity: 0.5;
    }

    .input:hover {
        border-color: #C0C4CC;
    }

    .input:focus {
        border-color: #409EFF;
    }

    .focus {
        border-color: #409EFF;
    }

    .disabled {
        background: #f5f7fa;
        color: #C0C4CC;
        opacity: 1;
    }

    .input-prefix, .input-suffix {
        display: flex;
        justify-content: center;
        align-items: center;
        color: #C0C4CC;
        height: 100%;
        width: 25px;
        font-size: 14px;
        // line-height: 34px;
        text-align: center;
    }

    .input-prefix {
        position: absolute;
        top: 0;
        left: 5px;
    }

    .input-suffix {
        position: absolute;
        top: 0;
        right: 5px;
        cursor: pointer;
    }

    .time-panel {
        position: absolute;
        top: 40px;
        width: 180px;
        border: 1px solid #e4e7ed;
        border-radius: 2px;
        box-shadow: 0 0 10px #ddd;
        user-select: none;
        background: #fff;
        z-index: 999;

        .time-panel-content {
            position: relative;
            display: flex;
            height: 180px;

            .active-flag {
                position: absolute;
                top: 80px;
                left: 50%;
                transform: translate(-50%);
                height: 31px;
                width: 120px;
                border-top: 1px solid #ddd;
                border-bottom: 1px solid #ddd;
                pointer-events: none;
            }

            .time-spinner-list {
                height: 100%;
                width: 50%;
                padding: 0;
                margin: 0;
                list-style: none;
                overflow-y: auto;

                .time-spinner-list-item {
                    height: 32px;
                    font-size: 12px;
                    color: #606266;
                    text-align: center;
                    font-weight: 400;
                    line-height: 32px;

                    &:hover {
                        background-color: #F5F7FA;
                    }

                    &.active {
                        color: #222222;
                        font-weight: bold;
                    }

                    &.active:hover {
                        background-color: #ffffff;
                    }
                }

                &::before {
                    content: '';
                    display: block;
                    height: 80px;
                }

                &::after {
                    content: '';
                    display: block;
                    height: 80px;
                }

                &::-webkit-scrollbar {
                    width: 6px;
                    opacity: 0;
                    background: #fff;
                }

                &::-webkit-scrollbar-thumb {
                    opacity: 0;
                }

                &:hover::-webkit-scrollbar {
                    opacity: 1;
                }

                &:hover::-webkit-scrollbar-thumb {
                    background: #ddd;
                    border-radius: 2px;
                    opacity: 1;
                }

            }
        }

        .time-panel-footer {
            box-sizing: border-box;
            border-top: 1px solid #e4e4e4;
            padding: 4px;
            height: 36px;
            line-height: 25px;
            text-align: right;
            font-size: 12px;
            color: #303133;
            font-weight: 400;

            .cancel-btn {
                padding: 0 5px;
                margin: 0 5px;
                cursor: pointer;
            }
        }
    }
}
</style>

使用方式

    <TimePicker 
      v-model="value"
      placeholder="select"
      :minutesStep="5" />

结束。