diff --git a/__test__/redis.test.ts b/__test__/redis.test.ts new file mode 100644 index 0000000..a8158b7 --- /dev/null +++ b/__test__/redis.test.ts @@ -0,0 +1,8 @@ +import { expect, test } from 'bun:test'; +import { getUidScore } from '../src/services/uidScoreStore'; + +test('read redis', async () => { + const uidScore = await getUidScore(['388663']); + expect(uidScore).toBeDefined(); + expect(uidScore['388663']).toBeDefined(); +}); \ No newline at end of file diff --git a/__test__/utils.load-html.test.ts b/__test__/utils.load-html.test.ts index 844171f..6531a4e 100644 --- a/__test__/utils.load-html.test.ts +++ b/__test__/utils.load-html.test.ts @@ -1,7 +1,7 @@ import { expect, test } from 'bun:test'; import path from 'path'; import fs from 'fs'; -import { fetchEventContentHTML, fetchEventListHTML } from '../src/utils'; +import { fetchEventContentHTML, fetchEventListHTML } from '../src/utils/utils'; test('load html', async () => { const saveTo = path.resolve(__dirname, 'data', 'view-event.html'); diff --git a/__test__/utils.test.ts b/__test__/utils.test.ts index 4252efe..1fe7d25 100644 --- a/__test__/utils.test.ts +++ b/__test__/utils.test.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import { expect, test } from 'bun:test'; import path from 'path'; -import { parseEventInfo, parseEventList, sneckGroup } from '../src/utils'; +import { parseEventInfo, parseEventList, sneckGroup } from '../src/utils/utils'; const matchId = '167684'; diff --git a/src/components/GroupMember.tsx b/src/components/GroupMember.tsx index 55d4260..4860ae8 100644 --- a/src/components/GroupMember.tsx +++ b/src/components/GroupMember.tsx @@ -1,7 +1,7 @@ import { useMemo, useState } from "react"; import { Button, Card, Divider, Drawer, Flex, Space, Table } from "antd"; import type { BasePlayer } from "../types"; -import { getRoundTable } from "../utils"; +import { getRoundTable } from "../utils/utils"; import User from "./User"; interface Props { diff --git a/src/components/GroupingPrediction.tsx b/src/components/GroupingPrediction.tsx index da4d876..0c51aac 100644 --- a/src/components/GroupingPrediction.tsx +++ b/src/components/GroupingPrediction.tsx @@ -1,30 +1,51 @@ import React, { useMemo, useState } from "react"; -import { Flex, Form, InputNumber, Switch } from "antd"; +import { Flex, Form, InputNumber, Segmented, Switch } from "antd"; import { chunk } from 'lodash'; import type { BasePlayer } from "../types"; import { GroupMember } from "./GroupMember"; -import { sneckGroup } from "../utils"; +import { sneckGroup } from "../utils/utils"; + +interface Player extends BasePlayer { + nowScore?: string; +} + +interface CustomPlayer extends Player { + nowScore: never; + index: number; + id: string; +} interface Props { - players?: BasePlayer[]; + players?: Player[]; sneckMode: boolean; } -type CustomPlayer = (BasePlayer & { index: number; id: string; }); +enum OrderScore { + 年度积分 = '年度积分', + 当前积分 = '当前积分', +} + export const GroupingPrediction: React.FC = props => { const [maxPlayerSize, setMaxPlayerSize] = useState(48); - const players: CustomPlayer[] = useMemo(() => { - return props.players + const [nowScoreGroup, setNowScoreGroup] = useState(OrderScore.当前积分); + const refactoredPlayers = useMemo(() => { + return nowScoreGroup === OrderScore.当前积分 ? props.players?.map(e => ({ + ...e, + score: e.nowScore ?? e.score, + })) : [...props.players ?? []]; + }, [nowScoreGroup, props.players]); + const players = useMemo(() => { + return (refactoredPlayers as CustomPlayer[]) ?.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]); + }, [refactoredPlayers, maxPlayerSize]); const [groupLen, setGroupLen] = useState(6); const [sneckMode, setSneckMode] = useState(props.sneckMode); const chunkSize = useMemo(() => Math.floor((players.length ?? 0) / groupLen) || 1, [players, groupLen]); const grouped = useMemo(() => { return chunk(players, chunkSize); - }, [chunkSize]); + }, [chunkSize, players]); const sneckedGroups = useMemo(() => { const sneckIndexGroups = sneckGroup(players.length, groupLen); return sneckIndexGroups.map(g => { @@ -33,7 +54,7 @@ export const GroupingPrediction: React.FC = props => { })).filter(Boolean) as CustomPlayer[]; return subGroup; }); - }, [grouped, groupLen, maxPlayerSize]); + }, [players, grouped, groupLen, maxPlayerSize]); return ( <> @@ -55,6 +76,12 @@ export const GroupingPrediction: React.FC = props => { + + + diff --git a/src/components/ScoreBoard/index.tsx b/src/components/ScoreBoard/index.tsx new file mode 100644 index 0000000..cf9debb --- /dev/null +++ b/src/components/ScoreBoard/index.tsx @@ -0,0 +1,14 @@ +import { Flex } from "antd"; + +interface Props { + +} +export function ScoreBoard(props: Props) { + return ( + + + + + + ); +} \ No newline at end of file diff --git a/src/components/Tags.tsx b/src/components/Tags.tsx index b4b8a44..e54051c 100644 --- a/src/components/Tags.tsx +++ b/src/components/Tags.tsx @@ -1,6 +1,7 @@ import { useRequest } from "ahooks"; import { Divider, Flex, Skeleton, Tag, Typography } from "antd"; import { EType, type XCXTag } from "../types"; +import { useLoaderData } from "react-router"; interface Props { uid?: string; @@ -17,25 +18,16 @@ const color: { }; export default function UserTags(props: Props) { - const fetchTags = useRequest(async () => (await fetch(`/api/user/${props.uid}/tags`)).json(), { - refreshDeps: [props.uid], - }); - if (!props.uid) return null; + const { tags } = useLoaderData<{ tags: XCXTag[] }>(); + if (!props.uid || !tags.length) return null; return ( <> 评价标签 - { fetchTags.loading ? ( - <> - - - - - ) : ( - fetchTags.data?.length - ? fetchTags.data?.map(e => ( + { (tags?.length ? tags?.map(e => ( {e.ename}({e.count}) - )): (暂时没人评价))} + )) : (暂时没人评价) + )} ); diff --git a/src/frontend.tsx b/src/frontend.tsx index f4bae4e..e8ba087 100644 --- a/src/frontend.tsx +++ b/src/frontend.tsx @@ -48,7 +48,8 @@ const route = createBrowserRouter([ score: e.score, realname: e.name, } as XCXMember)); - return { info, members }; + const uidScore = await fetch(`/api/user/nowScores?uids=${info.players.map(e => e.uid).join(',')}`); + return { info, members, uidScore: new Map(Object.entries(await uidScore.json())) }; }, Component: EventPage, HydrateFallback: () => @@ -58,7 +59,8 @@ const route = createBrowserRouter([ loader: async ({ params }) => { const { uid } = params; const profile = await (await fetch(`/api/user/${uid}`)).json(); - return { profile, uid }; + const tags = await (await fetch(`/api/user/${uid}/tags`)).json(); + return { profile, uid, tags }; }, Component: ProfilePage, HydrateFallback: () => diff --git a/src/index.tsx b/src/index.tsx index 23b3dc8..1352a3f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,14 +1,14 @@ import { serve } from "bun"; -import { getMatchInfo, listEvent } from "./utils"; +import { getMatchInfo, listEvent } from "./utils/utils"; import index from "./index.html"; -import { XCXAPI } from "./services/xcxApi"; +import { xcxApi } from "./utils/server"; +import { getUidScore } from "./services/uidScoreStore"; if (!process.env.KAIQIUCC_TOKEN) { console.error('env KAIQIUCC_TOKEN not found'); process.exit(1); } -const xcxApi = new XCXAPI(process.env.KAIQIUCC_TOKEN ?? ''); const server = serve({ port: process.env.PORT || 3000, @@ -43,6 +43,14 @@ const server = serve({ return Response.json(users); } }, + "/api/user/nowScores": { + async GET(req) { + const searchParams = new URL(req.url).searchParams; + const uids = searchParams.get('uids')?.split(',') ?? []; + const uidScore = await getUidScore(uids); + return Response.json(uidScore); + } + }, "/api/user/:uid": { async GET(req) { const uid = req.params.uid; diff --git a/src/page/EventPage.tsx b/src/page/EventPage.tsx index b4e16d2..8d64da6 100644 --- a/src/page/EventPage.tsx +++ b/src/page/EventPage.tsx @@ -1,25 +1,36 @@ -import { useLoaderData, useNavigate } from "react-router"; +import { useLoaderData } from "react-router"; import { GamePanel } from "../components/GamePanel"; import type { BasePlayer, MatchInfo, XCXMember } from "../types"; import { Typography } from "antd"; -import { HomeOutlined } from "@ant-design/icons"; import { useMemo } from "react"; import { useTitle } from "ahooks"; export default function EventPage() { const { info: game, - members - } = useLoaderData<{ info: MatchInfo, members: XCXMember[] }>(); - const navigate = useNavigate(); + members, + uidScore, + } = useLoaderData<{ + uidScore: Map, + info: MatchInfo, + members: XCXMember[], + }>(); 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 })) + .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(() => { - return members.map(e => ({ ...e, name: e.realname } as BasePlayer)) + return members.map(e => ({ + ...e, + name: e.realname, + nowScore: uidScore.get(e.uid), + } as BasePlayer)) }, [members]); useTitle(game.title, { restoreOnUnmount: true }); return ( diff --git a/src/page/FindUserPage.tsx b/src/page/FindUserPage.tsx index fa82f3a..6225961 100644 --- a/src/page/FindUserPage.tsx +++ b/src/page/FindUserPage.tsx @@ -2,7 +2,7 @@ import { useLocalStorageState, useRequest } from "ahooks"; import { Input, Table, Spin, Typography, Flex, Space } from "antd"; import type { XCXFindUser, XCXFindUserResp } from "../types"; import User from "../components/User"; -import { SEX } from "../utils"; +import { SEX } from "../utils/front"; import dayjs from "dayjs"; export function FindUserPage() { diff --git a/src/services/uidScoreStore.ts b/src/services/uidScoreStore.ts new file mode 100644 index 0000000..95590f0 --- /dev/null +++ b/src/services/uidScoreStore.ts @@ -0,0 +1,22 @@ +import { RedisClient } from 'bun'; +import { xcxApi } from '../utils/server'; + +const redis = new RedisClient('redis://default:redis_8YnmBw@192.168.50.126:6379'); + +const getKey = (uid: string) => `my-kaiqiuwang:uid-score:${uid}`; + +const TIMEOUT = 60 * 60 * 3; // 3H; + +export const getUidScore = async (uids: string[]): Promise> => { + const jobs = uids.map(async uid => { + const key = getKey(uid); + const value = await redis.get(key); + if (value) { + return [uid, value]; + } + const profile = await xcxApi.getAdvProfile(uid); + await redis.setex(key, TIMEOUT, profile?.score ?? ''); + return [uid, profile?.score ?? '']; + }); + return Object.fromEntries(await Promise.all(jobs)); +} \ No newline at end of file diff --git a/src/services/xcxApi.ts b/src/services/xcxApi.ts index ae57457..7e35117 100644 --- a/src/services/xcxApi.ts +++ b/src/services/xcxApi.ts @@ -1,5 +1,5 @@ import type { GamesData, XCXFindUser, XCXFindUserResp, XCXMember, XCXProfile, XCXTag } from "../types"; -import { BASE_URL } from "../utils"; +import { BASE_URL } from "../utils/utils"; const XCX_BASE_URL = `${BASE_URL}/xcx/public/index.php`; diff --git a/src/store/useGameStore.ts b/src/store/useGameStore.ts deleted file mode 100644 index cb5fa37..0000000 --- a/src/store/useGameStore.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { create } from "zustand"; -import { type IEventInfo } from '../types'; - -interface StoreType { - eventInfo?: IEventInfo; - setEventInfo: (info?: IEventInfo) => void; -} - - -const useGameStore = create((set) => { - return { - setEventInfo: (info) => { - set({ eventInfo: info }); - }, - } -}); - -export default useGameStore; \ No newline at end of file diff --git a/src/utils/front.ts b/src/utils/front.ts new file mode 100644 index 0000000..d57695a --- /dev/null +++ b/src/utils/front.ts @@ -0,0 +1,5 @@ + +export enum SEX { + '男' = 1, + '女' = 2, +} diff --git a/src/utils/server.ts b/src/utils/server.ts new file mode 100644 index 0000000..e0cff57 --- /dev/null +++ b/src/utils/server.ts @@ -0,0 +1,3 @@ +import { XCXAPI } from "../services/xcxApi"; + +export const xcxApi = new XCXAPI(process.env.KAIQIUCC_TOKEN ?? ''); \ No newline at end of file diff --git a/src/utils.ts b/src/utils/utils.ts similarity index 96% rename from src/utils.ts rename to src/utils/utils.ts index e1bf7a6..c529a25 100644 --- a/src/utils.ts +++ b/src/utils/utils.ts @@ -1,4 +1,4 @@ -import type { IEventInfo, Player } from "./types"; +import type { IEventInfo, Player } from "../types"; import * as cheerio from "cheerio"; import { chunk } from 'lodash'; @@ -116,11 +116,6 @@ export function sneckGroup(size: number, groupLen: number) { return newGroups; } -export enum SEX { - '男' = 1, - '女' = 2, -} - export function getRoundTable(nameList: T[], round: number) { const list = [...nameList]; const half = list.length / 2; @@ -139,4 +134,8 @@ export function getRoundTable(nameList: T[], round: number) { const left = [...newList].slice(0, half); const right = [...newList].slice(half).reverse(); return [left, right]; -} \ No newline at end of file +} + +export function createGameID(matchId: string, user1: string, user2: string) { + return [matchId, user1, user2].join('-'); +}