完善观影室

This commit is contained in:
mtvpls
2025-12-07 00:16:21 +08:00
parent 003050d134
commit 4363f4cdae
15 changed files with 1685 additions and 718 deletions

View File

@@ -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;

View File

@@ -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}

View File

@@ -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>
);
}

View File

@@ -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>
);
}