my-kaiqiuwang/src/components/BattleTable.tsx
kyuuseiryuu bb4ca8ae40 feat: add localStorage persistence for battle result map
- Initialize `resultMap` from `localStorage` on component mount if data exists.
- Save updated `resultMap` back to `localStorage` when the component unmounts.
- Removed debug console.log statement for cleaner logs.
2026-03-10 17:21:44 +09:00

122 lines
4.4 KiB
TypeScript

import type { BasePlayer } from "../types";
import { useCallback, useEffect, useMemo, useState } from "react";
import { calculate, getRoundTable } from "../utils/common";
import { Card, Divider, Flex, Space } from "antd";
import styled from "styled-components";
import { cloneDeep } from "lodash";
const StyledContainer = styled(Flex)`
.winer {
color: red;
position: relative;
::after {
content: '胜';
font-size: 0.8em;
background-color: red;
color: white;
padding: 0;
border-radius: 50%;
position: absolute;
}
}
`;
function BattleTable({
players: list,
}: {
players: BasePlayer[];
}) {
const playerList = useMemo(() => list.length % 2 === 0 ? list : [...list, {
uid: '',
score: '',
name: '-',
}], [list]);
const resultMap = useMemo(() => {
const cache = localStorage.getItem('match-result-map');
const cacheEntries: [string, { winer: string, score: number }][] = cache ? Object.entries(JSON.parse(cache)) : [];
return new Map<string, {
winer: string,
score: number,
}>(cacheEntries);
}, []);
useEffect(() => {
return () => {
const cache = JSON.stringify(Object.fromEntries(resultMap.entries()));
localStorage.setItem('match-result-map', cache);
}
}, [resultMap]);
const buildMatchGroupTable = useCallback((list: (BasePlayer | null)[]) => {
const roundNum = list.length - 1;
const dataList = cloneDeep(list);
const roundTable = new Array(roundNum).fill(0).map((_, i) => {
const [left, right] = getRoundTable(dataList, i);
left?.forEach((l, i) => {
const r = right?.[i];
const key = `${l?.uid}-${r?.uid}`;
const winer = resultMap.get(key)?.winer;
const score = resultMap.get(key)?.score;
if (winer && l && r) {
if (winer === l?.uid) {
l.score = (+l.score + (score ?? 0)).toFixed();
r.score = (+r.score - (score ?? 0)).toFixed();
} else {
r.score = (+r.score + (score ?? 0)).toFixed();
l.score = (+l.score - (score ?? 0)).toFixed();
}
}
});
return cloneDeep([left, right]);
});
return roundTable.filter(Boolean);
}, []);
const [matchGroupTable, setMatchGroupTable] = useState<ReturnType<typeof buildMatchGroupTable>>([]);
const handleWiner = useCallback((l?: BasePlayer, r?: BasePlayer, winer?: BasePlayer, loser?: BasePlayer) => {
if (!l?.uid || !r?.uid || !winer?.uid || !loser?.uid) return;
const key = `${l.uid}-${r.uid}`;
const score = calculate(+winer.score, +loser.score);
if (resultMap.get(key)?.winer) {
resultMap.delete(key);
} else {
resultMap.set(key, { winer: winer?.uid!, score });
}
setMatchGroupTable(buildMatchGroupTable(playerList));
}, []);
useEffect(() => {
const roundTable = buildMatchGroupTable(playerList);
setMatchGroupTable(roundTable);
}, [playerList]);
// console.debug('matchGroupTable', matchGroupTable, gameResultMap);
return (
<StyledContainer vertical gap={12} justify="center" align="center">
{matchGroupTable.map(([left, right], i) => {
return (
<Card size="small" key={`round-${i}`} title={`Round: ${i + 1}`}>
{left?.map((l, i) => {
const r = right?.[i];
const key = `${l?.uid}-${r?.uid}`;
const winer = resultMap.get(key)?.winer;
const score = resultMap.get(key)?.score;
const resultL = winer ? winer === l?.uid ? ` +${score}` : `-${score}` : ''
const resultR = winer ? winer === r?.uid ? ` +${score}` : `-${score}` : ''
const nameL = l?.uid ? `${l?.name}(${l?.score})${resultL}` : '-';
const nameR = r?.uid ? `${r?.name}(${r?.score})${resultR}` : '-';
return (
<div key={key}>
<Space style={{ textAlign: "center" }}>
<div className={winer === l?.uid ? 'winer' : ''} onClick={() => handleWiner(l!, r!, l!, r!)} style={{ width: 120 }}>{nameL}</div>
<span>VS</span>
<div className={winer === r?.uid ? 'winer' : ''} onClick={() => handleWiner(l!, r!, r!, l!)} style={{ width: 120 }}>{nameR}</div>
</Space>
{i < left.length - 1 && <Divider size="small" />}
</div>
);
})}
</Card>
);
})}
</StyledContainer>
);
}
export default BattleTable;