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

背景

在使用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.niftyadmin.cn/n/5467528.html

相关文章

神经网络与深度学习(二)

一、深度学习平台 张量&#xff08;Tensor&#xff09; 是一个物理量&#xff0c;对高维(维数 ≥ 2) 的物理量进行“量纲分析” 的一种工具。简单的可以理解为&#xff1a;一维数组称为矢量&#xff0c;二维数组为二阶张量&#xff0c;三维数组为三阶张量 计算图 用“结点”…

Linux 线程:使用管理线程、多线程、分离线程

目录 一、使用线程 1、pthread_create创建线程 2、pthread_join等待线程 主线程获取新线程退出结果 获取新线程退出返回的数组 3、线程异常导致进程终止 4、pthread_exit 5、pthread_cancel 6、主线程可以取消新线程&#xff0c;新线程可以取消主线程吗 二、如何管理线…

Go语言爬虫实战(线程池)

Go语言爬虫实战 目标 利用go语言爬取指定网站的图片。实现爬取网站任意页面所有所需的图片。实现使用go语言线程池开启多个线程爬取图片内容。最后实现创建多个文件夹存储图片。 爬取网站图片 步骤 对指定URL发去GET请求&#xff0c;获取对应的响应。 resp, err : http.Get(…

ES学习日记(九)-------logstash导入数据

一、安装和下载 es官网下载地址 官方介绍:Logstash是开源的服务器端数据处理管道&#xff0c;能够同时从多个来源采集数据&#xff0c;转换数据&#xff0c;然后将数据发送到您最喜欢的“存储库”中。(我们的存储库当然是 Elasticsearch。) 下载和ES一样的版本(很重要,必须这…

09. 【Android教程】表格布局 TableLayout

学完了 Android 两个经典布局&#xff0c;是不是觉得已经可以应对大多数场景了&#xff1f;我记得当我学完 LinearLayout 和 RelativeLayout 之后&#xff0c;我觉得 UI 布局已经可以出师了&#xff0c;在本人从事了多年的 Android 研究之后&#xff0c;可以很负责任的告诉你&a…

银行监管报送系统介绍(十五):金融审计平台

《“十四五”国家审计工作发展规划》中重点强调&#xff0c;金融审计&#xff1a;以防范化解重大风险、促进金融服务实体经济&#xff0c;推动深化金融供给侧结构性改革、建立安全高效的现代金融体系为目标&#xff0c;加强对金融监管部门、金融机构和金融市场运行的审计。 —…

.rmallox勒索病毒特点分析,数据被加密了还可以恢复吗?

. rmallox勒索病毒是什么&#xff1f; .rmallox勒索病毒是一种恶意软件&#xff0c;它属于勒索病毒的一种变体。这种病毒主要通过电子邮件附件、恶意链接或潜入被感染的网站等方式进行传播。一旦感染了用户的计算机系统&#xff0c;.rmallox会利用复杂的加密算法&#xff0c;如…

GPT 模型解析:ChatGPT 如何在语言处理领域引领潮流?

人工智能时代来临 我们正处于AI的iPhone时刻。——黄仁勋&#xff08;英伟达CEO&#xff09; ChatGPT 好得有点可怕了&#xff0c;我们距离危险的强人工智能不远了。——马斯克&#xff08;Tesla/SpaceX/Twitter CEO&#xff09; 以上的内容说明我们现在正处于一个技术大翻牌的…