uniapp写一个预约上课程序的日历组件

1,420 阅读10分钟

这篇笔记主要记录,uni-app写一个预约上课小程序的日历组件,日历实现的主要功能,

功能描述详细描述样式/交互
显示一周日期每屏显示一整周的日期,起始日期从周一开始。日期分为上下两部分:上面显示“周几”,下面显示日期。
特殊日期标注今天、明天、后天的日期不显示“周几”,而是显示“今天”、“明天”、“后天”。使用特殊文字标注,并与其他日期区分开来。
高亮显示今天当前日期(今天)默认高亮显示。使用不同背景颜色或样式来突出显示今天。
滚动查看本周日期如果今天不是周一,可以向左滚动查看本周的过去日期(周一到昨天),向右滚动查看未来的日期。向左滚动:本周起始日期 → 本周结束;向右滚动:今天 → 未来7天。
过去的日期灰色显示本周中所有已过去的日期用灰色样式表示(不含今天)。灰色字体或背景表示过去的日期。
点击日期高亮点击任意日期,切换高亮到该日期。高亮切换样式与默认高亮样式一致。
滚动查看未来一周向右滚动时,可以查看今天及其后的未来一周日期。持续向右滚动,直到未来七天显示完毕。

先看下效果

image.png

  • 中间屏幕:显示起始展示的日期为今天。
  • 左边屏幕:展示了点击日期后高亮对应的日期。
  • 右边屏幕:展示如果今天不是周一,可以向左滚动查看本周过去的日期(灰色样式表示过去的日期)。

具体实现

我们默认是展示本周和未来一周,可以用一个循环;然后每周有7天,在周循环里再嵌入一个循环,uni-app有一个scroll-view组件,把它包裹循环,这样日历就可以渲染出来并且支持滚动了

组件Template部分

按照前面说的逻辑,v-for嵌套v-for, 加一个scroll-view, 写下代码

<template>
  <view class="calendar-container">
    <scroll-view
      scroll-x="true"
      show-scrollbar="false"
      :scroll-into-view="todayId"
      class="calendar-scroll"
      @scroll="onScroll">
      <view v-for="(week, index) in weeks" :key="index" class="calendar-week">
        <view class="calendar-week-days">
          <view
            v-for="(day, i) in week"
            :key="i"
            class="calendar-day"
            :class="{
              selected: isSelected(day.date),
              past: isPast(day.date),
            }"
            :id="isToday(day.date) ? 'today' : ''"
            @click="handleDayClick(day.date)">
            <view class="calendar-day-weekday">
              {{ getWeekdayLabel(day.date) }}
            </view>
            <view class="calendar-day-date">{{ day.date.getDate() }}</view>
          </view>
        </view>
      </view>
    </scroll-view>
  </view>
</template>
<style lang="scss">
.calendar-container {
  width: 100%;
  overflow: hidden;
  background-color: #f0f0f0;
}

.calendar-scroll {
  white-space: nowrap; // 很重要 不然就不水平滚动了
  width: 100%;
  height: 127rpx;
  display: flex;
  .calendar-week {
    display: inline-block; // 很重要 不然就不水平滚动了
    width: 100%;
    height: 100%;
    .calendar-week-days {
      display: flex;
      height: 100%;
    }
  }
}
</style>

scroll-view的用法在这里,横向滚动的时候,

scroll-view所在的dom样式要设置white-space: nowrap; 里面第一级子元素要设置display: inline-block, 不要设置flex, 不然会影响滚动

组件Script部分

咱们需要先把日历渲染出来,再去处理里面的逻辑,那就要数据呀,先获取本周和下周的数据

获取本周和下周的日期

注释加的比较多,主要包括: 获取当前周的起始日期(周一), 怎么以起始日期为开始获取一周

// 定义日期对象类型,包含 Date 实例
interface Day {
  date: Date;
}

/**
 * 获取从周一开始的一周日期的辅助函数
 * @param {Date} startDate - 一周的起始日期(通常是某个周一)
 * @returns {Day[]} - 返回包含 7 天的数组,每个元素是 Day 类型对象,表示该周内每一天的日期
 */
const getWeekDates = (startDate: Date): Day[] => {
  const week: Day[] = [];
  const date = new Date(startDate);
  for (let i = 0; i < 7; i++) {
    week.push({ date: new Date(date) });
    date.setDate(date.getDate() + 1);
  }
  return week; // 返回一周内的所有日期
};

/**
 * 获取当前周的起始日期(周一)
 * 1. 获取当前日期对象(current)
 * 2. 通过 `current.getDay()` 获取当前日期是星期几(0 表示周日,1 表示周一,...,6 表示周六)
 * 3. 计算当前日期到周一的天数偏移量 `((current.getDay() + 6) % 7)`
 *    - `current.getDay() + 6` 是为了将周日(0)转为 6,将周一(1)转为 0...,将周六(6)转为 5
 *    - `(current.getDay() + 6) % 7` 将上述值控制在 [0, 6] 范围内,得到当前日期与周一的天数差
 * 4. 使用 `current.setDate(current.getDate() - ((current.getDay() + 6) % 7))` 计算出当前周一的日期
 */
const current = new Date();
const startOfWeek: Date = new Date(
  current.setDate(current.getDate() - ((current.getDay() + 6) % 7))
);

// 定义 `weeks` 状态变量,其中包含上周、本周和下周的日期列表
const weeks = ref<Day[][]>([
  // getWeekDates(new Date(startOfWeek.getTime() - 7 * 24 * 60 * 60 * 1000)), // 上一周的日期列表
  getWeekDates(startOfWeek), // 本周的日期列表,从当前周一开始
  getWeekDates(new Date(startOfWeek.getTime() + 7 * 24 * 60 * 60 * 1000)), // 下一周的日期列表,从下个周一开始
]);
显示周几和今天/明天/后天

显示周几

/**
 * 获取周几(周一至周日)
 * @param {Date} date - 目标日期
 * @returns {string} - 周几的字符串表示
 */
const getWeekday = (date: Date): string => {
  const days = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
  return days[(date.getDay() + 6) % 7]; // 将周一作为第一天
};

今天/明天/后天 思路就是把当前日期变为YYYY-MM-DD形式,然后getTime(), 减去今天的YYYY-MM-DD的getTime(), 然后除以1天多少秒;

/**
 * 获取用于显示的标签(今天、明天、后天或周几)
 * @param {Date} date - 目标日期
 * @returns {string} - 显示的标签内容
 */
const getWeekdayLabel = (date: Date): string => {
  const today = new Date();

  // 将日期的时分秒重置为 00:00:00,从而保证比较时只考虑日期而不是时间
  const clearTime = (d: Date) =>
    new Date(d.getFullYear(), d.getMonth(), d.getDate());

  const dateWithoutTime = clearTime(date);
  const todayWithoutTime = clearTime(today);

  // 获取天数差值,并根据差值判断显示的标签内容
  const diffInMs =
    (dateWithoutTime.getTime() - todayWithoutTime.getTime()) /
    (1000 * 60 * 60 * 24);

  if (diffInMs === 0) return "今天";
  if (diffInMs === 1) return "明天";
  if (diffInMs === 2) return "后天";

  return getWeekday(date); // 如果不是今天、明天或后天,则返回周几
};
默认选中高亮今天 & 点击日期选中高亮
  1. 设置一个变量selectedDay, 赋值为今天
  2. 判断selected: isSelected(day.date), isSelected(day.date)是不是等于selectedDay.value.toDateString() 点击日期选中高亮一样,点击时候把selectedDay赋值给点击事件的日期,再判断就行了
/**
 * 处理日期点击事件
 * @param {Date} date - 被点击的日期
 */
const handleDayClick = (date: Date) => {
  const weekday = getWeekday(date); // 获取点击的日期对应的周几
  selectedDay.value = date; // 更新选中的日期
  emits("dayClick", weekday); // 仅传递周几作为事件参数
};

·
/**
 * 判断是否为选中的日期
 * @param {Date} date - 目标日期
 * @returns {boolean} - 返回是否为选中的日期
 */
const isSelected = (date: Date): boolean => {
  return (
    selectedDay.value !== null &&
    date.toDateString() === selectedDay.value.toDateString()
  );
};

// 在组件挂载时,默认选中今天,并设置滚动目标为今天
onMounted(() => {
  selectedDay.value = new Date(); // 默认选中今天
});
默认滚动到今天

这个比较重要,咱们想要默认开始显示的是今天,在今天不是周一的情况下,需要向左滚动显示过去的日期,scroll-view有一个scroll-into-view的方法,

image.png

<template>
  <view class="calendar-container">
    <scroll-view
      :scroll-into-view="todayId"
      class="calendar-scroll"
      @scroll="onScroll">
      <view v-for="(week, index) in weeks" :key="index" class="calendar-week">
        <view class="calendar-week-days">
          <view
            v-for="(day, i) in week"
            :key="i"
            :id="isToday(day.date) ? 'today' : ''"
            @click="handleDayClick(day.date)">
            <view class="calendar-day-date">{{ day.date.getDate() }}</view>
          </view>
        </view>
      </view>
    </scroll-view>
  </view>
</template>
<script lang="ts">

/**
 * 判断是否为今天
 * @param {Date} date - 目标日期
 * @returns {boolean} - 返回是否为今天
 */
const isToday = (date: Date): boolean => {
  const today = new Date();
  return date.toDateString() === today.toDateString();
};

// 在组件挂载时,默认选中今天,并设置滚动目标为今天
onMounted(() => {
  todayId.value = "today"; // 设置滚动 ID 为今天
});
</script>
判断是不是以前的日期
/**
 * 判断日期是否在今天之前(过去的日期)
 * @param {Date} date - 目标日期
 * @returns {boolean} - 返回是否为过去的日期
 */
const isPast = (date: Date): boolean => {
  const today = new Date();
  return date < new Date(today.setHours(0, 0, 0, 0)); // 去除时间部分,仅比较日期
};
GIF展示完整效果

new写文章 - uniapp写一个预约上课程序的日历组件 - 掘金.gif 主要的逻辑完成了,附上完整代码

<template>
  <view class="calendar-container">
    <scroll-view
      scroll-x="true"
      show-scrollbar="false"
      :scroll-into-view="todayId"
      class="calendar-scroll"
      @scroll="onScroll">
      <view v-for="(week, index) in weeks" :key="index" class="calendar-week">
        <view class="calendar-week-days">
          <view
            v-for="(day, i) in week"
            :key="i"
            class="calendar-day"
            :class="{
              selected: isSelected(day.date),
              past: isPast(day.date),
            }"
            :id="isToday(day.date) ? 'today' : ''"
            @click="handleDayClick(day.date)">
            <view class="calendar-day-weekday">
              {{ getWeekdayLabel(day.date) }}
            </view>
            <view class="calendar-day-date">{{ day.date.getDate() }}</view>
          </view>
        </view>
      </view>
    </scroll-view>
  </view>
</template>

<script setup lang="ts">
import { ref, onMounted } from "vue";

const emits = defineEmits(["dayClick"]);

// 定义日期对象类型,包含 Date 实例
interface Day {
  date: Date;
}

// 定义滚动事件的处理函数类型
const onScroll = (event: Event): void => {
  console.log("Scrolling...", event);
};
/**
 * 获取从周一开始的一周日期的辅助函数
 * @param {Date} startDate - 一周的起始日期(通常是某个周一)
 * @returns {Day[]} - 返回包含 7 天的数组,每个元素是 Day 类型对象,表示该周内每一天的日期
 */
const getWeekDates = (startDate: Date): Day[] => {
  const week: Day[] = [];
  const date = new Date(startDate);
  for (let i = 0; i < 7; i++) {
    week.push({ date: new Date(date) });
    date.setDate(date.getDate() + 1);
  }
  return week; // 返回一周内的所有日期
};

/**
 * 获取当前周的起始日期(周一)
 * 1. 获取当前日期对象(current)
 * 2. 通过 `current.getDay()` 获取当前日期是星期几(0 表示周日,1 表示周一,...,6 表示周六)
 * 3. 计算当前日期到周一的天数偏移量 `((current.getDay() + 6) % 7)`
 *    - `current.getDay() + 6` 是为了将周日(0)转为 6,将周一(1)转为 0...,将周六(6)转为 5
 *    - `(current.getDay() + 6) % 7` 将上述值控制在 [0, 6] 范围内,得到当前日期与周一的天数差
 * 4. 使用 `current.setDate(current.getDate() - ((current.getDay() + 6) % 7))` 计算出当前周一的日期
 */
const current = new Date();
const startOfWeek: Date = new Date(
  current.setDate(current.getDate() - ((current.getDay() + 6) % 7))
);

// 定义 `weeks` 状态变量,其中包含上周、本周和下周的日期列表
const weeks = ref<Day[][]>([
  // getWeekDates(new Date(startOfWeek.getTime() - 7 * 24 * 60 * 60 * 1000)), // 上一周的日期列表
  getWeekDates(startOfWeek), // 本周的日期列表,从当前周一开始
  getWeekDates(new Date(startOfWeek.getTime() + 7 * 24 * 60 * 60 * 1000)), // 下一周的日期列表,从下个周一开始
]);

const selectedDay = ref<Date | null>(null); // 存储选中的日期
const todayId = ref(""); // 存储今天的 ID

/**
 * 获取用于显示的标签(今天、明天、后天或周几)
 * @param {Date} date - 目标日期
 * @returns {string} - 显示的标签内容
 */
const getWeekdayLabel = (date: Date): string => {
  const today = new Date();

  // 将日期的时分秒重置为 00:00:00,从而保证比较时只考虑日期而不是时间
  const clearTime = (d: Date) =>
    new Date(d.getFullYear(), d.getMonth(), d.getDate());

  const dateWithoutTime = clearTime(date);
  const todayWithoutTime = clearTime(today);

  // 获取天数差值,并根据差值判断显示的标签内容
  const diffInMs =
    (dateWithoutTime.getTime() - todayWithoutTime.getTime()) /
    (1000 * 60 * 60 * 24);

  if (diffInMs === 0) return "今天";
  if (diffInMs === 1) return "明天";
  if (diffInMs === 2) return "后天";

  return getWeekday(date); // 如果不是今天、明天或后天,则返回周几
};

/**
 * 获取周几(周一至周日)
 * @param {Date} date - 目标日期
 * @returns {string} - 周几的字符串表示
 */
const getWeekday = (date: Date): string => {
  const days = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
  return days[(date.getDay() + 6) % 7]; // 将周一作为第一天
};

/**
 * 处理日期点击事件
 * @param {Date} date - 被点击的日期
 */
const handleDayClick = (date: Date) => {
  const weekday = getWeekday(date); // 获取点击的日期对应的周几
  selectedDay.value = date; // 更新选中的日期
  emits("dayClick", weekday); // 仅传递周几作为事件参数
};

/**
 * 判断是否为今天
 * @param {Date} date - 目标日期
 * @returns {boolean} - 返回是否为今天
 */
const isToday = (date: Date): boolean => {
  const today = new Date();
  return date.toDateString() === today.toDateString();
};

/**
 * 判断日期是否在今天之前(过去的日期)
 * @param {Date} date - 目标日期
 * @returns {boolean} - 返回是否为过去的日期
 */
const isPast = (date: Date): boolean => {
  const today = new Date();
  return date < new Date(today.setHours(0, 0, 0, 0)); // 去除时间部分,仅比较日期
};

/**
 * 判断是否为选中的日期
 * @param {Date} date - 目标日期
 * @returns {boolean} - 返回是否为选中的日期
 */
const isSelected = (date: Date): boolean => {
  return (
    selectedDay.value !== null &&
    date.toDateString() === selectedDay.value.toDateString()
  );
};

// 在组件挂载时,默认选中今天,并设置滚动目标为今天
onMounted(() => {
  selectedDay.value = new Date(); // 默认选中今天
  todayId.value = "today"; // 设置滚动 ID 为今天
});
</script>

<style lang="scss">
.calendar-container {
  width: 100%;
  overflow: hidden;
  background-color: #f0f0f0;
}

.calendar-scroll {
  white-space: nowrap;
  width: 100%;
  height: 127rpx;
  display: flex;
  .calendar-week {
    display: inline-block;
    width: 100%; /* 确保一次只显示一周 */
    height: 100%;
    .calendar-week-days {
      display: flex;
      height: 100%;
    }
  }
}

.calendar-day {
  width: calc(100% / 7); /* 每天占宽度的 1/7 */
  text-align: center;
  position: relative;
  padding: 8px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  &.past .calendar-day-weekday,
  &.past .calendar-day-date {
    color: #ccc; /* 过去日期显示为灰色 */
  }
}

.calendar-day-weekday {
  font-size: 12px;
  color: #999;
}

.calendar-day-date {
  font-size: 20px;
  font-weight: bold;
}

.selected {
  border-radius: 5px;
  background-color: #ffe58f;
}
</style>