feat(BattleTable): share code
This commit is contained in:
parent
9044073afd
commit
6f1f8044dd
@ -1,28 +1,35 @@
|
||||
import type { BasePlayer } from "../types";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
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 { 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)`
|
||||
min-width: 300px;
|
||||
width: 80vw;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
.ant-card, .share-buttons {
|
||||
width: 100%;
|
||||
}
|
||||
.winer {
|
||||
color: red;
|
||||
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({
|
||||
players: list,
|
||||
}: {
|
||||
@ -35,7 +42,7 @@ function BattleTable({
|
||||
}], [list]);
|
||||
const resultMap = useMemo(() => {
|
||||
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, {
|
||||
winer: string,
|
||||
score: number,
|
||||
@ -54,7 +61,7 @@ function BattleTable({
|
||||
const [left, right] = getRoundTable(dataList, i);
|
||||
left?.forEach((l, i) => {
|
||||
const r = right?.[i];
|
||||
const key = `${l?.uid}-${r?.uid}`;
|
||||
const key = getMatchKey(l!, r!);
|
||||
const winer = resultMap.get(key)?.winer;
|
||||
const score = resultMap.get(key)?.score;
|
||||
if (winer && l && r) {
|
||||
@ -72,10 +79,20 @@ function BattleTable({
|
||||
return roundTable;
|
||||
}, []);
|
||||
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) => {
|
||||
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);
|
||||
if (resultMap.get(key)?.winer) {
|
||||
resultMap.delete(key);
|
||||
@ -87,7 +104,7 @@ function BattleTable({
|
||||
const handleClear = useCallback((left?: (BasePlayer | null | undefined)[], right?: (BasePlayer | null | undefined)[]) => {
|
||||
left?.forEach((l, i) => {
|
||||
const r = right?.[i];
|
||||
const key = `${l?.uid}-${r?.uid}`;
|
||||
const key = getMatchKey(l!, r!);
|
||||
resultMap.delete(key);
|
||||
});
|
||||
setMatchGroupTable(buildMatchGroupTable(playerList));
|
||||
@ -96,7 +113,7 @@ function BattleTable({
|
||||
matchGroupTable.forEach(([left, right]) => {
|
||||
left?.forEach((l, i) => {
|
||||
const r = right?.[i];
|
||||
const key = `${l?.uid}-${r?.uid}`;
|
||||
const key = getMatchKey(l!, r!);
|
||||
resultMap.delete(key);
|
||||
});
|
||||
});
|
||||
@ -107,13 +124,74 @@ function BattleTable({
|
||||
const newScoreMap = Object.fromEntries([...left, ...right].map(e => [e?.uid, e?.score]));
|
||||
return newScoreMap;
|
||||
}, [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(() => {
|
||||
const roundTable = buildMatchGroupTable(playerList);
|
||||
setMatchGroupTable(roundTable);
|
||||
}, [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);
|
||||
return (
|
||||
<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) => {
|
||||
return (
|
||||
<Card
|
||||
@ -132,7 +210,7 @@ function BattleTable({
|
||||
const nameL = l?.uid ? `${l?.name}(${l?.score})${resultL}` : '-';
|
||||
const nameR = r?.uid ? `${r?.name}(${r?.score})${resultR}` : '-';
|
||||
return (
|
||||
<div key={key} style={{ minWidth: 280, width: '80vw', maxWidth: 600 }}>
|
||||
<div key={key} className="round-row">
|
||||
<Flex align="center" justify="space-between">
|
||||
<div
|
||||
className={winer === l?.uid ? 'winer' : ''}
|
||||
@ -162,20 +240,21 @@ function BattleTable({
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
<Table
|
||||
title={() => (
|
||||
<Flex align="center" justify="space-between">
|
||||
<span>积分增减预测</span>
|
||||
<Card
|
||||
title={'积分增减预测'}
|
||||
extra={(
|
||||
<Button type="link" icon={<ClearOutlined />} onClick={handleClearAll}>清除本组所有</Button>
|
||||
</Flex>
|
||||
)}
|
||||
>
|
||||
<Table
|
||||
className="score-table"
|
||||
showHeader={false}
|
||||
pagination={false}
|
||||
rowHoverable={false}
|
||||
size="small"
|
||||
dataSource={playerList}
|
||||
rowKey={e => e.uid}
|
||||
style={{ minWidth: 300, width: '80vw', maxWidth: 620 }}
|
||||
style={{ width: '100%' }}
|
||||
columns={[
|
||||
{ dataIndex: 'name', align: 'center', },
|
||||
{ dataIndex: 'score', align: 'center', },
|
||||
@ -186,6 +265,15 @@ function BattleTable({
|
||||
} },
|
||||
]}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,7 +2,8 @@ import { serve } from "bun";
|
||||
import { getMatchInfo, listEvent, xcxApi } from "./utils/server";
|
||||
import index from "./index.html";
|
||||
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({
|
||||
port: process.env.PORT || 3000,
|
||||
@ -89,6 +90,21 @@ const server = serve({
|
||||
await unFavPlayer(req.params.aud, req.params.uid);
|
||||
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