feat(global): HydrateFallback

This commit is contained in:
kyuuseiryuu 2026-01-30 09:34:10 +09:00
parent bf74e99a47
commit f37be8aded
6 changed files with 72 additions and 45 deletions

View File

@ -1,8 +1,8 @@
import { Card, Divider, Flex, Select, Space, Statistic, Switch, Typography } from 'antd';
import { Card, Divider, Flex, Select, Skeleton, Space, Statistic, Switch, Typography } from 'antd';
import type React from 'react';
import { useRequest } from 'ahooks';
import { clubs } from './clubList';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import dayjs from 'dayjs';
import type { IEventInfo } from '../../types';
@ -11,25 +11,25 @@ interface Props {
}
export const GameSelector: React.FC<Props> = props => {
const requestEvents = useRequest<IEventInfo[], [string]>(
async (clubId: string) => (await fetch(`/api/events/${clubId}`)).json()
, { manual: true })
const [gameList, setGameList] = useState<(IEventInfo & { finished: boolean })[]>([]);
const [isEmpty, setIsEmpty] = useState(false);
const [clubId, setClubId] = useState(clubs[0].clubId);
const [clubId, setClubId] = useState<string>(clubs[0]?.clubId ?? '');
const requestEvents = useRequest<IEventInfo[], []>(
async () => {
if (!clubId) return [];
return (await fetch(`/api/events/${clubId}`)).json()
}, { manual: false, refreshDeps: [clubId] })
const [showFinished, setShowFinished] = useState(false);
const handleClubChange = useCallback(async (clubId: string) => {
const list = await requestEvents.runAsync(clubId);
const activeList = list.map(e => ({
const gameList = useMemo(() => {
const activeList = requestEvents.data?.map(e => ({
...e,
finished: e.info.join('').includes('已结束'),
}));
setGameList(activeList);
setIsEmpty(activeList.filter(e => !e.finished).length === 0);
}, []);
useEffect(() => {
const clubId = clubs[0].clubId;
handleClubChange(clubId);
return activeList;
}, [requestEvents.data]);
const isEmpty = useMemo(() => {
return (gameList ?? []).filter(e => !e.finished).length === 0
}, [gameList]);
const handleClubChange = useCallback(async (id: string) => {
setClubId(id);
}, []);
return (
<Space orientation='vertical' style={{ width: '100%' }}>
@ -48,12 +48,14 @@ export const GameSelector: React.FC<Props> = props => {
/>
</Flex>
<Divider>{isEmpty && (<Typography.Text type='secondary'></Typography.Text>)}</Divider>
{requestEvents.loading ? <Skeleton.Button active block style={{ height: 300 }} /> : (
<Flex wrap gap={12} justify='center'>
{gameList
.filter(e => showFinished || !e.finished)
.map(e => <EventCard key={e.matchId} eventInfo={e} onGameClick={props.onGameClick} />)
?.filter(e => showFinished || !e.finished)
?.map(e => <EventCard key={e.matchId} eventInfo={e} onGameClick={props.onGameClick} />)
}
</Flex>
)}
</Space>
);
}

View File

@ -3,4 +3,4 @@ export const clubs = [
name: '东华',
clubId: '47',
},
] as const;
];

View File

@ -1,5 +1,5 @@
import React, { useMemo, useState } from "react";
import { Flex, Form, InputNumber, Space, Switch, Typography } from "antd";
import { Flex, Form, InputNumber, Switch } from "antd";
import { chunk } from 'lodash';
import type { BasePlayer } from "../types";
import { GroupMember } from "./GroupMember";

View File

@ -1,7 +1,6 @@
import { useRequest } from "ahooks";
import { Divider, Flex, Skeleton, Tag, Typography } from "antd";
import { EType, type XCXTag } from "../types";
import { useEffect } from "react";
interface Props {
uid?: string;

View File

@ -5,40 +5,65 @@
* It is included in `src/index.html`.
*/
import { Component, StrictMode } from "react";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
import { ConfigProvider, theme } from "antd";
import { ConfigProvider, Spin, theme } from "antd";
import { createBrowserRouter, RouterProvider } from "react-router";
import ProfilePage from "./page/ProfilePage";
import EventPage from "./page/EventPage";
import type { MatchInfo } from "./types";
import { Outlet, useNavigation } from "react-router";
const elem = document.getElementById("root")!;
const route = createBrowserRouter([
{
path: '/',
element: <App />
element: <Layout />,
children: [
{
path: '',
index: true,
element: <App />,
HydrateFallback: () => <HydrateFallback />
},
{
path: '/event/:matchId',
path: 'event/:matchId',
loader: async ({ params }) => {
const info: MatchInfo = await (await fetch(`/api/match/${params.matchId}`)).json();
const members = await (await fetch(`/api/match/${params.matchId}/${info.itemId}`)).json();
return { info, members };
},
Component: EventPage,
HydrateFallback: () => <HydrateFallback />
},
{
path: '/profile/:uid',
path: 'profile/:uid',
loader: async ({ params }) => {
return fetch(`/api/user/${params.uid}`);
},
Component: ProfilePage,
HydrateFallback: () => <HydrateFallback />
},
],
},
]);
function HydrateFallback() {
return (
<Spin spinning>
<div style={{ height: '100vh' }} />
</Spin>
);
}
function Layout() {
const navigation = useNavigation();
const loading = navigation.state === 'loading';
return loading ? <HydrateFallback /> : <Outlet />
}
const app = (
<StrictMode>
<ConfigProvider theme={{

View File

@ -80,6 +80,7 @@ function PlayerList(props: { title: string; names?: string[]; uids?: string[] })
export default function ProfilePage() {
const profile = useLoaderData<XCXProfile | null>();
console.debug('profile', profile);
const navigate = useNavigate();
return (
<>