diff --git a/src/components/BattleTable.tsx b/src/components/BattleTable.tsx index de13025..a3ec331 100644 --- a/src/components/BattleTable.tsx +++ b/src/components/BattleTable.tsx @@ -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 { 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>([]); - + 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: ( + + {code} + + ), + }); + }, [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 = 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 ( + {contextHodler} + + + setShareCode(e.target.value)} + onPressEnter={handleImportCode} + /> + + {matchGroupTable.map(([left, right], i) => { return ( +
); })} - ( - - 积分增减预测 - - + } onClick={handleClearAll}>清除本组所有 )} - showHeader={false} - pagination={false} - rowHoverable={false} - size="small" - dataSource={playerList} - rowKey={e => e.uid} - style={{ minWidth: 300, width: '80vw', maxWidth: 620 }} - columns={[ - { dataIndex: 'name', align: 'center', }, - { dataIndex: 'score', align: 'center', }, - { 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; - } }, - ]} - /> + > +
e.uid} + style={{ width: '100%' }} + columns={[ + { dataIndex: 'name', align: 'center', }, + { dataIndex: 'score', align: 'center', }, + { 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; + } }, + ]} + /> + + + + + + + + + ); } diff --git a/src/index.tsx b/src/index.tsx index 84ff4b9..bc394df 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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); + } } }, diff --git a/src/services/BattleService.ts b/src/services/BattleService.ts new file mode 100644 index 0000000..2d86b05 --- /dev/null +++ b/src/services/BattleService.ts @@ -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 {}; + } + } +} \ No newline at end of file diff --git a/src/services/FavPlayer.ts b/src/services/favPlayerService.ts similarity index 100% rename from src/services/FavPlayer.ts rename to src/services/favPlayerService.ts