完善观影室
This commit is contained in:
@@ -8,6 +8,8 @@ import { Heart, Radio, Tv } from 'lucide-react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { Suspense, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useLiveSync } from '@/hooks/useLiveSync';
|
||||
|
||||
import {
|
||||
deleteFavorite,
|
||||
generateStorageKey,
|
||||
@@ -122,6 +124,20 @@ function LivePageClient() {
|
||||
const favoritedRef = useRef(false);
|
||||
const currentChannelRef = useRef<LiveChannel | null>(null);
|
||||
|
||||
// 观影室同步功能
|
||||
const liveSync = useLiveSync({
|
||||
currentChannelId: currentChannel?.id || '',
|
||||
currentChannelName: currentChannel?.name || '',
|
||||
currentChannelUrl: currentChannel?.url || '',
|
||||
onChannelChange: (channelId, channelUrl) => {
|
||||
// 房员接收到频道切换指令
|
||||
const channel = currentChannels.find(c => c.id === channelId);
|
||||
if (channel) {
|
||||
handleChannelChange(channel);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// EPG数据清洗函数 - 去除重叠的节目,保留时间较短的,只显示今日节目
|
||||
const cleanEpgData = (programs: Array<{ start: string; end: string; title: string }>) => {
|
||||
if (!programs || programs.length === 0) return programs;
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Heart } from 'lucide-react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { Suspense, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { usePlaySync } from '@/hooks/usePlaySync';
|
||||
|
||||
|
||||
import {
|
||||
deleteFavorite,
|
||||
@@ -328,7 +330,28 @@ function PlayPageClient() {
|
||||
needPreferRef.current = needPrefer;
|
||||
}, [needPrefer]);
|
||||
// 集数相关
|
||||
const [currentEpisodeIndex, setCurrentEpisodeIndex] = useState(0);
|
||||
const [currentEpisodeIndex, setCurrentEpisodeIndex] = useState(() => {
|
||||
const episodeParam = searchParams.get('episode');
|
||||
if (episodeParam) {
|
||||
const episode = parseInt(episodeParam, 10);
|
||||
return episode > 0 ? episode - 1 : 0; // URL 中是 1-based,内部是 0-based
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
// 监听 URL 参数变化,更新集数索引(用于房员跟随换集)
|
||||
useEffect(() => {
|
||||
const episodeParam = searchParams.get('episode');
|
||||
if (episodeParam) {
|
||||
const episode = parseInt(episodeParam, 10);
|
||||
const newIndex = episode > 0 ? episode - 1 : 0;
|
||||
console.log('[PlayPage] Checking episode from URL:', { urlEpisode: episode, currentIndex: currentEpisodeIndex, newIndex });
|
||||
if (newIndex !== currentEpisodeIndex) {
|
||||
console.log('[PlayPage] URL episode changed, updating index to:', newIndex);
|
||||
setCurrentEpisodeIndex(newIndex);
|
||||
}
|
||||
}
|
||||
}, [searchParams, currentEpisodeIndex]);
|
||||
|
||||
const currentSourceRef = useRef(currentSource);
|
||||
const currentIdRef = useRef(currentId);
|
||||
@@ -429,6 +452,9 @@ function PlayPageClient() {
|
||||
'initing' | 'sourceChanging'
|
||||
>('initing');
|
||||
|
||||
// 播放器就绪状态(用于触发 usePlaySync 的事件监听器设置)
|
||||
const [playerReady, setPlayerReady] = useState(false);
|
||||
|
||||
// 播放进度保存相关
|
||||
const saveIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const lastSaveTimeRef = useRef<number>(0);
|
||||
@@ -439,6 +465,19 @@ function PlayPageClient() {
|
||||
// Wake Lock 相关
|
||||
const wakeLockRef = useRef<WakeLockSentinel | null>(null);
|
||||
|
||||
// 观影室同步功能
|
||||
const playSync = usePlaySync({
|
||||
artPlayerRef,
|
||||
videoId: currentId || '', // 使用 currentId 状态而不是 searchParams
|
||||
videoName: videoTitle || detail?.title || '正在加载...',
|
||||
videoYear: videoYear || detail?.year || '',
|
||||
searchTitle: searchTitle || '',
|
||||
currentEpisode: currentEpisodeIndex + 1,
|
||||
currentSource: currentSource || '',
|
||||
videoUrl: videoUrl || '',
|
||||
playerReady: playerReady, // 传递播放器就绪状态
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// 工具函数(Utils)
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -2721,6 +2760,13 @@ function PlayPageClient() {
|
||||
html: '<i class="art-icon flex"><svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z" fill="currentColor"/></svg></i>',
|
||||
tooltip: '播放下一集',
|
||||
click: function () {
|
||||
// 房员禁用下一集按钮
|
||||
if (playSync.shouldDisableControls) {
|
||||
if (artPlayerRef.current) {
|
||||
artPlayerRef.current.notice.show = '房员无法切换集数,请等待房主操作';
|
||||
}
|
||||
return;
|
||||
}
|
||||
handleNextEpisode();
|
||||
},
|
||||
},
|
||||
@@ -2731,6 +2777,10 @@ function PlayPageClient() {
|
||||
artPlayerRef.current.on('ready', async () => {
|
||||
setError(null);
|
||||
|
||||
// 标记播放器已就绪,触发 usePlaySync 设置事件监听器
|
||||
setPlayerReady(true);
|
||||
console.log('[PlayPage] Player ready, triggering sync setup');
|
||||
|
||||
// 从 art.storage 读取弹幕设置并应用
|
||||
if (artPlayerRef.current) {
|
||||
const storedDanmakuSettings = artPlayerRef.current.storage.get('danmaku_settings');
|
||||
@@ -2905,8 +2955,17 @@ function PlayPageClient() {
|
||||
}
|
||||
});
|
||||
|
||||
// 监听视频播放结束事件,自动播放下一集
|
||||
// 监听视频播放结束事件,自动播放下一集(房员禁用)
|
||||
artPlayerRef.current.on('video:ended', () => {
|
||||
// 房员禁用自动播放下一集
|
||||
if (playSync.shouldDisableControls) {
|
||||
console.log('[PlayPage] Member cannot auto-play next episode');
|
||||
if (artPlayerRef.current) {
|
||||
artPlayerRef.current.notice.show = '等待房主切换下一集';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const d = detailRef.current;
|
||||
const idx = currentEpisodeIndexRef.current;
|
||||
if (d && d.episodes && idx < d.episodes.length - 1) {
|
||||
@@ -3686,18 +3745,29 @@ function PlayPageClient() {
|
||||
|
||||
{/* 选集和换源 - 在移动端始终显示,在 lg 及以上可折叠 */}
|
||||
<div
|
||||
className={`h-[300px] lg:h-full md:overflow-hidden transition-all duration-300 ease-in-out ${
|
||||
className={`h-[300px] lg:h-full md:overflow-hidden transition-all duration-300 ease-in-out relative ${
|
||||
isEpisodeSelectorCollapsed
|
||||
? 'md:col-span-1 lg:hidden lg:opacity-0 lg:scale-95'
|
||||
: 'md:col-span-1 lg:opacity-100 lg:scale-100'
|
||||
}`}
|
||||
>
|
||||
{/* 观影室房员禁用层 */}
|
||||
{playSync.isInRoom && playSync.shouldDisableControls && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="text-center p-4">
|
||||
<p className="text-white text-lg font-bold mb-2">👥 观影室模式</p>
|
||||
<p className="text-gray-300 text-sm">
|
||||
{playSync.isOwner ? '您是房主,可以控制播放' : '房主控制中,无法切换集数和播放源'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<EpisodeSelector
|
||||
totalEpisodes={totalEpisodes}
|
||||
episodes_titles={detail?.episodes_titles || []}
|
||||
value={currentEpisodeIndex + 1}
|
||||
onChange={handleEpisodeChange}
|
||||
onSourceChange={handleSourceChange}
|
||||
onChange={playSync.shouldDisableControls ? () => {} : handleEpisodeChange}
|
||||
onSourceChange={playSync.shouldDisableControls ? () => {} : handleSourceChange}
|
||||
currentSource={currentSource}
|
||||
currentId={currentId}
|
||||
videoTitle={searchTitle || videoTitle}
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
// 房间列表页面
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ArrowLeft, Users, Lock, RefreshCw } from 'lucide-react';
|
||||
import { useWatchRoomContext } from '@/components/WatchRoomProvider';
|
||||
import JoinRoomModal from '@/components/watch-room/JoinRoomModal';
|
||||
import type { Room } from '@/types/watch-room';
|
||||
|
||||
export default function RoomListPage() {
|
||||
const router = useRouter();
|
||||
const { getRoomList, isConnected } = useWatchRoomContext();
|
||||
|
||||
const [rooms, setRooms] = useState<Room[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedRoomId, setSelectedRoomId] = useState<string | null>(null);
|
||||
const [showJoinModal, setShowJoinModal] = useState(false);
|
||||
|
||||
const loadRooms = async () => {
|
||||
if (!isConnected) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const roomList = await getRoomList();
|
||||
setRooms(roomList);
|
||||
} catch (error) {
|
||||
console.error('[WatchRoom] Failed to load rooms:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadRooms();
|
||||
// 每5秒刷新一次房间列表
|
||||
const interval = setInterval(loadRooms, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, [isConnected]);
|
||||
|
||||
const handleJoinRoom = (roomId: string) => {
|
||||
setSelectedRoomId(roomId);
|
||||
setShowJoinModal(true);
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: number) => {
|
||||
const now = Date.now();
|
||||
const diff = now - timestamp;
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) return `${days}天前`;
|
||||
if (hours > 0) return `${hours}小时前`;
|
||||
if (minutes > 0) return `${minutes}分钟前`;
|
||||
return '刚刚';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 px-4 py-8">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
{/* 顶部栏 */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="flex items-center gap-2 rounded-lg bg-gray-800 px-4 py-2 text-white transition-colors hover:bg-gray-700"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
返回
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={loadRooms}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-2 rounded-lg bg-gray-800 px-4 py-2 text-white transition-colors hover:bg-gray-700 disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`h-5 w-5 ${loading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 标题 */}
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="mb-2 text-3xl font-bold text-white md:text-4xl">公开房间列表</h1>
|
||||
<p className="text-gray-400">
|
||||
找到{rooms.length}个公开的观影室
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 加载中 */}
|
||||
{loading && rooms.length === 0 && (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-center">
|
||||
<RefreshCw className="mx-auto mb-4 h-12 w-12 animate-spin text-gray-400" />
|
||||
<p className="text-gray-400">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 空状态 */}
|
||||
{!loading && rooms.length === 0 && (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-center">
|
||||
<Users className="mx-auto mb-4 h-16 w-16 text-gray-600" />
|
||||
<p className="mb-2 text-xl text-gray-400">暂无公开房间</p>
|
||||
<p className="text-sm text-gray-500">创建一个新房间或通过房间号加入私密房间</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 房间列表 */}
|
||||
{rooms.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{rooms.map((room) => (
|
||||
<div
|
||||
key={room.id}
|
||||
className="group rounded-xl bg-gray-800/50 p-6 backdrop-blur-sm transition-all hover:bg-gray-800/70 hover:shadow-xl"
|
||||
>
|
||||
<div className="mb-4 flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="mb-1 text-lg font-bold text-white">{room.name}</h3>
|
||||
{room.description && (
|
||||
<p className="mb-2 text-sm text-gray-400 line-clamp-2">{room.description}</p>
|
||||
)}
|
||||
</div>
|
||||
{room.password && (
|
||||
<Lock className="h-5 w-5 flex-shrink-0 text-yellow-400" title="需要密码" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-4 space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2 text-gray-400">
|
||||
<span className="text-gray-500">房间号:</span>
|
||||
<span className="font-mono text-lg font-bold text-white">{room.id}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-gray-400">
|
||||
<Users className="h-4 w-4" />
|
||||
<span>{room.memberCount} 人在线</span>
|
||||
</div>
|
||||
<div className="text-gray-400">
|
||||
<span className="text-gray-500">房主:</span> {room.ownerName}
|
||||
</div>
|
||||
<div className="text-gray-400">
|
||||
<span className="text-gray-500">创建时间:</span> {formatTime(room.createdAt)}
|
||||
</div>
|
||||
{room.currentState && (
|
||||
<div className="mt-2 rounded-lg bg-blue-500/20 px-3 py-2">
|
||||
<p className="text-xs text-blue-300">
|
||||
{room.currentState.type === 'play'
|
||||
? `正在播放: ${room.currentState.videoName}`
|
||||
: `正在观看: ${room.currentState.channelName}`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handleJoinRoom(room.id)}
|
||||
className="w-full rounded-lg bg-purple-500 py-3 font-medium text-white transition-colors hover:bg-purple-600"
|
||||
>
|
||||
加入房间
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 加入房间弹窗 */}
|
||||
{showJoinModal && selectedRoomId && (
|
||||
<JoinRoomModal
|
||||
roomId={selectedRoomId}
|
||||
onClose={() => {
|
||||
setShowJoinModal(false);
|
||||
setSelectedRoomId(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,114 +1,663 @@
|
||||
// 观影室首页
|
||||
// 观影室首页 - 选项卡式界面
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Users, UserPlus, List as ListIcon, Lock, RefreshCw } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Users, UserPlus, List } from 'lucide-react';
|
||||
import CreateRoomModal from '@/components/watch-room/CreateRoomModal';
|
||||
import JoinRoomModal from '@/components/watch-room/JoinRoomModal';
|
||||
import { useWatchRoomContext } from '@/components/WatchRoomProvider';
|
||||
import PageLayout from '@/components/PageLayout';
|
||||
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
||||
import type { Room } from '@/types/watch-room';
|
||||
|
||||
type TabType = 'create' | 'join' | 'list';
|
||||
|
||||
export default function WatchRoomPage() {
|
||||
const router = useRouter();
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showJoinModal, setShowJoinModal] = useState(false);
|
||||
const { getRoomList, isConnected, createRoom, joinRoom, currentRoom, isOwner, members } = useWatchRoomContext();
|
||||
const [activeTab, setActiveTab] = useState<TabType>('create');
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
icon: Users,
|
||||
label: '创建房间',
|
||||
description: '创建一个新的观影室',
|
||||
onClick: () => setShowCreateModal(true),
|
||||
color: 'bg-blue-500 hover:bg-blue-600',
|
||||
},
|
||||
{
|
||||
icon: UserPlus,
|
||||
label: '加入房间',
|
||||
description: '通过房间号加入观影室',
|
||||
onClick: () => setShowJoinModal(true),
|
||||
color: 'bg-green-500 hover:bg-green-600',
|
||||
},
|
||||
{
|
||||
icon: List,
|
||||
label: '房间列表',
|
||||
description: '查看所有公开的观影室',
|
||||
onClick: () => router.push('/watch-room/list'),
|
||||
color: 'bg-purple-500 hover:bg-purple-600',
|
||||
},
|
||||
// 获取当前登录用户(在客户端挂载后读取,避免 hydration 错误)
|
||||
const [currentUsername, setCurrentUsername] = useState<string>('游客');
|
||||
|
||||
useEffect(() => {
|
||||
const authInfo = getAuthInfoFromBrowserCookie();
|
||||
setCurrentUsername(authInfo?.username || '游客');
|
||||
}, []);
|
||||
|
||||
// 创建房间表单
|
||||
const [createForm, setCreateForm] = useState({
|
||||
roomName: '',
|
||||
description: '',
|
||||
password: '',
|
||||
isPublic: true,
|
||||
});
|
||||
|
||||
// 加入房间表单
|
||||
const [joinForm, setJoinForm] = useState({
|
||||
roomId: '',
|
||||
password: '',
|
||||
});
|
||||
|
||||
// 房间列表
|
||||
const [rooms, setRooms] = useState<Room[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [createLoading, setCreateLoading] = useState(false);
|
||||
const [joinLoading, setJoinLoading] = useState(false);
|
||||
|
||||
// 加载房间列表
|
||||
const loadRooms = async () => {
|
||||
if (!isConnected) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const roomList = await getRoomList();
|
||||
setRooms(roomList);
|
||||
} catch (error) {
|
||||
console.error('[WatchRoom] Failed to load rooms:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换到房间列表 tab 时加载房间
|
||||
useEffect(() => {
|
||||
if (activeTab === 'list') {
|
||||
loadRooms();
|
||||
// 每5秒刷新一次
|
||||
const interval = setInterval(loadRooms, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [activeTab, isConnected]);
|
||||
|
||||
// 处理创建房间
|
||||
const handleCreateRoom = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!createForm.roomName.trim()) {
|
||||
alert('请输入房间名称');
|
||||
return;
|
||||
}
|
||||
|
||||
setCreateLoading(true);
|
||||
try {
|
||||
await createRoom({
|
||||
name: createForm.roomName.trim(),
|
||||
description: createForm.description.trim(),
|
||||
password: createForm.password.trim() || undefined,
|
||||
isPublic: createForm.isPublic,
|
||||
userName: currentUsername,
|
||||
});
|
||||
|
||||
// 清空表单
|
||||
setCreateForm({
|
||||
roomName: '',
|
||||
description: '',
|
||||
password: '',
|
||||
isPublic: true,
|
||||
});
|
||||
} catch (error: any) {
|
||||
alert(error.message || '创建房间失败');
|
||||
} finally {
|
||||
setCreateLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理加入房间
|
||||
const handleJoinRoom = async (e: React.FormEvent, roomId?: string) => {
|
||||
e.preventDefault();
|
||||
const targetRoomId = roomId || joinForm.roomId.trim().toUpperCase();
|
||||
if (!targetRoomId) {
|
||||
alert('请输入房间ID');
|
||||
return;
|
||||
}
|
||||
|
||||
setJoinLoading(true);
|
||||
try {
|
||||
const result = await joinRoom({
|
||||
roomId: targetRoomId,
|
||||
password: joinForm.password.trim() || undefined,
|
||||
userName: currentUsername,
|
||||
});
|
||||
|
||||
// 清空表单
|
||||
setJoinForm({
|
||||
roomId: '',
|
||||
password: '',
|
||||
});
|
||||
|
||||
// 注意:加入房间后,isOwner 状态会在 useWatchRoom 中更新
|
||||
// 跳转逻辑会在 useEffect 中处理
|
||||
} catch (error: any) {
|
||||
alert(error.message || '加入房间失败');
|
||||
} finally {
|
||||
setJoinLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听房间状态,房员加入后自动跟随房主播放
|
||||
useEffect(() => {
|
||||
if (!currentRoom || isOwner) return;
|
||||
|
||||
// 房员加入房间后,检查房主的播放状态
|
||||
if (currentRoom.currentState) {
|
||||
const state = currentRoom.currentState;
|
||||
if (state.type === 'play') {
|
||||
// 跳转到播放页面,使用完整参数
|
||||
console.log('[WatchRoom] Member joining/following, redirecting to play page');
|
||||
|
||||
const params = new URLSearchParams({
|
||||
id: state.videoId,
|
||||
source: state.source,
|
||||
episode: String(state.episode || 1),
|
||||
});
|
||||
|
||||
// 添加可选参数
|
||||
if (state.videoName) params.set('title', state.videoName);
|
||||
if (state.videoYear) params.set('year', state.videoYear);
|
||||
if (state.searchTitle) params.set('stitle', state.searchTitle);
|
||||
|
||||
router.push(`/play?${params.toString()}`);
|
||||
} else if (state.type === 'live') {
|
||||
// 跳转到直播页面
|
||||
console.log('[WatchRoom] Member joining/following, redirecting to live page');
|
||||
router.push(`/live?id=${state.channelId}`);
|
||||
}
|
||||
}
|
||||
// 如果房主还没播放,留在观影室页面等待
|
||||
}, [currentRoom?.currentState, isOwner, router]);
|
||||
|
||||
// 从房间列表加入房间
|
||||
const handleJoinFromList = (room: Room) => {
|
||||
setJoinForm({
|
||||
roomId: room.id,
|
||||
password: '',
|
||||
});
|
||||
setActiveTab('join');
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: number) => {
|
||||
const now = Date.now();
|
||||
const diff = now - timestamp;
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) return `${days}天前`;
|
||||
if (hours > 0) return `${hours}小时前`;
|
||||
if (minutes > 0) return `${minutes}分钟前`;
|
||||
return '刚刚';
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'create' as TabType, label: '创建房间', icon: Users },
|
||||
{ id: 'join' as TabType, label: '加入房间', icon: UserPlus },
|
||||
{ id: 'list' as TabType, label: '房间列表', icon: ListIcon },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 px-4 py-8">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
{/* 标题 */}
|
||||
<div className="mb-12 text-center">
|
||||
<h1 className="mb-4 text-4xl font-bold text-white md:text-5xl">观影室</h1>
|
||||
<p className="text-lg text-gray-400">与好友一起看视频,实时同步播放</p>
|
||||
<PageLayout activePath="/watch-room">
|
||||
<div className="flex flex-col gap-4 py-4 px-5 lg:px-[3rem] 2xl:px-20">
|
||||
{/* 房员等待提示 */}
|
||||
{currentRoom && !isOwner && !currentRoom.currentState && (
|
||||
<div className="mb-4 bg-gradient-to-r from-blue-500 to-purple-600 rounded-xl p-6 shadow-lg">
|
||||
<div className="flex items-center justify-center gap-4 text-white">
|
||||
<div className="relative">
|
||||
<div className="w-12 h-12 border-4 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold mb-1">等待房主开始播放</h3>
|
||||
<p className="text-sm text-white/80">
|
||||
房间: {currentRoom.name} | 房主: {currentRoom.ownerName}
|
||||
</p>
|
||||
<p className="text-xs text-white/70 mt-1">
|
||||
当房主开始播放时,您将自动跳转到相同的视频或频道
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 页面标题 */}
|
||||
<div className="py-1">
|
||||
<h1 className="text-2xl font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
<Users className="w-6 h-6 text-blue-500" />
|
||||
观影室
|
||||
{currentRoom && (
|
||||
<span className="text-sm font-normal text-gray-500 dark:text-gray-400">
|
||||
({isOwner ? '房主' : '房员'})
|
||||
</span>
|
||||
)}
|
||||
</h1>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
与好友一起看视频,实时同步播放
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 按钮网格 */}
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
{buttons.map((button, index) => {
|
||||
const Icon = button.icon;
|
||||
{/* 选项卡 */}
|
||||
<div className="flex border-b border-gray-200 dark:border-gray-700">
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
onClick={button.onClick}
|
||||
className={`group flex flex-col items-center justify-center gap-4 rounded-2xl p-8 transition-all duration-300 ${button.color} transform hover:scale-105 hover:shadow-2xl`}
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`flex items-center gap-2 px-6 py-3 text-sm font-medium transition-colors relative
|
||||
${
|
||||
activeTab === tab.id
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="rounded-full bg-white/10 p-6 backdrop-blur-sm transition-all duration-300 group-hover:bg-white/20">
|
||||
<Icon className="h-12 w-12 text-white md:h-16 md:w-16" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h2 className="mb-2 text-xl font-bold text-white md:text-2xl">{button.label}</h2>
|
||||
<p className="text-sm text-white/80 md:text-base">{button.description}</p>
|
||||
</div>
|
||||
<Icon className="w-4 h-4" />
|
||||
{tab.label}
|
||||
{activeTab === tab.id && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600 dark:bg-blue-400" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 使用说明 */}
|
||||
<div className="mt-16 rounded-2xl bg-gray-800/50 p-6 backdrop-blur-sm md:p-8">
|
||||
<h3 className="mb-4 text-xl font-bold text-white">使用说明</h3>
|
||||
<ul className="space-y-3 text-gray-300">
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="text-blue-400">•</span>
|
||||
<span>
|
||||
<strong className="text-white">创建房间:</strong>作为房主创建观影室,可以设置房间名称、密码和公开状态
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="text-green-400">•</span>
|
||||
<span>
|
||||
<strong className="text-white">加入房间:</strong>通过房间号加入别人创建的观影室
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="text-purple-400">•</span>
|
||||
<span>
|
||||
<strong className="text-white">房间列表:</strong>浏览所有公开的观影室,点击即可加入
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="text-yellow-400">•</span>
|
||||
<span>
|
||||
<strong className="text-white">实时同步:</strong>
|
||||
房主的播放操作会实时同步给所有成员,包括播放、暂停、进度、换集等
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="text-pink-400">•</span>
|
||||
<span>
|
||||
<strong className="text-white">语音聊天:</strong>在观影过程中可以使用文字和语音与房间成员交流
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
{/* 选项卡内容 */}
|
||||
<div className="flex-1">
|
||||
{/* 创建房间 */}
|
||||
{activeTab === 'create' && (
|
||||
<div className="max-w-2xl mx-auto py-8">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg border border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100 mb-6">
|
||||
创建新房间
|
||||
</h2>
|
||||
|
||||
{/* 如果已在房间内,显示当前房间信息 */}
|
||||
{currentRoom ? (
|
||||
<div className="space-y-4">
|
||||
{/* 房间信息卡片 */}
|
||||
<div className="bg-gradient-to-r from-blue-500 to-purple-600 rounded-xl p-6 text-white">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold mb-1">{currentRoom.name}</h3>
|
||||
<p className="text-blue-100 text-sm">{currentRoom.description || '暂无描述'}</p>
|
||||
</div>
|
||||
{isOwner && (
|
||||
<span className="bg-yellow-400 text-yellow-900 px-3 py-1 rounded-full text-xs font-bold">
|
||||
房主
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mt-4">
|
||||
<div className="bg-white/10 backdrop-blur rounded-lg p-3">
|
||||
<p className="text-blue-100 text-xs mb-1">房间号</p>
|
||||
<p className="text-xl font-mono font-bold">{currentRoom.id}</p>
|
||||
</div>
|
||||
<div className="bg-white/10 backdrop-blur rounded-lg p-3">
|
||||
<p className="text-blue-100 text-xs mb-1">成员数</p>
|
||||
<p className="text-xl font-bold">{members.length} 人</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 成员列表 */}
|
||||
<div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-gray-100 mb-3">房间成员</h4>
|
||||
<div className="space-y-2">
|
||||
{members.map((member) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className="flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg p-3"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-blue-400 to-purple-500 flex items-center justify-center text-white font-bold">
|
||||
{member.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{member.name}
|
||||
</span>
|
||||
</div>
|
||||
{member.isOwner && (
|
||||
<span className="text-xs bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 px-2 py-1 rounded">
|
||||
房主
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 提示信息 */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
💡 前往播放页面或直播页面开始观影,房间成员将自动同步您的操作
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleCreateRoom} className="space-y-4">
|
||||
{/* 显示当前用户 */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3 border border-blue-200 dark:border-blue-800">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
<strong>当前用户:</strong>{currentUsername}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
房间名称 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={createForm.roomName}
|
||||
onChange={(e) => setCreateForm({ ...createForm, roomName: e.target.value })}
|
||||
placeholder="请输入房间名称"
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
maxLength={50}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
房间描述
|
||||
</label>
|
||||
<textarea
|
||||
value={createForm.description}
|
||||
onChange={(e) => setCreateForm({ ...createForm, description: e.target.value })}
|
||||
placeholder="请输入房间描述(可选)"
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
||||
rows={3}
|
||||
maxLength={200}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
房间密码
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={createForm.password}
|
||||
onChange={(e) => setCreateForm({ ...createForm, password: e.target.value })}
|
||||
placeholder="留空表示无需密码"
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
maxLength={20}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isPublic"
|
||||
checked={createForm.isPublic}
|
||||
onChange={(e) => setCreateForm({ ...createForm, isPublic: e.target.checked })}
|
||||
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<label htmlFor="isPublic" className="text-sm text-gray-700 dark:text-gray-300">
|
||||
在房间列表中公开显示
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={createLoading || !createForm.roomName.trim()}
|
||||
className="w-full bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white font-medium py-3 rounded-lg transition-colors"
|
||||
>
|
||||
{createLoading ? '创建中...' : '创建房间'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 使用说明 - 仅在未在房间内时显示 */}
|
||||
{!currentRoom && (
|
||||
<div className="mt-6 bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-800">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
<strong>提示:</strong>创建房间后,您将成为房主。所有成员的播放进度将自动跟随您的操作。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 加入房间 */}
|
||||
{activeTab === 'join' && (
|
||||
<div className="max-w-2xl mx-auto py-8">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-lg border border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100 mb-6">
|
||||
加入房间
|
||||
</h2>
|
||||
|
||||
{/* 如果已在房间内,显示当前房间信息 */}
|
||||
{currentRoom ? (
|
||||
<div className="space-y-4">
|
||||
{/* 房间信息卡片 */}
|
||||
<div className="bg-gradient-to-r from-green-500 to-teal-600 rounded-xl p-6 text-white">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold mb-1">{currentRoom.name}</h3>
|
||||
<p className="text-green-100 text-sm">{currentRoom.description || '暂无描述'}</p>
|
||||
</div>
|
||||
{isOwner && (
|
||||
<span className="bg-yellow-400 text-yellow-900 px-3 py-1 rounded-full text-xs font-bold">
|
||||
房主
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mt-4">
|
||||
<div className="bg-white/10 backdrop-blur rounded-lg p-3">
|
||||
<p className="text-green-100 text-xs mb-1">房间号</p>
|
||||
<p className="text-xl font-mono font-bold">{currentRoom.id}</p>
|
||||
</div>
|
||||
<div className="bg-white/10 backdrop-blur rounded-lg p-3">
|
||||
<p className="text-green-100 text-xs mb-1">成员数</p>
|
||||
<p className="text-xl font-bold">{members.length} 人</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 成员列表 */}
|
||||
<div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-gray-100 mb-3">房间成员</h4>
|
||||
<div className="space-y-2">
|
||||
{members.map((member) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className="flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg p-3"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-green-400 to-teal-500 flex items-center justify-center text-white font-bold">
|
||||
{member.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{member.name}
|
||||
</span>
|
||||
</div>
|
||||
{member.isOwner && (
|
||||
<span className="text-xs bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 px-2 py-1 rounded">
|
||||
房主
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 提示信息 */}
|
||||
<div className="bg-green-50 dark:bg-green-900/20 rounded-lg p-4 border border-green-200 dark:border-green-800">
|
||||
<p className="text-sm text-green-800 dark:text-green-200">
|
||||
💡 {isOwner ? '前往播放页面或直播页面开始观影,房间成员将自动同步您的操作' : '等待房主开始播放,您的播放进度将自动跟随房主'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleJoinRoom} className="space-y-4">
|
||||
{/* 显示当前用户 */}
|
||||
<div className="bg-green-50 dark:bg-green-900/20 rounded-lg p-3 border border-green-200 dark:border-green-800">
|
||||
<p className="text-sm text-green-800 dark:text-green-200">
|
||||
<strong>当前用户:</strong>{currentUsername}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
房间号 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={joinForm.roomId}
|
||||
onChange={(e) => setJoinForm({ ...joinForm, roomId: e.target.value.toUpperCase() })}
|
||||
placeholder="请输入6位房间号"
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 font-mono text-lg tracking-wider focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
maxLength={6}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
房间密码
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={joinForm.password}
|
||||
onChange={(e) => setJoinForm({ ...joinForm, password: e.target.value })}
|
||||
placeholder="如果房间有密码,请输入"
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
maxLength={20}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={joinLoading || !joinForm.roomId.trim()}
|
||||
className="w-full bg-green-500 hover:bg-green-600 disabled:bg-gray-400 text-white font-medium py-3 rounded-lg transition-colors"
|
||||
>
|
||||
{joinLoading ? '加入中...' : '加入房间'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 使用说明 - 仅在未在房间内时显示 */}
|
||||
{!currentRoom && (
|
||||
<div className="mt-6 bg-green-50 dark:bg-green-900/20 rounded-lg p-4 border border-green-200 dark:border-green-800">
|
||||
<p className="text-sm text-green-800 dark:text-green-200">
|
||||
<strong>提示:</strong>加入房间后,您的播放进度将自动跟随房主的操作。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 房间列表 */}
|
||||
{activeTab === 'list' && (
|
||||
<div className="py-4">
|
||||
{/* 顶部操作栏 */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
找到 <span className="font-medium text-gray-900 dark:text-gray-100">{rooms.length}</span> 个公开房间
|
||||
</p>
|
||||
<button
|
||||
onClick={loadRooms}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg text-gray-700 dark:text-gray-300 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 加载中 */}
|
||||
{loading && rooms.length === 0 && (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-center">
|
||||
<RefreshCw className="mx-auto mb-4 h-12 w-12 animate-spin text-gray-400" />
|
||||
<p className="text-gray-500 dark:text-gray-400">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 空状态 */}
|
||||
{!loading && rooms.length === 0 && (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-center">
|
||||
<Users className="mx-auto mb-4 h-16 w-16 text-gray-400" />
|
||||
<p className="mb-2 text-xl text-gray-600 dark:text-gray-400">暂无公开房间</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-500">
|
||||
创建一个新房间或通过房间号加入私密房间
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 房间卡片列表 */}
|
||||
{rooms.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{rooms.map((room) => (
|
||||
<div
|
||||
key={room.id}
|
||||
className="bg-white dark:bg-gray-800 rounded-xl p-5 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-lg font-bold text-gray-900 dark:text-gray-100 truncate">
|
||||
{room.name}
|
||||
</h3>
|
||||
{room.description && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mt-1">
|
||||
{room.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{room.password && (
|
||||
<Lock className="w-5 h-5 text-yellow-500 flex-shrink-0 ml-2" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm mb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-500 dark:text-gray-400">房间号</span>
|
||||
<span className="font-mono text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||
{room.id}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-gray-600 dark:text-gray-400">
|
||||
<Users className="w-4 h-4" />
|
||||
<span>{room.memberCount} 人在线</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-gray-600 dark:text-gray-400">
|
||||
<span>房主</span>
|
||||
<span className="font-medium">{room.ownerName}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-gray-600 dark:text-gray-400">
|
||||
<span>创建时间</span>
|
||||
<span>{formatTime(room.createdAt)}</span>
|
||||
</div>
|
||||
{room.currentState && (
|
||||
<div className="mt-2 rounded-lg bg-blue-50 dark:bg-blue-900/30 px-3 py-2 border border-blue-200 dark:border-blue-800">
|
||||
<p className="text-xs text-blue-700 dark:text-blue-300 truncate">
|
||||
{room.currentState.type === 'play'
|
||||
? `正在播放: ${room.currentState.videoName}`
|
||||
: `正在观看: ${room.currentState.channelName}`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handleJoinFromList(room)}
|
||||
className="w-full bg-purple-500 hover:bg-purple-600 text-white font-medium py-2.5 rounded-lg transition-colors"
|
||||
>
|
||||
加入房间
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 弹窗 */}
|
||||
{showCreateModal && <CreateRoomModal onClose={() => setShowCreateModal(false)} />}
|
||||
{showJoinModal && <JoinRoomModal onClose={() => setShowJoinModal(false)} />}
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user