uniapp 自定义日历组件 源码

效果图:

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

样式上来说实际困难点在于每个月的1号对应的位置:

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

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

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

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

三、代码 vue3

js 复制代码
<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>
  
相关推荐
无双_Joney19 分钟前
[更新迭代 - 1] Nestjs 在24年底更新了啥?(功能篇)
前端·后端·nestjs
在云端易逍遥20 分钟前
前端必学的 CSS Grid 布局体系
前端·css
EMT20 分钟前
在 Vue 项目中使用 URL Query 保存和恢复搜索条件
javascript·vue.js
ccnocare22 分钟前
选择文件夹路径
前端
艾小码22 分钟前
还在被超长列表卡到崩溃?3招搞定虚拟滚动,性能直接起飞!
前端·javascript·react.js
闰五月23 分钟前
JavaScript作用域与作用域链详解
前端·面试
泉城老铁26 分钟前
idea 优化卡顿
前端·后端·敏捷开发
前端康师傅26 分钟前
JavaScript 作用域常见问题及解决方案
前端·javascript
司宸28 分钟前
Prompt结构化输出:从入门到精通的系统指南
前端