feat(getMemberDetail)

This commit is contained in:
kyuuseiryuu 2026-01-28 18:11:52 +09:00
parent 235d3b0b81
commit cbbe47b772
10 changed files with 67 additions and 21 deletions

View File

@ -1,5 +1,5 @@
import type React from "react"; import type React from "react";
import type { Player } from "../types"; import type { BasePlayer, Player } from "../types";
import { Tabs } from "antd"; import { Tabs } from "antd";
import { PlayerList } from "./PlayerList"; import { PlayerList } from "./PlayerList";
import { GroupingPrediction } from "./GroupingPrediction"; import { GroupingPrediction } from "./GroupingPrediction";
@ -8,6 +8,7 @@ import { useMemo } from "react";
interface Props { interface Props {
title: string; title: string;
players?: Player[]; players?: Player[];
members?: BasePlayer[];
} }
export const GamePanel: React.FC<Props> = props => { export const GamePanel: React.FC<Props> = props => {
@ -21,7 +22,7 @@ export const GamePanel: React.FC<Props> = props => {
{ {
key: 'groups', key: 'groups',
label: '分组预测', label: '分组预测',
children: <GroupingPrediction sneckMode={sneckMode} players={props.players} /> children: <GroupingPrediction sneckMode={sneckMode} players={props.members} />
}, },
{ {
key: 'players', key: 'players',

View File

@ -1,11 +1,11 @@
import { Card, Table } from "antd"; import { Card, Table } from "antd";
import type { Player } from "../types"; import type { BasePlayer } from "../types";
import { useMemo } from "react"; import { useMemo } from "react";
import User from "./User"; import User from "./User";
interface Props { interface Props {
index: number; index: number;
players?: (Player & { id: string })[]; players?: (BasePlayer & { id: string })[];
} }
export const GroupMember: React.FC<Props> = props => { export const GroupMember: React.FC<Props> = props => {

View File

@ -1,20 +1,23 @@
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { Flex, Form, InputNumber, Space, Switch, Typography } from "antd"; import { Flex, Form, InputNumber, Space, Switch, Typography } from "antd";
import { chunk } from 'lodash'; import { chunk } from 'lodash';
import type { Player } from "../types"; import type { BasePlayer } from "../types";
import { GroupMember } from "./GroupMember"; import { GroupMember } from "./GroupMember";
import { sneckGroup } from "../utils"; import { sneckGroup } from "../utils";
interface Props { interface Props {
players?: Player[]; players?: BasePlayer[];
sneckMode: boolean; sneckMode: boolean;
} }
type CustomPlayer = (Player & { index: number; id: string; }); type CustomPlayer = (BasePlayer & { index: number; id: string; });
export const GroupingPrediction: React.FC<Props> = props => { export const GroupingPrediction: React.FC<Props> = props => {
const [maxPlayerSize, setMaxPlayerSize] = useState(48); const [maxPlayerSize, setMaxPlayerSize] = useState(48);
const players: CustomPlayer[] = useMemo(() => { const players: CustomPlayer[] = useMemo(() => {
return props.players?.slice(0, maxPlayerSize)?.map((e, i) => ({ ...e, index: i + 1, id: `${i}-${e.name}-${e.score}` })) ?? []; return props.players
?.slice(0, maxPlayerSize)
?.sort((a, b) => Number(b.score) - Number(a.score))
?.map((e, i) => ({ ...e, index: i + 1, id: `${i}-${e.name}-${e.score}` })) ?? [];
}, [props.players, maxPlayerSize]); }, [props.players, maxPlayerSize]);
const [groupLen, setGroupLen] = useState(6); const [groupLen, setGroupLen] = useState(6);
const [sneckMode, setSneckMode] = useState(props.sneckMode); const [sneckMode, setSneckMode] = useState(props.sneckMode);

View File

@ -21,7 +21,7 @@ export const PlayerList: React.FC<Props> = props => {
placement: ['topCenter', 'bottomCenter'], placement: ['topCenter', 'bottomCenter'],
}} }}
> >
<Table.Column width={32} dataIndex={'_'} align="center" render={(_, __, i) => `${i + 1}`} /> <Table.Column width={32} dataIndex={'number'} align="center" />
<Table.Column width={32} dataIndex={'avatar'} align="center" render={src => <Avatar src={src} />} /> <Table.Column width={32} dataIndex={'avatar'} align="center" render={src => <Avatar src={src} />} />
<Table.Column <Table.Column
width={200} width={200}
@ -31,7 +31,7 @@ export const PlayerList: React.FC<Props> = props => {
render={(name, { uid }) => <User name={name} uid={uid} />} render={(name, { uid }) => <User name={name} uid={uid} />}
/> />
<Table.Column width={200} dataIndex={'score'} title="积分" sorter={{ <Table.Column width={200} dataIndex={'score'} title="积分" sorter={{
compare: ({ score: a }: Player, { score: b}: Player) => a - b, compare: ({ score: a }: Player, { score: b}: Player) => +a - +b,
}} /> }} />
</Table> </Table>
</> </>

View File

@ -25,7 +25,8 @@ const route = createBrowserRouter([
path: '/event/:matchId', path: '/event/:matchId',
loader: async ({ params }) => { loader: async ({ params }) => {
const info: MatchInfo = await (await fetch(`/api/match/${params.matchId}`)).json(); const info: MatchInfo = await (await fetch(`/api/match/${params.matchId}`)).json();
return info; const members = await (await fetch(`/api/match/${params.matchId}/${info.itemId}`)).json();
return { info, members };
}, },
Component: EventPage, Component: EventPage,
}, },

View File

@ -1,6 +1,6 @@
import { serve } from "bun"; import { serve } from "bun";
import index from "./index.html"; import index from "./index.html";
import { getAdvProfile, getMatchInfo, getPlayerTags, listEvent } from "./utils"; import { getAdvProfile, getMatchInfo, getMemberDetail, getPlayerTags, listEvent } from "./utils";
const server = serve({ const server = serve({
port: process.env.PORT || 3000, port: process.env.PORT || 3000,
@ -19,6 +19,13 @@ const server = serve({
return Response.json(data); return Response.json(data);
} }
}, },
"/api/match/:matchId/:itemId": {
async GET(req) {
const { matchId, itemId } = req.params;
const data = await getMemberDetail(matchId, itemId);
return Response.json(data);
}
},
"/api/user/:uid": { "/api/user/:uid": {
async GET(req) { async GET(req) {
const uid = req.params.uid; const uid = req.params.uid;

View File

@ -1,19 +1,32 @@
import { useLoaderData, useNavigate } from "react-router"; import { useLoaderData, useNavigate } from "react-router";
import { GamePanel } from "../components/GamePanel"; import { GamePanel } from "../components/GamePanel";
import type { MatchInfo } from "../types"; import type { BasePlayer, MatchInfo, XCXMember } from "../types";
import { Typography } from "antd"; import { Typography } from "antd";
import { HomeOutlined } from "@ant-design/icons"; import { HomeOutlined } from "@ant-design/icons";
import { useMemo } from "react";
export default function EventPage() { export default function EventPage() {
const game = useLoaderData<MatchInfo>(); const {
info: game,
members
} = useLoaderData<{ info: MatchInfo, members: XCXMember[] }>();
const navigate = useNavigate(); const navigate = useNavigate();
const map = useMemo(() => Object.fromEntries(members.map(e => [e.uid, e])), [members]);
const players = useMemo(() => {
return game.players
.map(e => ({ ...e, name: map[e.uid]?.realname ?? e.name, number: map[e.uid]?.number ?? NaN }))
.sort(((a, b) => +a.number - +b.number));
}, [game, map]);
const basePlayers = useMemo<BasePlayer[]>(() => {
return members.map(e => ({ ...e, name: e.realname } as BasePlayer))
}, [members]);
return ( return (
<div style={{ width: '100%', padding: 10, boxSizing: 'border-box' }}> <div style={{ width: '100%', padding: 10, boxSizing: 'border-box' }}>
<Typography.Title level={3}> <Typography.Title level={3}>
<HomeOutlined style={{ marginRight: 4 }} onClick={() => navigate('/')}/> <HomeOutlined style={{ marginRight: 4 }} onClick={() => navigate('/')}/>
{game.title} {game.title}
</Typography.Title> </Typography.Title>
<GamePanel title={game.title} players={game.players} /> <GamePanel members={basePlayers} title={game.title} players={players} />
</div> </div>
); );
} }

View File

@ -13,10 +13,22 @@ export interface MatchInfo {
players: Player[]; players: Player[];
} }
export interface Player { export interface BasePlayer {
uid: string;
name: string; name: string;
score: number; score: string;
}
export interface Player extends BasePlayer {
avatar: string; avatar: string;
info: string; info: string;
uid: string; }
export interface XCXMember extends BasePlayer {
uid: string;
score: string;
realname: string;
name: string;
number: number;
age: string;
} }

View File

@ -1,5 +1,5 @@
// import { fetch } from 'bun'; // import { fetch } from 'bun';
import type { IEventInfo, Player } from "./types"; import type { IEventInfo, Player, XCXMember } from "./types";
import * as cheerio from "cheerio"; import * as cheerio from "cheerio";
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
@ -93,7 +93,7 @@ export function parseEventInfo(html: string) {
const name = $(player).find('h6').text().trim(); const name = $(player).find('h6').text().trim();
const uid = /space-(?<uid>\d+).html/.exec($(player).find('h6 a').attr('href') ?? '')?.groups?.uid ?? ''; const uid = /space-(?<uid>\d+).html/.exec($(player).find('h6 a').attr('href') ?? '')?.groups?.uid ?? '';
const info = $(player).find('p:nth-of-type(2)').text().replace(/\s/g, ''); const info = $(player).find('p:nth-of-type(2)').text().replace(/\s/g, '');
const score = Number(/^.*?\b(\d+)\b/.exec(info)?.[1]); const score = /^.*?\b(\d+)\b/.exec(info)?.[1] ?? '';
players.push({ name, avatar: img, score, info, uid }); players.push({ name, avatar: img, score, info, uid });
} }
return { return {
@ -161,3 +161,12 @@ export async function getPlayerTags(uid: string) {
if (data.code !== 1) return null; if (data.code !== 1) return null;
return (data.data as XCXTag[]).filter(e => Number(e.count) > 0); return (data.data as XCXTag[]).filter(e => Number(e.count) > 0);
} }
export async function getMemberDetail(matchId: string, itemId: string) {
const resp = await fetch(`${XCX_BASE_URL}/api/enter/get_member_detail?id=${itemId}&match_id=${matchId}`, {
headers: xcxDefaultHeaders,
});
const data = await resp.json();
if (data.code !== 1) return null;
return (data.data.list as XCXMember[]);
}