- 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.
134 lines
4.6 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
} |