feat(index)

This commit is contained in:
kyuuseiryuu 2026-01-28 16:02:45 +09:00
parent 0f33f63459
commit 7df7b70516
3 changed files with 74 additions and 51 deletions

View File

@ -0,0 +1,7 @@
import { createGlobalStyle } from "styled-components";
export const ChangeBackground = createGlobalStyle<{ url?: string }>`
body:before {
${({ url }) => url ? `background: url(${url});` : ''}
}
`

View File

@ -1,10 +1,10 @@
import { Card, Divider, Select, Space, Spin, Statistic, Typography } from 'antd'; import { Card, Divider, Flex, Select, 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, useMemo, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import type { IEventInfo } from '../../types';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import type { IEventInfo } from '../../types';
interface Props { interface Props {
onGameClick?: (info: IEventInfo) => void; onGameClick?: (info: IEventInfo) => void;
@ -14,14 +14,18 @@ export const GameSelector: React.FC<Props> = props => {
const requestEvents = useRequest<IEventInfo[], [string]>( const requestEvents = useRequest<IEventInfo[], [string]>(
async (clubId: string) => (await fetch(`/api/events/${clubId}`)).json() async (clubId: string) => (await fetch(`/api/events/${clubId}`)).json()
, { manual: true }) , { manual: true })
const [gameList, setGameList] = useState<IEventInfo[]>([]); const [gameList, setGameList] = useState<(IEventInfo & { finished: boolean })[]>([]);
const [isEmpty, setIsEmpty] = useState(false); const [isEmpty, setIsEmpty] = useState(false);
const [clubId, setClubId] = useState(clubs[0].clubId); const [clubId, setClubId] = useState(clubs[0].clubId);
const [showFinished, setShowFinished] = useState(false);
const handleClubChange = useCallback(async (clubId: string) => { const handleClubChange = useCallback(async (clubId: string) => {
const list = await requestEvents.runAsync(clubId); const list = await requestEvents.runAsync(clubId);
const activeList = list.filter(e => !e.info.join('').includes('已结束')); const activeList = list.map(e => ({
...e,
finished: e.info.join('').includes('已结束'),
}));
setGameList(activeList); setGameList(activeList);
setIsEmpty(activeList.length === 0); setIsEmpty(activeList.filter(e => !e.finished).length === 0);
}, []); }, []);
useEffect(() => { useEffect(() => {
const clubId = clubs[0].clubId; const clubId = clubs[0].clubId;
@ -29,6 +33,11 @@ export const GameSelector: React.FC<Props> = props => {
}, []); }, []);
return ( return (
<Space orientation='vertical' style={{ width: '100%' }}> <Space orientation='vertical' style={{ width: '100%' }}>
<Flex vertical gap={12} justify='center'>
<Space style={{ alignItems: 'self-end' }}>
<Typography.Text></Typography.Text>
<Switch checkedChildren='显示' unCheckedChildren='隐藏' checked={showFinished} onChange={e => setShowFinished(e)} />
</Space>
<Select <Select
style={{ width: '100%' }} style={{ width: '100%' }}
placeholder={'请选择俱乐部'} placeholder={'请选择俱乐部'}
@ -37,10 +46,25 @@ export const GameSelector: React.FC<Props> = props => {
options={clubs.map(e => ({ label: e.name, value: e.clubId }))} options={clubs.map(e => ({ label: e.name, value: e.clubId }))}
onChange={handleClubChange} onChange={handleClubChange}
/> />
<Divider /> </Flex>
{isEmpty && (<Typography.Text type='secondary'></Typography.Text>)} <Divider>{isEmpty && (<Typography.Text type='secondary'></Typography.Text>)}</Divider>
<Spin spinning={requestEvents.loading}> <Flex wrap gap={12} justify='center'>
{gameList.map(e => { {gameList
.filter(e => showFinished || !e.finished)
.map(e => <EventCard key={e.matchId} eventInfo={e} onGameClick={props.onGameClick} />)
}
</Flex>
</Space>
);
}
interface EventCardProps {
eventInfo: IEventInfo & { finished: boolean };
onGameClick?: (info: IEventInfo) => void;
}
function EventCard(props: EventCardProps) {
const { eventInfo: e } = props;
const { y, M, D } = /^(?<y>\d{4})年(?<M>\d+)月(?<D>\d+)日/.exec(e.title)?.groups ?? {} as { y: string; M: string; D: string}; const { y, M, D } = /^(?<y>\d{4})年(?<M>\d+)月(?<D>\d+)日/.exec(e.title)?.groups ?? {} as { y: string; M: string; D: string};
const hm = /(?<hm>\d+:\d+)\b/.exec(e.info.join('\n'))?.groups?.hm ?? '10:00'; const hm = /(?<hm>\d+:\d+)\b/.exec(e.info.join('\n'))?.groups?.hm ?? '10:00';
const day = dayjs(`${y}-${M}-${D} ${hm}`); const day = dayjs(`${y}-${M}-${D} ${hm}`);
@ -49,23 +73,21 @@ export const GameSelector: React.FC<Props> = props => {
key={e.matchId} key={e.matchId}
title={e.title} title={e.title}
hoverable hoverable
style={{ width: '100%' }}
onClick={() => props.onGameClick?.(e)} onClick={() => props.onGameClick?.(e)}
> >
<Typography.Text type='success'>{e.title}</Typography.Text> <Typography.Text type={e.finished ? undefined : 'success'}>{e.title}</Typography.Text>
<Statistic.Timer
type={e.finished ? 'countup' : 'countdown'}
value={day.toDate().getTime()}
format={`距离比赛${e.finished ? '结束' : '开始'}: DD 天${e.finished ? '' : ' HH 时'}`}
styles={{ content: e.finished ? { color: 'gray' } : {} }}
/>
{e.info.map(e => ( {e.info.map(e => (
<div key={e}> <div key={e}>
<Typography.Text type='secondary'>{e}</Typography.Text> <Typography.Text type='secondary'>{e}</Typography.Text>
</div> </div>
))} ))}
<Statistic.Timer
type='countdown'
value={day.toDate().getTime()}
format='距离比赛开始: DD 天 HH 时 mm 分 ss 秒'
/>
</Card> </Card>
); )
})}
</Spin>
</Space>
);
} }

View File

@ -5,13 +5,7 @@ import { HomeOutlined } from "@ant-design/icons";
import User from "../components/User"; import User from "../components/User";
import React from "react"; import React from "react";
import { createGlobalStyle } from "styled-components"; import { ChangeBackground } from "../components/ChangeBackground";
const BodyBG = createGlobalStyle<{ url?: string }>`
body:before {
${({ url }) => url ? `background: url(${url});` : ''}
}
`
function Honor(props: { honors?: XCXProfile['honors'] }) { function Honor(props: { honors?: XCXProfile['honors'] }) {
if (!props.honors?.length) return null; if (!props.honors?.length) return null;
@ -81,7 +75,7 @@ export default function ProfilePage() {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<> <>
<BodyBG 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 }}>
<Avatar src={profile?.realpic} size={128} /> <Avatar src={profile?.realpic} size={128} />