Initial commit: 网页联机狼人杀(FastAPI+WebSocket 后端 / React+TS+Sass 前端)
This commit is contained in:
commit
7f5ea6bee8
55 files changed
+6999
No files matched your search
@@ -0,0 +1,142 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user