天天看点

基于ant-design-vue的简易农历日历

    最近在用ant-design-vue开发过程中,想使用农历日历,不过现有的Calendar暂时不支持农历日历,于是基于ant-design-vue的组件Calendar,再此基础上进行扩展开发了一个简易的农历日历组件。

    核心思想就是有一个数组记录1900~2049之间每个农历年的信息(这些信息包括每个月的天数、闰月月份、闰月天数),取到公历日期后,计算此日期与公历1900年1月30日0时的差距天数(该天是农历1900年正月初一的前一天),根据差距天数计算出当前日期的农历日期。

    目前仅支持1900~2049年的农历日期,另外当前也比较简易,算法可以继续优化,功能也可以进行扩展,有需要可以再次基础进行扩展。

    下面直接贴出代码,代码就两个文件,一个.vue文件一个.js文件,代码中有注释,逻辑比较清晰。

    js文件:

/**
 * 每一年的农历信息共计存储在5位十六制数中,即20位二进制数中
 * 1111 1111 1111 1111 1111
 * 高3位暂时空置,第4位当该年是闰年时标志该年闰月是大月(30天)还是小月(29天),是大月则标志位为1
 * 低4位用来标志该年闰月是哪一月份
 * 中间12位由高到低依次用来标志该年1~12月份分别是大月(30天)还是小月(29天),是大月则标志位为1
 */
const LUNAR_INFO = [0x04bd8, 0x04ae0, 0x0a570,
  0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2,
  0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0,
  0x0ada2, 0x095b0, 0x14977, 0x04970, 0x0a4b0, 0x0b4b5, 0x06a50,
  0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970, 0x06566,
  0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0,
  0x1c8d7, 0x0c950, 0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4,
  0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557, 0x06ca0, 0x0b550,
  0x15355, 0x04da0, 0x0a5d0, 0x14573, 0x052d0, 0x0a9a8, 0x0e950,
  0x06aa0, 0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260,
  0x0f263, 0x0d950, 0x05b57, 0x056a0, 0x096d0, 0x04dd5, 0x04ad0,
  0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b5a0, 0x195a6,
  0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40,
  0x0af46, 0x0ab60, 0x09570, 0x04af5, 0x04970, 0x064b0, 0x074a3,
  0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0, 0x0c960,
  0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0,
  0x092d0, 0x0cab5, 0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9,
  0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930, 0x07954, 0x06aa0,
  0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65,
  0x0d530, 0x05aa0, 0x076a3, 0x096d0, 0x04bd7, 0x04ad0, 0x0a4d0,
  0x1d0b6, 0x0d250, 0x0d520, 0x0dd45, 0x0b5a0, 0x056d0, 0x055b2,
  0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0];

const LUNAR_MONTH_NUMBER = ["正", "二", "三", "四", "五", "六", "七", "八", "九", "十", "冬", "腊"];
const CHINESE_NUMBER = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];
const CHINESE_ZODIAC = ["鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪"];
const HEAVENLY_STEMS = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
const EARTHLY_BRANCHES = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"];
const CHINESE_TEN = ["初", "十", "廿", "卅"];
/**
 * 公历1900年1月30日0时的时间戳,该天是农历1900年正月初一的前一天
 */
const START_DATE_MS = -2206512343000;

/**
 * 最小和最大允许的公历日期范围
 * 农历1900年正与初一和农历1949年腊月廿九
 */
const MIN_LUNAR_DATE_MS = -2206425943000;
const MAX_LUNAR_DATE_MS = 2526480000000;

export default class Lunar {

  constructor(moment) {
    let time = moment.valueOf();
    if (time < MIN_LUNAR_DATE_MS || time >= MAX_LUNAR_DATE_MS) {
      throw new Error("date out of range,the range is 1900-01-31 ~ 2050-01-22");
    }
    //求出和1900年1月30日相差的天数
    let offset = parseInt((time - START_DATE_MS) / 86400000);
    //依次获取每农历年的天数,用offset减去每年天数
    //当offset小于当年总天数时,则当年即为该日期所属年份,剩余offset即为该年的第几天
    let iYear, daysOfYear;
    for (iYear = 1900; iYear < 2050; iYear++) {
      daysOfYear = Lunar.getYearDays(iYear);
      if (offset > daysOfYear) {
        offset -= daysOfYear;
      } else {
        break;
      }
    }
    // 农历年份
    this.year = iYear;

    this.leapMonth = Lunar.getLeapMonth(iYear); // 闰哪个月,1-12
    //用当年的天数offset,逐个减去每月(农历)的天数
    //当offset小于当月总天数时,则当月即为该日期所属月份,求出当天是本月的第几天
    //当该年为闰年是,遍历到闰月月份后,需减去闰月总天数,并标记该月是闰月
    let iMonth, daysOfMonth;
    for (iMonth = 1; iMonth < 13; iMonth++) {
      daysOfMonth = Lunar.getMonthDays(this.year, iMonth);
      if (offset > daysOfMonth) {
        offset -= daysOfMonth;
      } else {
        break;
      }
      //如果当前月份与闰月月份一致时开始处理闰月数据
      if (iMonth === this.leapMonth) {
        daysOfMonth = Lunar.getLeapMonthDays(this.year);
        if (offset > daysOfMonth) {
          offset -= daysOfMonth;
        } else {
          this.isLeapMonth = true;
          break;
        }
      }
    }
    this.month = iMonth;
    this.day = offset;
  }

  /**
   * 获取农历年份
   * @author bawy
   * @date 2019/11/4 22:56
   * @return 农历年份
   */
  getLunarYear() {
    return this.year;
  }

  /**
   * 获取农历月份
   * @author bawy
   * @date 2019/11/4 22:56
   * @return 农历月份
   */
  getLunarMonth() {
    return this.month;
  }

  /**
   * 获取农历天数
   * @author bawy
   * @date 2019/11/4 22:56
   * @return 农历天数
   */
  getLunarDay() {
    return this.day;
  }

  /**
   * 获取中文农历日期
   * @author bawy
   * @date 2019/11/7 0:51
   * @return 中文农历日期
   */
  getChineseLunarDay() {
    if (this.day === 1) {
      return (this.isLeapMonth === true ? '闰' :'') + LUNAR_MONTH_NUMBER[this.month - 1] + '月';
    }
    return Lunar.getChinaDayString(this.day);
  }

  /**
   * 是否闰年
   * @author bawy
   * @date 2019/11/4 22:55
   * @return 是闰年返回true
   */
  isLeap() {
    return this.leapMonth > 0;
  }

  /**
   * 当前日期所属月份是否为闰月
   * @author bawy
   * @date 2019/11/4 22:55
   * @return 是则返回true
   */
  isLeapMonth() {
    return this.isLeapMonth === true;
  }

  /**
   * 获取当前日期所属年份生肖
   * @author bawy
   * @date 2019/11/4 22:55
   * @return 生肖
   */
  getChinesZodiac() {
    return Lunar.getChinesZodiac(this.year);
  }

  /**
   * 获取当前农历日期的天干地支
   * @author bawy
   * @date 2019/11/4 22:56
   * @return 当前农历日期的天干地支
   */
  getChineseEra() {
    return Lunar.getChineseEra(this.year);
  }

  /**
   * 获取农历 y 年的总天数
   * 基础天数为12*29=348天
   * 遍历该年份每个月是大月还是小月,大月则在基础天数上增加一天
   * 获取该年闰月天数,加上闰月天数得到该年总天数
   * @author bawy
   * @date 2019/11/4 22:56
   * @param y 年份
   * @return 总天数
   */
  static getYearDays(y) {
    let i, sum = 348;
    for (i = 0x8000; i > 0x8; i >>= 1) {
      if ((LUNAR_INFO[y - 1900] & i) !== 0) {
        sum += 1;
      }
    }
    return (sum + Lunar.getLeapMonthDays(y));
  }

  /**
   * 获取农历 y 年闰月的天数(29或30天),没有闰月返回0
   * 判断该年份是否存在闰月
   * 如果存在闰月,提取闰月标志位,如果为1(大月)则返回30,反之返回29
   * @author bawy
   * @date 2019/11/4 22:56
   * @param y 年份
   * @return 闰月的天数
   */
  static getLeapMonthDays(y) {
    if (Lunar.getLeapMonth(y) === 0) {
      return 0;
    } else {
      if ((LUNAR_INFO[y - 1900] & 0x10000) === 0) {
        return 29;
      } else {
        return 30;
      }
    }
  }

  /**
   * 获取农历 y 年闰哪个月1-12,没闰月返回0
   * 取出指定年份信息的后四位,对应的值即为闰月的月份
   * @author bawy
   * @date 2019/11/4 22:57
   * @param y 年份
   * @return 闰月月份
   */
  static getLeapMonth(y) {
    return LUNAR_INFO[y - 1900] & 0xf;
  }

  /**
   * 获取指定农历 y 年 m 月的总天数(29或30天)
   * 提取指定年份信息中指定月份的标志位,标志位为1则返回30否则返回29
   * @author bawy
   * @date 2019/11/4 22:57
   * @param y 年份
   * @param m 月份
   * @return 该年该月总天数
   */
  static getMonthDays(y, m) {
    if ((LUNAR_INFO[y - 1900] & (0x10000 >> m)) === 0) {
      return 29;
    } else {
      return 30;
    }
  }

  /**
   * 获取指定农历年份对应的生肖
   * @author bawy
   * @date 2019/11/4 22:57
   * @param y 农历年份
   * @return 生肖
   */
  static getChinesZodiac(y) {
    return CHINESE_ZODIAC[(y - 4) % 12];
  }

  /**
   * 获取指定农历年份的天干地支
   * 1864年是甲子年,作为0坐标,根据偏移量计算天干地支
   * @author bawy
   * @date 2019/11/4 22:57
   * @param y 农历年份
   * @return 天干地支
   */
  static getChineseEra(y) {
    let num = y - 1864;
    return (HEAVENLY_STEMS[num % 10] + EARTHLY_BRANCHES[num % 12]);
  }

  /**
   * 指定日期转为中文
   * @author bawy
   * @date 2019/11/4 22:57
   * @param day 日期天数
   * @return 中文日期
   */
  static getChinaDayString(day) {
    if (day > 30) {
      throw new Error("the lunar month most has 30 days");
    }
    if (day === 10) {
      return "初十";
    } else {
      let n = day % 10 === 0 ? 9 : day % 10 - 1;
      return CHINESE_TEN[parseInt(day / 10)] + CHINESE_NUMBER[n];
    }
  }
}
           

    vue文件:

<template>
  <div :style="{width: width + 'px', border: '1px solid #d9d9d9', borderRadius: '4px'}">
    <a-calendar :locale="locale" :fullscreen="false" @panelChange="onPanelChange" @change="onDateChange">
      <div slot="dateFullCellRender" slot-scope="text" :class="getClass(text)">
        <li class="calendar-date">{{getDate(text)}}</li>
        <li>{{getLunarDate(text)}}</li>
      </div>
    </a-calendar>
  </div>
</template>

<script>
  import moment from "moment";
  import Lunar from "./lunar";
  import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN';

  export default {
    name: "LunarCalendar",
    data() {
      return {
        locale: zhCN,
        today: null,
        currentMonth: 0,
        selectDate: null
      }
    },
    props: {
      width: {
        type: Number,
        required: false,
        default() {
          return 422;
        }
      }
    },
    methods: {
      onPanelChange(value, mode) {
        this.currentMonth = parseInt(value.format("M"));
      },
      onDateChange(value) {
        this.selectDate = value;
        if (parseInt(value.format("M")) !== this.currentMonth) {
          this.currentMonth = parseInt(value.format("M"));
        }
      },
      getClass(value) {
        let classStr = "lunar-date-cell";
        if (value.format("YYYY-MM-DD") === this.today.format("YYYY-MM-DD")) {
          classStr += " lunar-date-cell-today"
        }
        let month = parseInt(value.format("M"));
        if (month === this.currentMonth - 1 || (month === 12 && this.currentMonth === 1)) {
          classStr += " lunar-date-cell-last-month"
        }
        if (month === this.currentMonth + 1 || (month === 1 && this.currentMonth === 12)) {
          classStr += " lunar-date-cell-next-month"
        }
        if (this.selectDate && value.format("YYYY-MM-DD") === this.selectDate.format("YYYY-MM-DD")) {
          classStr += " lunar-date-cell-selected-day"
        }
        return classStr;
      },
      getDate(value) {
        return value.format("D");
      },
      getLunarDate(value) {
        let lunar = new Lunar(value);
        return lunar.getChineseLunarDay();
      }
    },
    created() {
      this.today = new moment();
      this.currentMonth = parseInt(this.today.format("M"));
    }
  }
</script>

<style scoped >
  .lunar-date-cell {
    text-align: center;
  }
  .lunar-date-cell:hover {
    background: #e6f7ff;
    cursor: pointer;
  }
  .lunar-date-cell-today {
    box-shadow: 0 0 0 1px #1890ff inset;
  }
  .lunar-date-cell-selected-day {
    color: #fff;
    background: #1890ff;
  }

  //上一个月的日期单元格
  .lunar-date-cell-last-month {
    color: rgba(0, 0, 0, 0.25);
  }

  //下一个月的日期单元格
  .lunar-date-cell-next-month {
    color: rgba(0, 0, 0, 0.25);
  }


  //公历日期样式
  .calendar-date {
    font-weight: bold;
  }

</style>