Files
werewolf/frontend/src/pages/Lobby.tsx
T

143 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import type { BoardInfo } from '../types'
import ThemeToggle from '../components/ThemeToggle'
import './Lobby.scss'
export function getNickname(): string {
return localStorage.getItem('ww_name') || ''
}
export function setNickname(name: string) {
localStorage.setItem('ww_name', name)
}
export default function Lobby() {
const nav = useNavigate()
const [name, setName] = useState(getNickname())
const [boards, setBoards] = useState<BoardInfo[]>([])
const [board, setBoard] = useState('classic12')
const [joinCode, setJoinCode] = useState('')
const [health, setHealth] = useState<{ llm_configured: boolean; model: string | null } | null>(null)
const [busy, setBusy] = useState(false)
const [error, setError] = useState('')
useEffect(() => {
fetch('/api/health').then((r) => r.json()).then(setHealth).catch(() => {})
fetch('/api/boards').then((r) => r.json()).then((b: BoardInfo[]) => {
setBoards(b)
if (b.length && !b.some((x) => x.key === board)) setBoard(b[0].key)
}).catch(() => {})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const ensureName = (): boolean => {
if (!name.trim()) {
setError('请先输入你的昵称')
return false
}
setNickname(name.trim())
return true
}
const createRoom = async () => {
if (!ensureName()) return
setBusy(true)
setError('')
try {
const res = await fetch('/api/rooms', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ board, host_name: name.trim() }),
})
const data = await res.json()
nav(`/room/${data.code}`)
} catch {
setError('创建房间失败,请确认后端已启动')
} finally {
setBusy(false)
}
}
const joinRoom = () => {
if (!ensureName()) return
const code = joinCode.trim().toUpperCase()
if (!code) {
setError('请输入房间号')
return
}
nav(`/room/${code}`)
}
const selectedBoard = boards.find((b) => b.key === board)
return (
<div className="lobby">
<div className="lobby-card">
<div className="card-top">
<ThemeToggle />
</div>
<div className="logo">
<span className="moon">🌕</span>
<h1>狼人杀</h1>
<p className="subtitle">WEREWOLVES · 人机混战 · 多人联机</p>
</div>
<div className="field">
<label>你的昵称</label>
<input
value={name}
maxLength={12}
placeholder="给自己起个名字"
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && createRoom()}
/>
</div>
<div className="field">
<label>选择板子</label>
<select value={board} onChange={(e) => setBoard(e.target.value)}>
{boards.map((b) => (
<option key={b.key} value={b.key}>
{b.name}
</option>
))}
</select>
{selectedBoard && <p className="hint">共 {selectedBoard.seat_count} 个座位</p>}
</div>
<button className="primary big" disabled={busy} onClick={createRoom}>
{busy ? '创建中…' : '创建房间'}
</button>
<div className="divider">或者</div>
<div className="join-row">
<input
className="code-input"
value={joinCode}
maxLength={4}
placeholder="房间号"
onChange={(e) => setJoinCode(e.target.value.toUpperCase())}
onKeyDown={(e) => e.key === 'Enter' && joinRoom()}
/>
<button onClick={joinRoom}>加入房间</button>
</div>
{error && <p className="error">{error}</p>}
{health && (
<p className="ai-status">
AI 玩家:
{health.llm_configured ? (
<span className="ok">大模型已接入({health.model})</span>
) : (
<span className="fallback">未配置密钥,使用内置启发式 AI</span>
)}
</p>
)}
</div>
</div>
)
}