Initial commit: FunConnect project with server, relay, client and admin panel

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-24 20:56:36 +08:00
parent eb6e901440
commit b6891483ae
167 changed files with 16147 additions and 106 deletions

View File

@@ -0,0 +1,232 @@
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>
)
}