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:
kyuuseiryuu 2026-03-15 02:33:34 +09:00
parent 4825f41337
commit 0f9e80856b
9 changed files with 234 additions and 114 deletions

View File

@ -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}

View File

@ -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 (

View 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>
);
}

View File

@ -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' } : {} }}

View File

@ -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
View 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]);
}

View File

@ -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,

View File

@ -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
View File

@ -0,0 +1,3 @@
export const CLUB_SELECTOR_KEY = 'CLUB_SELECTOR';
export const STORE_PAGE_LIST_KEY = 'events-page-keys';