默认显示当前这周的日历

点击左上角日期后,进行周-月视图的切换

当有日期需要标记时,右上角显示,选择的日期显示蓝色,默认当日日期显示绿色

组件结构目录:

// index.jsx

import {Picker, Swiper, SwiperItem, View} from "@tarojs/components";
import {Component} from "react";
import {formatDate, getWeekDayList} from "./utils";
import Days from "./days/index";

import "./index.scss";

class Calendar extends Component {
  state = {
    current: formatDate(new Date(this.props.currentView)),
    selectedDate: this.props.selectedDate,
    currentCarouselIndex: 1,
    selectedRange: { start: "", end: "" },
  };
  componentWillMount() {
    if (this.props.bindRef) {
      this.props.bindRef(this);
    }
  }
  componentWillReceiveProps(nextProps) {
    if (
      nextProps.selectedDate &&
      nextProps.selectedDate !== this.props.selectedDate
    ) {
      this.setState({
        selectedDate: nextProps.selectedDate,
        current: nextProps.selectedDate,
      });
    }
    if (
      nextProps.currentView &&
      nextProps.currentView !== this.props.currentView
    ) {
      this.setState({ current: nextProps.currentView });
    }
  }

  getPickerText = () => {
    const { current } = this.state;
    const currentDateObj = new Date(current);
    return formatDate(currentDateObj, "month")
  };
  
  onClickDate = (value) => {
    console.log(value)
    const { onDayClick, onSelectDate } = this.props;
    let { current, currentCarouselIndex, selectedRange } = this.state;
    if (!selectedRange.start || selectedRange.end) {
      selectedRange = { start: value.fullDateStr, end: "" };
    } else {
      if (new Date(selectedRange.start) > new Date(value.fullDateStr)) {
        selectedRange = {
          start: value.fullDateStr,
          end: selectedRange.start,
        };
      } else {
        selectedRange.end = value.fullDateStr;
      }
    }
    
    this.setState({
      selectedDate: value.fullDateStr,
      selectedRange,
      currentCarouselIndex,
      current,
    });
    if (onDayClick) {
      onDayClick({ value: value.fullDateStr });
    }
    if (onSelectDate) {
      onSelectDate(selectedRange);
    }
  };

  goNext = () => {
    console.log("下个月");
    const { view } = this.props;
    const { currentCarouselIndex } = this.state;
    let dateObj = new Date(this.state.current);
    const { onClickNext, onCurrentViewChange } = this.props;
    let current = "";
    if (view === "month") {
      dateObj.setMonth(dateObj.getMonth() + 1);
      const nextMonth = formatDate(dateObj);
      current = nextMonth;
    }
    if (view === "week") {
      dateObj.setDate(dateObj.getDate() + 7);
      const nextWeek = formatDate(dateObj, "day");
      current = nextWeek;
    }
    if (onClickNext) onClickNext();
    if (onCurrentViewChange) onCurrentViewChange(current);
    this.setState({
      currentCarouselIndex: (currentCarouselIndex + 1) % 3,
      current,
    });
  };

  goPre = () => {
    console.log("上个月");
    const { view } = this.props;
    const { currentCarouselIndex } = this.state;
    let dateObj = new Date(this.state.current);
    let current = "";
    if (view === "month") {
      dateObj.setMonth(dateObj.getMonth() - 1);
      const preMonth = formatDate(dateObj);
      current = preMonth;
    }
    if (view === "week") {
      dateObj.setDate(dateObj.getDate() - 7);
      const preWeek = formatDate(dateObj, "day");
      current = preWeek;
    }
    const { onClickPre, onCurrentViewChange } = this.props;
    if (onClickPre) onClickPre();
    if (onCurrentViewChange) onCurrentViewChange(current);
    this.setState({
      currentCarouselIndex: (currentCarouselIndex + 2) % 3,
      current,
    });
  };

  render() {
    const {
      current,
      selectedDate,
      currentCarouselIndex,
      selectedRange,
    } = this.state;
    const {
      marks,
      isVertical,
      selectedDateColor,
      isSwiper,
      minDate,
      maxDate,
      onDayLongPress,
      showDivider,
      isMultiSelect,
      customStyleGenerator,
      headStyle,
      headCellStyle,
      bodyStyle,
      view,
      startDay,
      extraInfo,
    } = this.props;
    // 配合Swiper组件实现无限滚动
    // 原理:永远保持当前屏幕显示月份的左边是前一个月,右边是后一个月
    // current即当前月份,currentCarouselIndex即当前显示页面的index。一共3个页面,index分别为0 1 2 。
    // Swiper的无限循环就是类似0 1 2 0 1 2 这样。如果currentCarouselIndex是2 那么我只要保证 1显示的是前面一个月,0显示的是后面一个月 就完成了循环。
    let currentDate = new Date(current);

    let preDate = new Date(current);
    let nextDate = new Date(current);

    if (view === "month") {
      preDate.setMonth(currentDate.getMonth() - 1);
      nextDate.setMonth(currentDate.getMonth() + 1);
    }
    if (view === "week") {
      preDate.setDate(currentDate.getDate() - 7);
      nextDate.setDate(currentDate.getDate() + 7);
    }
    const preIndex = (currentCarouselIndex + 2) % 3;
    const nextIndex = (currentCarouselIndex + 1) % 3;
    let monthObj = [];
    monthObj[currentCarouselIndex] = currentDate;
    monthObj[preIndex] = preDate;
    monthObj[nextIndex] = nextDate;
    // 所有Days组件的公共Props
    const publicDaysProp = {
      marks: marks || [],
      onClick: this.onClickDate,
      selectedDate,
      minDate: minDate,
      maxDate,
      selectedDateColor,
      onDayLongPress,
      showDivider: showDivider,
      isMultiSelect: isMultiSelect,
      selectedRange: selectedRange,
      customStyleGenerator,
      view: view,
      startDay: startDay,
      extraInfo: extraInfo || [],
    };
    console.log(this.getPickerText())

    return (
      <View className='calendar'>
        <View className='calendar-head' style={headStyle}>
          {getWeekDayList(startDay).map((value) => (
            <View style={headCellStyle} key={value}>
              {value}
            </View>
          ))}
        </View>
        {isSwiper ? (
          <Swiper
            style={{
              height: view === "month" ? "14rem" : "3rem",
              ...bodyStyle,
            }}
            vertical={isVertical}
            circular
            current={currentCarouselIndex}
            onChange={(e) => {
              if (e.detail.source === "touch") {
                const currentIndex = e.detail.current;
                if ((currentCarouselIndex + 1) % 3 === currentIndex) {
                  // 当前月份+1
                  this.goNext();
                } else {
                  // 当前月份-1
                  this.goPre();
                }
              }
            }}
            className='calendar-swiper'
          >
            <SwiperItem style='position: absolute; width: 100%; height: 100%;'>
              <Days date={monthObj[0]} {...publicDaysProp} />
            </SwiperItem>
            <SwiperItem style='position: absolute; width: 100%; height: 100%;'>
              <Days date={monthObj[1]} {...publicDaysProp} />
            </SwiperItem>
            <SwiperItem style='position: absolute; width: 100%; height: 100%;'>
              <Days date={monthObj[2]} {...publicDaysProp} />
            </SwiperItem>
          </Swiper>
        ) : (
          <Days bodyStyle={bodyStyle} date={currentDate} {...publicDaysProp} />
        )}
      </View>
    );
  }
}

Calendar.defaultProps = {
  isVertical: false,
  marks: [],
  selectedDate: formatDate(new Date(), "day"),
  selectedDateColor: "#90b1ef",
  hideArrow: false,
  isSwiper: true,
  minDate: "1970-01-01",
  maxDate: "2100-12-31",
  showDivider: false,
  isMultiSelect: false,
  view: "month",
  currentView: formatDate(new Date()),
  startDay: 0,
  extraInfo: [],
};
export default Calendar;
// index.scss

.calendar {
  width:100%;
  .calendar-head > view {
    height: 2rem;
    display: inline-block;
    width: 14.28%;
    text-align: center;
    line-height: 2rem;
    color: #bfbfd1;
  }

  .calendar-head {
    font-size: 1rem;
  }

  .calendar-picker {
    position: relative;
    left: 50%;
    transform: translateX(-50%);
    width: fit-content;
  }

  .calendar-arrow-right,
  .calendar-arrow-left {
    height: 1rem;
    width: 1rem;
    display: inline-block;
    background-repeat: no-repeat;
    background-size: 100% 100%;
    position: relative;
    top: 0.2rem;
    opacity: 0.3;
  }

  .calendar-arrow-left {
    right: 0.5rem;
  }

  .calendar-arrow-right {
    left: 0.5rem;
  }

  .calendar-disabled-arrow {
    display: none;
  }

}
// utils.js

/** 填充0 */
export const fillWithZero = (target, length) => {
  return (Array(length).join('0') + target).slice(-length);
};

/**
 * 默认将日期格式化为 YYYY-MM-DD
 * @param date Date类型的时间
 * @param field 时间显示粒度
 */
export const formatDate = (date, field = 'day') => {
  const yearStr = date.getFullYear();
  const month = date.getMonth() + 1;
  const monthStr = fillWithZero(month, 2);
  const day = date.getDate();
  const dayStr = fillWithZero(day, 2);
  switch (field) {
    case 'year':
      return `${yearStr}`;
    case 'month':
      return `${yearStr}/${monthStr}`;
    case 'day':
      return `${yearStr}-${monthStr}-${dayStr}`;
  }
};

/**
 * 计算current增加add天后是周几
 * @param current 当前是第几天
 * @param add 要加多少天
 */
export const calcWeekDay = (current, add) => {
  return (current + add) % 7;
};

/**
 * 获取当月的date列表
 * @param date 属于目标月份的Date对象
 * @param startDay 一行的起点  比如以周一为起点 此时startDay = 1,以周日为起点,此时startDay = 0
 */
export const getDateListByMonth = (date, startDay) => {
  const month = date.getMonth();
  const year = date.getFullYear();
  /** 一周的最后一天 */
  const weekEndDay = calcWeekDay(startDay, 6);
  let result = [];
  /** 先获取该月份的起点 */
  date.setDate(1);

  let dateObj = new Date(date);
  dateObj.setDate(1);
  /** 前面一部分非当前月的日期 */
  for (let day = startDay; day != date.getDay(); day = calcWeekDay(day, 1)) {
    dateObj.setFullYear(year);
    dateObj.setMonth(month);
    dateObj.setDate(date.getDate() - (date.getDay() - day));
    const preDate={
      date: dateObj.getDate(),
      currentMonth: false,
      fullDateStr: formatDate(dateObj, 'day'),
    }
    result.push(preDate);
  }

  /** 当前月的日期 */
  console.log(date);
  while (date.getMonth() === month) {
    result.push({
      date: date.getDate(),
      currentMonth: true,
      fullDateStr: formatDate(date, 'day'),
    });
    date.setDate(date.getDate() + 1);
  }

  /** 后面一部分非当前月的日期 */
  for (let day = date.getDay(); day != weekEndDay; day = calcWeekDay(day, 1)) {
    result.push({
      date: date.getDate(),
      currentMonth: false,
      fullDateStr: formatDate(date, 'day'),
    });
    date.setDate(date.getDate() + 1);
  }

  result.push({
    date: date.getDate(),
    currentMonth: false,
    fullDateStr: formatDate(date, 'day'),
  });
  // 保证每个月的数据都是 42 个
  if (result.length === 35) {
    date.setDate(date.getDate() + 1);
    for (
      let day = date.getDay();
      day != weekEndDay;
      day = calcWeekDay(day, 1)
    ) {
      result.push({
        date: date.getDate(),
        currentMonth: false,
        fullDateStr: formatDate(date, 'day'),
      });
      date.setDate(date.getDate() + 1);
    }
    result.push({
      date: date.getDate(),
      currentMonth: false,
      fullDateStr: formatDate(date, 'day'),
    });
  }
  return result;
};

/** 获取指定日期所在周的所有天
 * @param date 属于目标星期的Date对象
 * @param startDay 一行的起点  比如以周一为起点 此时startDay = 1,以周日为起点,此时startDay = 0
 */
export const getDateListByWeek = (date, startDay) => {
  date.setDate(date.getDate() - ((date.getDay() - startDay + 7) % 7));
  /** 一周的最后一天 */
  const weekEndDay = calcWeekDay(startDay, 6);
  let result = [];
  while (date.getDay() !== weekEndDay) {
    result.push({
      date: date.getDate(),
      currentMonth: true,
      fullDateStr: formatDate(date, 'day'),
    });
    date.setDate(date.getDate() + 1);
  }
  result.push({
    date: date.getDate(),
    currentMonth: true,
    fullDateStr: formatDate(date, 'day'),
  });
  return result;
};

export const getWeekDayList = (startDay) => {
  const weekDays = ['日', '一', '二', '三', '四', '五', '六'];
  let result = [];
  for (let i = startDay; i < 7; i++) {
    result.push(weekDays[i]);
  }
  for (let i = 0; i < startDay; i++) {
    result.push(weekDays[i]);
  }
  return result;
};
// day/index.jsx

import React, { useEffect, useState } from 'react';
import { View } from '@tarojs/components';
import { formatDate } from '../utils';

const Day = (props) => {
  const {
    selected,
    onDayLongPress,
    onClick,
    value,
    markIndex,
    extraInfoIndex,
    customStyleGenerator,
    disable,
    isInRange,
    rangeStart,
    rangeEnd,
    isMultiSelectAndFinish,
    selectedDateColor,
    markColor,
    markSize,
    extraInfoColor,
    extraInfoSize,
    extraInfoText,
    showDivider,
  } = props;
  const [className, setClassName] = useState(['calendar-day']);
  const [customStyles, setCustomStyles] = useState({});

  useEffect(() => {
    let set = ['calendar-day'];
    const today = formatDate(new Date(), 'day');

    if (!value.currentMonth || disable) {
      // 非本月
      set.push('not-this-month');
    }
    if (selected && !isMultiSelectAndFinish) {
      // 选中
      // 范围选择模式显示已选范围时,不显示selected
      set.push('calendar-selected');
    }
    if (markIndex !== -1) {
      // 标记
      set.push('calendar-marked');
    }
    if (extraInfoIndex !== -1) {
      // 额外信息
      set.push('calendar-extra-info');
    }
    if (value.fullDateStr === today) {
      // 当天
      set.push('calendar-today');
    }
    if (showDivider) {
      // 分割线
      set.push('calendar-line-divider');
    }

    if (isInRange) {
      set.push('calendar-range');
    }

    if (rangeStart) {
      set.push('calendar-range-start');
    }

    if (rangeEnd) {
      set.push('calendar-range-end');
    }

    setClassName(set);
  }, [
    disable,
    extraInfoIndex,
    isMultiSelectAndFinish,
    markIndex,
    selected,
    showDivider,
    value.currentMonth,
    value.fullDateStr,
    isInRange,
    rangeStart,
    rangeEnd,
  ]);

  useEffect(() => {
    if (customStyleGenerator) {
      // 用户定制样式
      const generatorParams = {
        ...value,
        selected: selected,
        multiSelect: {
          multiSelected: isInRange,
          multiSelectedStar: rangeStart,
          multiSelectedEnd: rangeEnd,
        },
        marked: markIndex !== -1,
        hasExtraInfo: extraInfoIndex !== -1,
      };
      setCustomStyles(customStyleGenerator(generatorParams));
    }
  }, [
    selected,
    value,
    markIndex,
    extraInfoIndex,
    customStyleGenerator,
    isInRange,
    rangeStart,
    rangeEnd
  ]);

  return (
    <View
      onLongPress={
        onDayLongPress
          ? () => onDayLongPress({ value: value.fullDateStr })
          : undefined
      }
      className={className.join(' ')}
      onClick={() => {
        if (!disable) {
          onClick(value);
        }
      }}
      style={customStyles.containerStyle}
    >
      <View
        className='calendar-date'
        style={
          customStyles.dateStyle || customStyles.dateStyle === {}
            ? customStyles.dateStyle
            : {
              backgroundColor: selected || isInRange ? selectedDateColor : '',
            }
        }
      >
        {/* 日期 */}
        {value.date}
      </View>

      {/* 标记 */}
      <View
        className='calendar-mark'
        style={{
          backgroundColor: markIndex === -1 ? '' : markColor,
          height: markIndex === -1 ? '' : markSize,
          width: markIndex === -1 ? '' : markSize,
          ...customStyles.markStyle,
        }}
      />
      {extraInfoIndex !== -1 && (
        <View
          className='calendar-extra-info'
          style={{
            color: extraInfoIndex === -1 ? '' : extraInfoColor,
            fontSize: extraInfoIndex === -1 ? '' : extraInfoSize,
            ...customStyles.extraInfoStyle,
          }}
        >
          {/* 额外信息 */}
          {extraInfoText}
        </View>
      )}
    </View>
  );
};

export default React.memo(Day);
// days/index.jsx

import React, { useCallback, useEffect, useRef, useState } from 'react';
import { View } from '@tarojs/components';
import {
  formatDate,
  getDateListByMonth,
  getDateListByWeek,
} from '../utils';
import Day from '../day';
import './index.scss';

const Days = ({
                date,
                onClick,
                marks,
                selectedDate,
                selectedDateColor,
                minDate,
                maxDate,
                onDayLongPress,
                showDivider,
                isMultiSelect,
                selectedRange,
                customStyleGenerator,
                bodyStyle,
                view,
                startDay,
                extraInfo,
              }) => {
  const [days, setDays] = useState([]);
  const prevDateRef = useRef(null);
  const prevViewRef = useRef(null);
  const _onDayClick = useCallback(
    (value) => {
      console.log(value);
      onClick && onClick(value);
    },
    [onClick]
  );

  const _onDayLongPress = useCallback(
    (args) => {
      onDayLongPress && onDayLongPress(args);
    },
    [onDayLongPress]
  );

  useEffect(() => {
    //view和startDay基本不会变,就date会经常变化
    //由于传递的是date对象,需要判断date对象的值是否变化,防止因为days变化导致的重复刷新
    if (
      !prevDateRef.current ||
      formatDate(prevDateRef.current) !== formatDate(date) ||
      prevViewRef.current !== view
    ) {
      const dateObj = date ? new Date(date) : new Date();
      let tempDays = [];
      if (view === 'month') {
        tempDays = getDateListByMonth(dateObj, startDay);
      }
      if (view === 'week') {
        tempDays = getDateListByWeek(dateObj, startDay);
      }
      console.log('更新 days');
      setDays(tempDays);
    }

    prevDateRef.current = date;

    prevViewRef.current = view;
    // console.log(formatDate(prevDateRef.current));
  }, [view, date, startDay]);

  const maxDateObj = new Date(maxDate);
  const markDateList = marks.map((value) => value.value);
  const extraInfoDateList = extraInfo.map((value) => value.value);
  // console.log(extraInfo);
  let endDateStr = selectedRange ? selectedRange.end : '';
  const startDateObj = new Date(selectedRange ? selectedRange.start : '');
  const endDateObj = new Date(endDateStr);
  const minDateObj = new Date(minDate);
  // console.log(days);
  return (
    <View className='calendar-body' style={bodyStyle}>
      {days.map((value) => {
        const markIndex = markDateList.indexOf(value.fullDateStr);
        const extraInfoIndex = extraInfoDateList.indexOf(value.fullDateStr);
        if (extraInfoIndex !== -1) {
          console.log(extraInfoIndex);
        }
        let isInRange = false;
        let rangeStart = false;
        let rangeEnd = false;
        if (isMultiSelect && endDateStr) {
          // 范围选择模式
          const valueDateTimestamp = new Date(value.fullDateStr).getTime();
          if (
            valueDateTimestamp >= startDateObj.getTime() &&
            valueDateTimestamp <= endDateObj.getTime()
          ) {
            // 被选择(范围选择)
            isInRange = true;
            if (valueDateTimestamp === startDateObj.getTime()) {
              // 范围起点
              rangeStart = true;
            }
            if (valueDateTimestamp === endDateObj.getTime()) {
              // 范围终点
              rangeEnd = true;
            }
          }
        }
        let disable =
          new Date(value.fullDateStr).getTime() < minDateObj.getTime() ||
          (maxDate &&
            new Date(value.fullDateStr).getTime() > maxDateObj.getTime()) ||
          false;
        return (
          <Day
            key={value.fullDateStr}
            onDayLongPress={_onDayLongPress}
            selected={selectedDate === value.fullDateStr}
            isMultiSelectAndFinish={
              isMultiSelect && (selectedRange.end || '') != ''
            }
            markIndex={markIndex}
            extraInfoIndex={extraInfoIndex}
            showDivider={showDivider}
            minDate={minDate}
            value={value}
            onClick={_onDayClick}
            selectedDateColor={selectedDateColor}
            markColor={markIndex === -1 ? '' : marks[markIndex].color}
            markSize={markIndex === -1 ? '' : marks[markIndex].markSize}
            extraInfoColor={
              extraInfoIndex === -1 ? '' : extraInfo[extraInfoIndex].color
            }
            extraInfoSize={
              extraInfoIndex === -1 ? '' : extraInfo[extraInfoIndex].fontSize
            }
            extraInfoText={
              extraInfoIndex === -1 ? '' : extraInfo[extraInfoIndex].text
            }
            customStyleGenerator={customStyleGenerator}
            isInRange={isInRange}
            rangeStart={rangeStart}
            rangeEnd={rangeEnd}
            disable={disable}
          />
        );
      })}
    </View>
  );
};

export default React.memo(Days);
// days/index.scss

.calendar-body {
  display: flex;
  flex-wrap: wrap;
  padding-bottom: 1rem;
  // 分割线
  .calendar-line-divider {
    border-top: 1px dashed rgba(0, 0, 0, 0.1);
  }
  // 基础样式
  .calendar-date {
    width: 2rem;
    line-height: 2rem;
    margin-left: 0.5rem;
    border-radius: 50%;
  }
  // 选中样式
  .calendar-selected > .calendar-date {
    background-color: #90b1ef;
    color: white;
  }

  // 范围选择
  .calendar-range > .calendar-date {
    background-color: #90b1ef;
    color: white;
  }

  // 范围选择起点
  .calendar-range-end > .calendar-date {
    border-top-right-radius: 1rem;
    border-bottom-right-radius: 1rem;
  }

  // 范围选择终点
  .calendar-range-start > .calendar-date {
    border-top-left-radius: 1rem;
    border-bottom-left-radius: 1rem;
  }

  // 当天
  .calendar-today>view:first-child {
    border-radius: 50%;
    background-color: #57C27E;
    color: white;
  }

  // 非本月
  .not-this-month {
    color: #e2e5e8 !important;
  }

  // 农历
  .lunar-day {
    display: inline-block;
    font-size: 2.5vw;
    position: absolute;
    top: 1.2rem;
    left: 50%;
    transform: translateX(-50%);
    opacity: 0.7;
    width: 2rem;
  }

  // 农历初一
  .lunar-month {
    text-decoration-color: red;
    text-decoration-line: underline;
  }
  &>.calendar-day {
    position: relative;
    height: 3rem;
    width: 14.28%;
    text-align: center;
    color: #828ba6;
    // 标记
    .calendar-mark {
      position: absolute;
      top: 0;
      right: 0;
      height: 0.5rem;
      width: 0.5rem;
      border-radius: 50%;
    }

    // 额外信息
    .calendar-extra-info {
      margin-top: -0.5rem;
      color: #ff9800;
    }
  }

}

组件调用:

<Calendar
  marks={marks}
  extraInfo={[]}
  view={view}
  selectedDateColor="#346fc2"
  onDayClick={(item) => this.onDayClick(item)}
/>

 当点击日期后,以下内容中的value就是所点击的日期,格式为'2024-05-13'。

 onDayClick =(item)=>{
    const {value} =item
    console.log(value)
}

参数说明:

参数

说明

类型

默认值

view

视图模式

"week"|"month"

"month"

marks

需要标记的时间

Array<{value:string,color:string,markSize:string}>

[]

extraInfo

额外信息

Array<{value:string,text:string,color:string,fontSize:string}>

[]

onDayClick

点击日期时候触发

(item:{value:string}) => any

selectedDateColor

选中日期的颜色

string

参考文档:

可自定义部分样式的taro日历组件

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐