Bootstrap

uniapp 自定义日历组件 源码

效果图:
在这里插入图片描述
在这里插入图片描述

一、问题1:每个月的1号,样式上的起始位置

样式上来说实际困难点在于每个月的1号对应的位置:
在这里插入图片描述

解决方式就是判断1号是周几,就在前面放几个空盒子

在这里插入图片描述

二、问题2 : 状态样式控制

定义一个结构来存储各种状态值,以达到改变样式的目的

 dateList=[
 {
  month:'2025.01'
     dayList:[
        {
            number: "1",//年月日的日
            date: "2025.01.01",
            startIndex: "flag",//flag:起租 结束 标识 zhong:介于起租 和结束中间数据标识
            disable: false,//是否可以点击 true:不能点击  false:可以点击
            text: "起租",
          },
          ....以此类推
      ]
  },
  {
   month:'2025.02'
  ....以此类推
  },
  ]

三、代码 vue3

<template>
  <view class="calendar_box">
    <up-navbar
      leftIconSize="32"
      :placeholder="true"
      bgColor="#fff"
      style="height: 100rpx; width: 100%"
      title=""
      :autoBack="true"
    ></up-navbar>
    <!-- 日历 -->
    <view class="calendar_week_box">
      <!---->
      <view class="calendar_yellow_tishi">
        提示:至低起租日为{{ leaseToday }},请确认您的选择!
      </view>
      <view class="calendar_week_item_box">
        <view class="calendar_week_item"></view>
        <view class="calendar_week_item"></view>
        <view class="calendar_week_item"></view>
        <view class="calendar_week_item"></view>
        <view class="calendar_week_item"></view>
        <view class="calendar_week_item"></view>
        <view class="calendar_week_item"></view>
      </view>
      <!-- 日期 -->
  <!-- {{ daysBetween(leaseStartDate,leaseEndDate) }}--{{ leaseStartDate}}--{{leaseEndDate }} -->
      <view class="calendar_date_list_box">
        <view
          class="calendar_date_list_item"
          v-for="item in dateList"
          :key="item.month"
        >
          <view class="calendar_date_nian_yue">{{ item.month }}</view>
          <view class="calendar_date_ri_box">
            <view
              v-for="(j, index) in item.dayList"
              :key="index + item.month"
              :style="j.disable ? 'color:#BFBFBF' : ''"
              @tap="j.disable | !j.number ? '' : selectDate(item.month, index)"
              :class="
                j.startIndex == 'flag'
                  ? 'calendar_date_ri_item calendar_date_ri_active'
                  : 'calendar_date_ri_item'
              "
              :id="j.startIndex == 'zhong' ? (j.number ? 'zhong' : '') : ''"
            >
              <view class="text">{{ j.text }}</view>
              {{ j.number }}
            </view>
          </view>
        </view>
      </view>
      <!-- 按钮 -->
      <view class="calendar_add_button_box">
        <view class="calender_price_box" v-if="daysBetween(leaseStartDate,leaseEndDate)">
            <view class="calender_price_shifu">
              实付租金
              <image src="/static/img/goods/$.png"></image>
              <view>
                <text class="bigprice">126.</text>
                <text class="smallprice">49</text>
              </view>
            </view>
            <view class="calender_price_num_down">
              <view class="calender_price_yajin">
                总租金:
                <image src="/static/img/goods/$.png"></image>
                <text class="yajinprice">126.49</text>
              </view>
              <view class="calender_price_day">7天,18.07/</view>
            </view>
          <view class="calender_price_kuaidi">
            快递时间、开始时间、结束时间均是预计时间,具体时间以实际为准。
          </view>
        </view>
        <view class="calendar_add_button">
          <view class="address_but" @tap="toBackPage">确认</view>
        </view>
      </view>
    </view>
  </view>
</template>
  
  <script setup>
import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
import { computed, nextTick, ref, reactive } from "vue";
/**
 * 跳转页面---地址
 * */
function toBackPage(type) {
  uni.navigateBack({
    delta: 1,
  });
}
let leaseToday = ref("");
getstartDay();
function getstartDay() {
  // 获取当前日期
  const today = new Date();

  // 创建一个新的日期对象,表示今天的日期
  const tomorrow = new Date(today);

  // 将日期设置为明天
  tomorrow.setDate(today.getDate() + 3);

  // 格式化日期为 YYYY-MM-DD
  const year = tomorrow.getFullYear();
  const month = String(today.getMonth() + 1).padStart(2, "0"); // 月份从0开始,所以需要加1
  const day = String(tomorrow.getDate()).padStart(2, "0");

  leaseToday.value = `${year}-${month}-${day}`;
}
// 起租时间和结束时间
let leaseStartDate = ref("");
let leaseEndDate = ref("");
/**
 * 计算租赁的天数
 * */
function daysBetween(dateStr1, dateStr2) {
  if (!dateStr1 & !dateStr2) {
    return 0;
  }
  // 将日期字符串转换为 Date 对象
  const date1 = new Date(dateStr1);
  const date2 = new Date(dateStr2);

  // 获取时间戳(毫秒数)
  const timeDifference = Math.abs(date2 - date1);

  // 将时间戳转换为天数
  const dayDifference = Math.ceil(timeDifference / (1000 * 60 * 60 * 24));

  return dayDifference + 1;
}
/**
 * 生成一个包含从当前月份开始往后12个月的日期列表,并根据要求处理了以下内容:

确定每个月的第一天是星期几,从而决定前面需要填充多少个空对象。
标记今天的日期,并设置相应的属性。
为特定日期(如起租和结束)添加 startIndex 和 text 字段。
 * */
function generateDateList() {
  const today = new Date();
  const currentYear = today.getFullYear();
  const currentMonth = today.getMonth();
  const dateList = [];

  function createMonthData(year, month) {
    const monthStr = `${year}.${String(month + 1).padStart(2, "0")}`;
    const daysInMonth = new Date(year, month + 1, 0).getDate();
    const firstDayOfWeek = new Date(year, month, 1).getDay(); // 获取当月第一天是星期几
    const dayList = [];

    // 根据当月第一天是星期几,填充前面的空数据
    for (let i = 0; i < firstDayOfWeek; i++) {
      dayList.push({ number: "" });
    }

    for (let i = 1; i <= daysInMonth; i++) {
      const dateStr = `${year}.${String(month + 1).padStart(2, "0")}.${String(
        i
      ).padStart(2, "0")}`;
      const isToday =
        year === today.getFullYear() &&
        month === today.getMonth() &&
        i === today.getDate();
      const isMing =
        year === today.getFullYear() &&
        month === today.getMonth() &&
        i === today.getDate() + 1;
      const isHout =
        year === today.getFullYear() &&
        month === today.getMonth() &&
        i === today.getDate() + 2;
      const dayObj = {
        number: isToday ? "今天" : String(i),
        date: dateStr,
        startIndex: "",
        disable: isToday || isMing || isHout || new Date(dateStr) <= today,
        text: "",
      };

      dayList.push(dayObj);
    }

    return { month: monthStr, dayList };
  }

  // 生成从当前月份开始的往后12个月的数据
  for (let i = 0; i < 12; i++) {
    const month = (currentMonth + i) % 12;
    const year = currentMonth + i >= 12 ? currentYear + 1 : currentYear;
    dateList.push(createMonthData(year, month));
  }

  return dateList;
}

/**
 * dateList=[
 * {
 *  month:'2025.01'
 *      dayList:[
 *         {
            number: "1",//年月日的日
            date: "2025.01.01",
            startIndex: "flag",//flag:起租 结束 标识 zhong:介于起租 和结束中间数据标识
            disable: false,//是否可以点击 true:不能点击  false:可以点击
            text: "起租",
          },
          ....以此类推
 *      ]
 * },
 * {
 *  month:'2025.02'
 * ...
 * },
 * ]
 *  
 * */
let dateList = reactive(generateDateList());
/**
 * 点击 选择起租时间
 * 特殊日期的处理,例如起租和结束
 * */
let clickNum = ref(0);
function selectDate(month, index) {
  clickNum.value++;
  // console.log(month, index, "---时间");
  dateList.forEach((item) => {
    if (item.month == month) {
      if (clickNum.value % 2 == 0) {
        // 偶数
        item.dayList[index].text = "结束";
        item.dayList[index].startIndex = "flag";
      } else {
        // 遍历 dateList 并将每个对象的 startIndex 属性设置为空字符串
        leaseStartDate.value=''
        leaseEndDate.value=''
        dateList.forEach((n) => {
          n.dayList.forEach((m, z) => {
            m.startIndex = "";
            m.text = "";
          });
        });
        item.dayList[index].text = "起租";
        item.dayList[index].startIndex = "flag";
      }

      lisyZhong(item.dayList);
    }
  });
}

/**
 *给租赁期间的时间添加样式
 * */
function lisyZhong() {
  // 找到所有 startIndex 为 "flag" 的索引
  let flagIndices = [];
  let flagMonth = [];
  let arr = [];
  dateList.forEach((monthData, n) => {
    monthData.dayList.forEach((day, index) => {
      if (day.startIndex === "flag") {
        flagIndices.push(index);
        arr.push(n);
        // console.log("---前-", arr);
        // 使用 Set 对数组进行去重
        flagMonth = [...new Set(arr)];
        // 对去重后的数组进行排序
        flagMonth.sort((a, b) => a - b);
        // console.log("-后---", flagMonth);
      }
    });
  });
  let startIdx = "";
  let endIdx = "";
  if (flagIndices.length >= 2) {
    startIdx = flagIndices[0] + 1;
    endIdx = flagIndices[1];
  }
  flagMonth.forEach((item, index) => {
    if (flagMonth.length == 1) {
      leaseStartDate.value = dateList[item].dayList[startIdx - 1].date;
      leaseEndDate.value = dateList[item].dayList[endIdx].date;
      dateList[item].dayList[startIdx - 1].text = "起租";
      // dateList[item].dayList[startIdx - 2].text = "快递";
      // dateList[item].dayList[startIdx - 3].text = "快递";
      dateList[item].dayList[endIdx].text = "结束";
      for (let i = startIdx; i < endIdx; i++) {
        dateList[item].dayList[i].startIndex = "zhong";
      }
    }
    if (flagMonth.length > 1) {
      if (index == 0) {
        leaseStartDate.value = dateList[item].dayList[startIdx - 1].date;
        dateList[item].dayList[startIdx - 1].text = "起租";
        dateList[item].dayList.forEach(() => {
          for (let a = startIdx; a < dateList[item].dayList.length; a++) {
            dateList[item].dayList[a].startIndex = "zhong";
          }
        });
      } else {
        leaseEndDate.value = dateList[item].dayList[endIdx].date;
        dateList[item].dayList[endIdx].text = "结束";
        dateList[item].dayList.forEach(() => {
          for (let a = 0; a < endIdx; a++) {
            dateList[item].dayList[a].startIndex = "zhong";
          }
        });
      }
    }
  });
  findAndAssignTargetDays();
}
/**
 * 查找快递的时间
 * */
function findAndAssignTargetDays() {
  for (let i = 0; i < dateList.length; i++) {
    const month = dateList[i];
    for (let j = 0; j < month.dayList.length; j++) {
      const day = month.dayList[j];
      if (day.startIndex === "flag" && day.number !== "") {
        let targetDays;
        if (j >= 2) {
          // 返回当前 dayList 的前两个数据
          targetDays = month.dayList.slice(j - 2, j);
        } else if (i > 0) {
          // 获取前一个 month 的 dayList
          const previousMonth = dateList[i - 1];
          const previousDayList = previousMonth.dayList;
          targetDays = previousDayList.slice(-2);
        } else {
          // 如果这是第一个 month 且没有前一个 month,则返回空数组
          return [];
        }
        // 检查找到的两个数据的 number 是否为空,如果为空则继续往前找
        while (targetDays.some((d) => d.number === "")) {
          if (i > 0) {
            const previousMonth = dateList[--i];
            const previousDayList = previousMonth.dayList;
            targetDays = previousDayList.slice(-2);
          } else {
            return []; // 如果没有更多的 dayList 可以检查,则返回空数组
          }
        }
        // 将找到的两个数据的 text 属性赋值为 '快递'
        targetDays.forEach((targetDay) => {
          targetDay.text = "快递";
        });
        return targetDays;
      }
    }
  }
  // 如果没有找到 startIndex: "flag",则返回空数组
  return [];
}
</script>
  
  <style lang="less" scoped>
.calendar_box {
  position: relative;
  min-width: 750rpx;
  height: 100vh;
  background-color: #fff;
  color: #000000;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  //   justify-content: space-between;
  //   周
  .calendar_week_box {
    background: #ffffff;
    box-shadow: 0rpx 7rpx 10rpx 0rpx #f6f8f8;
    .calendar_yellow_tishi {
      height: 90rpx;
      line-height: 90rpx;
      font-family: PingFangSC, PingFang SC;
      font-weight: 400;
      font-size: 26rpx;
      color: #ff9348;
      text-align: center;
    }
    .calendar_week_item_box {
      padding: 0 28rpx;
      box-sizing: border-box;
      height: 90rpx;
      line-height: 90rpx;
      display: flex;
      justify-content: space-around;
      font-family: PingFangSC, PingFang SC;
      font-weight: bold;
      font-size: 24rpx;
      color: #333333;
      box-shadow: 0px 4px 8px #eeeeee;
    }
  }
  //   按钮
  .calendar_add_button_box {
    width: 100%;
    position: fixed;
    bottom: 0;
    background-color: #fff;
    .calendar_add_button {
      height: 114rpx;
      box-sizing: border-box;
      padding: 18rpx 32rpx 12rpx;
      border-top: 1px solid #dddddd;
      .address_but {
        height: 84rpx;
        line-height: 84rpx;
        background: #00c8be;
        border-radius: 42rpx;
        font-family: PingFangSC, PingFang SC;
        font-weight: 400;
        font-size: 30rpx;
        color: #ffffff;
        text-align: center;
      }
    }
    .calender_price_box {
      padding:19rpx 30rpx ;
      border-top: 1px solid #dddddd;
      height: 168rpx;
      background: #ffffff;
      width: 100%;
      box-sizing: border-box;
      display: flex;
        justify-content: space-between;
        flex-direction: column;
      .calender_price_shifu {
        font-family: PingFangSC, PingFang SC;
        font-weight: 600;
        font-size: 28rpx;
        color: #262626;
        line-height: 40rpx;
        display: flex;
        align-items: center;
        justify-content: flex-start;
        .bigprice {
          font-size: 50rpx;
          color: #ea4444;
        }
        .smallprice {
          font-size: 30rpx;
          color: #ea4444;
        }
        image {
          width: 18rpx;
          height: 24rpx;
        }
      }
      .calender_price_num_down {
        display: flex;
        align-items: center;
        justify-content: space-between;

        .calender_price_day {
          font-family: PingFangSC, PingFang SC;
          font-weight: 400;
          font-size: 20rpx;
          color: #8c8c8c;
          line-height: 28rpx;
        }

        .calender_price_yajin {
          font-family: PingFangSC, PingFang SC;
          font-weight: 400;
          font-size: 20rpx;
          color: #ea4444;
          line-height: 28rpx;
          display: flex;
          align-items: center;
          justify-content: flex-end;

          image {
            width: 12rpx;
            height: 16rpx;
          }
        }
      }
      .calender_price_kuaidi {
        padding-top: 8rpx;
        height: 28rpx;
        font-family: PingFangSC, PingFang SC;
        font-weight: 400;
        font-size: 20rpx;
        color: #bfbfbf;
        line-height: 28rpx;
      }
    }
  }

  //   日期
  .calendar_date_list_box {
    height: calc(100vh - 98rpx - 98rpx - 114rpx - 100rpx);
    overflow-y: auto;
    .calendar_date_list_item {
      padding-bottom: 35rpx;
    }
    .calendar_date_nian_yue {
      height: 125rpx;
      line-height: 125rpx;
      font-family: PingFangSC, PingFang SC;
      font-weight: bold;
      font-size: 32rpx;
      color: #333333;
      text-align: center;
    }
    .calendar_date_ri_box {
      margin: 0 28rpx;
      box-sizing: border-box;
      display: flex;
      flex-wrap: wrap;
      gap: 21rpx;
      overflow: hidden;
      .calendar_date_ri_item {
        margin-bottom: 6rpx;
        height: 80rpx;
        width: 80rpx;
        display: flex;
        // align-items: center;
        flex-direction: column;
        justify-content: center;
        text-align: center;
        // background-color: #e99898;
        .text {
          font-family: PingFangSC, PingFang SC;
          font-weight: bold;
          font-size: 20rpx;
          color: #c1c1c1;
        }
      }
      .calendar_date_ri_active {
        background-color: #00c8be;
        color: #fff;
        position: relative;
        border-radius: 10rpx;
        .text {
          font-family: PingFangSC, PingFang SC;
          font-weight: bold;
          font-size: 20rpx;
          color: #fff;
        }
        &:last-child {
          // &::after {
          //   content: "8888";
          //   display: block;
          //   position: absolute;
          //   top: -10rpx;
          //   left: 50%;
          //   width: 100rpx;
          //   height: 20rpx;
          //   background-color: #e27e7e;
          // }
        }
        .jingtao_modal {
          position: absolute;
          top: -45rpx;
          left: 50%;
          width: 364rpx;
          line-height: 78rpx;
          height: 78rpx;
          background: #4c4c4c;
          border-radius: 16rpx;
          font-family: PingFangSC, PingFang SC;
          font-weight: 400;
          font-size: 28rpx;
          color: #ffffff;
        }
      }
      #zhong {
        background: #e5f9f8;
        position: relative;
        &::after {
          content: "";
          height: 100%;
          width: 21rpx;
          position: absolute;
          right: -21rpx;
          background-color: #e5f9f8;
        }
        &::before {
          content: "";
          height: 100%;
          width: 21rpx;
          position: absolute;
          left: -21rpx;
          background-color: #e5f9f8;
        }
      }
    }
  }
}
</style>
  
;