feat(global): Skeleton
This commit is contained in:
parent
bf74e99a47
commit
6924cc873b
@ -1,6 +1,6 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import type { BasePlayer, Player } from "../types";
|
import type { BasePlayer, Player } from "../types";
|
||||||
import { Tabs } from "antd";
|
import { Skeleton, Spin, Tabs } from "antd";
|
||||||
import { PlayerList } from "./PlayerList";
|
import { PlayerList } from "./PlayerList";
|
||||||
import { GroupingPrediction } from "./GroupingPrediction";
|
import { GroupingPrediction } from "./GroupingPrediction";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
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';
|
||||||
@ -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 block active style={{ height: 200 }} /> : (
|
||||||
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,16 +24,14 @@ const route = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
path: '/event/:matchId',
|
path: '/event/:matchId',
|
||||||
loader: async ({ params }) => {
|
loader: async ({ params }) => {
|
||||||
const info: MatchInfo = await (await fetch(`/api/match/${params.matchId}`)).json();
|
return { matchId: params.matchId };
|
||||||
const members = await (await fetch(`/api/match/${params.matchId}/${info.itemId}`)).json();
|
|
||||||
return { info, members };
|
|
||||||
},
|
},
|
||||||
Component: EventPage,
|
Component: EventPage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/profile/:uid',
|
path: '/profile/:uid',
|
||||||
loader: async ({ params }) => {
|
loader: async ({ params }) => {
|
||||||
return fetch(`/api/user/${params.uid}`);
|
return params.uid;
|
||||||
},
|
},
|
||||||
Component: ProfilePage,
|
Component: ProfilePage,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,32 +1,42 @@
|
|||||||
import { useLoaderData, useNavigate } from "react-router";
|
import { useLoaderData, useNavigate } from "react-router";
|
||||||
import { GamePanel } from "../components/GamePanel";
|
import { GamePanel } from "../components/GamePanel";
|
||||||
import type { BasePlayer, MatchInfo, XCXMember } from "../types";
|
import type { BasePlayer, MatchInfo, XCXMember } from "../types";
|
||||||
import { Typography } from "antd";
|
import { Skeleton, Typography } from "antd";
|
||||||
import { HomeOutlined } from "@ant-design/icons";
|
import { HomeOutlined } from "@ant-design/icons";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { useRequest } from "ahooks";
|
||||||
|
|
||||||
export default function EventPage() {
|
export default function EventPage() {
|
||||||
const {
|
const {
|
||||||
info: game,
|
matchId,
|
||||||
members
|
} = useLoaderData<{ matchId: string }>();
|
||||||
} = useLoaderData<{ info: MatchInfo, members: XCXMember[] }>();
|
const fetchData = useRequest<{ info: MatchInfo, members: XCXMember[] }, []>(async () => {
|
||||||
|
const info: MatchInfo = await (await fetch(`/api/match/${matchId}`)).json();
|
||||||
|
const members = await (await fetch(`/api/match/${matchId}/${info.itemId}`)).json();
|
||||||
|
return { info, members };
|
||||||
|
}, { refreshDeps: [matchId]});
|
||||||
|
const { info: game, members = [] } = useMemo<{ info?: MatchInfo, members?: XCXMember[] }>(() => fetchData.data ?? {}, [fetchData]);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const map = useMemo(() => Object.fromEntries(members.map(e => [e.uid, e])), [members]);
|
const map = useMemo(() => Object.fromEntries(members.map(e => [e.uid, e])), [members]);
|
||||||
const players = useMemo(() => {
|
const players = useMemo(() => {
|
||||||
return game.players
|
return game?.players
|
||||||
.map(e => ({ ...e, name: map[e.uid]?.realname ?? e.name, number: map[e.uid]?.number ?? NaN }))
|
?.map(e => ({ ...e, name: map[e.uid]?.realname ?? e.name, number: map[e.uid]?.number ?? NaN }))
|
||||||
.sort(((a, b) => +a.number - +b.number));
|
?.sort(((a, b) => +a.number - +b.number));
|
||||||
}, [game, map]);
|
}, [game, map]);
|
||||||
const basePlayers = useMemo<BasePlayer[]>(() => {
|
const basePlayers = useMemo<BasePlayer[]>(() => {
|
||||||
return members.map(e => ({ ...e, name: e.realname } as BasePlayer))
|
return members.map(e => ({ ...e, name: e.realname } as BasePlayer))
|
||||||
}, [members]);
|
}, [members]);
|
||||||
return (
|
return (
|
||||||
<div style={{ width: '100%', padding: 10, boxSizing: 'border-box' }}>
|
<div style={{ width: '100%', padding: 10, boxSizing: 'border-box' }}>
|
||||||
|
{fetchData.loading ? (<Skeleton.Button block />) : (
|
||||||
<Typography.Title level={3}>
|
<Typography.Title level={3}>
|
||||||
<HomeOutlined style={{ marginRight: 4 }} onClick={() => navigate('/')}/>
|
<HomeOutlined style={{ marginRight: 4 }} onClick={() => navigate('/')}/>
|
||||||
{game.title}
|
{game?.title}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<GamePanel members={basePlayers} title={game.title} players={players} />
|
)}
|
||||||
|
{fetchData.loading ? <Skeleton.Button active block size="large" style={{ height: 400, marginTop: 40 }} /> : (
|
||||||
|
<GamePanel members={basePlayers} title={game?.title ?? ''} players={players} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,12 +1,13 @@
|
|||||||
import { Link, useLoaderData, useNavigate } from "react-router";
|
import { Link, useLoaderData, useNavigate } from "react-router";
|
||||||
import type { XCXProfile } from "../types/profile";
|
import type { XCXProfile } from "../types/profile";
|
||||||
import { Avatar, Descriptions, Divider, Flex, FloatButton, Image, Typography } from "antd";
|
import { Skeleton, Avatar, Descriptions, Divider, Flex, FloatButton, Image, Typography } from "antd";
|
||||||
import { HomeOutlined } from "@ant-design/icons";
|
import { HomeOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
import User from "../components/User";
|
import User from "../components/User";
|
||||||
import React from "react";
|
import React, { useMemo } from "react";
|
||||||
import { ChangeBackground } from "../components/ChangeBackground";
|
import { ChangeBackground } from "../components/ChangeBackground";
|
||||||
import UserTags from "../components/Tags";
|
import UserTags from "../components/Tags";
|
||||||
|
import { useRequest } from "ahooks";
|
||||||
|
|
||||||
function Honor(props: { honors?: XCXProfile['honors'] }) {
|
function Honor(props: { honors?: XCXProfile['honors'] }) {
|
||||||
if (!props.honors?.length) return null;
|
if (!props.honors?.length) return null;
|
||||||
@ -79,29 +80,40 @@ function PlayerList(props: { title: string; names?: string[]; uids?: string[] })
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const profile = useLoaderData<XCXProfile | null>();
|
const uid = useLoaderData<string | null>();
|
||||||
|
const fetchData = useRequest<XCXProfile | undefined, []>(async () => {
|
||||||
|
return (await fetch(`/api/user/${uid}`)).json();
|
||||||
|
}, { refreshDeps: [uid] });
|
||||||
|
const loading = fetchData.loading;
|
||||||
|
const profile = fetchData.data;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const tags = useMemo<React.ReactNode[]>(() =>
|
||||||
|
[profile?.province, profile?.sex, profile?.bg ?? '', ...(profile?.allCities ?? [])]
|
||||||
|
?.filter(Boolean), []);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ChangeBackground url={profile?.realpic} />
|
<ChangeBackground url={profile?.realpic} />
|
||||||
<FloatButton icon={<HomeOutlined />} onClick={() => navigate('/')} />
|
<FloatButton icon={<HomeOutlined />} onClick={() => navigate('/')} />
|
||||||
<Flex vertical align="center" style={{ padding: 24 }}>
|
<Flex vertical align="center" style={{ padding: 24 }}>
|
||||||
|
{ loading ? <Skeleton.Avatar active size={128} /> : (
|
||||||
<Avatar src={profile?.realpic} size={128} />
|
<Avatar src={profile?.realpic} size={128} />
|
||||||
|
) }
|
||||||
|
<Skeleton active loading={loading}>
|
||||||
<Typography.Title level={2}>{profile?.username}</Typography.Title>
|
<Typography.Title level={2}>{profile?.username}</Typography.Title>
|
||||||
<Typography.Text>姓名:{profile?.realname}</Typography.Text>
|
<Typography.Text>姓名:{profile?.realname}</Typography.Text>
|
||||||
<Typography.Text>积分:{profile?.score}</Typography.Text>
|
<Typography.Text>积分:{profile?.score}</Typography.Text>
|
||||||
<Typography.Text style={{ textAlign: 'center' }}>
|
<Typography.Text style={{ textAlign: 'center' }}>
|
||||||
{
|
{ tags.length ? tags.reduce((a, b) => (<>{a}<Divider orientation="vertical" />{b}</>)) : null }
|
||||||
([profile?.province, profile?.sex, profile?.bg ?? '', ...(profile?.allCities ?? [])] as React.ReactNode[])
|
|
||||||
.filter(Boolean)
|
|
||||||
.reduce((a, b) => (<>{a}<Divider orientation="vertical" />{b}</>))
|
|
||||||
}
|
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
|
</Skeleton>
|
||||||
<Divider>简介</Divider>
|
<Divider>简介</Divider>
|
||||||
|
<Skeleton active loading={loading}>
|
||||||
<Typography.Paragraph>
|
<Typography.Paragraph>
|
||||||
{profile?.description}
|
{profile?.description}
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
|
</Skeleton>
|
||||||
<UserTags uid={profile?.uid} />
|
<UserTags uid={profile?.uid} />
|
||||||
|
<Skeleton active loading={loading}>
|
||||||
<Raket profile={profile} />
|
<Raket profile={profile} />
|
||||||
<Flex wrap gap={24} justify="center">
|
<Flex wrap gap={24} justify="center">
|
||||||
<Flex vertical align="center">
|
<Flex vertical align="center">
|
||||||
@ -126,6 +138,7 @@ export default function ProfilePage() {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Honor honors={profile?.honors} />
|
<Honor honors={profile?.honors} />
|
||||||
|
</Skeleton>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user