feat(getMemberDetail)
This commit is contained in:
parent
235d3b0b81
commit
cbbe47b772
@ -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',
|
||||||
|
|||||||
@ -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 => {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
13
src/utils.ts
13
src/utils.ts
@ -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[]);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user