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

View File

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

View File

@ -1,5 +1,5 @@
import React, { useMemo, useState } from "react"; 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 { chunk } from 'lodash';
import type { BasePlayer } from "../types"; import type { BasePlayer } from "../types";
import { GroupMember } from "./GroupMember"; import { GroupMember } from "./GroupMember";

View File

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

View File

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

View File

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