refactor(components): optimize event list and search table with pagination persistence
- Refactor ClubEventList to support persistent page state in sessionStorage and dynamic toggle for finished events. - Move pagination logic to useRunOnce hook for initialization, ensuring correct state restoration. - Update ClubSearchTable to support "My Favorites" tab, using useRef to cache search results for different club types. - Enhance EventCard with precise time formatting (HH:mm:ss) and dynamic countdown logic using dayjs timezone. - Persist club selection in GameSelector using local storage and integrate ClubSummary component. - Fix geo data handling in KaiqiuService, ensuring coordinates are correctly passed to iCalendar generator. - Remove unused imports and simplify component structure across affected files.
This commit is contained in:
parent
4825f41337
commit
0f9e80856b
@ -1,26 +1,36 @@
|
|||||||
import { Button, Divider, Empty, Flex, Pagination, Skeleton, Space, Switch, Typography } from "antd";
|
import { Button, Divider, Empty, Flex, Pagination, Skeleton, Space, Switch, Typography } from "antd";
|
||||||
import type { IEventInfo } from "../types";
|
import type { IEventInfo } from "../types";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { EventCard } from "./EventCard";
|
import { EventCard } from "./EventCard";
|
||||||
import { useRequest } from "ahooks";
|
import { useRequest } from "ahooks";
|
||||||
import { CalendarOutlined } from "@ant-design/icons";
|
import { CalendarOutlined } from "@ant-design/icons";
|
||||||
|
import { STORE_PAGE_LIST_KEY } from "../utils/constants";
|
||||||
|
import { useRunOnce } from "../hooks/useRunOnce";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
clubId: string;
|
clubId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getStorageKey = (clubId: string) => `events-page-${clubId}`;
|
||||||
|
|
||||||
export const ClubEvenList = (props: Props) => {
|
export const ClubEvenList = (props: Props) => {
|
||||||
const [showAllDisable, setShowAllDiable] = useState(false);
|
const [paginationControlVisible, setPaginationControlVisible] = useState(true);
|
||||||
const [showAll, setShowAll] = useState(false);
|
const [showFinishedEvents, setShowFinishedEvents] = useState(false);
|
||||||
const requestEvents = useRequest<{data: IEventInfo[]; total: number; }, [string, number]>(
|
const requestEvents = useRequest<{data: IEventInfo[]; total: number; }, [string, number]>(
|
||||||
async (clubId: string, page = 1) => {
|
async (clubId: string, page = 1) => {
|
||||||
if (!clubId) return [];
|
if (!clubId) return { data: [], total: 0 };
|
||||||
return (await fetch(`/api/club/${clubId}/events?page=${page}`)).json()
|
return fetch(`/api/club/${clubId}/events?page=${page}`).then(e => e.json());
|
||||||
}, { manual: true });
|
}, { manual: true, refreshDeps: [] });
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPageState] = useState(1);
|
||||||
|
const setPage = useCallback((page: number) => {
|
||||||
|
const key = getStorageKey(props.clubId);
|
||||||
|
sessionStorage.setItem(key, page.toString());
|
||||||
|
setPageState(page);
|
||||||
|
}, [props.clubId])
|
||||||
const onPageChange = useCallback((page: number) => {
|
const onPageChange = useCallback((page: number) => {
|
||||||
setPage(page);
|
setPage(page);
|
||||||
requestEvents.runAsync(props.clubId, page);
|
requestEvents.runAsync(props.clubId, page);
|
||||||
}, [props.clubId]);
|
}, [props.clubId, setPage]);
|
||||||
const pagination = useMemo(() => {
|
const pagination = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
page,
|
page,
|
||||||
@ -30,26 +40,26 @@ export const ClubEvenList = (props: Props) => {
|
|||||||
}, [requestEvents.data?.total, page]);
|
}, [requestEvents.data?.total, page]);
|
||||||
const visibleEvents = useMemo(() => {
|
const visibleEvents = useMemo(() => {
|
||||||
const events = requestEvents.data?.data || [];
|
const events = requestEvents.data?.data || [];
|
||||||
if (showAll) return events;
|
if (showFinishedEvents) return events;
|
||||||
return events.filter(e => !e.isFinished);
|
return events.filter(e => !e.isFinished);
|
||||||
}, [showAll, requestEvents.data?.data]);
|
}, [showFinishedEvents, requestEvents.data?.data]);
|
||||||
useEffect(() => {
|
const onPageInit = useCallback(async () => {
|
||||||
if (!showAll && page > 1) {
|
const storePageKey = getStorageKey(props.clubId);
|
||||||
onPageChange(1); // Reset page
|
const keys: string[] = JSON.parse(localStorage.getItem(STORE_PAGE_LIST_KEY) ?? '[]');
|
||||||
|
if (!keys.includes(storePageKey)) {
|
||||||
|
keys.push(storePageKey);
|
||||||
|
localStorage.setItem(STORE_PAGE_LIST_KEY, JSON.stringify(keys));
|
||||||
}
|
}
|
||||||
}, [showAll, onPageChange]);
|
const prePage = Number(sessionStorage.getItem(storePageKey)) || 1;
|
||||||
useEffect(() => {
|
const data = await requestEvents.runAsync(props.clubId, prePage).then(res => res.data);
|
||||||
const id = setTimeout(async () => {
|
setPage(prePage);
|
||||||
const data = await requestEvents.runAsync(props.clubId, 1).then(res => res.data);
|
if (data.length) {
|
||||||
setPage(1);
|
const isAllFinishedOrAllNotFinished = data.every(e => e.isFinished) || data.every(e => !e.isFinished);
|
||||||
if (data.length) {
|
setShowFinishedEvents(prePage !== 1 || isAllFinishedOrAllNotFinished);
|
||||||
const isAllFinishedOrAllNotFinished = data.every(e => e.isFinished) || data.every(e => !e.isFinished);
|
setPaginationControlVisible(prePage === 1 ? !isAllFinishedOrAllNotFinished : false);
|
||||||
setShowAll(isAllFinishedOrAllNotFinished);
|
}
|
||||||
setShowAllDiable(data.every(e => !e.isFinished));
|
}, [props]);
|
||||||
}
|
useRunOnce(onPageInit);
|
||||||
}, 100);
|
|
||||||
return () => clearTimeout(id);
|
|
||||||
}, [props.clubId]);
|
|
||||||
const handleAddToCalendar = useCallback(() => {
|
const handleAddToCalendar = useCallback(() => {
|
||||||
const url = `${window.location.origin}/api/club/${props.clubId}/calendar.ics`;
|
const url = `${window.location.origin}/api/club/${props.clubId}/calendar.ics`;
|
||||||
const uri = url.replace(/^http(s)?/, 'webcal');
|
const uri = url.replace(/^http(s)?/, 'webcal');
|
||||||
@ -59,12 +69,12 @@ export const ClubEvenList = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Divider>
|
<Divider>
|
||||||
{showAllDisable ? null : (
|
{paginationControlVisible ? (
|
||||||
<Space>
|
<Space>
|
||||||
<Typography.Text type="secondary">显示已结束的活动</Typography.Text>
|
<Typography.Text type="secondary">显示已结束的活动</Typography.Text>
|
||||||
<Switch checked={showAll} onChange={() => setShowAll(!showAll)} unCheckedChildren="隐藏" checkedChildren="显示" />
|
<Switch checked={showFinishedEvents} onChange={() => setShowFinishedEvents(!showFinishedEvents)} unCheckedChildren="隐藏" checkedChildren="显示" />
|
||||||
</Space>
|
</Space>
|
||||||
)}
|
) : null}
|
||||||
</Divider>
|
</Divider>
|
||||||
<Flex wrap vertical gap={12} justify="center" align="center">
|
<Flex wrap vertical gap={12} justify="center" align="center">
|
||||||
<Button
|
<Button
|
||||||
@ -74,7 +84,7 @@ export const ClubEvenList = (props: Props) => {
|
|||||||
>
|
>
|
||||||
订阅该俱乐部比赛
|
订阅该俱乐部比赛
|
||||||
</Button>
|
</Button>
|
||||||
{showAll ? (
|
{showFinishedEvents ? (
|
||||||
<Pagination
|
<Pagination
|
||||||
showQuickJumper
|
showQuickJumper
|
||||||
total={pagination.total}
|
total={pagination.total}
|
||||||
@ -96,7 +106,7 @@ export const ClubEvenList = (props: Props) => {
|
|||||||
);
|
);
|
||||||
}) : <Empty description="暂无活动" />}</>)}
|
}) : <Empty description="暂无活动" />}</>)}
|
||||||
|
|
||||||
{showAll ? (
|
{showFinishedEvents ? (
|
||||||
<Pagination
|
<Pagination
|
||||||
showQuickJumper
|
showQuickJumper
|
||||||
total={pagination.total}
|
total={pagination.total}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useRequest } from "ahooks";
|
import { useRequest } from "ahooks";
|
||||||
import { Avatar, Button, Flex, Input, Radio, Space, Table } from "antd";
|
import { Avatar, Button, Flex, Input, Radio, Space, Table } from "antd";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import type { ClubInfo } from "../types";
|
import type { ClubInfo } from "../types";
|
||||||
import { EyeOutlined, StarFilled, StarOutlined } from "@ant-design/icons";
|
import { EyeOutlined, StarFilled, StarOutlined } from "@ant-design/icons";
|
||||||
import { useClubStore } from "../store/useClubStore";
|
import { useClubStore } from "../store/useClubStore";
|
||||||
@ -12,65 +12,102 @@ interface Props {
|
|||||||
handleClick?: (type: ClickType, club: ClubInfo) => void;
|
handleClick?: (type: ClickType, club: ClubInfo) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ClubType {
|
||||||
|
积分俱乐部,
|
||||||
|
俱乐部,
|
||||||
|
我的收藏,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Data = { clubs: ClubInfo[], total: number, page: number; };
|
||||||
|
|
||||||
|
const initData = { clubs: [], total: 0, page: 1 };
|
||||||
|
|
||||||
export const ClubSearchTable = (props: Props) => {
|
export const ClubSearchTable = (props: Props) => {
|
||||||
const [searchKey, setSearchKey] = useState('');
|
const [searchKey, setSearchKey] = useState('');
|
||||||
const searchClub = useRequest<{ clubs: ClubInfo[], total: number }, [string, number, number]>(async (
|
const [datasource, setDatasource] = useState<Data>(initData);
|
||||||
|
const dataRef = useRef<Partial<Record<ClubType, Data>>>({});
|
||||||
|
const searchClub = useRequest<Data, [string, number, ClubType]>(async (
|
||||||
searchKey: string,
|
searchKey: string,
|
||||||
page = 1,
|
page = 1,
|
||||||
clubType = 0,
|
clubType = ClubType.积分俱乐部,
|
||||||
) => {
|
) => {
|
||||||
const resp: { clubs: ClubInfo[], total: number } = await fetch(
|
const resp: Data = await fetch(
|
||||||
`/api/club/find?key=${searchKey}&page=${page}${clubType ? '&normalClub=1' : ''}`
|
`/api/club/find?key=${searchKey}&page=${page}${clubType ? '&normalClub=1' : ''}`
|
||||||
).then(e => e.json());
|
).then(e => e.json());
|
||||||
|
dataRef.current[clubType] = {
|
||||||
|
...resp,
|
||||||
|
page,
|
||||||
|
};
|
||||||
return resp;
|
return resp;
|
||||||
}, { manual: true });
|
}, { manual: true });
|
||||||
const [page, setPage] = useState(1);
|
const clubStore = useClubStore(store => store);
|
||||||
const [clubType, setClubType] = useState(0);
|
const [clubType, setClubType] = useState<ClubType>(ClubType.积分俱乐部);
|
||||||
const handleSearch = useCallback((searchKey: string, clubType: number) => {
|
const handleSearch = useCallback(async (searchKey: string, clubType: ClubType, page = 1) => {
|
||||||
setPage(1);
|
switch (clubType) {
|
||||||
searchClub.runAsync(searchKey, 1, clubType);
|
case ClubType.积分俱乐部:
|
||||||
|
case ClubType.俱乐部: {
|
||||||
|
const resp = await searchClub.runAsync(searchKey, page, clubType);
|
||||||
|
setDatasource({ ...resp, page });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
const handleTypeChange = useCallback(async (type: ClubType) => {
|
||||||
|
setClubType(type);
|
||||||
|
switch (type) {
|
||||||
|
case ClubType.积分俱乐部:
|
||||||
|
case ClubType.俱乐部: {
|
||||||
|
if (!searchKey) {
|
||||||
|
setDatasource(dataRef.current[type] ?? initData);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await handleSearch(searchKey, type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ClubType.我的收藏:
|
||||||
|
setDatasource({ clubs: clubStore.clubs, total: clubStore.clubs.length, page: 1 });
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}, [searchKey, searchClub, clubStore]);
|
||||||
return (
|
return (
|
||||||
<Flex vertical gap={12} justify="center" align="center">
|
<Flex vertical gap={12} justify="center" align="center">
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
optionType="button"
|
optionType="button"
|
||||||
value={clubType}
|
value={clubType}
|
||||||
onChange={e => {
|
onChange={e => handleTypeChange(e.target.value)}
|
||||||
setClubType(e.target.value);
|
|
||||||
if (!searchKey) return;
|
|
||||||
handleSearch(searchKey, e.target.value);
|
|
||||||
}}
|
|
||||||
options={[
|
options={[
|
||||||
{ label: '积分俱乐部', value: 0 },
|
{ label: '积分俱乐部', value: ClubType.积分俱乐部 },
|
||||||
{ label: '俱乐部', value: 1 },
|
{ label: '俱乐部', value: ClubType.俱乐部 },
|
||||||
|
{ label: '我的收藏', value: ClubType.我的收藏 },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Input.Search
|
{clubType !== ClubType.我的收藏 ? (
|
||||||
allowClear
|
<Input.Search
|
||||||
onSearch={e => {
|
allowClear
|
||||||
setSearchKey(e);
|
onSearch={e => {
|
||||||
handleSearch(e, clubType);
|
setSearchKey(e);
|
||||||
}}
|
handleSearch(e, clubType);
|
||||||
onPressEnter={() => handleSearch(searchKey, clubType)}
|
}}
|
||||||
value={searchKey}
|
onPressEnter={() => handleSearch(searchKey, clubType)}
|
||||||
onChange={e => setSearchKey(e.target.value)}
|
value={searchKey}
|
||||||
/>
|
onChange={e => setSearchKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<Table
|
<Table
|
||||||
rowKey={e => e.id}
|
rowKey={e => e.id}
|
||||||
loading={searchClub.loading}
|
loading={searchClub.loading}
|
||||||
dataSource={searchClub.data?.clubs}
|
dataSource={datasource.clubs}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
scroll={{ x: 400 }}
|
scroll={{ x: 400 }}
|
||||||
pagination={{
|
pagination={clubType !== ClubType.我的收藏 ? {
|
||||||
current: page,
|
current: datasource.page,
|
||||||
showSizeChanger: false,
|
showSizeChanger: false,
|
||||||
total: searchClub.data?.total,
|
total: datasource.total,
|
||||||
onChange: (page) => {
|
onChange: (page) => {
|
||||||
setPage(page);
|
handleSearch(searchKey, clubType, page);
|
||||||
searchClub.runAsync(searchKey, page, clubType);
|
|
||||||
console.debug('onPageChange', { searchKey, page, clubType });
|
|
||||||
}
|
}
|
||||||
}}
|
} : false}
|
||||||
>
|
>
|
||||||
<Table.Column dataIndex="name" width={140} fixed="left" render={(name, { img }) => {
|
<Table.Column dataIndex="name" width={140} fixed="left" render={(name, { img }) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
38
src/components/ClubSummary.tsx
Normal file
38
src/components/ClubSummary.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Avatar, Button, Drawer, Flex, Skeleton, Typography } from "antd";
|
||||||
|
import { ChangeBackground } from "./ChangeBackground";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRequest } from "ahooks";
|
||||||
|
import type { ClubDetail } from "../types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
clubId: string;
|
||||||
|
}
|
||||||
|
export const ClubSummary = (props: Props) => {
|
||||||
|
|
||||||
|
const [isArticleOpen, setIsArticleOpen] = useState(false);
|
||||||
|
const requestClubSummary = useRequest<ClubDetail, []>(async () => {
|
||||||
|
return fetch(`/api/club/${props.clubId}`).then(r => r.json());
|
||||||
|
}, { manual: false, refreshDeps: [props.clubId] })
|
||||||
|
const info = requestClubSummary.data;
|
||||||
|
return (
|
||||||
|
<div style={{ width: '100%' }}>
|
||||||
|
{requestClubSummary.loading ? (
|
||||||
|
<Skeleton.Button block size="large" style={{ height: 300 }} active={requestClubSummary.loading} />
|
||||||
|
) : (
|
||||||
|
<Flex vertical align="center" justify="center">
|
||||||
|
<ChangeBackground url={info?.img} />
|
||||||
|
<Avatar src={info?.img} size={80} />
|
||||||
|
<Typography.Title>{info?.name}</Typography.Title>
|
||||||
|
<Flex gap={12}>
|
||||||
|
<Button onClick={() => setIsArticleOpen(true)}>查看公告</Button>
|
||||||
|
</Flex>
|
||||||
|
<Drawer size={'60vh'} open={isArticleOpen} onClose={() => setIsArticleOpen(false)} placement="bottom">
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<Typography.Paragraph style={{ whiteSpace: 'pre', textWrap: 'auto' }}>{info?.article}</Typography.Paragraph>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -4,8 +4,12 @@ import dayjs from "dayjs";
|
|||||||
import utc from 'dayjs/plugin/utc';
|
import utc from 'dayjs/plugin/utc';
|
||||||
import timezone from 'dayjs/plugin/timezone';
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
import { EyeOutlined } from "@ant-design/icons";
|
import { EyeOutlined } from "@ant-design/icons";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
import { useRunOnce } from "../hooks/useRunOnce";
|
||||||
|
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
interface EventCardProps {
|
interface EventCardProps {
|
||||||
eventInfo: IEventInfo;
|
eventInfo: IEventInfo;
|
||||||
@ -13,20 +17,35 @@ interface EventCardProps {
|
|||||||
|
|
||||||
export function EventCard(props: EventCardProps) {
|
export function EventCard(props: EventCardProps) {
|
||||||
const { eventInfo: e } = props;
|
const { eventInfo: e } = props;
|
||||||
const day = dayjs(e.startDate);
|
const day = useMemo(() => dayjs.tz(e.startDate, 'Asia/Tokyo'), [e]);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const handleView = useCallback(() => {
|
const handleView = useCallback(() => {
|
||||||
navigate(`/event/${e.matchId}`);
|
navigate(`/event/${e.matchId}`);
|
||||||
}, [e]);
|
}, [e]);
|
||||||
const messageFormat = useMemo(() => {
|
const [messageFormat, setMessageFormat] = useState('');
|
||||||
|
const getMessageFormat = useCallback(() => {
|
||||||
if (e.isFinished) {
|
if (e.isFinished) {
|
||||||
|
if (dayjs().diff(day, 'days') < 1) return `已结束`;
|
||||||
return `已结束 DD 天`;
|
return `已结束 DD 天`;
|
||||||
}
|
}
|
||||||
if (e.isProcessing) {
|
if (e.isProcessing) {
|
||||||
return '比赛进行中';
|
return '比赛进行中 HH:mm:ss';
|
||||||
}
|
}
|
||||||
return `还有 DD 天 HH 时开始`;
|
return `距离比赛开始还有 DD 天 HH:mm:ss`;
|
||||||
}, [e]);
|
}, [e, day]);
|
||||||
|
const updateMessageFormat = useCallback(() => {
|
||||||
|
const format = getMessageFormat();
|
||||||
|
setMessageFormat(format);
|
||||||
|
console.debug('format: %s', format);
|
||||||
|
}, [getMessageFormat])
|
||||||
|
useRunOnce(updateMessageFormat);
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout = day.toDate().getTime() - Date.now();
|
||||||
|
const id = setTimeout(() => {
|
||||||
|
updateMessageFormat();
|
||||||
|
}, timeout);
|
||||||
|
return () => clearTimeout(id);
|
||||||
|
}, [updateMessageFormat]);
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={e.matchId}
|
key={e.matchId}
|
||||||
@ -42,9 +61,9 @@ export function EventCard(props: EventCardProps) {
|
|||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Typography.Text type={e.isFinished ? undefined : 'success'}>{e.title}</Typography.Text>
|
<Typography.Text type={e.isFinished ? 'secondary' : 'success'}>{e.title}</Typography.Text>
|
||||||
<Statistic.Timer
|
<Statistic.Timer
|
||||||
type={e.isFinished ? 'countup' : 'countdown'}
|
type={e.isProcessing ? 'countup' : 'countdown'}
|
||||||
value={day.toDate().getTime()}
|
value={day.toDate().getTime()}
|
||||||
format={messageFormat}
|
format={messageFormat}
|
||||||
styles={{ content: e.isFinished ? { color: 'gray' } : {} }}
|
styles={{ content: e.isFinished ? { color: 'gray' } : {} }}
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
import { Button, Drawer, Flex, Select, Space } from 'antd';
|
import { Button, Drawer, Flex, Select } from 'antd';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { clubs } from './clubList';
|
import { clubs } from './clubList';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { ClubEvenList } from '../ClubEventList';
|
import { ClubEvenList } from '../ClubEventList';
|
||||||
import { DeleteOutlined, SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
import { useClubStore } from '../../store/useClubStore';
|
import { useClubStore } from '../../store/useClubStore';
|
||||||
import { ClubSearchTable } from '../ClubSearchTable';
|
import { ClubSearchTable } from '../ClubSearchTable';
|
||||||
import { createGlobalStyle } from 'styled-components';
|
import styled, { createGlobalStyle } from 'styled-components';
|
||||||
|
import { useLocalStorageState } from 'ahooks';
|
||||||
|
import { CLUB_SELECTOR_KEY } from '../../utils/constants';
|
||||||
|
import { ClubSummary } from '../ClubSummary';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
}
|
}
|
||||||
@ -21,9 +24,15 @@ const ChangeDrawerHeight = createGlobalStyle`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
.club-selector {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const GameSelector: React.FC<Props> = () => {
|
export const GameSelector: React.FC<Props> = () => {
|
||||||
const [options, setOptions] = useState<{ label: React.ReactNode, value: string }[]>(defaultOptions);
|
const [options, setOptions] = useState<{ label: React.ReactNode, value: string }[]>(defaultOptions);
|
||||||
const [clubId, setClubId] = useState<string>(clubs[0]?.clubId ?? '');
|
const [clubId, setClubId] = useLocalStorageState<string>(CLUB_SELECTOR_KEY, { defaultValue: clubs[0]?.clubId ?? '' });
|
||||||
const clubStore = useClubStore(store => store);
|
const clubStore = useClubStore(store => store);
|
||||||
const handleClubChange = useCallback(async (id: string) => {
|
const handleClubChange = useCallback(async (id: string) => {
|
||||||
setClubId(id);
|
setClubId(id);
|
||||||
@ -32,15 +41,7 @@ export const GameSelector: React.FC<Props> = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (clubStore.clubs.length) {
|
if (clubStore.clubs.length) {
|
||||||
setOptions([...defaultOptions, ...clubStore.clubs.map(info => ({
|
setOptions([...defaultOptions, ...clubStore.clubs.map(info => ({
|
||||||
label: (
|
label: info.name,
|
||||||
<Flex align='center' justify='space-between'>
|
|
||||||
<span>{info.name}</span>
|
|
||||||
<Button type='link' icon={<DeleteOutlined />} onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
clubStore.unFav(info.id);
|
|
||||||
}} />
|
|
||||||
</Flex>
|
|
||||||
),
|
|
||||||
value: info.id
|
value: info.id
|
||||||
}))]);
|
}))]);
|
||||||
return;
|
return;
|
||||||
@ -49,23 +50,28 @@ export const GameSelector: React.FC<Props> = () => {
|
|||||||
setClubId(defaultOptions[0]?.value ?? '');
|
setClubId(defaultOptions[0]?.value ?? '');
|
||||||
}, [clubStore]);
|
}, [clubStore]);
|
||||||
return (
|
return (
|
||||||
<Space orientation='vertical' style={{ width: '100%' }}>
|
<StyledContainer style={{ width: '100%' }}>
|
||||||
<ChangeDrawerHeight />
|
<ChangeDrawerHeight />
|
||||||
<Flex gap={12} justify='center' align='center'>
|
<div style={{ display: 'block', marginBottom: 12 }}>
|
||||||
<Select
|
<Flex wrap gap={12} justify='center' align='center'>
|
||||||
style={{ width: '100%' }}
|
<Flex flex={4}>
|
||||||
placeholder={'请选择俱乐部'}
|
<Select
|
||||||
size='large'
|
className='club-selector'
|
||||||
value={clubId}
|
placeholder={'请选择俱乐部'}
|
||||||
options={options}
|
size='large'
|
||||||
onChange={handleClubChange}
|
value={clubId}
|
||||||
/>
|
options={options}
|
||||||
<Flex gap={12}>
|
onChange={handleClubChange}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
<Flex flex={1}>
|
<Flex flex={1}>
|
||||||
<Button block size='large' icon={<SearchOutlined />} onClick={() => setSearchOpen(true)}>查找</Button>
|
<Button block size='large' icon={<SearchOutlined />} onClick={() => setSearchOpen(true)}>
|
||||||
|
查找
|
||||||
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</div>
|
||||||
|
<ClubSummary clubId={clubId} />
|
||||||
<ClubEvenList clubId={clubId} />
|
<ClubEvenList clubId={clubId} />
|
||||||
<Drawer
|
<Drawer
|
||||||
className='search-table'
|
className='search-table'
|
||||||
@ -91,6 +97,6 @@ export const GameSelector: React.FC<Props> = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</Space>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/hooks/useRunOnce.ts
Normal file
13
src/hooks/useRunOnce.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { useEffect, useRef } from "react"
|
||||||
|
|
||||||
|
export const useRunOnce = (callback: () => void) => {
|
||||||
|
const done = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setTimeout(() => {
|
||||||
|
if (done.current) return;
|
||||||
|
done.current = true;
|
||||||
|
callback();
|
||||||
|
}, 0);
|
||||||
|
return () => clearTimeout(id);
|
||||||
|
}, [callback]);
|
||||||
|
}
|
||||||
@ -56,11 +56,11 @@ const server = serve({
|
|||||||
events = await KaiqiuService.listClubEvents(id, page);
|
events = await KaiqiuService.listClubEvents(id, page);
|
||||||
allEvents = allEvents.concat(...events.data);
|
allEvents = allEvents.concat(...events.data);
|
||||||
}
|
}
|
||||||
const noGeo = !events.geo.lat && !events.geo.lng;
|
|
||||||
const geo = { lat: events.geo.lat, lon: events.geo.lng };
|
|
||||||
|
|
||||||
const configs: ics.EventAttributes[] = allEvents.filter(e => !e.isFinished).map(e => ({
|
const configs: ics.EventAttributes[] = allEvents.filter(e => !e.isFinished).map(e => ({
|
||||||
...(noGeo ? {} : { geo }),
|
...(clubInfo?.geo ? { geo: {
|
||||||
|
lat: clubInfo.geo.lat,
|
||||||
|
lon: clubInfo.geo.lng,
|
||||||
|
} } : {}),
|
||||||
start: dayjs.tz(e.startDate, 'Asia/Tokyo').format('YYYY-MM-DD-HH-mm').split('-').map(v => Number(v)) as any,
|
start: dayjs.tz(e.startDate, 'Asia/Tokyo').format('YYYY-MM-DD-HH-mm').split('-').map(v => Number(v)) as any,
|
||||||
duration: { hours: 6, minutes: 30 },
|
duration: { hours: 6, minutes: 30 },
|
||||||
title: e.title,
|
title: e.title,
|
||||||
|
|||||||
@ -51,7 +51,8 @@ export class KaiqiuService {
|
|||||||
const src = $('#space_avatar img').attr('src') ?? '';
|
const src = $('#space_avatar img').attr('src') ?? '';
|
||||||
const img = src.startsWith('http') ? src : 'https://kaiqiuwang.cc/home/image/nologo.jpg';
|
const img = src.startsWith('http') ? src : 'https://kaiqiuwang.cc/home/image/nologo.jpg';
|
||||||
const article = $('.article').text().trim();
|
const article = $('.article').text().trim();
|
||||||
return { id: clubId, name, img, article };
|
const geo = await this.#getClubLocation(clubId);
|
||||||
|
return { id: clubId, name, img, article, geo };
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #fetchEventListHTML(clubId: string, page: number) {
|
static async #fetchEventListHTML(clubId: string, page: number) {
|
||||||
@ -64,10 +65,6 @@ export class KaiqiuService {
|
|||||||
if (!clubId) return {
|
if (!clubId) return {
|
||||||
data: [],
|
data: [],
|
||||||
total: 0,
|
total: 0,
|
||||||
geo: {
|
|
||||||
lat: 0,
|
|
||||||
lng: 0,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const key = `my-kaiqiuwang:club-events:${clubId}:page-${page}`;
|
const key = `my-kaiqiuwang:club-events:${clubId}:page-${page}`;
|
||||||
let html = await redis.get(key).catch(() => '');
|
let html = await redis.get(key).catch(() => '');
|
||||||
@ -75,12 +72,8 @@ export class KaiqiuService {
|
|||||||
html = await this.#fetchEventListHTML(clubId, page);
|
html = await this.#fetchEventListHTML(clubId, page);
|
||||||
redis.setex(key, 60 * 60 * REDIS_CACHE_HOUR, html);
|
redis.setex(key, 60 * 60 * REDIS_CACHE_HOUR, html);
|
||||||
}
|
}
|
||||||
const geo = await this.#getClubLocation(clubId);
|
|
||||||
const data = await this.#parseEventList(html);
|
const data = await this.#parseEventList(html);
|
||||||
return {
|
return data;
|
||||||
...data,
|
|
||||||
geo,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #parseEventList(html: string) {
|
static async #parseEventList(html: string) {
|
||||||
@ -130,6 +123,7 @@ export class KaiqiuService {
|
|||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
const lng = Number($('#lng').val());
|
const lng = Number($('#lng').val());
|
||||||
const lat = Number($('#lat').val());
|
const lat = Number($('#lat').val());
|
||||||
|
if (!lng && !lat) return null;
|
||||||
return { lng, lat };
|
return { lng, lat };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
src/utils/constants.ts
Normal file
3
src/utils/constants.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
export const CLUB_SELECTOR_KEY = 'CLUB_SELECTOR';
|
||||||
|
export const STORE_PAGE_LIST_KEY = 'events-page-keys';
|
||||||
Loading…
Reference in New Issue
Block a user