Vue3 Element Plus自定义年份区间选择组件

环境:

"dependencies": {
    "@rollup/plugin-alias": "^3.1.9",
    "@types/node": "^17.0.43",
    "element-plus": "^2.2.15",
    "three": "^0.148.0",
    "vue": "^3.2.25",
    "vue-router": "^4.0.16"
  },
  "devDependencies": {
    "typescript": "^4.5.4",
    "vite": "^2.9.9",
    "vite-plugin-vue-setup-extend": "^0.4.0",
    "vue-tsc": "^0.34.7"
  }

效果:(按照element操作方式来写的,颜色自定义的)

组件代码:(composition API的方式)

<template>
    <div class="yearPicker" :ref="yearPicker">
        <div class="_inner" :style="{ width: props.labelWidth + 'px' }">{{ props.labelText }}</div>

        <input class="_inner" :ref="inputLeft" v-model="data.startShowYear" @focus="onFocus" @click="clickInput" type="text"
            name="yearInput" @input="checkStartInput($event)" placeholder="选择年份" />
        <span>{{ props.sp }}</span>
        <input class="_inner" :ref="inputRight" v-model="data.endShowYear" @focus="onFocus" @click="clickInput" type="text"
            name="yearInput" @input="checkEndInput($event)" placeholder="选择年份" />
        <div class="_inner floatPanel" v-if="data.showPanel">
            <div class="_inner leftPanel">
                <div class="_inner panelHead">
                    <i class="_inner el-icon-d-arrow-left" @click="onClickLeft">&lt;&lt;</i>
                    {{ leftYearList[0] + "-" + leftYearList[9] }}
                </div>
                <div class="_inner panelContent">
                    <div :class="{
                        disabled: checkValidYear(item),
                        oneSelected: item === data.startYear && oneSelected,
                        startSelected: item === data.startYear,
                        endSelected: item === data.endYear,
                        _inner: true,
                        betweenSelected: item > data.startYear && item < data.endYear,
                    }" v-for="item in leftYearList" :key="item">
                        <a :class="{
                            cell: true,
                            _inner: true,
                            selected: item === data.startYear || item === data.endYear,
                        }" @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">&gt;&gt;</i>
                    {{ rightYearList[0] + "-" + rightYearList[9] }}
                </div>
                <div class="_inner panelContent">
                    <div :class="{
                        disabled: checkValidYear(item),
                        startSelected: item === data.startYear,
                        endSelected: item === data.endYear,
                        betweenSelected: item > data.startYear && item < data.endYear,
                    }" v-for="item in rightYearList" :key="item">
                        <a :class="{
                            cell: true,
                            _inner: true,
                            selected: item === data.endYear || item === data.startYear,
                        }" @click="onClickItem(item)" @mouseover="onHoverItem(item)">
                            {{ item }}
                        </a>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>
   
<script lang="ts" setup>
import { computed, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref } from "vue";

interface Emits {
    (e: "updateTimeRange", startYear: string, endYear: string): void;
}
interface Props { labelWidth: number, labelText: string, sp?: string, initYear?: {startYear:number, endYear:number} }
const SELECT_STATE = {
    unselect: 0,
    selecting: 1,
    selected: 2,
};

const data = reactive({
    itemBg: {},
    startShowYear: null,
    endShowYear: null,
    yearList: [],
    showPanel: false,
    startYear: null,
    endYear: null,
    curYear: 0,
    curSelectedYear: 0,
    curState: SELECT_STATE.unselect,
})

const yearPicker = ref<HTMLElement>(null)
const inputLeft = ref<HTMLElement>(null)
const inputRight = ref<HTMLElement>(null)

const oneSelected = computed(() => {
    return (
        data.curState === SELECT_STATE.selecting &&
        (data.startYear === data.endYear || data.endYear == null)
    );
})

const leftYearList = computed(() => {
    return data.yearList.slice(0, 10);
})
const rightYearList = computed(() => {
    return data.yearList.slice(10, 20);
})

const emits = defineEmits<Emits>()

const props = withDefaults(defineProps<Props>(), {
    labelWidth: 80,
    labelText: "时间标签",
    sp: "至",
})


const clickInput = (e) => {
    e.stopPropagation();
    return false;
}

const checkValidYear = (iYear) => {
    if (props.initYear) {
        if (iYear > props.initYear.endYear) {
            return 1
        } else if (iYear < props.initYear.startYear) {
            return -1
        }
    }
    return 0
}
const checkStartInput = (event) => {
    if (isNaN(data.startShowYear)) {
        data.startShowYear = data.startYear;
    } else {
        data.startYear = data.startShowYear * 1;
    }
}

const checkEndInput = () => {
    if (isNaN(data.endShowYear)) {
        data.endShowYear = data.endYear;
    } else {
        data.endYear = data.endShowYear * 1;
    }
}
const changeYear = () => {
    if (data.startYear > data.endYear) {
        let tmp = data.endYear;
        data.endYear = data.startYear;
        data.startYear = tmp;

    }
    if (props.initYear) {
        data.startYear = Math.max(data.startYear, props.initYear.startYear)
        data.endYear = Math.min(data.endYear, props.initYear.endYear)
    }
    data.startShowYear = data.startYear;
    data.endShowYear = data.endYear;

    if (data.startYear && data.endYear) {
        emits("updateTimeRange",
            data.startYear,
            data.endYear,
        );
    } else {
        console.warn("WARN:年份不合法", data.startYear, data.endYear);
    }
}
const onHoverItem = (iYear) => {
    if (checkValidYear(iYear) != 0) {
        return;
    }
    if (data.curState === SELECT_STATE.selecting) {
        let tmpStart = data.curSelectedYear;
        data.endYear = Math.max(tmpStart, iYear);
        data.startYear = Math.min(tmpStart, iYear);
    }
}
const onClickItem = (iYear) => {
    if (checkValidYear(iYear) != 0) {
        return;
    }
    if (
        data.curState === SELECT_STATE.unselect ||
        data.curState === SELECT_STATE.selected
    ) {
        data.startYear = iYear;
        data.curSelectedYear = iYear;
        data.endYear = null;
        data.curState = SELECT_STATE.selecting;
    } else if (data.curState === SELECT_STATE.selecting) {
        data.endShowYear = data.endYear;
        data.startShowYear = data.startYear;
        data.curState = SELECT_STATE.selected;
        emits("updateTimeRange",
            data.startYear,
            data.endYear,
        );

        setTimeout(() => {
            //为动画留的时间,可优化
            data.showPanel = false;
        }, 300);
    }
}
const onFocus = () => {
    nextTick(() => {
        data.showPanel = true;
    });
}

const updateYearList = () => {
    let iStart = Math.floor(data.curYear / 10) * 10 - 10;
    iStart = iStart < 0 ? 0 : iStart;
    data.yearList = [];
    for (let index = 0; index < 20; index++) {
        data.yearList.push(iStart + index);
    }
}
const closePanel = (e) => {
    if (!data.showPanel) {
        return;
    }
    if (typeof e.target.className !== "string" || e.target.className === "") {
        nextTick(() => {
            changeYear()
            data.showPanel = false;
        });
        return;
    }

    if (
        e.target.className.indexOf("_inner") === -1 ||
        (e.target.name === "yearInput" &&
            e.target !== inputLeft.value &&
            e.target !== inputRight.value)
    ) {
        nextTick(() => {
            changeYear()
            data.showPanel = false;
        });
    }

    e.stopPropagation();
    return false;
}
const onClickLeft = (e) => {
    data.curYear = data.curYear * 1 - 10;
    updateYearList();


}
const onClickRight = (e) => {
    data.curYear = data.curYear * 1 + 10;
    updateYearList();
}

onBeforeMount(() => {
    data.curYear = new Date().getFullYear();
    updateYearList();
})
onBeforeUnmount(() => {
    document.removeEventListener("click", closePanel.bind(data));
})

onMounted(() => {
    if (yearPicker.value) {
        yearPicker.value.style = "padding-left:" + props.labelWidth + "px";
    }
    document.addEventListener("click", closePanel.bind(data));
}) 
</script>
<style lang="scss" scoped>
.yearPicker {
    font-size: 14px;
    display: flex;
    position: relative;
    transition: all 0.3s;

    input:first-child {
        text-align: right;
    }

    background-color: #fff;

    span {
        padding: 0 8px;
        height: 32px;
        line-height: 32px;
    }

    border: 1px solid #eff1f3;
    height: 34px;
    line-height: 34px;
    border-radius: 4px;
    padding: 0 8px;
    box-sizing: border-box;

    .floatPanel {
        >div {
            width: 50%;
        }

        padding: 0 16px;
        position: absolute;
        display: flex;
        background-color: #fff;
        z-index: 2000;
        border-radius: 4px;
        width: 650px;
        height: 250px;
        top: 40px;
        left: -50px;
        box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);

        .panelContent {
            display: flex;
            flex-wrap: wrap;
            width: 100%;
            height: calc(100% - 70px);

            .disabled {
                color: #ccc;
            }

            .oneSelected {
                border-top-right-radius: 24px;
                border-bottom-right-radius: 24px;
            }

            .startSelected {
                background-color: #f6f6f7;
                border-top-left-radius: 24px;
                border-bottom-left-radius: 24px;
            }

            .endSelected {
                background-color: #f6f6f7;
                border-top-right-radius: 24px;
                border-bottom-right-radius: 24px;
            }

            .betweenSelected {
                background-color: #f6f6f7;
            }

            >div {
                width: 75px;
                height: 48px;
                line-height: 48px;
                margin: 3px 0;
                // border-radius: 24px;
                text-align: center;

                a {
                    display: inline-block;
                    width: 60px;
                    height: 36px;
                    cursor: pointer;
                    line-height: 36px;
                    border-radius: 18px;
                }

                .selected {
                    background-color: #3e77fc;
                    color: #fff;
                }
            }
        }

        .panelHead {
            position: relative;
            height: 46px;
            line-height: 46px;
            text-align: center;

            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%;
        position: absolute;
        left: 50%;
        width: 1px;
        border-left: 1px solid #e4e4e4;
    }
}

input {
    width: 60px;
    border: none;
    height: 32px;
    text-align: center;
    line-height: 32px;
    box-sizing: border-box;
    background-color: transparent;
}

input:focus {
    outline: none;
    background-color: transparent;
}

.yearPicker:hover {
    border-color: #3e77fc;
}

.dateIcon {
    position: absolute;
    right: 16px;
    top: 9px;
    color: #adb2bc;
}
</style>
   
  ​

外部调用:

<template>
    <div>
        <yearPicker
               style="width:250px"
              ref="statisticPicker"
              :labelWidth = "50"
              labelText="统计期"
              :initYear="dateValue"
              @updateTimeRange="updateStatisticYear"
            />
    </div>
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import yearPicker from "./component/yearPick.vue"

//可选择区间,initYear传参,不传则所有年份有效,小于0判断一下?
const dateValue= ref<any>({startYear:2000, endYear: new Date().getFullYear()});
//选完/输入完成的回调
const updateStatisticYear:any = (startYear:number, endYear:number)=>{
    console.log("选中年份", startYear, endYear)
}
</script>

<style lang="scss" scoped>

</style>

这个是vue2升级上来的,修复了输入的逻辑问题,添加了有效年份区间(区间外的禁止选择,禁止输入),优化了点击关闭页面的逻辑:

vue2版本电梯:http://t.csdnimg.cn/ngmgL

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/256666.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

鸿蒙(HarmonyOS)项目方舟框架(ArkUI)之Button按钮组件

鸿蒙&#xff08;HarmonyOS&#xff09;项目方舟框架&#xff08;ArkUI&#xff09;之Button按钮组件 一、操作环境 操作系统: Windows 10 专业版 IDE:DevEco Studio 3.1 SDK:HarmonyOS 3.1 二、Button按钮组件 Button 组件也是基础组件之一&#xff0c;和其它基础组件不…

【深度学习】Prompt

1.Prompt的通俗解释 Prompt就是“提示”的意思&#xff0c;通俗解释可以参考你画我猜游戏。如下图所示&#xff1a;提示词就作为Prompt&#xff0c;指导对方说出正确答案。而自然语言处理任务中的Prompt也有同样的效果&#xff0c;指导模型输出正确的答案。 2.Prompt的不通俗解…

【密码学】群的证明(习题)

0.前置知识 1.习题 记录一次密码学作业~群的判定 2.求解

Linux发行版比较:Ubuntu、CentOS、Red Hat与其他系统的优劣分析

导言 Linux作为开源操作系统&#xff0c;有众多不同的发行版&#xff0c;每个发行版都有其独特的特性和适用场景。本文将聚焦于比较Ubuntu、CentOS、Red Hat和其他系统&#xff0c;深入分析它们的优势、用途以及在不同领域的应用。Linux操作系统的生态系统中&#xff0c;Ubuntu…

传输层—TCP核心机制(确认应答、超时重传、三次握手四次挥手、滑动串口等……)

传输层—TCP核心机制 ​ 文章目录 传输层—TCP核心机制TCP1.1 确认应答机制 (可靠传输机制)1.2 超时重传机制 (可靠传输机制)1.3 连接管理机制 (可靠传输机制)1.3.1 三次握手&#xff08;建立连接&#xff09;1.3.2 四次握手&#xff08;断开连接&#xff09; 1.4 滑动窗口 (提…

如何使用示波器探头对被测电路进行检测

对电路信号进行检测之前首先要知道被测电路是什么电路&#xff0c;被测信号是什么信号。盲目地测试或者使用不正确的测量方法&#xff0c;有可能得到错误的波形甚至损坏仪器危及安全。 1、什么是差分信号&#xff1f;什么是单端信号&#xff1f; 差分传输是一种信号传输的技术…

Selenium自动化测试框架(超详细总结分享)

设计思路 本文整理归纳以往的工作中用到的东西&#xff0c;现汇总成基础测试框架提供分享。 框架采用python3 selenium3 PO yaml ddt unittest等技术编写成基础测试框架&#xff0c;能适应日常测试工作需要。 1、使用Page Object模式将页面定位和业务操作分开&#xff…

SecureCRT for Mac/win强大安全的终端SSH工具,SecureCRT助您网络连接无忧

在当今数字化时代&#xff0c;网络连接已成为生活和工作中不可或缺的一部分。而对于需要进行远程访问和管理的用户来说&#xff0c;一个稳定、安全的终端SSH工具是至关重要的。SecureCRT作为一款强大的终端SSH工具&#xff0c;为用户提供了安全、高效的远程连接解决方案。 首先…

如何压缩视频发邮件?帮你整理了几个必备的!

不同邮件附件上限大小有所不同&#xff0c;QQ邮箱的附件大小限制为2GB&#xff0c;这意味着用户可以发送最大为2GB的视频文件&#xff1b;Gmail邮箱的附件大小限制为25MB&#xff1b;163邮箱的附件大小限制为2GB&#xff0c;但是为了保证文件传输的成功率&#xff0c;建议最好不…

SpringBoot 多环境开发配置文件

在开发过程中&#xff0c;往往开发环境和生产环境需要不同的配置。为了兼容两种运行环境&#xff0c;提高开发效率&#xff0c;可以使用多环境开发配置文件。 配置文件结构大概是这样&#xff1a; application.yml -主启动配置文件&#xff08;用于控制使用哪种环境配…

金和OA jc6 clobfield接口存在SQL注入漏洞

文章目录 产品简介漏洞概述指纹识别漏洞利用修复建议 产品简介 金和OA协同办公管理系统j6软件是一种综合性的协同办公解决方案&#xff0c;旨在提高企业内部的协作效率和工作效率。它提供了一系列功能和工具&#xff0c;帮助组织进行任务管理、日程安排、文件共享、团队协作和…

关于腾讯位置服务路线规划-微信小程序-插件未授权使用

我们点击添加插件会发现添加错误,主要是因为他说不开放给个人用户. 这是我们可以进入微信服务市场直接搜索腾讯位置服务路线规划 | 微信服务市场 (qq.com)点击添加插件即可完成

实战|Smartbi---意外的福利

目录 漏洞描述 影响版本 fofa语法 漏洞复现 构造poc 修复建议 本文由掌控安全学院 - beize 投稿 漏洞描述 Smartbi在安装时会内置几个用户&#xff0c;在使用特定接口时&#xff0c;可绕过用户身份认证机制获取其身份凭证&#xff0c;随后可使用获取的身份凭证调用后台…

统计分析绘图软件 GraphPad Prism 10 mac功能介绍

GraphPad Prism mac是一款专业的统计和绘图软件&#xff0c;主要用于生物医学研究、实验设计和数据分析。 GraphPad Prism mac功能和特点 数据导入和整理&#xff1a;GraphPad Prism 可以导入各种数据格式&#xff0c;并提供直观的界面用于整理、编辑和管理数据。用户可以轻松地…

三菱PLC FX3U滑动平均值滤波

三菱PLC滑动平均值滤波其它相关写法,请参考下面文章链接: https://rxxw-control.blog.csdn.net/article/details/125044013https://rxxw-control.blog.csdn.net/article/details/125044013滑动平均值滤波程序总共分为三部分,第一步为:滑动采样。 第二步为:队列求和,第三…

TypeScript 中的高级类型(联合、交叉、泛型、映射类型)

文章目录 一、联合类型&#xff08;Union Types&#xff09;二、交叉类型&#xff08;Intersection Types&#xff09;三、泛型3.1 泛型结合extends3.2 泛型结合 keyof3.3 extends keyof 和 in keyof 的区别 四、条件类型&#xff08;Conditional Types&#xff09; TypeScript…

德人合科技 | 公司电脑文件加密系统

公司电脑文件加密系统是一种可以对电脑文件进行加密的保护机制。它使用驱动层透明加密技术&#xff0c;能够在用户无感知的情况下对文件进行加密&#xff0c;从源头上保障数据安全和使用安全。 PC端访问地址&#xff1a; www.drhchina.com 此类系统主要有以下几个特点和功能&a…

k8s链接数据库故障Waiting for table metadata lock

场景&#xff1a;早上来发现一个程序&#xff0c;链接mysql数据库有点问题&#xff0c;随后排查&#xff0c;因为容器在k8s里面。所以尝试重启了pod没有效果 一、重启pod: 这里是几种在Kubernetes中重启Pod的方法: 删除Pod,利用Deployment重建 kubectl delete pod mypodDepl…

NPDP证书含金量高吗?跟PMP相比含金量怎么样?

两个证方向不太一样&#xff0c;含金量都挺高的&#xff0c;具体怎么选呢&#xff1f;接着往下看~ PS&#xff1a;不想看长篇大论的&#xff0c;来找我&#xff0c;直接把你的经历甩出来&#xff0c;我帮你判断~ 一、产品经理跟项目经理的区别 表面上&#xff0c;项目经理和产…

第十三章 SpringCloud Alibaba 实现 Seata--分布式事务

分布式事务基础 事务 事务指的就是一个操作单元&#xff0c;在这个操作单元中的所有操作最终要保持一致的行为&#xff0c;要么所有操作 都成功&#xff0c;要么所有的操作都被撤销。简单地说&#xff0c;事务提供一种“要么什么都不做&#xff0c;要么做全套”机制。 本地事…