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-plus 2.4.1
lunar-typescript ^1.7.5
typescript 5.2.2
vue-types ^5.1.1

相关推荐

  1. element-plus日期选择器英文改成中文

    2024-04-08 20:00:04       32 阅读
  2. element-plus日期选择器英文改成中文

    2024-04-08 20:00:04       32 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-04-08 20:00:04       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-08 20:00:04       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-08 20:00:04       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-08 20:00:04       20 阅读

热门阅读

  1. js与jq之间的联系(补)

    2024-04-08 20:00:04       15 阅读
  2. RPA投资:成本效益分析秘籍

    2024-04-08 20:00:04       13 阅读
  3. 基于 Spring Task实现单体项目架构的定时任务

    2024-04-08 20:00:04       15 阅读
  4. jquery

    2024-04-08 20:00:04       14 阅读
  5. js中filter,map,forEach,indexOf的用法和区别详解

    2024-04-08 20:00:04       15 阅读
  6. 网络安全教程及案例分析

    2024-04-08 20:00:04       14 阅读
  7. 前端开发语言都有哪些?

    2024-04-08 20:00:04       15 阅读
  8. 防抖和节流

    2024-04-08 20:00:04       13 阅读
  9. LeetCode 494. 目标和

    2024-04-08 20:00:04       18 阅读
  10. 【LeetCode热题100】【技巧】颜色分类

    2024-04-08 20:00:04       16 阅读
  11. Shell学习 - 2.24 Shell let命令:对整数进行数学运算

    2024-04-08 20:00:04       15 阅读
  12. 【备忘录】Linux kill 多个进程命令备忘

    2024-04-08 20:00:04       13 阅读