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

View File

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

View File

@ -1,20 +1,23 @@
import React, { useMemo, useState } from "react";
import { Flex, Form, InputNumber, Space, Switch, Typography } from "antd";
import { chunk } from 'lodash';
import type { Player } from "../types";
import type { BasePlayer } from "../types";
import { GroupMember } from "./GroupMember";
import { sneckGroup } from "../utils";
interface Props {
players?: Player[];
players?: BasePlayer[];
sneckMode: boolean;
}
type CustomPlayer = (Player & { index: number; id: string; });
type CustomPlayer = (BasePlayer & { index: number; id: string; });
export const GroupingPrediction: React.FC<Props> = props => {
const [maxPlayerSize, setMaxPlayerSize] = useState(48);
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]);
const [groupLen, setGroupLen] = useState(6);
const [sneckMode, setSneckMode] = useState(props.sneckMode);

View File

@ -21,7 +21,7 @@ export const PlayerList: React.FC<Props> = props => {
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={200}
@ -31,7 +31,7 @@ export const PlayerList: React.FC<Props> = props => {
render={(name, { uid }) => <User name={name} uid={uid} />}
/>
<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>
</>

View File

@ -25,7 +25,8 @@ const route = createBrowserRouter([
path: '/event/:matchId',
loader: async ({ params }) => {
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,
},

View File

@ -1,6 +1,6 @@
import { serve } from "bun";
import index from "./index.html";
import { getAdvProfile, getMatchInfo, getPlayerTags, listEvent } from "./utils";
import { getAdvProfile, getMatchInfo, getMemberDetail, getPlayerTags, listEvent } from "./utils";
const server = serve({
port: process.env.PORT || 3000,
@ -19,6 +19,13 @@ const server = serve({
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": {
async GET(req) {
const uid = req.params.uid;

View File

@ -1,19 +1,32 @@
import { useLoaderData, useNavigate } from "react-router";
import { GamePanel } from "../components/GamePanel";
import type { MatchInfo } from "../types";
import type { BasePlayer, MatchInfo, XCXMember } from "../types";
import { Typography } from "antd";
import { HomeOutlined } from "@ant-design/icons";
import { useMemo } from "react";
export default function EventPage() {
const game = useLoaderData<MatchInfo>();
const {
info: game,
members
} = useLoaderData<{ info: MatchInfo, members: XCXMember[] }>();
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 (
<div style={{ width: '100%', padding: 10, boxSizing: 'border-box' }}>
<Typography.Title level={3}>
<HomeOutlined style={{ marginRight: 4 }} onClick={() => navigate('/')}/>
{game.title}
</Typography.Title>
<GamePanel title={game.title} players={game.players} />
<GamePanel members={basePlayers} title={game.title} players={players} />
</div>
);
}

View File

@ -13,10 +13,22 @@ export interface MatchInfo {
players: Player[];
}
export interface Player {
export interface BasePlayer {
uid: string;
name: string;
score: number;
score: string;
}
export interface Player extends BasePlayer {
avatar: 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 type { IEventInfo, Player } from "./types";
import type { IEventInfo, Player, XCXMember } from "./types";
import * as cheerio from "cheerio";
import fs from 'fs';
import path from 'path';
@ -93,7 +93,7 @@ export function parseEventInfo(html: string) {
const name = $(player).find('h6').text().trim();
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 score = Number(/^.*?\b(\d+)\b/.exec(info)?.[1]);
const score = /^.*?\b(\d+)\b/.exec(info)?.[1] ?? '';
players.push({ name, avatar: img, score, info, uid });
}
return {
@ -161,3 +161,12 @@ export async function getPlayerTags(uid: string) {
if (data.code !== 1) return null;
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[]);
}