feat(BattleTable): share code
This commit is contained in:
parent
9044073afd
commit
6f1f8044dd
@ -1,28 +1,35 @@
|
|||||||
import type { BasePlayer } from "../types";
|
import type { BasePlayer } from "../types";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { calculate, getRoundTable } from "../utils/common";
|
import { calculate, getRoundTable } from "../utils/common";
|
||||||
import { Button, Card, Divider, Flex, Space, Table } from "antd";
|
import { Button, Card, Divider, Flex, Table, Modal, Typography, Input } from "antd";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import { ClearOutlined } from "@ant-design/icons";
|
import { ClearOutlined, EditOutlined, ShareAltOutlined } from "@ant-design/icons";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
const StyledContainer = styled(Flex)`
|
const StyledContainer = styled(Flex)`
|
||||||
|
min-width: 300px;
|
||||||
|
width: 80vw;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
.ant-card, .share-buttons {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.winer {
|
.winer {
|
||||||
color: red;
|
color: red;
|
||||||
position: relative;
|
position: relative;
|
||||||
::after {
|
|
||||||
width: 4px
|
|
||||||
content: '';
|
|
||||||
font-size: 0.8em;
|
|
||||||
background-color: red;
|
|
||||||
color: white;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 50%;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
type Result = {
|
||||||
|
winer: string;
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMatchKey(player1: BasePlayer, player2: BasePlayer) {
|
||||||
|
return `${player1.uid}-${player2.uid}`;
|
||||||
|
}
|
||||||
|
|
||||||
function BattleTable({
|
function BattleTable({
|
||||||
players: list,
|
players: list,
|
||||||
}: {
|
}: {
|
||||||
@ -35,7 +42,7 @@ function BattleTable({
|
|||||||
}], [list]);
|
}], [list]);
|
||||||
const resultMap = useMemo(() => {
|
const resultMap = useMemo(() => {
|
||||||
const cache = localStorage.getItem('match-result-map');
|
const cache = localStorage.getItem('match-result-map');
|
||||||
const cacheEntries: [string, { winer: string, score: number }][] = cache ? Object.entries(JSON.parse(cache)) : [];
|
const cacheEntries: [string, Result][] = cache ? Object.entries(JSON.parse(cache)) : [];
|
||||||
return new Map<string, {
|
return new Map<string, {
|
||||||
winer: string,
|
winer: string,
|
||||||
score: number,
|
score: number,
|
||||||
@ -54,7 +61,7 @@ function BattleTable({
|
|||||||
const [left, right] = getRoundTable(dataList, i);
|
const [left, right] = getRoundTable(dataList, i);
|
||||||
left?.forEach((l, i) => {
|
left?.forEach((l, i) => {
|
||||||
const r = right?.[i];
|
const r = right?.[i];
|
||||||
const key = `${l?.uid}-${r?.uid}`;
|
const key = getMatchKey(l!, r!);
|
||||||
const winer = resultMap.get(key)?.winer;
|
const winer = resultMap.get(key)?.winer;
|
||||||
const score = resultMap.get(key)?.score;
|
const score = resultMap.get(key)?.score;
|
||||||
if (winer && l && r) {
|
if (winer && l && r) {
|
||||||
@ -72,10 +79,20 @@ function BattleTable({
|
|||||||
return roundTable;
|
return roundTable;
|
||||||
}, []);
|
}, []);
|
||||||
const [matchGroupTable, setMatchGroupTable] = useState<ReturnType<typeof buildMatchGroupTable>>([]);
|
const [matchGroupTable, setMatchGroupTable] = useState<ReturnType<typeof buildMatchGroupTable>>([]);
|
||||||
|
const matchKeys = useMemo(() => {
|
||||||
|
const keys: string[] = [];
|
||||||
|
matchGroupTable.forEach(([left, right]) => {
|
||||||
|
left?.forEach((l, i) => {
|
||||||
|
const r = right?.[i];
|
||||||
|
const key = getMatchKey(l!, r!);
|
||||||
|
keys.push(key);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return keys;
|
||||||
|
}, [matchGroupTable]);
|
||||||
const handleWiner = useCallback((l?: BasePlayer, r?: BasePlayer, winer?: BasePlayer, loser?: BasePlayer) => {
|
const handleWiner = useCallback((l?: BasePlayer, r?: BasePlayer, winer?: BasePlayer, loser?: BasePlayer) => {
|
||||||
if (!l?.uid || !r?.uid || !winer?.uid || !loser?.uid) return;
|
if (!l?.uid || !r?.uid || !winer?.uid || !loser?.uid) return;
|
||||||
const key = `${l.uid}-${r.uid}`;
|
const key = getMatchKey(l!, r!);
|
||||||
const score = calculate(+winer.score, +loser.score);
|
const score = calculate(+winer.score, +loser.score);
|
||||||
if (resultMap.get(key)?.winer) {
|
if (resultMap.get(key)?.winer) {
|
||||||
resultMap.delete(key);
|
resultMap.delete(key);
|
||||||
@ -87,7 +104,7 @@ function BattleTable({
|
|||||||
const handleClear = useCallback((left?: (BasePlayer | null | undefined)[], right?: (BasePlayer | null | undefined)[]) => {
|
const handleClear = useCallback((left?: (BasePlayer | null | undefined)[], right?: (BasePlayer | null | undefined)[]) => {
|
||||||
left?.forEach((l, i) => {
|
left?.forEach((l, i) => {
|
||||||
const r = right?.[i];
|
const r = right?.[i];
|
||||||
const key = `${l?.uid}-${r?.uid}`;
|
const key = getMatchKey(l!, r!);
|
||||||
resultMap.delete(key);
|
resultMap.delete(key);
|
||||||
});
|
});
|
||||||
setMatchGroupTable(buildMatchGroupTable(playerList));
|
setMatchGroupTable(buildMatchGroupTable(playerList));
|
||||||
@ -96,7 +113,7 @@ function BattleTable({
|
|||||||
matchGroupTable.forEach(([left, right]) => {
|
matchGroupTable.forEach(([left, right]) => {
|
||||||
left?.forEach((l, i) => {
|
left?.forEach((l, i) => {
|
||||||
const r = right?.[i];
|
const r = right?.[i];
|
||||||
const key = `${l?.uid}-${r?.uid}`;
|
const key = getMatchKey(l!, r!);
|
||||||
resultMap.delete(key);
|
resultMap.delete(key);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -107,13 +124,74 @@ function BattleTable({
|
|||||||
const newScoreMap = Object.fromEntries([...left, ...right].map(e => [e?.uid, e?.score]));
|
const newScoreMap = Object.fromEntries([...left, ...right].map(e => [e?.uid, e?.score]));
|
||||||
return newScoreMap;
|
return newScoreMap;
|
||||||
}, [playerList, matchGroupTable]);
|
}, [playerList, matchGroupTable]);
|
||||||
|
const [modal, contextHodler] = Modal.useModal();
|
||||||
|
const eventId = useLocation().pathname.split('/').pop() ?? '';
|
||||||
|
const handleShareCode = useCallback(async () => {
|
||||||
|
const entries = matchKeys.map(e => [e, resultMap.get(e)]);
|
||||||
|
const data = JSON.stringify(Object.fromEntries(entries));
|
||||||
|
const code = await fetch(`/api/battle/${eventId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ data }),
|
||||||
|
}).then(res => res.json()).then(json => json.code);
|
||||||
|
modal.success({
|
||||||
|
maskClosable: true,
|
||||||
|
icon: null,
|
||||||
|
footer: null,
|
||||||
|
style: { width: 300, height: 300 },
|
||||||
|
title: '分享码',
|
||||||
|
content: (
|
||||||
|
<Flex vertical align="center" justify="center">
|
||||||
|
<Typography.Text copyable>{code}</Typography.Text>
|
||||||
|
</Flex>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}, [resultMap, matchKeys, eventId]);
|
||||||
|
const [importCodeModalVisible, setImportModalVisible] = useState(false);
|
||||||
|
const handleImportCodeClick = useCallback(() => {
|
||||||
|
setImportModalVisible(true);
|
||||||
|
}, []);
|
||||||
|
const handleScanModalClose = useCallback(() => {
|
||||||
|
setImportModalVisible(false);
|
||||||
|
}, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const roundTable = buildMatchGroupTable(playerList);
|
const roundTable = buildMatchGroupTable(playerList);
|
||||||
setMatchGroupTable(roundTable);
|
setMatchGroupTable(roundTable);
|
||||||
}, [playerList]);
|
}, [playerList]);
|
||||||
|
const [shareCode, setShareCode] = useState('');
|
||||||
|
const handleImportCode = useCallback(async () => {
|
||||||
|
const data: Record<string, Result> = await fetch(`/api/battle/${eventId}?code=${shareCode}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(json => json);
|
||||||
|
Object.entries(data).forEach(([k, v]) => {
|
||||||
|
const { winer = '', score = 0 } = v ?? {};
|
||||||
|
resultMap.set(k, { winer, score });
|
||||||
|
});
|
||||||
|
setMatchGroupTable(buildMatchGroupTable(playerList));
|
||||||
|
setImportModalVisible(false);
|
||||||
|
}, [shareCode, playerList]);
|
||||||
// console.debug('matchGroupTable', matchGroupTable, gameResultMap);
|
// console.debug('matchGroupTable', matchGroupTable, gameResultMap);
|
||||||
return (
|
return (
|
||||||
<StyledContainer vertical gap={12} justify="center" align="center">
|
<StyledContainer vertical gap={12} justify="center" align="center">
|
||||||
|
{contextHodler}
|
||||||
|
<Modal
|
||||||
|
forceRender
|
||||||
|
title="导入分享码"
|
||||||
|
open={importCodeModalVisible}
|
||||||
|
onCancel={handleScanModalClose}
|
||||||
|
onOk={handleImportCode}
|
||||||
|
okText="导入"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Flex vertical align="center" justify="center">
|
||||||
|
<Input
|
||||||
|
allowClear
|
||||||
|
value={shareCode}
|
||||||
|
placeholder="输入分享码"
|
||||||
|
onChange={e => setShareCode(e.target.value)}
|
||||||
|
onPressEnter={handleImportCode}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Modal>
|
||||||
{matchGroupTable.map(([left, right], i) => {
|
{matchGroupTable.map(([left, right], i) => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@ -132,7 +210,7 @@ function BattleTable({
|
|||||||
const nameL = l?.uid ? `${l?.name}(${l?.score})${resultL}` : '-';
|
const nameL = l?.uid ? `${l?.name}(${l?.score})${resultL}` : '-';
|
||||||
const nameR = r?.uid ? `${r?.name}(${r?.score})${resultR}` : '-';
|
const nameR = r?.uid ? `${r?.name}(${r?.score})${resultR}` : '-';
|
||||||
return (
|
return (
|
||||||
<div key={key} style={{ minWidth: 280, width: '80vw', maxWidth: 600 }}>
|
<div key={key} className="round-row">
|
||||||
<Flex align="center" justify="space-between">
|
<Flex align="center" justify="space-between">
|
||||||
<div
|
<div
|
||||||
className={winer === l?.uid ? 'winer' : ''}
|
className={winer === l?.uid ? 'winer' : ''}
|
||||||
@ -162,30 +240,40 @@ function BattleTable({
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<Table
|
<Card
|
||||||
title={() => (
|
title={'积分增减预测'}
|
||||||
<Flex align="center" justify="space-between">
|
extra={(
|
||||||
<span>积分增减预测</span>
|
<Button type="link" icon={<ClearOutlined />} onClick={handleClearAll}>清除本组所有</Button>
|
||||||
<Button type="link" icon={<ClearOutlined />} onClick={handleClearAll}>清除本组所有</Button>
|
|
||||||
</Flex>
|
|
||||||
)}
|
)}
|
||||||
showHeader={false}
|
>
|
||||||
pagination={false}
|
<Table
|
||||||
rowHoverable={false}
|
className="score-table"
|
||||||
size="small"
|
showHeader={false}
|
||||||
dataSource={playerList}
|
pagination={false}
|
||||||
rowKey={e => e.uid}
|
rowHoverable={false}
|
||||||
style={{ minWidth: 300, width: '80vw', maxWidth: 620 }}
|
size="small"
|
||||||
columns={[
|
dataSource={playerList}
|
||||||
{ dataIndex: 'name', align: 'center', },
|
rowKey={e => e.uid}
|
||||||
{ dataIndex: 'score', align: 'center', },
|
style={{ width: '100%' }}
|
||||||
{ dataIndex: 'uid', align: 'center', render: (uid, { score }) => scoreDiff[uid] || score },
|
columns={[
|
||||||
{ dataIndex: 'uid', align: 'right', render: (uid, { score }) => {
|
{ dataIndex: 'name', align: 'center', },
|
||||||
const diff = +(scoreDiff[uid] || score) - +score;
|
{ dataIndex: 'score', align: 'center', },
|
||||||
return diff > 0 ? `+${diff}` : diff;
|
{ dataIndex: 'uid', align: 'center', render: (uid, { score }) => scoreDiff[uid] || score },
|
||||||
} },
|
{ dataIndex: 'uid', align: 'right', render: (uid, { score }) => {
|
||||||
]}
|
const diff = +(scoreDiff[uid] || score) - +score;
|
||||||
/>
|
return diff > 0 ? `+${diff}` : diff;
|
||||||
|
} },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<Flex className="share-buttons" gap={24} align="center" justify="space-between">
|
||||||
|
<Flex flex={1}>
|
||||||
|
<Button block onClick={handleShareCode} icon={<ShareAltOutlined />}>生成分享码</Button>
|
||||||
|
</Flex>
|
||||||
|
<Flex flex={1}>
|
||||||
|
<Button block onClick={handleImportCodeClick} icon={<EditOutlined />}>输入分享码</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import { serve } from "bun";
|
|||||||
import { getMatchInfo, listEvent, xcxApi } from "./utils/server";
|
import { getMatchInfo, listEvent, xcxApi } from "./utils/server";
|
||||||
import index from "./index.html";
|
import index from "./index.html";
|
||||||
import { getUidScore } from "./services/uidScoreStore";
|
import { getUidScore } from "./services/uidScoreStore";
|
||||||
import { checkIsUserFav, favPlayer, listFavPlayers, unFavPlayer } from "./services/FavPlayer";
|
import { checkIsUserFav, favPlayer, listFavPlayers, unFavPlayer } from "./services/favPlayerService";
|
||||||
|
import { BattleService } from "./services/BattleService";
|
||||||
|
|
||||||
const server = serve({
|
const server = serve({
|
||||||
port: process.env.PORT || 3000,
|
port: process.env.PORT || 3000,
|
||||||
@ -89,6 +90,21 @@ const server = serve({
|
|||||||
await unFavPlayer(req.params.aud, req.params.uid);
|
await unFavPlayer(req.params.aud, req.params.uid);
|
||||||
return Response.json({ ok: 'ok' });
|
return Response.json({ ok: 'ok' });
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'/api/battle/:eventId': {
|
||||||
|
async PUT(req) {
|
||||||
|
const { data } = await req.json();
|
||||||
|
const eventId = req.params.eventId;
|
||||||
|
const code = await BattleService.publishBattle(eventId, data);
|
||||||
|
return Response.json({ code });
|
||||||
|
},
|
||||||
|
async GET(req) {
|
||||||
|
const code = new URL(req.url).searchParams.get('code');
|
||||||
|
const eventId = req.params.eventId;
|
||||||
|
if (!code) return Response.json({});
|
||||||
|
const data = await BattleService.getBattle(eventId, code);
|
||||||
|
return Response.json(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
22
src/services/BattleService.ts
Normal file
22
src/services/BattleService.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import Bun from "bun";
|
||||||
|
import { redis } from "../utils/server";
|
||||||
|
|
||||||
|
export class BattleService {
|
||||||
|
public static async publishBattle(eventId: string, jsonData: string) {
|
||||||
|
const hasher = new Bun.CryptoHasher('md5');
|
||||||
|
const hash = hasher.update(jsonData).digest('hex').slice(0, 6);
|
||||||
|
console.log(`Battle published with hash: ${hash}`);
|
||||||
|
await redis.setex(`my-kaiqiuwang:battle:${eventId}:${hash}`, 60 * 10, jsonData);
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
public static async getBattle(eventId: string, hash: string) {
|
||||||
|
const battleData = await redis.get(`my-kaiqiuwang:battle:${eventId}:${hash}`);
|
||||||
|
if (!battleData) return {};
|
||||||
|
try {
|
||||||
|
return JSON.parse(battleData);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error", e);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user