From bda917ce37e37655330fc133af2e0839be81675d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=88=AA=E5=AE=87?= <3364451258@qq.com> Date: Sun, 31 May 2026 15:16:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Online=20=E6=A8=A1=E5=BC=8F=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=20UI=20=E2=80=94=20=E6=88=BF=E9=97=B4=E7=AE=A1?= =?UTF-8?q?=E7=90=86/=E8=BF=9E=E6=8E=A5=E7=8A=B6=E6=80=81/remote-move/?= =?UTF-8?q?=E7=A6=81=E6=82=94=E6=A3=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- src/components/board/BoardCanvas.tsx | 15 ++++++ src/components/game/GameControls.tsx | 3 +- src/components/game/GameView.tsx | 29 +++++++++++ src/components/menu/MainMenu.tsx | 8 +--- src/components/menu/OnlineSetup.tsx | 72 ++++++++++++++++++++++------ src/i18n/en.json | 4 +- src/i18n/zh-CN.json | 4 +- 7 files changed, 111 insertions(+), 24 deletions(-) diff --git a/src/components/board/BoardCanvas.tsx b/src/components/board/BoardCanvas.tsx index 9e08d46..5fcfd4e 100644 --- a/src/components/board/BoardCanvas.tsx +++ b/src/components/board/BoardCanvas.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useCallback, useMemo } from 'react'; +import { listen } from '@tauri-apps/api/event'; import { useGameStore, buildReplayBoard } from '../../store/gameStore'; import { computeBoardDimensions, @@ -59,6 +60,20 @@ export default function BoardCanvas() { return () => window.removeEventListener('resize', handleResize); }, [render]); + useEffect(() => { + if (mode !== 'Online') return; + let unlisten: (() => void) | undefined; + + const setup = async () => { + unlisten = await listen<{ x: number; y: number }>('remote-move', (event) => { + placePiece(event.payload.x, event.payload.y); + }); + }; + setup(); + + return () => { unlisten?.(); }; + }, [mode, placePiece]); + const handleClick = useCallback( (e: React.MouseEvent) => { if (status !== 'playing') return; diff --git a/src/components/game/GameControls.tsx b/src/components/game/GameControls.tsx index d427c14..1c7d253 100644 --- a/src/components/game/GameControls.tsx +++ b/src/components/game/GameControls.tsx @@ -10,6 +10,7 @@ export default function GameControls({ onBackToMenu }: Props) { const { t } = useTranslation(); const undo = useGameStore((s) => s.undo); const status = useGameStore((s) => s.status); + const mode = useGameStore((s) => s.mode); const refreshBoard = useGameStore((s) => s.refreshBoard); const handleUndo = () => { @@ -38,7 +39,7 @@ export default function GameControls({ onBackToMenu }: Props) { return (
- - +
diff --git a/src/components/menu/OnlineSetup.tsx b/src/components/menu/OnlineSetup.tsx index ad318e5..d5bc625 100644 --- a/src/components/menu/OnlineSetup.tsx +++ b/src/components/menu/OnlineSetup.tsx @@ -1,6 +1,8 @@ +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useGameStore } from '../../store/gameStore'; -import { useState } from 'react'; +import { invoke } from '@tauri-apps/api/core'; +import { listen } from '@tauri-apps/api/event'; import { MIN_BOARD_SIZE, MAX_BOARD_SIZE } from '../../core/constants'; import type { GameConfig } from '../../core/types'; @@ -9,25 +11,67 @@ interface Props { onBack: () => void; onStart: () => void; } export default function OnlineSetup({ onBack, onStart }: Props) { const { t } = useTranslation(); const startGame = useGameStore((s) => s.startGame); - const [ip, setIp] = useState(''); const [boardSize, setBoardSize] = useState(15); - - const baseConfig: GameConfig = { - boardSize, useForbiddenRules: true, useTimer: false, - timeLimitSecs: 60, aiDifficulty: 3, playerColor: 'Black', isServer: false, - remoteAddress: '', - }; + const [ip, setIp] = useState(''); + const [myAddress, setMyAddress] = useState(''); + const [isHosting, setIsHosting] = useState(false); const handleHost = async () => { - await startGame('Online', { ...baseConfig, isServer: true }); - onStart(); + try { + const port: number = await invoke('host_game', { port: 0 }); + setMyAddress(`127.0.0.1:${port}`); + setIsHosting(true); + + const unlisten = await listen('connection-status', async (event) => { + if (event.payload === 'connected') { + unlisten(); + const config: GameConfig = { + boardSize, useForbiddenRules: true, useTimer: false, + timeLimitSecs: 60, aiDifficulty: 3, playerColor: 'Black', + isServer: true, remoteAddress: '', hostPort: port, + useLlm: false, llmEndpoint: '', llmApiKey: '', llmModel: '', + }; + await startGame('Online', config); + onStart(); + } + }); + } catch (e) { + alert('创建房间失败: ' + e); + } }; const handleJoin = async () => { - await startGame('Online', { ...baseConfig, remoteAddress: ip }); - onStart(); + try { + const [_, portStr] = ip.split(':'); + const port = parseInt(portStr) || 0; + const config: GameConfig = { + boardSize, useForbiddenRules: true, useTimer: false, + timeLimitSecs: 60, aiDifficulty: 3, playerColor: 'White', + isServer: false, remoteAddress: ip, hostPort: port, + useLlm: false, llmEndpoint: '', llmApiKey: '', llmModel: '', + }; + await startGame('Online', config); + await invoke('join_game', { address: ip }); + onStart(); + } catch (e) { + alert('加入房间失败: ' + e); + } }; + if (isHosting) { + return ( +
+

{t('menu.online_game')}

+

等待对手加入...

+

+ {myAddress} +

+

将此地址发给对手

+ +
+ ); + } + return (

{t('menu.online_game')}

@@ -35,13 +79,13 @@ export default function OnlineSetup({ onBack, onStart }: Props) { {t('settings.board_size')}:
- setIp(e.target.value)} placeholder={t('menu.ip_placeholder') as string} /> + setIp(e.target.value)} placeholder={t('menu.ip_placeholder')} />
diff --git a/src/i18n/en.json b/src/i18n/en.json index fafe6ce..06d1ba3 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -33,7 +33,9 @@ "new_game": "New Game", "waiting_opponent": "Waiting for Opponent...", "your_turn": "Your Turn", - "opponent_turn": "Opponent's Turn" + "opponent_turn": "Opponent's Turn", + "opponent_connected": "Opponent Connected", + "opponent_disconnected": "Opponent Disconnected" }, "replay": { "play": "Play", diff --git a/src/i18n/zh-CN.json b/src/i18n/zh-CN.json index 8824310..fcb587f 100644 --- a/src/i18n/zh-CN.json +++ b/src/i18n/zh-CN.json @@ -33,7 +33,9 @@ "new_game": "新游戏", "waiting_opponent": "等待对手加入...", "your_turn": "你的回合", - "opponent_turn": "对手回合" + "opponent_turn": "对手回合", + "opponent_connected": "对手已连接", + "opponent_disconnected": "对手已断开" }, "replay": { "play": "播放",