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 type { IEventInfo } from "../types";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { EventCard } from "./EventCard";
|
||||
import { useRequest } from "ahooks";
|
||||
import { CalendarOutlined } from "@ant-design/icons";
|
||||
import { STORE_PAGE_LIST_KEY } from "../utils/constants";
|
||||
import { useRunOnce } from "../hooks/useRunOnce";
|
||||
|
||||
interface Props {
|
||||
clubId: string;
|
||||
}
|
||||
|
||||
const getStorageKey = (clubId: string) => `events-page-${clubId}`;
|
||||
|
||||
export const ClubEvenList = (props: Props) => {
|
||||
const [showAllDisable, setShowAllDiable] = useState(false);
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const [paginationControlVisible, setPaginationControlVisible] = useState(true);
|
||||
const [showFinishedEvents, setShowFinishedEvents] = useState(false);
|
||||
const requestEvents = useRequest<{data: IEventInfo[]; total: number; }, [string, number]>(
|
||||
async (clubId: string, page = 1) => {
|
||||
if (!clubId) return [];
|
||||
return (await fetch(`/api/club/${clubId}/events?page=${page}`)).json()
|
||||
}, { manual: true });
|
||||
const [page, setPage] = useState(1);
|
||||
if (!clubId) return { data: [], total: 0 };
|
||||
return fetch(`/api/club/${clubId}/events?page=${page}`).then(e => e.json());
|
||||
}, { manual: true, refreshDeps: [] });
|
||||
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) => {
|
||||
setPage(page);
|
||||
requestEvents.runAsync(props.clubId, page);
|
||||
}, [props.clubId]);
|
||||
}, [props.clubId, setPage]);
|
||||
const pagination = useMemo(() => {
|
||||
return {
|
||||
page,
|
||||
@ -30,26 +40,26 @@ export const ClubEvenList = (props: Props) => {
|
||||
}, [requestEvents.data?.total, page]);
|
||||
const visibleEvents = useMemo(() => {
|
||||
const events = requestEvents.data?.data || [];
|
||||
if (showAll) return events;
|
||||
if (showFinishedEvents) return events;
|
||||
return events.filter(e => !e.isFinished);
|
||||
}, [showAll, requestEvents.data?.data]);
|
||||
useEffect(() => {
|
||||
if (!showAll && page > 1) {
|
||||
onPageChange(1); // Reset page
|
||||
}, [showFinishedEvents, requestEvents.data?.data]);
|
||||
const onPageInit = useCallback(async () => {
|
||||
const storePageKey = getStorageKey(props.clubId);
|
||||
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]);
|
||||
useEffect(() => {
|
||||
const id = setTimeout(async () => {
|
||||
const data = await requestEvents.runAsync(props.clubId, 1).then(res => res.data);
|
||||
setPage(1);
|
||||
if (data.length) {
|
||||
const isAllFinishedOrAllNotFinished = data.every(e => e.isFinished) || data.every(e => !e.isFinished);
|
||||
setShowAll(isAllFinishedOrAllNotFinished);
|
||||
setShowAllDiable(data.every(e => !e.isFinished));
|
||||
}
|
||||
}, 100);
|
||||
return () => clearTimeout(id);
|
||||
}, [props.clubId]);
|
||||
const prePage = Number(sessionStorage.getItem(storePageKey)) || 1;
|
||||
const data = await requestEvents.runAsync(props.clubId, prePage).then(res => res.data);
|
||||
setPage(prePage);
|
||||
if (data.length) {
|
||||
const isAllFinishedOrAllNotFinished = data.every(e => e.isFinished) || data.every(e => !e.isFinished);
|
||||
setShowFinishedEvents(prePage !== 1 || isAllFinishedOrAllNotFinished);
|
||||
setPaginationControlVisible(prePage === 1 ? !isAllFinishedOrAllNotFinished : false);
|
||||
}
|
||||
}, [props]);
|
||||
useRunOnce(onPageInit);
|
||||
const handleAddToCalendar = useCallback(() => {
|
||||
const url = `${window.location.origin}/api/club/${props.clubId}/calendar.ics`;
|
||||
const uri = url.replace(/^http(s)?/, 'webcal');
|
||||
@ -59,12 +69,12 @@ export const ClubEvenList = (props: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Divider>
|
||||
{showAllDisable ? null : (
|
||||
{paginationControlVisible ? (
|
||||
<Space>
|
||||
<Typography.Text type="secondary">显示已结束的活动</Typography.Text>
|
||||
<Switch checked={showAll} onChange={() => setShowAll(!showAll)} unCheckedChildren="隐藏" checkedChildren="显示" />
|
||||
<Switch checked={showFinishedEvents} onChange={() => setShowFinishedEvents(!showFinishedEvents)} unCheckedChildren="隐藏" checkedChildren="显示" />
|
||||
</Space>
|
||||
)}
|
||||
) : null}
|
||||
</Divider>
|
||||
<Flex wrap vertical gap={12} justify="center" align="center">
|
||||
<Button
|
||||
@ -74,7 +84,7 @@ export const ClubEvenList = (props: Props) => {
|
||||
>
|
||||
订阅该俱乐部比赛
|
||||
</Button>
|
||||
{showAll ? (
|
||||
{showFinishedEvents ? (
|
||||
<Pagination
|
||||
showQuickJumper
|
||||
total={pagination.total}
|
||||
@ -96,7 +106,7 @@ export const ClubEvenList = (props: Props) => {
|
||||
);
|
||||
}) : <Empty description="暂无活动" />}</>)}
|
||||
|
||||
{showAll ? (
|
||||
{showFinishedEvents ? (
|
||||
<Pagination
|
||||
showQuickJumper
|
||||
total={pagination.total}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useRequest } from "ahooks";
|
||||
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 { EyeOutlined, StarFilled, StarOutlined } from "@ant-design/icons";
|
||||
import { useClubStore } from "../store/useClubStore";
|
||||
@ -12,65 +12,102 @@ interface Props {
|
||||
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) => {
|
||||
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,
|
||||
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' : ''}`
|
||||
).then(e => e.json());
|
||||
dataRef.current[clubType] = {
|
||||
...resp,
|
||||
page,
|
||||
};
|
||||
return resp;
|
||||
}, { manual: true });
|
||||
const [page, setPage] = useState(1);
|
||||
const [clubType, setClubType] = useState(0);
|
||||
const handleSearch = useCallback((searchKey: string, clubType: number) => {
|
||||
setPage(1);
|
||||
searchClub.runAsync(searchKey, 1, clubType);
|
||||
const clubStore = useClubStore(store => store);
|
||||
const [clubType, setClubType] = useState<ClubType>(ClubType.积分俱乐部);
|
||||
const handleSearch = useCallback(async (searchKey: string, clubType: ClubType, page = 1) => {
|
||||
switch (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 (
|
||||
<Flex vertical gap={12} justify="center" align="center">
|
||||
<Radio.Group
|
||||
optionType="button"
|
||||
value={clubType}
|
||||
onChange={e => {
|
||||
setClubType(e.target.value);
|
||||
if (!searchKey) return;
|
||||
handleSearch(searchKey, e.target.value);
|
||||
}}
|
||||
onChange={e => handleTypeChange(e.target.value)}
|
||||
options={[
|
||||
{ label: '积分俱乐部', value: 0 },
|
||||
{ label: '俱乐部', value: 1 },
|
||||
{ label: '积分俱乐部', value: ClubType.积分俱乐部 },
|
||||
{ label: '俱乐部', value: ClubType.俱乐部 },
|
||||
{ label: '我的收藏', value: ClubType.我的收藏 },
|
||||
]}
|
||||
/>
|
||||
<Input.Search
|
||||
allowClear
|
||||
onSearch={e => {
|
||||
setSearchKey(e);
|
||||
handleSearch(e, clubType);
|
||||
}}
|
||||
onPressEnter={() => handleSearch(searchKey, clubType)}
|
||||
value={searchKey}
|
||||
onChange={e => setSearchKey(e.target.value)}
|
||||
/>
|
||||
{clubType !== ClubType.我的收藏 ? (
|
||||
<Input.Search
|
||||
allowClear
|
||||
onSearch={e => {
|
||||
setSearchKey(e);
|
||||
handleSearch(e, clubType);
|
||||
}}
|
||||
onPressEnter={() => handleSearch(searchKey, clubType)}
|
||||
value={searchKey}
|
||||
onChange={e => setSearchKey(e.target.value)}
|
||||
/>
|
||||
) : null}
|
||||
<Table
|
||||
rowKey={e => e.id}
|
||||
loading={searchClub.loading}
|
||||
dataSource={searchClub.data?.clubs}
|
||||
dataSource={datasource.clubs}
|
||||
style={{ width: '100%' }}
|
||||
scroll={{ x: 400 }}
|
||||
pagination={{
|
||||
current: page,
|
||||
pagination={clubType !== ClubType.我的收藏 ? {
|
||||
current: datasource.page,
|
||||
showSizeChanger: false,
|
||||
total: searchClub.data?.total,
|
||||
total: datasource.total,
|
||||
onChange: (page) => {
|
||||
setPage(page);
|
||||
searchClub.runAsync(searchKey, page, clubType);
|
||||
console.debug('onPageChange', { searchKey, page, clubType });
|
||||
handleSearch(searchKey, clubType, page);
|
||||
}
|
||||
}}
|
||||
} : false}
|
||||
>
|
||||
<Table.Column dataIndex="name" width={140} fixed="left" render={(name, { img }) => {
|
||||
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 timezone from 'dayjs/plugin/timezone';
|
||||
import { EyeOutlined } from "@ant-design/icons";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useRunOnce } from "../hooks/useRunOnce";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
interface EventCardProps {
|
||||
eventInfo: IEventInfo;
|
||||
@ -13,20 +17,35 @@ interface EventCardProps {
|
||||
|
||||
export function EventCard(props: EventCardProps) {
|
||||
const { eventInfo: e } = props;
|
||||
const day = dayjs(e.startDate);
|
||||
const day = useMemo(() => dayjs.tz(e.startDate, 'Asia/Tokyo'), [e]);
|
||||
const navigate = useNavigate();
|
||||
const handleView = useCallback(() => {
|
||||
navigate(`/event/${e.matchId}`);
|
||||
}, [e]);
|
||||
const messageFormat = useMemo(() => {
|
||||
const [messageFormat, setMessageFormat] = useState('');
|
||||
const getMessageFormat = useCallback(() => {
|
||||
if (e.isFinished) {
|
||||
if (dayjs().diff(day, 'days') < 1) return `已结束`;
|
||||
return `已结束 DD 天`;
|
||||
}
|
||||
if (e.isProcessing) {
|
||||
return '比赛进行中';
|
||||
return '比赛进行中 HH:mm:ss';
|
||||
}
|
||||
return `还有 DD 天 HH 时开始`;
|
||||
}, [e]);
|
||||
return `距离比赛开始还有 DD 天 HH:mm:ss`;
|
||||
}, [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 (
|
||||
<Card
|
||||
key={e.matchId}
|
||||
@ -42,9 +61,9 @@ export function EventCard(props: EventCardProps) {
|
||||
</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
|
||||
type={e.isFinished ? 'countup' : 'countdown'}
|
||||
type={e.isProcessing ? 'countup' : 'countdown'}
|
||||
value={day.toDate().getTime()}
|
||||
format={messageFormat}
|
||||
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 { clubs } from './clubList';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { ClubEvenList } from '../ClubEventList';
|
||||
import { DeleteOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { useClubStore } from '../../store/useClubStore';
|
||||
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 {
|
||||
}
|
||||
@ -21,9 +24,15 @@ const ChangeDrawerHeight = createGlobalStyle`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
.club-selector {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const GameSelector: React.FC<Props> = () => {
|
||||
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 handleClubChange = useCallback(async (id: string) => {
|
||||
setClubId(id);
|
||||
@ -32,15 +41,7 @@ export const GameSelector: React.FC<Props> = () => {
|
||||
useEffect(() => {
|
||||
if (clubStore.clubs.length) {
|
||||
setOptions([...defaultOptions, ...clubStore.clubs.map(info => ({
|
||||
label: (
|
||||
<Flex align='center' justify='space-between'>
|
||||
<span>{info.name}</span>
|
||||
<Button type='link' icon={<DeleteOutlined />} onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
clubStore.unFav(info.id);
|
||||
}} />
|
||||
</Flex>
|
||||
),
|
||||
label: info.name,
|
||||
value: info.id
|
||||
}))]);
|
||||
return;
|
||||
@ -49,23 +50,28 @@ export const GameSelector: React.FC<Props> = () => {
|
||||
setClubId(defaultOptions[0]?.value ?? '');
|
||||
}, [clubStore]);
|
||||
return (
|
||||
<Space orientation='vertical' style={{ width: '100%' }}>
|
||||
<StyledContainer style={{ width: '100%' }}>
|
||||
<ChangeDrawerHeight />
|
||||
<Flex gap={12} justify='center' align='center'>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
placeholder={'请选择俱乐部'}
|
||||
size='large'
|
||||
value={clubId}
|
||||
options={options}
|
||||
onChange={handleClubChange}
|
||||
/>
|
||||
<Flex gap={12}>
|
||||
<div style={{ display: 'block', marginBottom: 12 }}>
|
||||
<Flex wrap gap={12} justify='center' align='center'>
|
||||
<Flex flex={4}>
|
||||
<Select
|
||||
className='club-selector'
|
||||
placeholder={'请选择俱乐部'}
|
||||
size='large'
|
||||
value={clubId}
|
||||
options={options}
|
||||
onChange={handleClubChange}
|
||||
/>
|
||||
</Flex>
|
||||
<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>
|
||||
</div>
|
||||
<ClubSummary clubId={clubId} />
|
||||
<ClubEvenList clubId={clubId} />
|
||||
<Drawer
|
||||
className='search-table'
|
||||
@ -91,6 +97,6 @@ export const GameSelector: React.FC<Props> = () => {
|
||||
}}
|
||||
/>
|
||||
</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);
|
||||
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 => ({
|
||||
...(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,
|
||||
duration: { hours: 6, minutes: 30 },
|
||||
title: e.title,
|
||||
|
||||
@ -51,7 +51,8 @@ export class KaiqiuService {
|
||||
const src = $('#space_avatar img').attr('src') ?? '';
|
||||
const img = src.startsWith('http') ? src : 'https://kaiqiuwang.cc/home/image/nologo.jpg';
|
||||
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) {
|
||||
@ -64,10 +65,6 @@ export class KaiqiuService {
|
||||
if (!clubId) return {
|
||||
data: [],
|
||||
total: 0,
|
||||
geo: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
};
|
||||
const key = `my-kaiqiuwang:club-events:${clubId}:page-${page}`;
|
||||
let html = await redis.get(key).catch(() => '');
|
||||
@ -75,12 +72,8 @@ export class KaiqiuService {
|
||||
html = await this.#fetchEventListHTML(clubId, page);
|
||||
redis.setex(key, 60 * 60 * REDIS_CACHE_HOUR, html);
|
||||
}
|
||||
const geo = await this.#getClubLocation(clubId);
|
||||
const data = await this.#parseEventList(html);
|
||||
return {
|
||||
...data,
|
||||
geo,
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
static async #parseEventList(html: string) {
|
||||
@ -130,6 +123,7 @@ export class KaiqiuService {
|
||||
const $ = cheerio.load(html);
|
||||
const lng = Number($('#lng').val());
|
||||
const lat = Number($('#lat').val());
|
||||
if (!lng && !lat) return null;
|
||||
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