React でカレンダー コンポーネントを構築するためのステップ バイ ステップ ガイド

React でカレンダー コンポーネントを構築するためのステップ バイ ステップ ガイド

事業背景

まず、ビジネスシナリオを簡単に説明します。WeChat for EnterpriseやDingTalkなどのオフィスソフトウェアでユーザーのスケジュール情報を呼び出し、スケジュールをWeb上に表示します。スケジュールが競合する場合は、競合する日のスケジュールが表示されるため、図に示すように、スケジューラーは競合を回避するために合理的にスケジュールを調整できます。

テクノロジーの活用

  1. UIフレームワーク: React (Hook);
  2. プラグイン: moment (18 位の怠け者のプログラマーには必須のプラグイン。そうでなければ自分で回すのは面倒すぎるでしょう);

技術的な問題

  1. API 設計;
  2. コンポーネントの分割。
  3. UI とビジネスの分離。
  4. 箱から出してすぐに使えます。

デザインのアイデア

😱 困惑と苦痛に満ちた顔

プロジェクトを開発する際に、antd と同じコンポーネント ライブラリを使用しました。それを確認した後、無意識のうちに antd にアクセスして、すぐに使用できるコンポーネントがあるかどうかを確認しました。

残念です!!! そのような週ごとまたは日ごとのフィルタリング コンポーネントはありません。とてもイライラします。Alibaba は多くのコンポーネントを作成したのに、なぜこのコンポーネントを見逃したのでしょうか?

そこで、全能の Baidu に頼って、関連するコンポーネントがあるかどうかを確認しました。その後、fullcalendar コンポーネントを見つけましたが、そのドキュメントやデモを読んでいませんでした。私は決心して、自分で作成することにしました。

要約すると、いくつかの理由があります。

  1. コンポーネントは適切に記述されていますが、多くのビジネスは常に変化しており、すべてのビジネス ニーズを満たせない可能性があります。
  2. 第二に、新しい開発者がこのコンポーネントに精通していない場合、ドキュメントを読む必要があり、メンテナンスコストが増加します。
  3. 3つ目のポイントは、限られた時間の中で自分自身に挑戦することです。

🙄考え始める

実は、構想を始めた頃は、優れたコンポーネントの API 設計を参考にしたいと思っていました。しかし、一部のコンポーネントは本当に使いにくく、なぜこのように書かれているのか理解できませんでした。そこで、ユーザーの観点から考えて、18 流、低レベル、最も怠惰なプログラマーとしての自分の呼び出し方法に従って設計するべきだと今でも思っています。つまり、すぐに使えるようにするのです。

もう 1 つの重要な点は、他のプロジェクトが直接使用できるように、ビジネスから切り離すことです。なぜそうしないのでしょうか?

そこで私は午前中ずっと、自分のアイデアに基づいたデザインを描きました。

ProcessOnを使って描きました。あまり使ってないので絵が下手ですがご容赦ください!

🌲ディレクトリ構造

└─カレンダー
│ data.d.ts 型定義ファイル
│ index.tsx エントリファイル

├─コンポーネント
│ ├─CalendatrHeader ヘッダーコンテナコンポーネント
│ │ │ インデックス.less
│ │ │ インデックス.tsx
│ │ │
│ │ └─コンポーネント
│ │ ├─DailyOptions トップスイッチ日付とスイッチモードステータスコンポーネント
│ │ │ インデックス.less
│ │ │ インデックス.tsx
│ │ │
│ │ └─WeeklyOptions 週次モードの日付と曜日コンポーネント
│ │ インデックスなし
│ │ インデックス.tsx
│ │
│ ├─コンテナ コンテナコンポーネント
│ │ コンテナ.tsx
│ │ インデックスなし
│ │
│ ├─ScheduleCantainer 下部スケジュールコンテナ
│ │ インデックスなし
│ │ インデックス.tsx
│ │
│ └─ScheduleItem 各スケジュールコンポーネントの灰色部分
│ インデックス
│ インデックス.tsx

└─ユーティリティ
index.ts ツールファイル

🛠 コンポーネントを分割する

図をよく見ると、コンポーネントを 3 つの部分に分割していることが簡単にわかります。
コンテナー: このコンポーネントはコンポーネント全体のコンテナーであり、UI コア状態データを担当し、次の 2 つの状態を維持します。

  1. targetDay: 現在選択されている日付のタイムスタンプ (タイムスタンプが使用される理由については後で説明します);
  2. switchWeekAndDay: 日と週の状態を保存します。

CalendatrHeader ヘッダー コンテナー コンポーネント: Container コンテナーのサブコンポーネントであるこのコンポーネントは、日付の切り替えと、コンポーネントの週と日の状態の変更を担当します。このコンポーネントには、カレンダー コンポーネント、週コンポーネント、日付フィルター コンポーネント、日と週の切り替えコンポーネント、今日ボタン コンポーネント、そして最後にビジネス コンポーネント コンテナー (businessRender) が含まれます。

ScheduleCantainer スケジュール コンテナ コンポーネント: このコンポーネントは 25 個の scheduleRender コンポーネントによってサポートされており (今日の 0:00 から翌朝 0:00 までであるため)、そのサブコンポーネントには時間スケール コンポーネントも含まれます。

scheduleRender: このコンポーネントは、JSX を返すコールバックを受け入れます。この JSX は、呼び出し元によって渡されるカスタム スタイルのスケジュール コンポーネントです (詳細については後で説明します)。

コンポーネントの大まかな内訳です​​。テキストだけでは不十分ですが、画像と組み合わせることができます。

それでは始めましょう!!!

コードの実装

まず、受け入れられるパラメータ タイプの定義を見てみましょう。

タイプデータ型 = {
  startTime: DOMTimeStamp; // 開始タイムスタンプ endTime: DOMTimeStamp; // 終了タイムスタンプ [propsName: string]: any; // ビジネス データ };

タイプ ContainerType = {
  data: dataType[]; // ビジネスデータ initDay?: DOMTimeStamp; // 初期化タイムスタンプ onChange?: (params: DOMTimeStamp) => void; // 日付変更時のonChangeメソッド height?: number; // ScheduleCantainerコンテナの高さ scheduleRender?: ({
    データ: データ型、
    タイムスタンプ範囲: [DOMTimeStamp, DOMTimeStamp],
  }) => JSX.Element; // 渡されたコールバックは、現在のデータのビジネス データと現在のビジネス データのタイムスタンプ範囲を受け取ります。
  businessRender?: ({ timestamp: DOMTimeStamp }) => React.ReactNode; // ビジネスコンポーネントが渡され、フロントエンドの Cai Xukun にクエリが送信されます。画像を見て、覚えていますか?
  mode?: 'day' | 'week'; // 日と週の表示モードを初期化します};

容器

コード:

constコンテナ: React.FC<ContainerType> = ({
  初期化日、
  変更時、
  スケジュールレンダリング、
  ビジネスレンダリング、
  データ、
  高さ = 560、
  モード = '日'、
}) => {
  //現在選択されている日付タイムスタンプ const [targetDay, setTargetDay] = useState<DOMTimeStamp>(initDay);
  // 曜日と週を切り替える const [switchWeekandDay, setSwitchWeekandDay] = useState<'day' | 'week'>(mode);

  戻る (
    <div className={style.Calendar_Container}>
      <カレンダーヘッダー
        対象日={対象日}
        setTargetDay={(タイムスタンプ) => {
          onChange(タイムスタンプ);
          ターゲット日を設定します(タイムスタンプ)。
        }}
        ビジネスレンダー = {ビジネスレンダー}
        switchWeekandDay={switchWeekandDay}
        setSwitchWeekandDay={setSwitchWeekandDay}
      />
      <スケジュールコンテナ
        高さ={高さ}
        データ={データ}
        対象日={対象日}
        スケジュールレンダリング={スケジュールレンダリング}
      />
    </div>
  );
};

コードを見ると、考えることができます。制御のためにグローバル状態データを最高レベルに上げることは間違いなく必要であり、これは React のコンポーネント設計哲学にも沿っています。

現在のタイムスタンプと日/週のステータスを維持し、すべてのサブコンポーネントのステータスは targetDay に基づいて表示されます。

CalendatrHeader ヘッダー コンテナ コンポーネント

ヘッダーコンテナの残りの部分は問題ないと思います。曜日は固定されているので(主にAppleのカレンダーコンポーネントを参考にしていますが、Appleの曜日は変わっていないので、大手メーカーの優れたデザインを参考にしています)、最も難しいのは曜日を正確に表示する方法です。

実際、私は曜日を表示する 2 つの方法を書きました。

最初の方法は、現在の曜日を基準にして、それぞれ前方と後方に計算し、最終的に [29, 30, 31, 1, 2, 3, 4] のようなリストを出力することです。今日がたまたま 1 日または 2 日の場合は、前月の最終日の日付を取得して前方にカウントします。

2 番目の方法は、次のコード メソッドです。これも、現在の日付の曜日を特定し、タイムスタンプを通じて動的に計算します。前日から何日減算し、次の日に何日加算するかがわかっていれば、問題ありません。

実際のところ、どちらの方法でも問題ありません。私は最終的に、明らかにより簡潔な 2 番目の方法を使用しました。

以下のように表示されます。

今週は[12、13、14、15、16、17、18]を出力します。

以下は、上記の困難を具体的に実装するためのコードです。

const calcWeekDayList: (params: number) => WeekType = (params) => {
    定数結果 = [];
    (i = 1 とします; i < weekDay(params); i++) {
      結果.unshift(パラメータ - 3600 * 1000 * 24 * i);
    }
    (i = 0; i < 7 - weekDay(params) + 1; i++ とします) {
      結果.push(パラメータ + 3600 * 1000 * 24 * i);
    }
    WeekType として [...result] を返します。
  };

コード:

定数CalendatrHeader: React.FC<CalendatrHeaderType> = ({
  対象日、
  ターゲット日の設定、
  週と曜日を切り替える、
  ビジネスレンダリング、
  曜日と曜日を切り替える、
}) => {
  // 現在の週の日付 const [dateTextList, setDateTextList] = useState<WeekType | []>([]);
  // この状態は、週を切り替えるときに、1 週間のタイムスタンプを直接増減し、次の週または前の週の日付が自動的に計算されるときです。
  const [currTime, setCurrTime] = useState<number>(targetDay); 

  使用効果(() => {
    日付テキストリストを設定します(計算週日リスト(対象日))。
  }, [対象日]);

  // 現在のタイムスタンプの前後の日付を計算します。週は固定されているので、現在の週の日付を計算するだけです。const calcWeekDayList: (params: number) => WeekType = (params) => {
    定数結果 = [];
    (i = 1 とします; i < weekDay(params); i++) {
      結果.unshift(パラメータ - 3600 * 1000 * 24 * i);
    }
    (i = 0; i < 7 - weekDay(params) + 1; i++ とします) {
      結果.push(パラメータ + 3600 * 1000 * 24 * i);
    }
    WeekType として [...result] を返します。
  };

  const onChangeWeek: (type: 'prevWeek' | 'nextWeek', switchWay: 'week' | 'day') => void = (
    タイプ、
    スイッチウェイ、
  ) => {
    スイッチウェイ === 'week' の場合 {
      定数calcWeekTime =
        type === 'prevWeek' ? currTime - 3600 * 1000 * 24 * 7 : currTime + 3600 * 1000 * 24 * 7;
      現在の時刻を設定します(週の時刻を計算します);
      日付テキストリストを設定します([...calcWeekDayList(calcWeekTime)]);
    }

    (スイッチウェイ === '日')の場合{
      定数calcWeekTime =
        type === 'prevWeek' ? targetDay - 3600 * 1000 * 24 : targetDay + 3600 * 1000 * 24;
      現在の時刻を設定します(週の時刻を計算します);
      ターゲット日を設定します(計算週時間)。
    }
  };

  戻る (
    <div className={style.Calendar_Header}>
      <デイリーオプション
        対象日={対象日}
        setCurrTime={setCurrTime}
        setTargetDay={setTargetDay}
        dateTextList={dateTextList}
        switchWeekandDay={switchWeekandDay}
        setSwitchWeekandDay={(値) => {
          曜日と曜日を設定します。
          if (値 === '週') {
            日付テキストリストを設定します(計算週日リスト(対象日))。
          }
        }}
        onChangeWeek={(type) => onChangeWeek(type, switchWeekandDay)}
      />
      {switchWeekandDay === 'week' && (
        <ウィークリーオプション
          対象日={対象日}
          setTargetDay={setTargetDay}
          dateTextList={dateTextList}
        />
      )}
      <div className={style.Calendar_Header_businessRender}>
        <div className={style.Calendar_Header_Zone}>GMT+8</div>
        {businessRender({ タイムスタンプ: ターゲット日 })}
      </div>
    </div>
  );
};

DailyOptions: これは実際には、ヘッダー内の「曜日」と「曜日と週のモード」および「今日」を切り替えるコンポーネントのコンテナーです。

WeeklyOptions: 曜日と日付を表示するコンポーネントです。日に切り替えると表示されなくなります: 図に示すように:

businessRender: これは、Xiao Zhan のコラムでユーザーによって渡されたビジネス コンポーネントです。

ScheduleCantainer 詳細スケジュールコンテナ

これは写真の一部です:

実際、この部分のコードはかなり長いので、すべてを投稿するのは不便です。機能的なポイントに応じていくつかのスニペットを投稿します。

左スケール

左のスケールは実際には 00:00 - 01:00 ---> 23:00 - 00:00 にハードコードされていますが、記述時に小さな問題が発生しました。つまり、このコンポーネントは左にフロートされており、右側の項目のスクロールに合わせてスクロールする必要があります。実際、最初はボックス内に記述し、スクロールコンテナーが一緒にスクロールするようにしましたが、小さな問題が発生しました。右側の項目が広くなりすぎるため、水平スクロールバーが表示されます。コンテナー全体を水平にスクロールすると、左側の時間スケールが表示領域からスクロールアウトします。

したがって、絶対配置した後、右側のスケジュール項目のスクロール イベントをリッスンし、左側のスタイルの上の値を動的に変更し、反対方向に値を割り当てます。下にスクロールするため、左側のタイム スケールは上にスクロールする必要があるため、同期効果を実現するために上の値が反転されます。なんて賢い小さな幽霊でしょう。このコードはスペースを占有しません。誰でも自由に使用できます。もっと良い方法がある場合は、コメント エリアにメッセージを残してください。

ScheduleItem スケジュール コンテナ エントリ

まず、このコンポーネントのコードを見てみましょう。

const ScheduleItem: React.FC<ScheduleItemType> = ({
  タイムスタンプ範囲、
  データ項目、
  スケジュールレンダリング、
  幅、
  データ項目の長さ、
}) => {
  // コンテナの高さを計算する const calcHeight: (timestampList: [number, number]) => number = (timestampList) =>
    timestampList.length > 1 ? (timestampList[1] - timestampList[0]) / 1000 / 60 / 2 : 30;
  const calcTop: (startTime: number) => number = (startTime) => moment(startTime).minute() / 2;
  // ScheduleItemの幅を計算する const calcWidth: (w: number, d: number) => string = (w, d) =>
    幅 === 0 || dataItemLength * 幅 < 347 ? '100%' : `${d * w}px`;

  戻る (
    <div style={{ position: 'relative' }} className={style.Calendar_ScheduleItem_Fath}>
      <div
        クラス名={style.Calendar_ScheduleItem}
        スタイル={{ 幅: calcWidth(幅, データ項目の長さ) }}
      >
        {dataItem.map((データ, インデックス) => {
          戻る (
            <フラグメントキー={インデックス}>
              {data.startTime >= タイムスタンプ範囲[0] && data.startTime < タイムスタンプ範囲[1] && (
                <div
                  クラス名={`${style.Calendar_ScheduleItem_container} Calendar_ScheduleItem_container`}
                  スタイル={{
                    高さ: `${calcHeight([data.startTime, data.endTime]) || 30}px`,
                    上: calcTop(data.startTime),
                  }}
                >
                  {scheduleRender({ データ、タイムスタンプ範囲 })}
                </div>
              )}
            </フラグメント>
          );
        })}
      </div>
    </div>
  );
};

この部分 (下の灰色の部分) に別のコンポーネントが必要なのはなぜでしょうか? まず考えてみましょう...

わかりました、皆さんをハラハラさせないようにします。実際、ユーザーのスケジュールデータ、たとえば今日の 10:00-11:00 がどこにあるかを特定することです。

この API を覚えていますか?

レンダリングをスケジュールしますか?: ({
    データ: データ型、
    タイムスタンプ範囲: [DOMTimeStamp, DOMTimeStamp],
  }) => JSX.Element; 

このコンポーネントには、パラメータ [DOMTimeStamp, DOMTimeStamp] があります (DOMTimeStamp はタイムスタンプを意味します)。これら 2 つのタイムスタンプは、実際には現在の期間 10:00-11:00 の開始タイムスタンプと終了タイムスタンプです。受け入れる startTime と endTime もタイムスタンプであるため、サイズがこの範囲内にあるかどうかを比較することで、表示と非表示を制御できます。これで、タイムスタンプが使用される理由がわかりました。数字のサイズを直接比較するだけです。

このもののスタイルについて話しましょう:

実は、1 時間は 60 分あるので、これを 30px に設定しました。60px だと高すぎるので、配置しやすいように 30px に設定しました。結局、私は面倒くさがりなので、あまり複雑な計算はしたくないのです。

つまり、位置計算はたった 1 行のコードです: const calcTop: (startTime: number) => number = (startTime) => moment(startTime).minute() / 2; 高さの位置合わせの問題が解決しました! ハハ~~

次に、図に示すように、高さの問題という別の問題があります。

高さの計算は難しくありません。主に、現在の開始時間と終了時間の間隔範囲に基づいて計算されます (1px は 2 分)。具体的な実装については、コードを参照してください。

const calcHeight: (timestampList: [number, number]) => number = (timestampList) =>
    timestampList.length > 1 ? (timestampList[1] - timestampList[0]) / 1000 / 60 / 2 : 30;

まず、入力したタイムスタンプに時間が 1 つしかないかどうかを判断します。開始時間のみがあり、終了時間がない場合は、30px にハードコードします。開始時間と終了時間がある場合は、分に変換して動的に計算します。

最後に、もう 1 つの質問があります。ビジネス データはどのように渡され、コンポーネントにどのようにレンダリングされるのでしょうか。

まず、データ フィールドに渡した JSON を見てみましょう。

[
  {
    startTime: 1626057075000, // 開始時刻 endTime: 1626070875000, // 終了時刻 value: 'any', // ビジネス データ},
  {
    開始時間: 1626057075000、
    終了時間: 1626070875000、
    値: '任意'、
  },
  {
    開始時間: 1626057075000、
    終了時間: 1626070875000、
    値: '任意'、
  },
  {
    開始時間: 1626057075000、
    終了時間: 1626070875000、
    値: '任意'、
  },
];

実際、ScheduleItem コンポーネントをループしてレンダリングするときには、ハードコードされた 24 時間リストを使用してループします。次に、ループ時に、現在のループの時間範囲に一致するビジネス データを動的に検索し、そのデータをコンポーネントに挿入します。一般的なコードは次のとおりです。

(i = 0 とします; i < HoursList.length; i++) {
      結果.push({
        タイムスタンプ範囲: [今日の時刻 + i * 3600 * 1000、今日の時刻 + (i + 1) * 3600 * 1000]、
        dataItem: [ // 現在の時間帯によりスケジュールが競合する可能性があるため、リストをコンポーネントに渡す必要があります...data.filter((item) => {
            戻る (
              アイテム開始時刻 >= 今日時刻 + i * 3600 * 1000 &&
              アイテムの開始時刻 < 今日時刻 + (i + 1) * 3600 * 1000
            );
          })、
        ]、
      });
    }

要約する

上記は、要件の受け取りからコンポーネントの設計、そして最終的な実装の詳細に至るまで、このコンポーネントの大部分の実装です。包括的ではないかもしれませんが、基本的な実装のアイデアでもあります。

技術的な難しさの実装の詳細も記載されています。実際、少し頭を使う限り、難しくはないようです。

私の実装は完璧ではないかもしれません。プログラムを実装する方法は何千通りもあります。私はただ自分のデザインのアイデアを表現しただけなので、皆さんが私から学んでくれることを願っています。何か間違っている点があれば、コメント欄で指摘してください。一緒に進歩していきましょう。

React を使用してスケジュール コンポーネントを構築する方法についての記事はこれで終わりです。React スケジュール コンポーネントに関するその他のコンテンツについては、123WORDPRESS.COM の以前の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • ReactプロジェクトでのTypeScriptの実装
  • React+tsは二次リンク効果を実現します
  • React プロジェクトにおける TypeScript の使用の概要
  • TypeScript ジェネリックパラメータのデフォルト型と新しい厳密なコンパイルオプション
  • フロントエンド React Nextjs での TS 型フィルタリングの実用的なヒント

<<:  動的および静的分離を実現する nginx のサンプルコード

>>:  MySQLトリガーの概念、原理、使用法の詳細な説明

推薦する

きれいなJavaScriptコードの書き方を教える記事

目次1. 変数意味のある名前を使う不必要なコンテキストを追加しないようにするハードコードされた値を避...

Dockerイメージを構築する2つの方法

目次既存のイメージからイメージを更新します。イメージを最初から構築する: Docker イメージ リ...

MySQL に大量のデータを挿入する 4 つの方法の例

序文この記事では主に、MySQLに大量のデータを挿入する4つの方法を紹介し、参考と学習のために共有し...

Jenkins Docker 静的エージェント ノードのビルド プロセス

静的ノードはマシン上に固定されており、いくつかの固定コマンドを通じて起動されます。動的ノードには複数...

nginx で仮想ホストを構成するための詳細な手順

仮想ホストは、インターネット上で実行されているサーバー ホストを複数の「仮想」ホストに分割する特殊な...

MySQL ユーザー権限管理の分析例

この記事では、MySQL ユーザー権限管理の例について説明します。ご参考までに、詳細は以下の通りです...

MySQLで大きなテーブルを正常に削除する方法の詳細な説明

序文テーブルを削除するには、無意識に思い浮かぶコマンドは、DROP TABLE "テーブル...

Linux環境でrmによって誤って削除されたファイルを回復する方法

目次序文RMの後には希望はあるのでしょうか?最前線を使ってファイルを取得するextundeleteを...

WeChatミニプログラムで検索キーワードを強調表示するサンプルコード

1. はじめにプロジェクトで要件に遭遇したら、データを検索してキーワードを強調表示します。要件を受け...

デザインのヒント: きっと気に入っていただけると思います

<br />このタイトルを見ると、見覚えがあるかもしれません。多くのウェブサイトが同様の...

MySQL 8.x msi バージョンのインストール チュートリアル (画像とテキスト付き)

1. MySQLをダウンロードする公式サイトのダウンロードアドレス https://dev.mys...

Dockerパッケージイメージの実装と構成の変更

最近、Docker の学習や実際の運用で多くの問題に遭遇したので、それを記録するためにブログを書きま...

本をめくる効果を実現するネイティブJS

この記事では、ネイティブ JS で実装された本をめくる効果の図を紹介します。効果は次のとおりです。 ...

vscode で console.log を書く 2 つの簡単な方法の詳細な説明

(I) 方法 1: 事前にスクリプト タグ内に直接定義します。この HTML ファイルにのみ適用され...

MySQL 8.0.22 winx64 のインストールと設定のグラフィックチュートリアル

mysql 8.0.22 winx64のインストールと設定のグラフィックチュートリアルは参考までに、...