143 lines
4.2 KiB
TypeScript
143 lines
4.2 KiB
TypeScript
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>
|
||
)
|
||
}
|