my-kaiqiuwang/src/components/GroupingPrediction.tsx
kyuuseiryuu 76b68c0ea6 refactor(ws): unify WebSocket topic handling and add event subscription
- Refactor `WebSocketService` and `common.ts` to use a unified topic system instead of custom prefixes.
- Replace manual topic string concatenation with `getEventSubKey` and defined `WsServerSendTopics` types.
- Update client-side components (`EventCard`, `GroupingPrediction`) to support real-time event subscriptions and notifications.
- Move `useAuthSocket` and `WebScoketContext` initialization into `AppBarLayout` to ensure WebSocket state is available globally.
- Add error handling to WebSocket message processing in the Bun server.
- Implement a manual "Refresh Current Scores" button for `GroupingPrediction` to fetch fresh `nowScore` data.
- Update `HydrateFallback` UI to display a loading message instead of a refresh button during long load times.
- Add Service Worker (`sw.js`) build route to the Bun server configuration.
2026-03-22 13:00:50 +09:00

134 lines
4.6 KiB
TypeScript

import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Button, Flex, Form, InputNumber, Segmented, Space, Switch } from "antd";
import { chunk } from 'lodash';
import type { BasePlayer } from "../types";
import { GroupMember } from "./GroupMember";
import { sneckGroup } from "../utils/common";
import { useLoaderData } from "react-router";
import { SyncOutlined } from "@ant-design/icons";
import { useRequest } from "ahooks";
interface Player extends BasePlayer {
}
interface CustomPlayer extends Player {
nowScore: never;
index: number;
id: string;
}
interface Props {
players?: Player[];
sneckMode: boolean;
isPassedGame: boolean;
}
enum OrderScore {
= '年度积分',
= '当前积分',
}
export const GroupingPrediction: React.FC<Props> = props => {
const { uidScore } = useLoaderData<{ uidScore: Map<string, string>}>();
const uidScoreRequest = useRequest(async () => {
const uids = props.players?.map(player => player.uid).filter(Boolean);
const data = await fetch(`/api/user/nowScores`, {
method: "POST",
body: JSON.stringify({ uids }),
}).then(res => res.json()).catch(() => ({}));
return new Map(Object.entries(data));
}, { manual: true, refreshDeps: [props.players]});
console.debug('uidScore', uidScore);
const [maxPlayerSize, setMaxPlayerSize] = useState(48);
const [nowScoreGroup, setNowScoreGroup] = useState(
props.isPassedGame
? OrderScore.年度积分
: OrderScore.当前积分
);
const refactoredPlayers = useMemo(() => {
return nowScoreGroup === OrderScore. ? props.players?.map(e => {
const nowScore = uidScore.get(e.uid) || uidScoreRequest.data?.get(e.uid);
return {
...e,
score: !Number.isNaN(Number(nowScore)) ? nowScore : e.score,
}
}) : [...props.players ?? []];
}, [nowScoreGroup, props.players, uidScore, uidScoreRequest]);
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}` })) ?? [];
}, [refactoredPlayers, maxPlayerSize]);
const [groupLen, setGroupLen] = useState(6);
useEffect(() => {
if (props.players) {
if (players.length < 48) {
setMaxPlayerSize(players.length);
}
if (players.length <= 12) {
setGroupLen(1);
}
}
}, [props.players?.length]);
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, players]);
const sneckedGroups = useMemo(() => {
const sneckIndexGroups = sneckGroup(players.length, groupLen);
return sneckIndexGroups.map(g => {
const subGroup = g.map(i => ({
...players[i],
})).filter(Boolean) as CustomPlayer[];
return subGroup;
});
}, [players, grouped, groupLen, maxPlayerSize]);
const handleSyncUidScore = useCallback(() => {
uidScoreRequest.runAsync();
}, []);
return (
<>
<Flex gap={10} wrap>
<Form.Item label={'取人数'}>
<InputNumber
value={maxPlayerSize}
onChange={e => setMaxPlayerSize(e || props.players?.length || 0)}
max={props.players?.length}
/>
</Form.Item>
<Form.Item label="分组数">
<InputNumber
value={groupLen}
onChange={e => setGroupLen(e ?? 1)}
min={1}
max={26}
/>
</Form.Item>
<Form.Item label="蛇形分组">
<Switch checked={sneckMode} onChange={setSneckMode} />
</Form.Item>
<Form.Item label="排序积分">
<Segmented value={nowScoreGroup} onChange={setNowScoreGroup} options={[
OrderScore.,
OrderScore.,
]} />
</Form.Item>
<Form.Item hidden={uidScore.size > 0}>
<Button loading={uidScoreRequest.loading} onClick={handleSyncUidScore} icon={<SyncOutlined />}>
</Button>
</Form.Item>
</Flex>
<Flex gap='middle' wrap align="center" justify="center">
<React.Fragment key={'normal'}>
{ !sneckMode && grouped.map((p, i) => <GroupMember key={i} players={p} index={i} />)}
</React.Fragment>
<React.Fragment key={'sneck'}>
{ sneckMode && sneckedGroups.map((p, i) => <GroupMember key={i} players={p} index={i} />)}
</React.Fragment>
</Flex>
</>
);
}