Skip to content

日历卡片

什么是日历卡片?您可以在博客风格首页的卡片栏看到效果。

创建日历卡片组件

.vitepress/theme/components 目录下创建 CalendarCard.vue 文件,并添加以下代码:

vue
<template>
  <TkPageCard>
    <div class="card-widget" id="card-widget-calendar">
      <div class="item-headline">
        <i class="icon-calendar"></i>
      </div>
      <div class="item-content">
        <div id="calendar-area-left">
          <div id="calendar-week">第{{ weekNumber }}周&nbsp;{{ weekDays[today.getDay()] }}</div>
          <div id="calendar-date">{{ today.getDate() }}</div>
          <div id="calendar-solar">{{ today.getFullYear() }}年{{ today.getMonth() + 1 }}月第{{ dayOfYear }}天</div>
          <div id="calendar-lunar">{{ lunarYear }}&nbsp;{{ lunarMonth }}&nbsp;{{ lunarDay }}</div>
        </div>
        <div id="calendar-area-right">
          <div id="calendar-main">
            <!-- 星期标题行 -->
            <div class="calendar-r0">
              <div class="calendar-d0"><a>日</a></div>
              <div class="calendar-d1"><a>一</a></div>
              <div class="calendar-d2"><a>二</a></div>
              <div class="calendar-d3"><a>三</a></div>
              <div class="calendar-d4"><a>四</a></div>
              <div class="calendar-d5"><a>五</a></div>
              <div class="calendar-d6"><a>六</a></div>
            </div>

            <!-- 日期行 -->
            <div v-for="(week, weekIndex) in calendarWeeks" :key="weekIndex" :class="`calendar-r${weekIndex + 1}`">
              <div v-for="(day, dayIndex) in week" :key="dayIndex" :class="`calendar-d${dayIndex}`">
                <a :class="{ now: day.isToday, 'other-month': day.isOtherMonth }" v-if="day.date">
                  {{ day.date }}
                </a>
                <a v-else></a>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </TkPageCard>
</template>

<script setup>
import { TkPageCard } from "vitepress-theme-teek";
import { ref, onMounted, computed } from "vue";

// 星期几中文映射
const weekDays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];

// 当前日期
const today = ref(new Date());

// 生成当前月份的日历数据
const calendarWeeks = computed(() => {
  const year = today.value.getFullYear();
  const month = today.value.getMonth();

  // 当月第一天
  const firstDay = new Date(year, month, 1);
  // 当月最后一天
  const lastDay = new Date(year, month + 1, 0);

  // 日历需要显示的第一天(可能是上月的日期)
  const startDay = new Date(firstDay);
  startDay.setDate(firstDay.getDate() - firstDay.getDay());

  // 日历需要显示的最后一天(可能是下月的日期)
  const endDay = new Date(lastDay);
  if (endDay.getDay() < 6) {
    endDay.setDate(lastDay.getDate() + (6 - endDay.getDay()));
  }

  // 生成日历数据
  const weeks = [];
  let currentDay = new Date(startDay);

  while (currentDay <= endDay) {
    const week = [];
    for (let i = 0; i < 7; i++) {
      const date = currentDay.getDate();
      const isToday = currentDay.toDateString() === today.value.toDateString();
      const isOtherMonth = currentDay.getMonth() !== month;

      week.push({ date, isToday, isOtherMonth });

      currentDay.setDate(currentDay.getDate() + 1);
    }
    weeks.push(week);
  }

  return weeks;
});

// 计算当前是今年的第几天
const dayOfYear = computed(() => {
  const start = new Date(today.value.getFullYear(), 0, 0);
  const diff = today.value - start;
  const oneDay = 1000 * 60 * 60 * 24;
  return Math.floor(diff / oneDay);
});

// 计算当前是第几周
const weekNumber = computed(() => {
  const firstDay = new Date(today.value.getFullYear(), 0, 1);
  const pastDaysOfYear = (today.value - firstDay) / 86400000;
  return Math.ceil((pastDaysOfYear + firstDay.getDay() + 1) / 7);
});

// 农历转换相关
const lunarInfo = [
  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, 0x0a5b0, 0x14573, 0x052b0, 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 gan = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
const zhi = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"];
const animals = ["鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪"];
const lunarMonths = [
  "正月",
  "二月",
  "三月",
  "四月",
  "五月",
  "六月",
  "七月",
  "八月",
  "九月",
  "十月",
  "十一月",
  "十二月",
];
const lunarDays = [
  "初一",
  "初二",
  "初三",
  "初四",
  "初五",
  "初六",
  "初七",
  "初八",
  "初九",
  "初十",
  "十一",
  "十二",
  "十三",
  "十四",
  "十五",
  "十六",
  "十七",
  "十八",
  "十九",
  "二十",
  "廿一",
  "廿二",
  "廿三",
  "廿四",
  "廿五",
  "廿六",
  "廿七",
  "廿八",
  "廿九",
  "三十",
];

// 转换为农历
const getLunarDate = date => {
  const year = date.getFullYear();
  const month = date.getMonth() + 1;
  const day = date.getDate();

  let springStart = new Date(2000, 1, 4); // 2000年春节是2月4日
  if (year > 2000) {
    // 简单计算春节日期,实际应用中可能需要更精确的算法
    springStart = new Date(year, 1, 4 + Math.floor((year - 2000) * 0.2422));
  }

  let lunarYear = year;
  let isLeap = false;
  let lunarMonthIdx = 0;
  let lunarDayIdx = 0;

  // 简化的农历计算,实际应用可能需要更复杂的算法
  const offset = Math.floor((date - new Date(year, 0, 0)) / 86400000);
  let days = 0;
  let i = 0;

  for (; i < 12; i++) {
    const monthDays = (lunarInfo[year - 1900] >> (12 - i)) & 0x1 ? 30 : 29;
    if (days + monthDays >= offset) {
      lunarDayIdx = offset - days - 1;
      break;
    }
    days += monthDays;
  }
  lunarMonthIdx = i;

  // 计算农历年的干支和生肖
  const ganIndex = (year - 3) % 10;
  const zhiIndex = (year - 3) % 12;
  const lunarYearStr = `${gan[ganIndex]}${zhi[zhiIndex]}${animals[zhiIndex]}年`;

  return {
    lunarYear: lunarYearStr,
    lunarMonth: lunarMonths[lunarMonthIdx],
    lunarDay: lunarDays[lunarDayIdx],
  };
};

// 响应式农历数据
const lunarDate = computed(() => getLunarDate(today.value));
const lunarYear = computed(() => lunarDate.value.lunarYear);
const lunarMonth = computed(() => lunarDate.value.lunarMonth);
const lunarDay = computed(() => lunarDate.value.lunarDay);

// 每天更新一次日历
onMounted(() => {
  // 检查是否需要更新(跨天)
  const checkUpdate = () => {
    const now = new Date();
    if (now.toDateString() !== today.value.toDateString()) {
      today.value = now;
    }
  };

  // 每分钟检查一次
  setInterval(checkUpdate, 60000);
});
</script>

<style scoped>
.card-widget {
  max-height: calc(100vh - 100px);
  position: relative;
}

#card-widget-calendar .item-headline {
  padding-bottom: 0;
  margin-left: 8px;
  font-size: 1em;
  font-weight: 700;
  display: flex;
  align-items: center;
  gap: 5px;
  margin-bottom: 10px;
}

#card-widget-calendar .item-headline i {
  font-size: 18px;
}

#card-widget-calendar .item-content {
  display: flex;
}

#calendar-area-left,
#calendar-area-right {
  height: 100%;
  padding: 4px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

#calendar-area-left {
  width: 45%;
}

#calendar-week {
  height: 1.2rem;
  font-size: 14px;
  letter-spacing: 1px;
  font-weight: 700;
  align-items: center;
  display: flex;
}

#calendar-date {
  height: 3rem;
  line-height: 1.3;
  font-size: 36px;
  letter-spacing: 3px;
  color: var(--vp-c-brand-1);
  font-weight: 700;
  align-items: center;
  display: flex;
  position: absolute;
  top: calc(50% - 2.1rem);
}

#calendar-solar {
  bottom: 2.1rem;
}

#calendar-lunar,
#calendar-solar {
  height: 1rem;
  font-size: 11px;
  align-items: center;
  display: flex;
  position: absolute;
}

#calendar-lunar {
  bottom: 1rem;
}

#calendar-area-right {
  width: 55%;
}

#calendar-main {
  width: 100%;
}

.calendar-r0,
.calendar-r1,
.calendar-r2,
.calendar-r3,
.calendar-r4,
.calendar-r5,
.calendar-r6,
.calendar-rh {
  height: 1.2rem;
  display: flex;
}

.calendar-d0,
.calendar-d1,
.calendar-d2,
.calendar-d3,
.calendar-d4,
.calendar-d5,
.calendar-d6 {
  width: calc(100% / 7);
  display: flex;
  justify-content: center;
  align-items: center;
}

#calendar-main a {
  height: 1.2rem;
  width: 1.2rem;
  border-radius: 50%;
  font-size: 12px;
  line-height: 12px;
  display: flex;
  justify-content: center;
  align-items: center;
  text-decoration: none;
}

#calendar-main a.now {
  background: var(--vp-c-brand-1);
  color: #fff;
}
</style>

使用日历卡片组件

.vitepress/theme/index.ts 中通过 Teek 提供的卡片栏插槽插入日历卡片组件。

ts
import Teek from "vitepress-theme-teek";
import CalendarCard from "./components/CalendarCard.vue";
import { h } from "vue";

export default {
  extends: Teek,
  Layout: () =>
    h(Teek.Layout, null, {
      "teek-home-card-my-after": () => h(CalendarCard),
    }),
};