好看的日历组件设计

好看前端日历组件

在这里插入图片描述

环境前提

Vue3 + element-plus
依赖第三方组件

# 1. 时间日期相关
npm install date-fns
# 2. 农历相关
npm install lunar-javascript

使用的图片
在这里插入图片描述

组件代码

<template>
  <div class="calendar-container text-unselect" :style="{width: width, height: height }">
    <div class="calendar-header " :style="{height: `calc(${height} * 0.13)`, '--calendar-header-height': `calc(${height} * 0.13)` }">
      <div class="ch-now">
        <div class="now-time" :time-show="`${timeShow} (星期${daysOfWeek[currentDate.getDay() == 0 ? 6 : currentDate.getDay() - 1]})`">{{ year }}年{{ month + 1 }}月{{ day }}日</div>
      </div>
      <div class="now-select">
          <div class="ch-title" @click="selectDate(null)"><label style="font-weight: 600;">{{ currentDateObj.star }}座</label><br/>探索每一天精彩内容</div>
          <div class="ch-select">
            <div style="width:80px; font-size: 12px;">年份</div>
            <el-date-picker v-model="formDay.year" type="year" size="small" :editable="false" :clearable="false" format="YYYY" value-format="YYYY" @change="changeYear"/>
            <div style="width: 80px; font-size: 12px; margin-left: 10px;">月份</div>
            <el-date-picker v-model="formDay.month" type="month"  size="small" :editable="false" :clearable="false"  format="MM" value-format="MM" @change="changeMonth"/>
            <div class="now-btn" @click="toNowEvent">Now</div>
          </div>
      </div>
    </div>
    <div class="calendar-toolbar text-unselect" :style="{height: `calc(${height} * 0.07)`}" >
      <div class="toolbar-item" v-for="day in daysOfWeek" :key="day">
        <div :style="{color: day == '' || day == '' ? 'red' : 'blank' }">{{ day }}</div>
      </div>
    </div>
    <div class="calendar-body text-unselect" :style="{height: `calc(${height} * 0.6)`, '--calendar-body-height': `calc(${height} * 0.7)` }" 
      :data-month="currentDateObj.month">
      <div class="calendar-cell" v-for="(date, index) in calendarPage" :key="index">
        <div class="calendar-cell-item" 
          :style="{ color: index % 7 == 6 || index % 7 == 5 ? 'red' : 'blank', opacity: date.isCurrentMonth ? 0.9 : 0.3 }"
          :class="{
            rest: date.rest,
            active: currentDateObj.dateStr == date.dateStr, 
            toDay: date.dateStr == format(new Date(), 'yyyy-MM-dd'),
          }"
          @click="selectDate(date)"
        >
          <div style="font-size: 25px; font-family: fangsong;">{{ date.day }}</div>
          <div style="font-size: 10px; font-weight: 500; color: blank;">{{ date.lunar }}</div>
        </div>
      </div>
    </div>
    <div class="calendar-footer" :style="{height: `calc(${height} * 0.2)` }">
      <div class="footer-card">
        <div style="flex: 2; display: flex; align-items: center; justify-content: left; width: 100%;">
            <div style="font-family: CALENDAR_FONT; font-weight: bold; color: #333; width: 30%;">
              <div style="font-size: 18px;  margin-bottom: 5px;">{{ currentDateObj.lunarStr }}</div>
              <div style="font-size: 14px; ">{{ currentDateObj.lunaryear }} 年</div>
            </div>
            <div style="font-size: 14px; font-family: Arial, sans-serif; color: #333; width: 70%;">
              <div style="overflow: hidden;white-space: nowrap; margin-bottom: 5px; text-overflow: ellipsis; height: 20px; width:  100%;">
                <span style="color: white; background-color: red; padding: 1px; border-radius: 5px; margin-right: 5px;"></span> {{ currentDateObj.favorable }}
              </div>
              <div style="overflow: hidden;white-space: nowrap; text-overflow: ellipsis; width:  100%; height: 20px;">
                <span style="color: white; background-color: #333; padding: 1px; border-radius: 5px; margin-right: 5px;" ></span> {{ currentDateObj.adverse }}
              </div>
            </div>
        </div>
        <div style="flex: 1; font-size: 14px; border-top: 1px solid #ccc; padding-top: 5px; display: flex; align-items: center;">
          <Icon icon="ic:round-access-time" width="20px" style="margin-right: 10px; color: gray;"/>{{ currentDateObj.desc }}
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { format, startOfMonth, endOfMonth, eachDayOfInterval, getDay, addDays } from 'date-fns';
// https://6tail.cn/calendar/api.html  农历信息
const {Solar, Lunar, HolidayUtil} = require('lunar-javascript');

defineProps({
  width: { type: String, default: '500px' },
  height: { type: String, default: '600px' },
});
const daysOfWeek = ['一', '二', '三', '四', '五', '六', '日'];

// 初始化当前日期
const currentDate = new Date();
const currentDateObj = ref(makeDate(currentDate, true));
const year = ref(currentDate.getFullYear());
const month = ref(currentDate.getMonth());
const day = ref(currentDate.getDate());
const calendarPage = ref([]);
const timeShow = ref(format(new Date(), 'HH:mm:ss'));
var daysUntilNextYear = null;
const formDay = ref({
  year: year.value + '',
  month: (month.value) + 1 + '',
})

// 计算当月的日期数组
/**
 * 计算指定月份的 42 个日期对象
 * @param {number} year - 年份
 * @param {number} month - 月份(0-11)
 * @returns {Array} - 包含 42 个日期对象的数组
 */
function makeDate(date, isCurrentMonth) {
  return fillNongLi({
      date: date,
      year: date.getFullYear(),
      month: date.getMonth() + 1, // 月份从 0 开始,所以加 1
      day: date.getDate(),
      isCurrentMonth: isCurrentMonth,
      dateStr: format(date, 'yyyy-MM-dd'),
    });
}

// 定义一个函数来计算当前时间距离下一年1月1日的天数
function daysUntilNextYearJanuaryFirst() {
  // 获取当前日期
  const currentDate = new Date();

  // 计算下一年1月1日的日期
  const nextYear = currentDate.getFullYear() + 1;
  const nextYearJanuaryFirst = new Date(nextYear, 0, 1); // 月份从0开始,所以0表示1月

  // 计算当前日期和下一年1月1日之间的毫秒差
  const timeDifference = nextYearJanuaryFirst - currentDate;

  // 将毫秒差转换为天数
  const daysDifference = Math.ceil(timeDifference / (1000 * 60 * 60 * 24));

  return daysDifference;
}
// 填充农历信息
function fillNongLi(date) {
      // 设置农历信息 或者 节日
    let nongli = Lunar.fromDate(date.date);
    let xingzuo = Solar.fromDate(date.date);
    date.lunar = nongli.getDayInChinese();
    date.lunarStr = nongli.getMonthInChinese() + '月' + nongli.getDayInChinese();
    date.lunaryear = nongli.getYearInGanZhi() + nongli.getYearShengXiao();
    date.favorable = nongli.getDayYi()?.join(', ');
    date.adverse = nongli.getDayJi()?.join(', ');
    date.star = xingzuo.getXingZuo();
    date.desc = '距离 ' + (Number(new Date().getFullYear()) + 1) + ' 年元旦 还有' + daysUntilNextYear + '天';
    return date;
}
function getCalendarDates(year, month) {
  // 获取指定月份的第一天和最后一天
  const firstDayOfMonth = startOfMonth(new Date(year, month));
  const lastDayOfMonth = endOfMonth(new Date(year, month));

  // 计算当月的日期数组
  const daysInMonth = eachDayOfInterval({ start: firstDayOfMonth, end: lastDayOfMonth });

  // 找到当月第一天是星期几(0 表示星期日)
  let startDayOfWeek = getDay(firstDayOfMonth);
  if (startDayOfWeek === 0) {
    startDayOfWeek = 7;
  }

  // 创建 42 个格子的日期数组
  let allDates = [];

  // 填充上个月的日期
  for (let i = 1; i < startDayOfWeek; i++) {
    const prevDate = addDays(firstDayOfMonth, -startDayOfWeek + i);
    allDates.push(makeDate(prevDate, false));
  }

  // 填充当月的日期
  daysInMonth.forEach(day => allDates.push(makeDate(day, true)));

  // 填充下个月的日期直到有 42 个元素
  while (allDates.length < 42) {
    const nextDate = addDays(lastDayOfMonth, allDates.length - daysInMonth.length + 2 - startDayOfWeek);
    allDates.push(makeDate(nextDate, false));
  }
  // 设置农历或者节假日信息
  allDates.forEach((date, index) => {
    // 设置农历信息
    fillNongLi(date);
    // 节气
    var j = Lunar.fromDate(date.date);
    if (j && j.getJieQi()) {
      date.lunar = j.getJieQi();
    }
    // 节假日
    let d = HolidayUtil.getHoliday(Number(date.year), Number(date.month), Number(date.day));
    // 今天不做处理
    if (!(date.dateStr == format(new Date(), 'yyyy-MM-dd'))) {
      // 默认周末 
      date.rest = (index % 7 === 5 || index % 7 === 6);
      if (d && d.getName()) {
        date.rest = !d.isWork();
      }
    }
    // 设置假日名
    if(d && d.getName() && d.getTarget() && d.getTarget() == date.dateStr) {
      date.lunar = d.getName();
    }

  });
  return allDates;
}

// 年份变化
const changeYear = y => {
  currentDateObj.value.year = y + '';
  fillNongLi(currentDateObj.value);
  calendarPage.value = getCalendarDates(Number(formDay.value.year), Number(formDay.value.month) - 1);
}

// 月份变化
const changeMonth = m => {
  currentDateObj.value.month = m + '';
  fillNongLi(currentDateObj.value);
  calendarPage.value = getCalendarDates(Number(formDay.value.year), Number(formDay.value.month) - 1);
}

// 回调 当拖动变化的时候
const emits = defineEmits(['click']);
// 选择日期出发
const selectDate = (date) => {
  if (date) {
    currentDateObj.value = date;
    emits('click', currentDateObj.value);
  } else {
    emits('click', makeDate(new Date(), true));
  }
}

// 恢复当前日期
const toNowEvent = () => {
  currentDateObj.value = makeDate(new Date(), true);
  calendarPage.value = getCalendarDates(year.value, month.value);
  formDay.value.year = year.value + '';
  formDay.value.month = (month.value + 1) + '';
}

let intervalId;
onMounted(() => {
  daysUntilNextYear = daysUntilNextYearJanuaryFirst();
  // 当组件加载时执行的操作
  toNowEvent();
  // 时间定时器
  intervalId = setInterval(() => timeShow.value = format(new Date(), 'HH:mm:ss'), 1000); // 每秒更新时间
});
// 清除定时器
onUnmounted(() => {
  clearInterval(intervalId);
});

// 假日更新
// https://6tail.cn/calendar/api.html#holiday-util.fix.html  节假日补充
// 2024 年: 202312300120240101202312310120240101202401010120240101202402041020240210202402101120240210202402111120240210202402121120240210202402131120240210202402141120240210202402151120240210202402161120240210202402171120240210202402181020240210202404042120240404202404052120240404202404062120240404202404072020240404202404283020240501202405013120240501202405023120240501202405033120240501202405043120240501202405053120240501202405113020240501202406084120240610202406094120240610202406104120240610202409145020240917202409155120240917202409165120240917202409175120240917202409296020241001202410016120241001202410026120241001202410036120241001202410046120241001202410056120241001202410066120241001202410076120241001202410126020241001
// HolidayUtil.fix('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
// 格式:当天年月日YYYYMMDD(8位)+节假日名称下标(1位)+调休标识(1位)+节假日当天YYYYMMDD(8位)  一共18位
// 0(元旦节)  1(春节)  2(清明节)  3(劳动节)  4(端午节)  5(中秋节)  6(国庆节) 7(国庆中秋) 8(抗战胜利日)
</script>

<style scoped lang="scss">
.calendar-container {
  background-color: rgb(246, 245, 255);
  margin: 10px;
  padding: 2px;
  border-radius: 22px;
  .calendar-header {
    position: relative;
    background-color: white;
    background-image: linear-gradient(187deg, #c6dfe7f0 20%, #c7dee57d 40%, white 70%);;
    border-radius: 20px 20px 0 0 ;
    display: flex;
    width: 100%;
    &::after {
      content: '';
      position: absolute;
      top: -15px;
      right: 30px;
      z-index: 1;
      width: calc(var(--calendar-header-height) * 0.77);
      height: calc(var(--calendar-header-height) * 0.77);
      background-image: url('@/assets/img/rili_icon.png'); /* 图片路径 */
      background-size: cover; /* 使图片覆盖整个区域 */
      background-repeat: no-repeat; /* 防止图片重复 */
    }
    .ch-now {
      display: flex;
      width: 40%;
      min-width: 170px;
      padding: calc(var(--calendar-header-height) * 0.2) 25px;
      .now-time {
          position: relative;
          width: 100%;
          font-size: calc(var(--calendar-header-height) * 0.3);
          font-family: '宋体';
          font-weight: 600;
          color: #044053e0;
          border-right: 1px solid rgba(150, 184, 201 , 0.57);
          &::after {
            content: attr(time-show);
            position: absolute;
            left: 0px;
            top: 30px;
            font-size: small;
            color: #044053a1;
          }
      }
    }
    .now-select {
      position: relative;
      width: 60%;
      .ch-title {
        margin-top: 10px;
        width: calc(100% - var(--calendar-header-height) * 1.3);
        height: calc(var(--calendar-header-height) * 0.5);
        overflow: hidden;
        color: #20093a;
        font-size: 12px;
        &:hover {
          cursor: pointer;
          text-decoration: underline;
        }
      }
      .ch-select {
        width: 90%;
        display: flex;
        justify-content: center;
        align-items: center;
        .now-btn {
          width: 50px; 
          margin-left: 10px; 
          margin-right: 20px; 
          cursor: pointer;
          &:hover {
            background-color: #cedaf5;
          }
        }
      }

    }
  }
  .calendar-toolbar {
    display: flex;
    .toolbar-item {
      display: flex;
      flex: 1;
      background-color: #ecf4ff;
      justify-content: center;
      align-items: center;
      font-weight: 600;
    }
  }
  .calendar-body {
    position: relative;
    display: grid;
    grid-template-columns: repeat(7, 14.286%);
    grid-template-rows: repeat(6, 16.667%);
    &::before {
      content: attr(data-month);
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: calc(var(--calendar-body-height) / 1.5);
      color: rgba(255, 0, 0, 0.03);
    }
    .calendar-cell {
      display: flex;
      flex: 1;
      justify-content: center;
      align-items: center;
      background-color: #ffffff;
      font-weight: 600;
      .calendar-cell-item {
        position: relative;
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        width: 85%;
        height: 85%;
        cursor: pointer;
        border-radius: 10px;
        &:hover {
          background-color: #ecf4ff;
        }
        &.active {
          border: 2px solid rgb(44, 44, 211);
        }
        &.toDay {
          color: rgb(44, 44, 211) !important;
          background-color: #ecf4ff;
        }
        &.toDay::before {
            content: '今';
            position: absolute;
            width: 10px;
            height: 10px;
            top: 0;
            right: 10px;
            font-size: small;
        }
        &.rest {
          background-color: #fffae6c7;
        }
        &.rest::before {
            content: '休';
            position: absolute;
            width: 10px;
            height: 10px;
            top: 1px;
            right: 5px;
            font-size: 10px;
        }
      }
    }
  }
  .calendar-footer {
    position: relative;
    display: flex;
    width: 100%;
    background-color: white;
    border-radius: 0 0 20px 20px;
    align-items: center;
    justify-content: center;
    .footer-card {
      position: relative;
      width: calc(100% - 60px);
      height: calc(100% - 20px);
      background-color: #f4f5f8;
      border-radius: 10px;
      display: flex;
      flex-direction: column;
      padding: 0 20px;
    }
  }
}
</style>

组件使用

<template>
    <Calendar></Calendar>
</template>
<script setup>
import Calendar from './CalendarComponent.vue';
</script>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值