feat(search): support province and city filtering for club search
- Add CascaderProvinceCity component to ClubSearchTable for selecting province/city. - Update KaiqiuService.findClub to accept and append province/city parameters to the query URL. - Update server-side API handler to extract province/city from query parameters. - Improve date parsing logic in event processing to handle invalid start dates gracefully. - Refactor uidScoreStore to use a loop instead of map for better performance/clarity. - Add debug logging for profile caching in xcxApi. The search functionality now supports filtering clubs by administrative division, providing users with more precise search results.
This commit is contained in:
parent
03aa0ead18
commit
30339596d7
36
src/components/CascaderProvinceCity.tsx
Normal file
36
src/components/CascaderProvinceCity.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { Cascader } from "antd";
|
||||
import { useMemo } from "react";
|
||||
|
||||
const provinceCityConfig = require('../private/city.json');
|
||||
|
||||
interface Props {
|
||||
onChange?: (province?: string, city?: string) => void;
|
||||
}
|
||||
|
||||
export const CascaderProvinceCity = (props: Props) => {
|
||||
const options = useMemo(() => {
|
||||
const keys = Object.keys(provinceCityConfig);
|
||||
const options = keys.map(key => {
|
||||
return {
|
||||
label: key,
|
||||
value: key,
|
||||
children: (provinceCityConfig[key] as string[]).map(k => ({
|
||||
label: k,
|
||||
value: k,
|
||||
}))
|
||||
}
|
||||
});
|
||||
return options;
|
||||
}, []);
|
||||
return (
|
||||
<Cascader
|
||||
allowClear
|
||||
changeOnSelect
|
||||
showSearch
|
||||
placeholder={'省/市'}
|
||||
style={{ width: '100%' }}
|
||||
onChange={([a, b] = []) => props.onChange?.(a, b)}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -4,6 +4,7 @@ import { useCallback, useRef, useState } from "react";
|
||||
import type { ClubInfo } from "../types";
|
||||
import { EyeOutlined, StarFilled, StarOutlined } from "@ant-design/icons";
|
||||
import { useClubStore } from "../store/useClubStore";
|
||||
import { CascaderProvinceCity } from "./CascaderProvinceCity";
|
||||
|
||||
|
||||
type ClickType = 'VIEW' | 'FAV';
|
||||
@ -24,6 +25,7 @@ const initData = { clubs: [], total: 0, page: 1 };
|
||||
|
||||
export const ClubSearchTable = (props: Props) => {
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const [provinceCity, setProvinceCity] = useState<{ province?: string, city?: string }>({ province: '', city: '' });
|
||||
const [datasource, setDatasource] = useState<Data>(initData);
|
||||
const dataRef = useRef<Partial<Record<ClubType, Data>>>({});
|
||||
const searchClub = useRequest<Data, [string, number, ClubType]>(async (
|
||||
@ -31,15 +33,23 @@ export const ClubSearchTable = (props: Props) => {
|
||||
page = 1,
|
||||
clubType = ClubType.积分俱乐部,
|
||||
) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('key', searchKey);
|
||||
searchParams.append('page', page.toString());
|
||||
searchParams.append('province', provinceCity.province ?? '');
|
||||
searchParams.append('city', provinceCity.city ?? '');
|
||||
if (clubType) {
|
||||
searchParams.append('normalClub', '1');
|
||||
}
|
||||
const resp: Data = await fetch(
|
||||
`/api/club/find?key=${searchKey}&page=${page}${clubType ? '&normalClub=1' : ''}`
|
||||
`/api/club/find?${searchParams.toString()}`
|
||||
).then(e => e.json());
|
||||
dataRef.current[clubType] = {
|
||||
...resp,
|
||||
page,
|
||||
};
|
||||
return resp;
|
||||
}, { manual: true });
|
||||
}, { manual: true, refreshDeps: [provinceCity] });
|
||||
const clubStore = useClubStore(store => store);
|
||||
const [clubType, setClubType] = useState<ClubType>(ClubType.积分俱乐部);
|
||||
const handleSearch = useCallback(async (searchKey: string, clubType: ClubType, page = 1) => {
|
||||
@ -83,16 +93,23 @@ export const ClubSearchTable = (props: Props) => {
|
||||
]}
|
||||
/>
|
||||
{clubType !== ClubType.我的收藏 ? (
|
||||
<Input.Search
|
||||
allowClear
|
||||
onSearch={e => {
|
||||
setSearchKey(e);
|
||||
handleSearch(e, clubType);
|
||||
}}
|
||||
onPressEnter={() => handleSearch(searchKey, clubType)}
|
||||
value={searchKey}
|
||||
onChange={e => setSearchKey(e.target.value)}
|
||||
/>
|
||||
<Flex wrap gap={8} style={{ width: '100%' }}>
|
||||
<Flex flex={1}>
|
||||
<CascaderProvinceCity onChange={(province, city) => setProvinceCity({ province, city })} />
|
||||
</Flex>
|
||||
<Flex flex={2} style={{ minWidth: 400 }}>
|
||||
<Input.Search
|
||||
allowClear
|
||||
onSearch={e => {
|
||||
setSearchKey(e);
|
||||
handleSearch(e, clubType);
|
||||
}}
|
||||
onPressEnter={() => handleSearch(searchKey, clubType)}
|
||||
value={searchKey}
|
||||
onChange={e => setSearchKey(e.target.value)}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
) : null}
|
||||
<Table
|
||||
rowKey={e => e.id}
|
||||
|
||||
@ -110,7 +110,9 @@ const server = Bun.serve({
|
||||
const key = searchParams.get('key') ?? '';
|
||||
const normalClub = searchParams.get('normalClub');
|
||||
const page = Number(searchParams.get('page'));
|
||||
const data = await KaiqiuService.findClub(key, page, Boolean(normalClub));
|
||||
const province = searchParams.get('province') ?? '';
|
||||
const city = searchParams.get('city') ?? '';
|
||||
const data = await KaiqiuService.findClub(key, page, Boolean(normalClub), province, city);
|
||||
return Response.json(data);
|
||||
}
|
||||
},
|
||||
|
||||
@ -11,9 +11,9 @@ dayjs.extend(timezone);
|
||||
export class KaiqiuService {
|
||||
static #baseURL = 'https://kaiqiuwang.cc';
|
||||
|
||||
public static async findClub(name: string, page = 1, normalClub?: boolean) {
|
||||
public static async findClub(name: string, page = 1, normalClub?: boolean, province?: string, city?: string) {
|
||||
const searchKey = encodeURIComponent(name);
|
||||
const url = `${this.#baseURL}/home/space.php?province=&city=&searchkey=${searchKey}&searchsubmit=%E6%90%9C%E7%B4%A2&searchmode=1&do=mtag&view=hot&${normalClub ? 'page2': 'page'}=${page}`;
|
||||
const url = `${this.#baseURL}/home/space.php?province=${province}&city=${city}&searchkey=${searchKey}&searchsubmit=%E6%90%9C%E7%B4%A2&searchmode=1&do=mtag&view=hot&${normalClub ? 'page2': 'page'}=${page}`;
|
||||
const html = await fetch(url, {
|
||||
headers: htmlRequestHeaders,
|
||||
}).then(res => res.text());
|
||||
@ -146,13 +146,13 @@ export class KaiqiuService {
|
||||
const eventContent = $('.event_content').text().replace(/(\r|\n)/g, ',').split(',').filter(Boolean).join(' ');
|
||||
const { y, M, D, H, m} = /比赛开始:.*?(?<y>\d{4})年(?<M>\d{2})月(?<D>\d{2})日 \w+ (?<H>\d{2}):(?<m>\d{2})/
|
||||
.exec(eventContent)?.groups ?? {};
|
||||
const startDate = y ? `${y}-${M}-${D} ${H}:${m}` : '';
|
||||
const startDate = [y, M, D, H, m].every(Boolean) ? `${y}-${M}-${D} ${H}:${m}` : '0000-00';
|
||||
const title = $('#mainarea > h2 > a:nth-child(3)').text().trim();
|
||||
const nums = $('#content > div:nth-child(1) > div > div.event_content > ul > li:nth-child(2)').text();
|
||||
const see = /(?<see>\d+)\b/.exec($('#content > div:nth-child(1) > div > div.event_content > ul > li:nth-child(1)').text())?.groups?.see ?? '';
|
||||
const location = $('#content > div:nth-child(1) > div > div.event_content > dl > dd:nth-child(6) > a')
|
||||
.text().split(' ')?.[1]?.trim() ?? '';
|
||||
const startTime = dayjs.tz(startDate, 'Asia/Tokyo');
|
||||
const startTime = dayjs.tz(startDate || new Date(), 'Asia/Tokyo');
|
||||
const overTime = startTime.add(6, 'hour');
|
||||
const now = dayjs();
|
||||
const isProcessing = now.isAfter(startTime) && now.isBefore(overTime);
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { xcxApi } from '../utils/server';
|
||||
|
||||
export const getUidScore = async (uids: string[], force?: boolean): Promise<Record<string, string>> => {
|
||||
const jobs = uids.map(async uid => {
|
||||
const entries: [string, string][] = [];
|
||||
for (const uid of uids) {
|
||||
const profile = await xcxApi.getAdvProfile(uid, force);
|
||||
return [uid, profile?.score ?? ''];
|
||||
});
|
||||
return Object.fromEntries(await Promise.all(jobs));
|
||||
entries.push([uid, profile?.score ?? '']);
|
||||
}
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
@ -38,6 +38,7 @@ export class XCXAPI {
|
||||
if (!cacheProfile || force) {
|
||||
const url = `/api/User/adv_profile?uid=${uid}`;
|
||||
const profile = await this.#fetch<XCXProfile>(url);
|
||||
console.debug('Save profile: %s - %s', profile?.uid, profile?.realname);
|
||||
await redis.setex(`my-kaiqiuwang:profile:${uid}`, 60 * 60 * 24, JSON.stringify(profile));
|
||||
return profile;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user