Game-Events-Calendar项目

链接

https://gameevent.sikara.asia/

主要目标

通过调用手游官网和 Wiki 的 API,实时获取各大手游活动信息并整理到日历中,助您轻松规划游戏时间。

功能

目前已接入游戏:PCR 原神 星穹铁道 FGO 明日方舟

一个信息整合网站,支持以上游戏里最近(大概一个月范围)的活动日程显示,且包含基础的日历功能显示

项目难点

如何将一串对象数组渲染成日历+活动日程的复合页面显示

对象数组的结构

export declare interface CalendarActivity {
  id: number;
  title: string;
  start_time: string;
  end_time: string;
  banner?: string;
  content?: string;
}

大致设计图

demo图

大致分成两部分
  1. 日历部分

    借助dayjs获取今天的日期,根据这个日期获取这个月的天数(一般是35或42天),然后将这些item做成一个二维数组,以周为单位,渲染出基础的日历布局

  2. 日程部分

    对于这种要跨多个日期的日程就需要使用position定位的方法进行控制日程的位置排布,接着就是日程的排布逻辑细节:

    • 日程越早的排的层级越高
    • 日程一旦确定层级就是连续的,直至结束还是确定好的层级位置
    • 为了保证空间充分利用,一个新日程的起始日期要加入,会看当前每个层级进行日程的结束时间,从高到低一旦有个层级结束日期等于或早过新日程的起始日期,新日程就是这个层级接着排布下去

    按照日程逻辑设计出层级分配算法

    然后需要知道每一周发生的日程,数组*数组,结果是一个二维数组

    而且需要按照各个日程的层级去计算每周发生的各个日程其在日历面板上的位置定位,日程的父元素是那一周7天构成的大div,相对其位置调整left,top,width属性,其中top属性与日程层级直接关联,其他属性控制日程在这一周的显示起始位置和结束位置,同时使用百分比,calc函数和相对单位计算日程的位置相对父元素的像素大小来准确渲染

完整算法

import dayjs, { Dayjs } from "dayjs";
import { CalendarActivity, CalendarWeekItem } from "./CalendarType";
// import classNames from "classnames";

export const rainbowColors = [
  "#fca5a5",
  "#fdba74",
  "#fde047",
  "#86efac",
  "#5eead4",
  "#93c5fd",
  "#d8b4fe",
];

export const generateCalendarGrid = (date: Dayjs | string) => {
  const startM = dayjs(date).startOf("M");
  const endM = dayjs(date).endOf("M");
  const start = startM.subtract(parseInt(startM.format("d")), "day");
  const end = endM.add(6 - parseInt(endM.format("d")), "day");
  const length = end.diff(start, "day") + 1;
  const res: string[][] = [];
  const temp: string[] = [];
  Array.from({ length }).forEach((_, i) => {
    temp.push(start.add(i, "day").format("YYYY-MM-DD"));
  });
  for (let i = 0; i < temp.length; i += 7) {
    res.push(temp.slice(i, i + 7));
  }
  return res;
};

// 层级分配算法
// 日程的排布逻辑细节:
// - 日程越早的排的层级越高
// - 日程一旦确定层级就是连续的,直至结束还是确定好的层级位置
// - 为了保证空间充分利用,一个新日程的起始日期要加入,会看当前每个层级进行日程的结束时间,
// 从高到低一旦有个层级结束日期等于或早过新日程的起始日期,新日程就是这个层级接着排布下去
export const levelAssignment = (events: CalendarActivity[]) => {
  // 记录每一层级的当前最新的结束日期
  // 用于判断新日程是直接接在某层级日程的后面还是为新日程添加新层级
  const check: Dayjs[] = [];
  // 按日程的起始日期进行排序
  const sortActivity = events.sort((a, b) => {
    return dayjs(a.start_time).isBefore(b.start_time) ? -1 : 1;
  });
  return sortActivity.map((item) => {
    const { start_time, end_time } = item;
    if (check.length === 0) {
      check.push(dayjs(end_time));
      return { ...item, level: 1 };
    }
    // 遍历层级,是否需要另起一行
    const isNeedNewLine = check.findIndex((endDate) => {
      return dayjs(start_time).isSameOrAfter(endDate);
    });
    if (isNeedNewLine === -1) {
      // 没有超过任一层级的最新的活动结束日期,就需要新建一行
      check.push(dayjs(end_time));
      return { ...item, level: check.length };
    } else {
      // 有 就在符合的层级进行添加,标记,并更新活动的结束日期
      check[isNeedNewLine] = dayjs(end_time);
      return { ...item, level: isNeedNewLine + 1 };
    }
  });
};

export const calculateEventPosition = (
  date: Dayjs | string,
  events: CalendarActivity[]
): CalendarWeekItem[][] => {
  const res = [];
  const temp = generateCalendarGrid(date);
  const eventsWithLevel = levelAssignment(events);
  // 一个活动对应一种颜色 尽量区分开
  const colorMap = new Map<number, string>(
    events.map((item, i) => [item.id, rainbowColors[i % rainbowColors.length]])
  );

  for (let i = 0; i < temp.length; i++) {
    const week = temp[i];
    const thisWeekEvents = [];
    for (let j = 0; j < eventsWithLevel.length; j++) {
      const { start_time, end_time } = eventsWithLevel[j];
      // 控制日程在这一周的显示起始位置 left 和结束位置 width
      let left = 0;
      let width = 0;
      week.forEach((day) => {
        // 判断这周的第 n 天是否在当前日程的起始和结束时间内
        if (dayjs(day).isBetween(start_time, end_time, "day", "[]")) {
          width++;
        }
        // 不在就向左偏移一格,空出这一天
        if (width === 0) {
          left++;
        }
      });
      width > 0 &&
        thisWeekEvents.push({
          ...eventsWithLevel[j],
          left,
          width,
          color: colorMap.get(eventsWithLevel[j].id) as string,
        });
    }
    res.push(thisWeekEvents);
  }
  return res;
};

项目亮点

通过使用Vite和Tailwind CSS,可以获得更好的性能和开发体验。Vite提供了快速的开发环境,而Tailwind CSS则提供了灵活的样式定制能力,能够更加高效地开发和设计项目。

将日历组件封装成单独一个组件和配置好相应props入参,方便组件复用

总结

tailwindcss的写法能摆脱css的文件困扰,应该是未来非常流行的写法

vite也是一个很有潜力的打包工具

后期可能将网站往桌面端,移动端发展,ui设计还需要构思一下