Element-Plus日期选择组件封装农历日期

news2024/10/12 2:20:59

背景

在使用element-plus开发项目过程中,需要填入人员的生卒日期,经观察,对于大部分人来说,这类日期通常是农历日期,然而我们在系统建设过程中,对于日期字段,约定成俗的都会使用公历日期,这就存在一个问题,用户只记得自己的农历日期,那么在录入生卒日期的时候,往往就需要通过其他工具,查找到农历对应的公历日期,才能正确的录入系统中,并且,录入系统后,只能看到公历日期,不能直观的将农历日期反馈到用户,所以可能日期录入错误,也不能迅速的发现并修正,于是从实际需求出发,对element-plus组件库中的DatePicker组件进行自定义,在弹窗选择日期面板中,引入农历日期的显示,方便用户操作,减少错误发生。

组件设计

通过对element-plus组件库官方文档DatePicker 日期选择器 | Element Plus (element-plus.org)的查阅,DatePicker组件提供了一个默认的插槽,用于支持对弹出框内容的自定义,因此,我们需要借助此插槽来添加农历日期的显示。

根据日常使用惯例,大部分的日历工具,都是上面显示公历日期,下面显示对应的农历日期,如果日期是传统节日或者节气的,还会显示对应的节日或节气名称,因此,我们需要在自定义组件中,增加属性showFestival用于控制是否显示节日、showJieQi用于控制是否显示节气,如果都不显示,那么全都统一显示为农历日期天数。

我们知道,农历日期和公历日期是存在差异的,差异大的时候可能会相差一个月以上,然而日期选择组件的弹窗面板空间有限,因此我们需要将农历的月份融入日期中,也就是每个月的第一天显示当前农历月份,对于农历日期,用户往往还会注重当前年份的天干与地支,他们可以根据天干地支来进一步核实是否为当前年份,因此,我们还需要增加一个属性showLunarTip,用于控制显示当前日期的完整农历日期,如二〇二四年二月廿五 【甲辰(龙)年】,这样用户可以直观的看出当前日期正不正确,当然,出于对用户体验的改善,我们希望自定义组件更加人性化,比如,有时希望鼠标悬停到对应日期上,就马上弹出tip显示完整的农历日期信息,有时候,我希望鼠标悬停1秒以上才显示农历日期,减少对日期选择的干扰,因此我们再增加一个属性lunarTipShowAfter用于控制完整农历日期的弹出触发时常。

最终效果

效果图

工具选择

毋庸置疑,要显示公历对应的具体农历日期,肯定会存在日期间的换算,农历相对公历来说,规律性比较复杂,要完全自己实现公历转对应的农历,工作量较大,因此,我们优先选择三方工具,来完成两种历法的换算。

通过对几个工具库的对比,我最终选择了lunar (6tail.cn)工具库,它提供了丰富的接口,满足绝大部分场景下的使用需求,工具的强大性,请看官方文档介绍。

代码实现

因为项目使用vue3+typescript开发,因此自定义组件也是在此环境下完成。我们需要的是对原组件DatePicker的增强封装,因此我们的自定义组件需要保留绝大部分原组件的功能。

下面,直接贴出自定义组件的实现代码

<template>
  <el-date-picker v-model="dateValue" v-bind="$props">
    <template #default="dateCell">
      <el-tooltip
        :disabled="!showLunarTip"
        :show-after="lunarTipShowAfter"
        :content="getLunarDateStr(dateCell.date)"
        placement="bottom"
      >
        <div :class="getDateClass(dateCell)">
          <span class="solar-text">{{ dateCell.date.getDate() }}</span>
          <span class="lunar-tex">{{ getLunarDay(dateCell.date) }}</span>
        </div>
      </el-tooltip>
    </template>
  </el-date-picker>
</template>

<script setup lang="ts">
import { JieQi, Solar } from 'lunar-typescript'
import { propTypes } from '@/utils/propTypes'
import { isEmpty } from '@/utils/is'
import { datePickerProps } from 'element-plus'
import type { DateCell } from 'element-plus/es/components/date-picker/src/date-picker.type'
// 带农历日期显示的选择组件
defineOptions({ name: 'LunarDatePicker' })

const emit = defineEmits(['update:modelValue'])

const props = defineProps({
  ...datePickerProps,
  showFestival: propTypes.bool.def(true), // 是否显示节日
  showJieQi: propTypes.bool.def(true), // 是否显示节气
  showLunarTip: propTypes.bool.def(true), // 是否使用 tooltip 显示农历日期
  lunarTipShowAfter: propTypes.number.def(0) // 在触发后多久使用 tooltip 显示农历日期,单位毫秒
})

const dateValue: Ref<typeof props.modelValue> = ref<typeof props.modelValue>('')

watch(
  () => props.modelValue,
  (val: typeof props.modelValue) => {
    dateValue.value = val
  },
  {
    immediate: true
  }
)

watch(
  () => dateValue.value,
  (val) => {
    emit('update:modelValue', val)
  }
)

/**
 * 获取当前日期显示样式
 * @param dateCell 单元格日期信息
 */
const getDateClass = (dateCell: DateCell) => {
  let cla = 'date-wrapper'
  if (dateCell.type === 'today') {
    cla += ' today'
  }

  if (dateCell.isCurrent || dateCell.isSelected || dateCell.start || dateCell.end) {
    cla += ' active'
  } else if (dateCell.inRange) {
    cla += ' in-range'
  }

  if (dateCell.disabled) {
    cla += ' disabled-date'
  }
  return cla
}

/**
 * 获取农历 day 显示文字
 */
const getLunarDay = (date) => {
  const solarDate = Solar.fromDate(date)
  const lunarDate = solarDate.getLunar()
  // 每月第一天显示月数
  if (lunarDate.getDay() == 1) {
    return lunarDate.getMonthInChinese() + '月'
  }

  // 显示节日
  if (props.showFestival) {
    const festivals = lunarDate.getFestivals()
    if (!isEmpty(festivals)) {
      return festivals[0]
    }
  }

  // 显示节气
  if (props.showJieQi) {
    const currJieQi: JieQi = lunarDate.getCurrentJieQi() as JieQi
    if (currJieQi && currJieQi?.getName()) {
      return currJieQi?.getName()
    }
  }

  return lunarDate.getDayInChinese()
}

/**
 * 根据日历获取农历日期,包含年份干支和生肖
 */
const getLunarDateStr = (date: Date): string => {
  const solarDate = Solar.fromDate(date)
  const lunarDate = solarDate.getLunar()
  return `${lunarDate.getYearInChinese()}${lunarDate.getMonthInChinese()}${lunarDate.getDayInChinese()}${lunarDate.getYearInGanZhi()}(${lunarDate.getYearShengXiao()})年】`
}
</script>

<style lang="scss" scoped>
.date-wrapper {
  position: relative;
  display: flex;
  align-items: center;
  flex-direction: column;
  padding: 4px 0;
  line-height: 18px;
  text-align: center;

  .solar-text {
    font-size: 14px;
  }

  .lunar-text {
    white-space: nowrap;
  }
}

.today {
  font-weight: 700;
  color: var(--el-color-primary);
}

.active {
  color: #fff;
  background-color: var(--el-datepicker-active-color);
  border-radius: 5px;
}

.in-range {
  background-color: var(--el-datepicker-inrange-bg-color);
}

.disabled-date {
  cursor: not-allowed;
}
</style>

相关代码

引入历法换算工具

npm i lunar-typescript

propTypes 工具代码

import { VueTypeValidableDef, VueTypesInterface, createTypes, toValidableType } from 'vue-types'
import { CSSProperties } from 'vue'

type PropTypes = VueTypesInterface & {
  readonly style: VueTypeValidableDef<CSSProperties>
}
const newPropTypes = createTypes({
  func: undefined,
  bool: undefined,
  string: undefined,
  number: undefined,
  object: undefined,
  integer: undefined
}) as PropTypes

class propTypes extends newPropTypes {
  static get style() {
    return toValidableType('style', {
      type: [String, Object]
    })
  }
}

export { propTypes }

is 工具代码

// copy to vben-admin

const toString = Object.prototype.toString

export const is = (val: unknown, type: string) => {
  return toString.call(val) === `[object ${type}]`
}

export const isDef = <T = unknown>(val?: T): val is T => {
  return typeof val !== 'undefined'
}

export const isUnDef = <T = unknown>(val?: T): val is T => {
  return !isDef(val)
}

export const isObject = (val: any): val is Record<any, any> => {
  return val !== null && is(val, 'Object')
}

export const isEmpty = <T = unknown>(val: T): val is T => {
  if (val === null) {
    return true
  }
  if (isArray(val) || isString(val)) {
    return val.length === 0
  }

  if (val instanceof Map || val instanceof Set) {
    return val.size === 0
  }

  if (isObject(val)) {
    return Object.keys(val).length === 0
  }

  return false
}

export const isDate = (val: unknown): val is Date => {
  return is(val, 'Date')
}

export const isNull = (val: unknown): val is null => {
  return val === null
}

export const isNullAndUnDef = (val: unknown): val is null | undefined => {
  return isUnDef(val) && isNull(val)
}

export const isNullOrUnDef = (val: unknown): val is null | undefined => {
  return isUnDef(val) || isNull(val)
}

export const isNumber = (val: unknown): val is number => {
  return is(val, 'Number')
}

export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
  return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch)
}

export const isString = (val: unknown): val is string => {
  return is(val, 'String')
}

export const isFunction = (val: unknown): val is Function => {
  return typeof val === 'function'
}

export const isBoolean = (val: unknown): val is boolean => {
  return is(val, 'Boolean')
}

export const isRegExp = (val: unknown): val is RegExp => {
  return is(val, 'RegExp')
}

export const isArray = (val: any): val is Array<any> => {
  return val && Array.isArray(val)
}

export const isWindow = (val: any): val is Window => {
  return typeof window !== 'undefined' && is(val, 'Window')
}

export const isElement = (val: unknown): val is Element => {
  return isObject(val) && !!val.tagName
}

export const isMap = (val: unknown): val is Map<any, any> => {
  return is(val, 'Map')
}

export const isServer = typeof window === 'undefined'

export const isClient = !isServer

export const isUrl = (path: string): boolean => {
  const reg =
    /(((^https?:(?:\/\/)?)(?:[-:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&%@.\w_]*)#?(?:[\w]*))?)$/
  return reg.test(path)
}

export const isDark = (): boolean => {
  return window.matchMedia('(prefers-color-scheme: dark)').matches
}

// 是否是图片链接
export const isImgPath = (path: string): boolean => {
  return /(https?:\/\/|data:image\/).*?\.(png|jpg|jpeg|gif|svg|webp|ico)/gi.test(path)
}

export const isEmptyVal = (val: any): boolean => {
  return val === '' || val === null || val === undefined
}

相关组件库版本

组件版本
vue^3.3.7
element-plus2.4.1
lunar-typescript^1.7.5
typescript5.2.2
vue-types^5.1.1

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

机器学习的模型校准

背景知识 之前一直没了解过模型校准是什么东西&#xff0c;最近上班业务需要看了一下&#xff1a; 模型校准是指对分类模型进行修正以提高其概率预测的准确性。在分类模型中&#xff0c;预测结果通常以类别标签形式呈现&#xff08;例如&#xff0c;0或1&#xff09;&#xf…

day03-Docker

1.初识 Docker 1.1.什么是 Docker 1.1.1.应用部署的环境问题 大型项目组件较多&#xff0c;运行环境也较为复杂&#xff0c;部署时会碰到一些问题&#xff1a; 依赖关系复杂&#xff0c;容易出现兼容性问题开发、测试、生产环境有差异 例如一个项目中&#xff0c;部署时需要依…

拯救者Legion R9000X 2021(82HN)原装出厂Win10系统镜像ISO下载

lenovo联想拯救者笔记本R9000X 2021款原厂Windows10系统安装包&#xff0c;恢复出厂开箱状态预装OEM系统 链接&#xff1a;https://pan.baidu.com/s/1tx_ghh6k0Y9vXBz-7FEQng?pwd7mih 提取码&#xff1a;7mih 原装出厂系统自带所有驱动、出厂主题壁纸、系统属性联机支持标…

C++核心编程——4.2(2)对象的初始化和清理

4.2.5 深拷贝与浅拷贝 浅拷贝&#xff1a;编译器提供的简单的赋值拷贝操作 深拷贝&#xff1a;在堆区重新申请空间&#xff0c;进行拷贝操作 示例&#xff1a; class Person { public://无参&#xff08;默认&#xff09;构造函数Person() {cout << "无参构造函数…

并发编程之线程池的应用以及一些小细节的详细解析

线程池在实际中的使用 实际开发中&#xff0c;最常用主要还是利用ThreadPoolExecutor自定义线程池&#xff0c;可以给出一些关键的参数来自定义。 在下面的代码中可以看到&#xff0c;该线程池的最大并行线程数是5&#xff0c;线程等候区&#xff08;阻塞队列)是3&#xff0c;即…

基于Python的微博旅游情感分析、微博舆论可视化系统

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

JAVAEE之Cookie/Session

1.Cookie HTTP 协议自身是属于 "无状态" 协议. "无状态" 的含义指的是: 默认情况下 HTTP 协议的客户端和服务器之间的这次通信, 和下次通信之间没有直接的联系. 但是实际开发中, 我们很多时候是需要知道请求之间的关联关系的. 例如登陆网站成功后, 第二…

绩效考核存在合理性、公平性、客观性吗?

目录 一、绩效考核流于形式&#xff1a;没有实际考核过 二、考核结果的确定: 主管一人说了算 三、考核结果&#xff1a; 与绩效奖金挂钩吗&#xff1f; 四、考核的滥用&#xff1a;成为公司排挤迫使员工离职的手段 五、公司说&#xff1a; 让你滚蛋&#xff0c;谁还会发你奖…

[HackMyVM]靶场Boxing

难度:Medium kali:192.168.56.104 靶机:192.168.56.143 端口扫描 ┌──(root㉿kali2)-[~/Desktop] └─# nmap 192.168.56.143 Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-04-03 19:25 CST Nmap scan report for staging-env.boxing.hmv (192.168.56.143) Host …

智慧展览馆:基于AI智能识别技术的视频智慧监管解决方案

一、建设背景 随着科技的不断进步和社会安全需求的日益增长&#xff0c;展览馆作为展示文化、艺术和科技成果的重要场所&#xff0c;其安全监控系统的智能化升级已成为当务之急。为此&#xff0c;旭帆科技&#xff08;TSINGSEE青犀&#xff09;基于视频智能分析技术推出了展览馆…

路径规划——曲线拟合详解(一):多项式轨迹与QP优化(minimum-snap算法核心部分)

前言 历经一个多星期时间&#xff0c;我们在路径规划——搜索算法部分讲解了7种常见的路径搜索算法&#xff0c;每一种算法的链接放在下面了&#xff0c;有需要的朋友点击跳转即可&#xff1a; 路径规划——搜索算法详解&#xff08;一&#xff09;&#xff1a;Dijkstra算法详…

redis之穿透、击穿、雪崩

目录 缓存雪崩 问题描述 缓存雪崩 问题描述 key 对应的数据存在&#xff0c;但在 redis 中过期&#xff0c;此时若有大量并发请求过来&#xff0c;这些请求发现缓存过期一般都会从后端数据库加载数据并回设到缓存&#xff0c;这个时候大并发的请求可能会瞬间把后端数据库压垮。…

GPTfinger赋能指纹识别技术,德施曼赢下这一局

生成式AI这场战局里&#xff0c;德施曼跑赢同行&#xff0c;成为行业里第一个引入GPT技术的智能锁厂商。 4月2日&#xff0c;德施曼智能锁在北京举办了新品发布会之前的“创新技术预沟通会”&#xff0c;向到场媒体揭晓了最新技术突破之一——GPTfinger。 「智哪儿」认为&…

关闭PyCharm中因双击Shift而跳出的搜索框

有时候老是多次按到shift而跳出一个搜索框&#xff0c;本来在编写代码&#xff0c;怎么突然就开始搜索了&#xff0c;非常的烦人。 其实这个搜索框叫做“随处搜索”。 关闭步骤 1、打开PyCharm的设置。 2、在设置-高级设置中勾选-禁用双击修改键快捷键即可。

4.3学习总结

[HNCTF 2022 WEEK2]Canyource&#xff08;无参数&#xff09; 通过这题又接触了一种无参数RCE的方法&#xff0c;前面学习的getallheaders只有在apache环境下才能使用&#xff0c;具有一定的局限性 这里是利用php函数来构造读取flag的方法 localeconv() – 函数返回一个包含本…

Ribbon有哪些负载均衡策略

负载均衡类都实现了IRule接口。 RandomRule&#xff1a;随机的选用一个实例 RoundRobinRule&#xff1a;轮询的使用实例 RetryRule&#xff1a;在轮询的基础上加了一个错误重试机制&#xff0c;在deadline时间内会不断的重试 WeightResponeTimeRule&#xff1a;根据权重去做…

Golang 开发实战day07 - Functions

Golang 教程07 - Functions 1. Functions 1.1 什么是函数&#xff1f; 在 Golang 中&#xff0c;函数就像是代码的超级组合体&#xff0c;可以将一段代码封装成一个独立的单元&#xff0c;以便重复使用。 1.2 函数声明 func funcName(parameter1 type1, parameter2 type2)…

必看!香港Web3活动周跑会指南

星移斗转&#xff0c;又是一年嘉年华。 去年4月&#xff0c;在《虚拟资产发展政策宣言》的影响下&#xff0c;四散的华人Web3生态再度汇集&#xff0c;由万向区块链实验室、HashKey Group、W3ME联合举办的Hong Kong Web3 Festival 2023率先奏响了以香港为鼓点的加密奏曲&#x…

Celery的任务流

Celery的任务流 在之前调用任务的时候只是使用delay()和apply_async()方法。但是有时我们并不想简单的执行单个异步任务&#xff0c;比如说需要将某个异步任务的结果作为另一个异步任务的参数或者需要将多个异步任务并行执行&#xff0c;返回一组返回值&#xff0c;为了实现此…

个人医疗开支预测项目

注意&#xff1a;本文引用自专业人工智能社区Venus AI 更多AI知识请参考原站 &#xff08;[www.aideeplearning.cn]&#xff09; 项目背景 随着医疗成本的持续上涨&#xff0c;个人医疗开支成为一个重要议题。理解影响医疗费用的多种因素对于医疗保险公司、政府机构以及个人…