简单的日历组件

闲着没事,写着玩

需求分析:

  1. 头部

    用于切换月份使用(下面的没有实现自定义查找年月)

  2. 星期Title

    展示周一,二,三.....

  3. 实现日历table

    我自己是这么想的,借鉴网上的日历来说,大家都是使用的 x * y 的二维布局,那么我自己就以 6 * 7的布局来设置

(1)日历第一行一般会有上个月和这个月的日期,因此我需要根据当前月的1号是星期几,确定上个月在第一行展示的日期 - 例如这个月的1号是星期天,那么日历的第一行就是 上个月的末尾6天 + 这个月的第一天

(2)这个月肯定是28 29 30 或者31个展示日期

(3)下个月就是 6 * 7 - 前两个月展示的天数,就是下个月可以展示的天数

(4)然后我们将前面三部分组合成一个 队列(queue) ,这个就是日历上当前页所有展示内容

代码分解

类型声明

tsx 复制代码
// 用于切换上下月使用的枚举类型
enum ToggleMonth {
  Last = -1,
  Next = 1,
}

// 用于week-title的枚举类型(周一,二,。。。)
enum ZhCnNumberCharactor {
  一,
  二,
  三,
  四,
  五,
  六,
  日,
}

// 声明当前的年月日数据类型
type FilterMonthDate = {
  year: number;
  month: number;
  day?: number;
};

// 声明队列成员类型
type DateItem = {
  day: number;
  isLastMonth?: ToggleMonth;
  isNextMonth?: ToggleMonth;
  [keys: string]: any;
};

// 声明队列类型
type TableData = Array<DateItem>;

初始化状态

tsx 复制代码
// header
// 当前:年-月-日
const [filterMonthDate, setFilterMonthDate] = useState<FilterMonthDate>({
  year: new Date().getFullYear(),
  month: new Date().getMonth() + 1,
  day: new Date().getDay(),
});

// table
// 日历当前页数据
const [tableData, setTableData] = useState<TableData>([]);
// 选中日期
const [selectedDate, setSelectedDate] = useState(
  `${filterMonthDate.year}/${filterMonthDate.month}/${filterMonthDate.day}`
);
 

三个渲染函数(切换月份,title,日历table)

tsx 复制代码
// render-header-filter 切换月份
const renderHeaderFilter = () => {
  return (
    <div className={styles.headerFilter}>
      <div onClick={() => toggleMonth(ToggleMonth.Last)}>&lt;</div>
      <div>
        <span>{`${filterMonthDate.year}-${
          filterMonthDate.month < 10
            ? `0${filterMonthDate.month}`
            : filterMonthDate.month
        }`}</span>
      </div>
      <div onClick={() => toggleMonth(ToggleMonth.Next)}>&gt;</div>
    </div>
  );
};

// render-week-title  title
const renderWeekTitle = () => {
  return (
    <div className={styles.weekTitle}>
      {new Array(7).fill(1).map((_item: unknown, index: number) => (
        <span
          key={ZhCnNumberCharactor[index]}
        >{`周${ZhCnNumberCharactor[index]}`}</span>
      ))}
    </div>
  );
};

// render-calender-table  渲染日历表格,我自己通过flex-wrap的换行实现的,没有使用二维数组去渲染
const renderCalenderTable = () => {
  return (
    <div className={styles.tableContent}>
      {tableData.map((item: DateItem, index: number) => (
        <div
          key={index}
          className={classNames([
            styles.tableItem,
            {
              [styles.isOpacity]: item.isLastMonth || item.isNextMonth,
            },
          ])}
        >
          <span
            onClick={() => {
              // 这里选中的日期,可能是上个月,这个月,下个月的日期
              toggleMonth(item.isLastMonth || item.isNextMonth, item.day);
            }}
            className={classNames([
              styles.tableItemContent,
              {
                // 判断是否为周末
                [styles.isWeekend]: index % 7 === 5 || index % 7 === 6,
                // 如果选中的日期是当前年,月,日那么就展示选中
                [styles.selected]:
                  selectedDate ===
                    `${filterMonthDate.year}/${filterMonthDate.month}/${item.day}` &&
                  !item.isLastMonth &&
                  !item.isNextMonth,
              },
            ])}
          >
            {item.day}
          </span>
        </div>
      ))}
    </div>
  );
};

切换月份 或者 在当前页选中上一个或下一个月的日期

tsx 复制代码
// 点击前一个月或者后一个月,或者是点击了日期
const toggleMonth = (type?: ToggleMonth, day?: number) => {
 // type可选是因为可能点的是这个月的日期,在数据队列中没有给成员定义isNextMonth or isLastMonth
 // 下面newYear和newMonth在判断是否是从1月切换到上一年,或者是从12月切换到下一年,type! 表示不为空的type
 let newYear = type
   ? filterMonthDate.month === (type === ToggleMonth.Last ? 1 : 12)
     ? filterMonthDate.year + type!
     : filterMonthDate.year
   : filterMonthDate.year;
 let newMonth = type
   ? filterMonthDate.month === (type === ToggleMonth.Last ? 1 : 12)
     ? type === ToggleMonth.Last
       ? 12
       : 1
     : filterMonthDate.month + type!
   : filterMonthDate.month;

 setFilterMonthDate((prev) =>
   Object.assign({}, prev, {
     year: newYear,
     month: newMonth,
   })
 );

 if (day) {
   setSelectedDate(`${newYear}/${newMonth}/${day}`);
   console.log(`${newYear}/${newMonth}/${day}`);
 }
};

更新日历

和需求分析的第三步一样

tsx 复制代码
// 当年或月发生改变的时候
useEffect(() => {
  let allDaysOnThisMonth = new Date(
    filterMonthDate.year,
    filterMonthDate.month,
    0
  ).getDate(); // 获取当前年月中的天数

  // 获取当月的1号是星期几
  const currentDayOfFirstDate = new Date(
    `${filterMonthDate.year}/${filterMonthDate.month}/01`
  ).getDay();

  // 计算第一行需要补几天(补上一个月末尾的几天),currentDayOfFirstDate只会取到0-6
  let supplyDaysOfLastMonth = 0;
  if (currentDayOfFirstDate > 1 && currentDayOfFirstDate <= 6) {
    supplyDaysOfLastMonth = currentDayOfFirstDate - 1;
  } else {
    supplyDaysOfLastMonth = 6;
    默认补6天;
  }

  // 获取上个月的最后一天是几号(获取上个月一共多少天)
  let lastDayOflastMonth = new Date(
    filterMonthDate.year === 12
      ? filterMonthDate.year - 1
      : filterMonthDate.year,
    filterMonthDate.month - 1,
    0
  ).getDate();

  // 计算下一个月需要补充多少天(42 - (单月天数+上个月补充的天数))
  let supplyDaysOfNextMonth = 42 - (allDaysOnThisMonth + supplyDaysOfLastMonth);

  // 组装队列
  let queue = [];
  // 上一个月
  let lastMonthQueue = new Array(supplyDaysOfLastMonth).fill(undefined);
  while (supplyDaysOfLastMonth > 0) {
    --supplyDaysOfLastMonth;
    lastMonthQueue[supplyDaysOfLastMonth] = {
      day: lastDayOflastMonth,
      isLastMonth: ToggleMonth.Last,
    };
    lastDayOflastMonth--;
  }

  // 当前月
  let currentMonthQueue = new Array(allDaysOnThisMonth).fill(undefined);
  while (allDaysOnThisMonth > 0) {
    currentMonthQueue[allDaysOnThisMonth - 1] = {
      day: allDaysOnThisMonth,
    };
    allDaysOnThisMonth--;
  }

  // 下一个月
  let nextMonthQueue = new Array(supplyDaysOfNextMonth).fill(undefined);
  while (supplyDaysOfNextMonth > 0) {
    nextMonthQueue[supplyDaysOfNextMonth - 1] = {
      day: supplyDaysOfNextMonth,
      isNextMonth: ToggleMonth.Next,
    };
    supplyDaysOfNextMonth--;
  }

  queue = lastMonthQueue.concat(currentMonthQueue, nextMonthQueue);

  setTableData(queue);
}, [filterMonthDate.year, filterMonthDate.month]);

渲染组件

tsx 复制代码
<div className={styles.calender}>
  {/* render-header-filter */}
  {renderHeaderFilter()}

  {/* render-week-title */}
  {renderWeekTitle()}

  {/* render-calender-table */}
  {renderCalenderTable()}
</div>;

样式文件

scss 复制代码
.calender {
    width: 560px;
    height: auto
}

.headerFilter{
    display: flex;
    justify-content: space-between;
    padding: 16px 24px;
    padding-bottom: 0;
    
    > * {
        cursor: pointer;
    }
}

.weekTitle{
    display: flex;
    width: 100%;
    > span{
        display: inline-block;
        width: calc(100% / 7);
        text-align: center;
        padding: 12px 0;
    }
}

.tableContent{
    width: 100%;
    display: flex;
    flex-wrap: wrap;

    .tableItem{
        width: calc(100% / 7);
        text-align: center;
        margin-top: 10px;
        box-sizing: border-box;
        padding: 0 10px 0px 10px;
        cursor: pointer;
        &.isOpacity{
            opacity: 0.3;
        }
    }

    .tableItemContent{
        display: flex;
        justify-content: center;
        align-items: center;
        width: 60px;
        height: 54px;
        padding: 10px 12px;
        color: #333;
        font-size: 24px;
        border-radius: 10px;
        font-weight: bold;
        box-sizing: border-box;

        &:hover{
            background-color: rgba($color: #4e6ef2, $alpha: 0.2);
        }

        &.isWeekend{
            color: #eb3333;
        }

        &.selected {
            border: 2px solid #4e6ef2;
            background-color: rgba($color: #4e6ef2, $alpha: 0.2);
        }
    }
}

呈现效果

CodeSandBox

codesandbox.io/p/devbox/ba...

当然这也仅仅是一个基础版的日历,如果需要实现其他效果,还可以进一步封装

相关推荐
飞翔的渴望4 小时前
antd3升级antd5总结
前端·react.js·ant design
╰つ゛木槿7 小时前
深入了解 React:从入门到高级应用
前端·react.js·前端框架
用户305875848912511 小时前
Connected-react-router核心思路实现
react.js
哑巴语天雨1 天前
React+Vite项目框架
前端·react.js·前端框架
初遇你时动了情1 天前
react 项目打包二级目 使用BrowserRouter 解决页面刷新404 找不到路由
前端·javascript·react.js
码农老起1 天前
掌握 React:组件化开发与性能优化的实战指南
react.js·前端框架
前端没钱1 天前
从 Vue 迈向 React:平滑过渡与关键注意点全解析
前端·vue.js·react.js
高山我梦口香糖1 天前
[react] <NavLink>自带激活属性
前端·javascript·react.js
撸码到无法自拔1 天前
React:组件、状态与事件处理的完整指南
前端·javascript·react.js·前端框架·ecmascript
高山我梦口香糖1 天前
[react]不能将类型“string | undefined”分配给类型“To”。 不能将类型“undefined”分配给类型“To”
前端·javascript·react.js