233 lines
10 KiB
TypeScript
233 lines
10 KiB
TypeScript
import { useEffect, useState } from 'react'
|
||
import { useNavigate } from 'react-router-dom'
|
||
import { useRoomStore, Room } from '../stores/roomStore'
|
||
import { useAuthStore } from '../stores/authStore'
|
||
|
||
function RoomCard({ room, onJoin }: { room: Room; onJoin: (room: Room) => void }) {
|
||
const statusColors: Record<string, string> = {
|
||
open: 'text-accent-green',
|
||
in_game: 'text-accent-orange',
|
||
closed: 'text-text-muted',
|
||
}
|
||
const statusLabels: Record<string, string> = {
|
||
open: '开放',
|
||
in_game: '游戏中',
|
||
closed: '已关闭',
|
||
}
|
||
|
||
return (
|
||
<div className="card hover:border-border-DEFAULT hover:bg-bg-tertiary transition-colors duration-150 cursor-pointer group"
|
||
onClick={() => onJoin(room)}>
|
||
<div className="flex items-start justify-between gap-3">
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2">
|
||
<h3 className="font-semibold text-text-primary truncate">{room.name}</h3>
|
||
{room.has_password && (
|
||
<svg className="w-3.5 h-3.5 text-text-muted flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||
</svg>
|
||
)}
|
||
</div>
|
||
<p className="text-text-muted text-xs mt-1">房主: {room.owner_username}</p>
|
||
</div>
|
||
<div className="text-right flex-shrink-0">
|
||
<span className={`text-xs font-medium ${statusColors[room.status] ?? 'text-text-muted'}`}>
|
||
{statusLabels[room.status] ?? room.status}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-4 mt-3 pt-3 border-t border-border">
|
||
<div className="flex items-center gap-1.5 text-text-secondary text-xs">
|
||
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||
<circle cx="9" cy="7" r="4"/>
|
||
<path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/>
|
||
</svg>
|
||
{room.current_players}/{room.max_players}
|
||
</div>
|
||
<div className="flex items-center gap-1.5 text-text-secondary text-xs">
|
||
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<polyline points="16 18 22 12 16 6"/>
|
||
<polyline points="8 6 2 12 8 18"/>
|
||
</svg>
|
||
{room.game_version}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default function DashboardPage() {
|
||
const { rooms, loading, fetchRooms, joinRoom, createRoom } = useRoomStore()
|
||
const { user } = useAuthStore()
|
||
const navigate = useNavigate()
|
||
|
||
const [showCreate, setShowCreate] = useState(false)
|
||
const [createName, setCreateName] = useState('')
|
||
const [createVersion, setCreateVersion] = useState('1.20')
|
||
const [createMax, setCreateMax] = useState('10')
|
||
const [createPw, setCreatePw] = useState('')
|
||
const [createPublic, setCreatePublic] = useState(true)
|
||
const [createError, setCreateError] = useState('')
|
||
const [creating, setCreating] = useState(false)
|
||
|
||
useEffect(() => {
|
||
fetchRooms()
|
||
const id = setInterval(fetchRooms, 10000)
|
||
return () => clearInterval(id)
|
||
}, [])
|
||
|
||
const handleJoin = async (room: Room) => {
|
||
try {
|
||
let password: string | undefined
|
||
if (room.has_password) {
|
||
password = window.prompt(`房间 "${room.name}" 需要密码:`) ?? undefined
|
||
if (password === undefined) return
|
||
}
|
||
await joinRoom(room.id, password)
|
||
navigate(`/room/${room.id}`)
|
||
} catch (e: any) {
|
||
alert(`加入失败: ${e}`)
|
||
}
|
||
}
|
||
|
||
const handleCreate = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
setCreateError('')
|
||
setCreating(true)
|
||
try {
|
||
const roomId = await createRoom({
|
||
name: createName,
|
||
maxPlayers: parseInt(createMax) || 10,
|
||
isPublic: createPublic,
|
||
password: createPw || undefined,
|
||
gameVersion: createVersion,
|
||
})
|
||
await joinRoom(roomId)
|
||
navigate(`/room/${roomId}`)
|
||
} catch (e: any) {
|
||
setCreateError(String(e))
|
||
setCreating(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="h-full flex flex-col overflow-hidden">
|
||
{/* Header */}
|
||
<div className="px-6 py-4 border-b border-border flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-xl font-bold text-text-primary">游戏大厅</h1>
|
||
<p className="text-text-muted text-xs mt-0.5">找到一个房间加入,或创建你自己的</p>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => fetchRooms()}
|
||
className="btn-secondary flex items-center gap-2"
|
||
>
|
||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<polyline points="23 4 23 10 17 10"/>
|
||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
||
</svg>
|
||
刷新
|
||
</button>
|
||
<button
|
||
onClick={() => setShowCreate(true)}
|
||
className="btn-primary flex items-center gap-2"
|
||
>
|
||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||
</svg>
|
||
创建房间
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Room grid */}
|
||
<div className="flex-1 overflow-y-auto p-6">
|
||
{loading && rooms.length === 0 ? (
|
||
<div className="flex items-center justify-center h-32 text-text-muted text-sm">
|
||
加载中...
|
||
</div>
|
||
) : rooms.length === 0 ? (
|
||
<div className="flex flex-col items-center justify-center h-48 text-text-muted">
|
||
<svg className="w-12 h-12 mb-3 opacity-30" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
|
||
<rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>
|
||
</svg>
|
||
<p className="text-sm">暂无公开房间</p>
|
||
<p className="text-xs mt-1">创建第一个房间开始游戏</p>
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||
{rooms.map((room) => (
|
||
<RoomCard key={room.id} room={room} onJoin={handleJoin} />
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Create room modal */}
|
||
{showCreate && (
|
||
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50 animate-fade-in">
|
||
<div className="card w-full max-w-sm animate-slide-up">
|
||
<div className="flex items-center justify-between mb-5">
|
||
<h2 className="font-semibold text-text-primary">创建房间</h2>
|
||
<button onClick={() => setShowCreate(false)} className="text-text-muted hover:text-text-primary">
|
||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleCreate} className="space-y-3">
|
||
<div>
|
||
<label className="block text-xs text-text-secondary mb-1">房间名称</label>
|
||
<input className="input-field" placeholder="我的房间" value={createName}
|
||
onChange={(e) => setCreateName(e.target.value)} required />
|
||
</div>
|
||
<div className="flex gap-3">
|
||
<div className="flex-1">
|
||
<label className="block text-xs text-text-secondary mb-1">游戏版本</label>
|
||
<input className="input-field" placeholder="1.20" value={createVersion}
|
||
onChange={(e) => setCreateVersion(e.target.value)} />
|
||
</div>
|
||
<div className="flex-1">
|
||
<label className="block text-xs text-text-secondary mb-1">最大人数</label>
|
||
<input className="input-field" type="number" min="2" max="20" value={createMax}
|
||
onChange={(e) => setCreateMax(e.target.value)} />
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs text-text-secondary mb-1">密码(可选)</label>
|
||
<input className="input-field" type="password" placeholder="留空表示无密码" value={createPw}
|
||
onChange={(e) => setCreatePw(e.target.value)} />
|
||
</div>
|
||
<label className="flex items-center gap-2 cursor-pointer">
|
||
<input type="checkbox" checked={createPublic}
|
||
onChange={(e) => setCreatePublic(e.target.checked)}
|
||
className="w-4 h-4 rounded accent-accent-green" />
|
||
<span className="text-sm text-text-secondary">公开房间</span>
|
||
</label>
|
||
|
||
{createError && (
|
||
<div className="px-3 py-2 rounded-lg bg-accent-red/10 border border-accent-red/30 text-accent-red text-xs">
|
||
{createError}
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex gap-2 pt-1">
|
||
<button type="button" className="btn-secondary flex-1" onClick={() => setShowCreate(false)}>取消</button>
|
||
<button type="submit" className="btn-primary flex-1" disabled={creating}>
|
||
{creating ? '创建中...' : '创建'}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|