feat(global): find user

This commit is contained in:
kyuuseiryuu 2026-02-10 11:11:08 +09:00
parent 4dd3daf909
commit bbc6a592f4
10 changed files with 135 additions and 6 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"postman.settings.dotenv-detection-notification-visibility": false
}

View File

@ -3,6 +3,7 @@ import { ClubSelector } from "./components/GameSelector";
import type { IEventInfo } from "./types"; import type { IEventInfo } from "./types";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import "./index.css"; import "./index.css";
import { FindUserButton } from "./components/FindUser";
export function App() { export function App() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -14,6 +15,7 @@ export function App() {
<div className="app"> <div className="app">
<h1></h1> <h1></h1>
<ClubSelector onGameClick={handleGameClick} /> <ClubSelector onGameClick={handleGameClick} />
<FindUserButton />
</div> </div>
); );
} }

View File

@ -0,0 +1,70 @@
import { SearchOutlined } from "@ant-design/icons";
import { useLocalStorageState, useRequest } from "ahooks";
import { FloatButton, Drawer, Input, Table } from "antd";
import { useState } from "react";
import type { XCXFindUser, XCXFindUserResp } from "../../types";
import User from "../User";
import { SEX } from "../../utils";
import dayjs from "dayjs";
export function FindUserButton() {
const [open, setOpen] = useState(false);
const [searchKey, setSearchKey] = useLocalStorageState<string>('findUser:searchKey');
const findUserReq = useRequest(async (page: number = 1) => {
const findOutUsers: XCXFindUserResp = await (await fetch(`/api/user/find?page=${page}&key=${searchKey}`)).json();
return findOutUsers;
}, { manual: true, refreshDeps: [searchKey], cacheKey: 'findUser:result' });
return (
<>
<FloatButton
icon={<SearchOutlined />}
onClick={() => setOpen(true)}
/>
<Drawer
placement="bottom"
open={open}
onClose={() => setOpen(false)}
loading={findUserReq.loading}
size="80vh"
>
<Input.Search
allowClear
size="large"
placeholder="输入昵称或姓名查找"
value={searchKey}
onChange={e => setSearchKey(e.target.value)}
onSearch={async () => findUserReq.runAsync()}
/>
<Table
size="small"
rowKey={e => e.uid}
dataSource={findUserReq.data?.data}
pagination={{
total: findUserReq.data?.total,
current: findUserReq.data?.current_page,
pageSize: findUserReq.data?.per_page,
onChange(page) {
findUserReq.run(page);
},
}}
>
<Table.Column
align="center"
title="昵称/姓名"
dataIndex={'username2'}
render={(username2, { realname, uid }: XCXFindUser) => (
<User
uid={uid}
name={`${username2}${(realname && username2 !== realname) ? `${realname}` : ''}`}
/>
)}
/>
<Table.Column align="center" title="积分" dataIndex={'score'} />
<Table.Column align="center" title="地区" dataIndex={'resideprovince'} />
<Table.Column align="center" title="性别" dataIndex={'sex'} render={sex => SEX[sex]} />
<Table.Column align="center" title="年龄" dataIndex={'birthyear'} render={year => dayjs().diff(dayjs(year), 'year')} />
</Table>
</Drawer>
</>
);
}

View File

@ -70,6 +70,19 @@ export function GameTable(props: Props) {
)} )}
> >
<Table.Column align="center" dataIndex={''} render={(_, __, i) => i + 1} /> <Table.Column align="center" dataIndex={''} render={(_, __, i) => i + 1} />
<Table.Column
dataIndex='gameid'
align="center"
render={((_, record: GamesData) => {
const { uid1 } = record;
const { team1, team2 } = toTeams(record);
return (
<Flex gap={48} justify="center">
<PlayerTeam idNames={props.uid === uid1 ? team1 : team2} />
</Flex>
);
})}
/>
<Table.Column <Table.Column
dataIndex='gameid' dataIndex='gameid'
align="center" align="center"

View File

@ -5,7 +5,7 @@ interface Props {
uid?: string; uid?: string;
} }
export default function User(props: Props) { export default function User(props: Props) {
if (!props.uid) return <span>{props.name}</span> if (!Number(props.uid)) return <span>{props.name}</span>
return ( return (
<Link to={`/profile/${props.uid}`}>{props.name}</Link> <Link to={`/profile/${props.uid}`}>{props.name}</Link>
); );

View File

@ -34,6 +34,15 @@ const server = serve({
return Response.json(data); return Response.json(data);
} }
}, },
"/api/user/find": {
async GET(req) {
const searchParams = new URL(req.url).searchParams;
const key = searchParams.get('key') ?? '';
const page = Number(searchParams.get('page'));
const users = await xcxApi.findUser(key, page);
return Response.json(users);
}
},
"/api/user/:uid": { "/api/user/:uid": {
async GET(req) { async GET(req) {
const uid = req.params.uid; const uid = req.params.uid;

View File

@ -67,13 +67,14 @@ function Raket(props: { profile?: XCXProfile | null }) {
} }
function PlayerList(props: { title: string; names?: string[]; uids?: string[] }) { function PlayerList(props: { title: string; names?: string[]; uids?: string[] }) {
if (!props.names?.length) return null; const { names = [], uids = [] } = props;
if (!names.length) return null;
return ( return (
<> <>
<Typography.Title level={5}>{props.title}</Typography.Title> <Typography.Title level={5}>{props.title}</Typography.Title>
<Flex vertical align="center"> <Flex vertical align="center">
{props.names.map((e, i) => ( {names.map((e, i) => (
<User key={e} name={e} uid={props.uids?.[i] ?? ''} /> <User key={e} name={e} uid={uids?.[i] ?? ''} />
))} ))}
</Flex> </Flex>
</> </>
@ -96,7 +97,7 @@ export default function ProfilePage() {
<Typography.Text>{profile?.score}</Typography.Text> <Typography.Text>{profile?.score}</Typography.Text>
<Typography.Text style={{ textAlign: 'center' }}> <Typography.Text style={{ textAlign: 'center' }}>
{ {
([profile?.province, profile?.sex, profile?.bg ?? '', ...(profile?.allCities ?? [])] as React.ReactNode[]) ([profile?.province, profile?.sex, profile?.bg ?? '', ...Object.values(profile?.allCities ?? [])] as React.ReactNode[])
.filter(Boolean) .filter(Boolean)
.reduce((a, b) => (<>{a}<Divider orientation="vertical" />{b}</>)) .reduce((a, b) => (<>{a}<Divider orientation="vertical" />{b}</>))
} }

View File

@ -1,4 +1,4 @@
import type { GamesData, XCXMember, XCXProfile, XCXTag } from "../types"; import type { GamesData, XCXFindUser, XCXFindUserResp, XCXMember, XCXProfile, XCXTag } from "../types";
import { BASE_URL } from "../utils"; import { BASE_URL } from "../utils";
const XCX_BASE_URL = `${BASE_URL}/xcx/public/index.php`; const XCX_BASE_URL = `${BASE_URL}/xcx/public/index.php`;
@ -51,4 +51,9 @@ export class XCXAPI {
const url = `/api/User/getGames?uid=${uid}&page=${page}&size=50`; const url = `/api/User/getGames?uid=${uid}&page=${page}&size=50`;
return (await this.#fetch<{ data: GamesData[] }>(url))?.data; return (await this.#fetch<{ data: GamesData[] }>(url))?.data;
} }
async findUser(key: string = '', page: number = 1) {
if (!key) return;
const url = `/api/user/lists?page=${page}&key=${key}`;
return (await this.#fetch<XCXFindUserResp>(url));
}
} }

View File

@ -34,4 +34,25 @@ export interface XCXMember extends BasePlayer {
name: string; name: string;
number: number; number: number;
age: string; age: string;
}
export interface XCXFindUser extends BasePlayer {
name: never;
username2: string;
realname: string;
residecity: string;
resideprovince: string;
minscore: string;
maxscore: string;
birthyear: string;
sex: string;
th?: string;
}
export interface XCXFindUserResp {
current_page: number;
last_page: number;
per_page: number;
total: number;
data: XCXFindUser[];
} }

View File

@ -114,4 +114,9 @@ export function sneckGroup(size: number, groupLen: number) {
newGroups.push(group); newGroups.push(group);
} }
return newGroups; return newGroups;
}
export enum SEX {
'男' = 1,
'女' = 2,
} }