观影室起步
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { Cat, Clover, Film, Home, Radio, Star, Tv } from 'lucide-react';
|
||||
import { Cat, Clover, Film, Home, Radio, Star, Tv, Users } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -52,6 +52,11 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
|
||||
label: '直播',
|
||||
href: '/live',
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
label: '观影室',
|
||||
href: '/watch-room',
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { Cat, Clover, Film, Home, Menu, Radio, Search, Star, Tv } from 'lucide-react';
|
||||
import { Cat, Clover, Film, Home, Menu, Radio, Search, Star, Tv, Users } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import {
|
||||
@@ -143,6 +143,11 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
||||
label: '直播',
|
||||
href: '/live',
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
label: '观影室',
|
||||
href: '/watch-room',
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// WatchRoom 全局状态管理 Provider
|
||||
'use client';
|
||||
|
||||
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
||||
import { useWatchRoom } from '@/hooks/useWatchRoom';
|
||||
import type { Room, Member, ChatMessage, WatchRoomConfig } from '@/types/watch-room';
|
||||
import type { WatchRoomSocket } from '@/lib/watch-room-socket';
|
||||
|
||||
interface WatchRoomContextType {
|
||||
socket: WatchRoomSocket | null;
|
||||
isConnected: boolean;
|
||||
currentRoom: Room | null;
|
||||
members: Member[];
|
||||
chatMessages: ChatMessage[];
|
||||
isOwner: boolean;
|
||||
isEnabled: boolean;
|
||||
config: WatchRoomConfig | null;
|
||||
|
||||
// 房间操作
|
||||
createRoom: (data: {
|
||||
name: string;
|
||||
description: string;
|
||||
password?: string;
|
||||
isPublic: boolean;
|
||||
userName: string;
|
||||
}) => Promise<Room>;
|
||||
joinRoom: (data: {
|
||||
roomId: string;
|
||||
password?: string;
|
||||
userName: string;
|
||||
}) => Promise<{ room: Room; members: Member[] }>;
|
||||
leaveRoom: () => void;
|
||||
getRoomList: () => Promise<Room[]>;
|
||||
|
||||
// 聊天
|
||||
sendChatMessage: (content: string, type?: 'text' | 'emoji') => void;
|
||||
|
||||
// 播放控制(供 play/live 页面使用)
|
||||
updatePlayState: (state: any) => void;
|
||||
seekPlayback: (currentTime: number) => void;
|
||||
play: () => void;
|
||||
pause: () => void;
|
||||
changeVideo: (state: any) => void;
|
||||
changeLiveChannel: (state: any) => void;
|
||||
}
|
||||
|
||||
const WatchRoomContext = createContext<WatchRoomContextType | null>(null);
|
||||
|
||||
export const useWatchRoomContext = () => {
|
||||
const context = useContext(WatchRoomContext);
|
||||
if (!context) {
|
||||
throw new Error('useWatchRoomContext must be used within WatchRoomProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// 安全版本,可以在非 Provider 内使用
|
||||
export const useWatchRoomContextSafe = () => {
|
||||
return useContext(WatchRoomContext);
|
||||
};
|
||||
|
||||
interface WatchRoomProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function WatchRoomProvider({ children }: WatchRoomProviderProps) {
|
||||
const [config, setConfig] = useState<WatchRoomConfig | null>(null);
|
||||
const [isEnabled, setIsEnabled] = useState(false);
|
||||
|
||||
const watchRoom = useWatchRoom();
|
||||
|
||||
// 加载配置
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
// 默认配置:启用内部服务器
|
||||
const defaultConfig: WatchRoomConfig = {
|
||||
enabled: true,
|
||||
serverType: 'internal',
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/config');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const watchRoomConfig: WatchRoomConfig = {
|
||||
enabled: data.watchRoom?.enabled ?? true,
|
||||
serverType: data.watchRoom?.serverType ?? 'internal',
|
||||
externalServerUrl: data.watchRoom?.externalServerUrl,
|
||||
externalServerAuth: data.watchRoom?.externalServerAuth,
|
||||
};
|
||||
setConfig(watchRoomConfig);
|
||||
setIsEnabled(watchRoomConfig.enabled);
|
||||
|
||||
// 如果启用了观影室,自动连接
|
||||
if (watchRoomConfig.enabled) {
|
||||
console.log('[WatchRoom] Connecting with config:', watchRoomConfig);
|
||||
await watchRoom.connect(watchRoomConfig);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Failed to load config');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[WatchRoom] Using default config (internal server enabled)');
|
||||
setConfig(defaultConfig);
|
||||
setIsEnabled(true);
|
||||
|
||||
try {
|
||||
await watchRoom.connect(defaultConfig);
|
||||
} catch (connectError) {
|
||||
console.error('[WatchRoom] Failed to connect:', connectError);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadConfig();
|
||||
|
||||
// 清理
|
||||
return () => {
|
||||
watchRoom.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const contextValue: WatchRoomContextType = {
|
||||
socket: watchRoom.socket,
|
||||
isConnected: watchRoom.isConnected,
|
||||
currentRoom: watchRoom.currentRoom,
|
||||
members: watchRoom.members,
|
||||
chatMessages: watchRoom.chatMessages,
|
||||
isOwner: watchRoom.isOwner,
|
||||
isEnabled,
|
||||
config,
|
||||
createRoom: watchRoom.createRoom,
|
||||
joinRoom: watchRoom.joinRoom,
|
||||
leaveRoom: watchRoom.leaveRoom,
|
||||
getRoomList: watchRoom.getRoomList,
|
||||
sendChatMessage: watchRoom.sendChatMessage,
|
||||
updatePlayState: watchRoom.updatePlayState,
|
||||
seekPlayback: watchRoom.seekPlayback,
|
||||
play: watchRoom.play,
|
||||
pause: watchRoom.pause,
|
||||
changeVideo: watchRoom.changeVideo,
|
||||
changeLiveChannel: watchRoom.changeLiveChannel,
|
||||
};
|
||||
|
||||
return (
|
||||
<WatchRoomContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</WatchRoomContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
// 全局聊天悬浮窗
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { MessageCircle, X, Send, Smile, Minimize2, Maximize2 } from 'lucide-react';
|
||||
import { useWatchRoomContextSafe } from '@/components/WatchRoomProvider';
|
||||
|
||||
const EMOJI_LIST = ['😀', '😂', '😍', '🥰', '😎', '🤔', '👍', '👏', '🎉', '❤️', '🔥', '⭐'];
|
||||
|
||||
export default function ChatFloatingWindow() {
|
||||
const watchRoom = useWatchRoomContextSafe();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isMinimized, setIsMinimized] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 自动滚动到底部
|
||||
useEffect(() => {
|
||||
if (messagesEndRef.current && watchRoom?.currentRoom) {
|
||||
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [watchRoom?.chatMessages, watchRoom?.currentRoom]);
|
||||
|
||||
// 如果没有加入房间,不显示聊天按钮
|
||||
if (!watchRoom?.currentRoom) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { chatMessages, sendChatMessage, members, isOwner } = watchRoom;
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (!message.trim()) return;
|
||||
|
||||
sendChatMessage(message.trim(), 'text');
|
||||
setMessage('');
|
||||
setShowEmojiPicker(false);
|
||||
};
|
||||
|
||||
const handleSendEmoji = (emoji: string) => {
|
||||
sendChatMessage(emoji, 'emoji');
|
||||
setShowEmojiPicker(false);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
// 悬浮按钮
|
||||
if (!isOpen) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="fixed bottom-20 right-4 z-[700] flex h-14 w-14 items-center justify-center rounded-full bg-green-500 text-white shadow-2xl transition-all hover:scale-110 hover:bg-green-600 md:bottom-4"
|
||||
aria-label="打开聊天"
|
||||
>
|
||||
<MessageCircle className="h-6 w-6" />
|
||||
{chatMessages.length > 0 && (
|
||||
<span className="absolute right-0 top-0 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-xs font-bold">
|
||||
{chatMessages.length > 99 ? '99+' : chatMessages.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// 最小化状态
|
||||
if (isMinimized) {
|
||||
return (
|
||||
<div className="fixed bottom-20 right-4 z-[700] flex items-center gap-2 rounded-lg bg-gray-800 px-4 py-2 shadow-2xl md:bottom-4">
|
||||
<MessageCircle className="h-5 w-5 text-white" />
|
||||
<span className="text-sm text-white">聊天室</span>
|
||||
<button
|
||||
onClick={() => setIsMinimized(false)}
|
||||
className="ml-2 rounded p-1 text-gray-400 transition-colors hover:bg-gray-700 hover:text-white"
|
||||
aria-label="展开"
|
||||
>
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="rounded p-1 text-gray-400 transition-colors hover:bg-gray-700 hover:text-white"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 完整聊天窗口
|
||||
return (
|
||||
<div className="fixed bottom-20 right-4 z-[700] flex w-80 flex-col rounded-2xl bg-gray-800 shadow-2xl md:bottom-4 md:w-96">
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between rounded-t-2xl bg-green-500 px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageCircle className="h-5 w-5 text-white" />
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-white">聊天室</h3>
|
||||
<p className="text-xs text-white/80">{members.length} 人在线</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => setIsMinimized(true)}
|
||||
className="rounded p-1 text-white/80 transition-colors hover:bg-white/20 hover:text-white"
|
||||
aria-label="最小化"
|
||||
>
|
||||
<Minimize2 className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="rounded p-1 text-white/80 transition-colors hover:bg-white/20 hover:text-white"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 消息列表 */}
|
||||
<div className="flex-1 space-y-3 overflow-y-auto p-4" style={{ maxHeight: '400px' }}>
|
||||
{chatMessages.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center text-center">
|
||||
<div>
|
||||
<MessageCircle className="mx-auto mb-2 h-12 w-12 text-gray-600" />
|
||||
<p className="text-sm text-gray-400">还没有消息</p>
|
||||
<p className="text-xs text-gray-500">发送第一条消息吧</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{chatMessages.map((msg) => (
|
||||
<div key={msg.id} className="flex flex-col gap-1">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-xs font-medium text-green-400">{msg.userName}</span>
|
||||
<span className="text-xs text-gray-500">{formatTime(msg.timestamp)}</span>
|
||||
</div>
|
||||
<div
|
||||
className={`max-w-[80%] rounded-lg px-3 py-2 ${
|
||||
msg.type === 'emoji'
|
||||
? 'text-3xl'
|
||||
: 'bg-gray-700 text-white'
|
||||
}`}
|
||||
>
|
||||
{msg.content}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 输入区域 */}
|
||||
<div className="border-t border-gray-700 p-3">
|
||||
{/* 表情选择器 */}
|
||||
{showEmojiPicker && (
|
||||
<div className="mb-2 grid grid-cols-6 gap-2 rounded-lg bg-gray-700 p-2">
|
||||
{EMOJI_LIST.map((emoji) => (
|
||||
<button
|
||||
key={emoji}
|
||||
onClick={() => handleSendEmoji(emoji)}
|
||||
className="rounded p-1 text-2xl transition-colors hover:bg-gray-600"
|
||||
>
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
||||
className="rounded-lg bg-gray-700 p-2 text-gray-300 transition-colors hover:bg-gray-600 hover:text-white"
|
||||
aria-label="表情"
|
||||
>
|
||||
<Smile className="h-5 w-5" />
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="输入消息..."
|
||||
className="flex-1 rounded-lg bg-gray-700 px-3 py-2 text-sm text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
maxLength={200}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
disabled={!message.trim()}
|
||||
className="rounded-lg bg-green-500 p-2 text-white transition-colors hover:bg-green-600 disabled:opacity-50"
|
||||
aria-label="发送"
|
||||
>
|
||||
<Send className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 房间信息提示 */}
|
||||
<div className="rounded-b-2xl bg-gray-900/50 px-4 py-2 text-center text-xs text-gray-400">
|
||||
{isOwner ? (
|
||||
<span className="text-yellow-400">👑 您是房主</span>
|
||||
) : (
|
||||
<span>房间: {watchRoom.currentRoom.name}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// 创建房间弹窗
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { X, Lock, Eye, EyeOff } from 'lucide-react';
|
||||
import { useWatchRoomContext } from '@/components/WatchRoomProvider';
|
||||
|
||||
interface CreateRoomModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function CreateRoomModal({ onClose }: CreateRoomModalProps) {
|
||||
const router = useRouter();
|
||||
const { createRoom } = useWatchRoomContext();
|
||||
|
||||
const [roomName, setRoomName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [userName, setUserName] = useState('');
|
||||
const [isPublic, setIsPublic] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
if (!roomName.trim()) {
|
||||
setError('请输入房间名称');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userName.trim()) {
|
||||
setError('请输入您的昵称');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const room = await createRoom({
|
||||
name: roomName.trim(),
|
||||
description: description.trim(),
|
||||
password: password.trim() || undefined,
|
||||
isPublic,
|
||||
userName: userName.trim(),
|
||||
});
|
||||
|
||||
console.log('[WatchRoom] Room created:', room);
|
||||
onClose();
|
||||
|
||||
// 创建成功后跳转到播放页面(等待播放)
|
||||
// router.push(`/play?roomId=${room.id}`);
|
||||
} catch (err: any) {
|
||||
setError(err.message || '创建房间失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4 backdrop-blur-sm">
|
||||
<div className="relative w-full max-w-md rounded-2xl bg-gray-800 p-6 shadow-2xl">
|
||||
{/* 关闭按钮 */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute right-4 top-4 rounded-full p-2 text-gray-400 transition-colors hover:bg-gray-700 hover:text-white"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
{/* 标题 */}
|
||||
<h2 className="mb-6 text-2xl font-bold text-white">创建观影室</h2>
|
||||
|
||||
{/* 表单 */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* 昵称 */}
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-300">您的昵称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={userName}
|
||||
onChange={(e) => setUserName(e.target.value)}
|
||||
placeholder="输入您的昵称"
|
||||
className="w-full rounded-lg bg-gray-700 px-4 py-3 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
maxLength={20}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 房间名 */}
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-300">房间名称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={roomName}
|
||||
onChange={(e) => setRoomName(e.target.value)}
|
||||
placeholder="输入房间名称"
|
||||
className="w-full rounded-lg bg-gray-700 px-4 py-3 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
maxLength={30}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 备注 */}
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-300">房间简介(可选)</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="输入房间简介"
|
||||
rows={3}
|
||||
className="w-full resize-none rounded-lg bg-gray-700 px-4 py-3 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
maxLength={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 密码 */}
|
||||
<div>
|
||||
<label className="mb-2 flex items-center gap-2 text-sm font-medium text-gray-300">
|
||||
<Lock className="h-4 w-4" />
|
||||
密码(可选)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="不设置则无需密码"
|
||||
className="w-full rounded-lg bg-gray-700 px-4 py-3 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
maxLength={20}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 公开/隐藏 */}
|
||||
<div className="flex items-center justify-between rounded-lg bg-gray-700 px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{isPublic ? <Eye className="h-5 w-5 text-green-400" /> : <EyeOff className="h-5 w-5 text-gray-400" />}
|
||||
<span className="text-sm font-medium text-gray-300">
|
||||
{isPublic ? '公开房间' : '隐藏房间'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPublic(!isPublic)}
|
||||
className={`relative h-6 w-11 rounded-full transition-colors ${
|
||||
isPublic ? 'bg-green-500' : 'bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-0.5 h-5 w-5 transform rounded-full bg-white transition-transform ${
|
||||
isPublic ? 'left-5' : 'left-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{error && (
|
||||
<div className="rounded-lg bg-red-500/20 px-4 py-3 text-sm text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 按钮 */}
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex-1 rounded-lg bg-gray-700 py-3 font-medium text-white transition-colors hover:bg-gray-600"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex-1 rounded-lg bg-blue-500 py-3 font-medium text-white transition-colors hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{loading ? '创建中...' : '创建房间'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* 提示 */}
|
||||
<p className="mt-4 text-center text-xs text-gray-400">
|
||||
创建后您将成为房主,可以控制播放内容
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// 加入房间弹窗
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { X, Lock } from 'lucide-react';
|
||||
import { useWatchRoomContext } from '@/components/WatchRoomProvider';
|
||||
|
||||
interface JoinRoomModalProps {
|
||||
onClose: () => void;
|
||||
roomId?: string; // 可选的预填房间号
|
||||
}
|
||||
|
||||
export default function JoinRoomModal({ onClose, roomId: initialRoomId }: JoinRoomModalProps) {
|
||||
const router = useRouter();
|
||||
const { joinRoom } = useWatchRoomContext();
|
||||
|
||||
const [roomId, setRoomId] = useState(initialRoomId || '');
|
||||
const [password, setPassword] = useState('');
|
||||
const [userName, setUserName] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
if (!roomId.trim()) {
|
||||
setError('请输入房间号');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userName.trim()) {
|
||||
setError('请输入您的昵称');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const { room, members } = await joinRoom({
|
||||
roomId: roomId.trim().toUpperCase(),
|
||||
password: password.trim() || undefined,
|
||||
userName: userName.trim(),
|
||||
});
|
||||
|
||||
console.log('[WatchRoom] Joined room:', room, 'Members:', members);
|
||||
onClose();
|
||||
|
||||
// 加入成功后跳转到对应页面
|
||||
// 如果房主已经在播放,跳转到播放页面
|
||||
// 否则等待房主开始播放
|
||||
} catch (err: any) {
|
||||
setError(err.message || '加入房间失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4 backdrop-blur-sm">
|
||||
<div className="relative w-full max-w-md rounded-2xl bg-gray-800 p-6 shadow-2xl">
|
||||
{/* 关闭按钮 */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute right-4 top-4 rounded-full p-2 text-gray-400 transition-colors hover:bg-gray-700 hover:text-white"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
{/* 标题 */}
|
||||
<h2 className="mb-6 text-2xl font-bold text-white">加入观影室</h2>
|
||||
|
||||
{/* 表单 */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* 昵称 */}
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-300">您的昵称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={userName}
|
||||
onChange={(e) => setUserName(e.target.value)}
|
||||
placeholder="输入您的昵称"
|
||||
className="w-full rounded-lg bg-gray-700 px-4 py-3 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
maxLength={20}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 房间号 */}
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-300">房间号</label>
|
||||
<input
|
||||
type="text"
|
||||
value={roomId}
|
||||
onChange={(e) => setRoomId(e.target.value.toUpperCase())}
|
||||
placeholder="输入6位房间号"
|
||||
className="w-full rounded-lg bg-gray-700 px-4 py-3 font-mono text-lg tracking-wider text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
maxLength={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 密码 */}
|
||||
<div>
|
||||
<label className="mb-2 flex items-center gap-2 text-sm font-medium text-gray-300">
|
||||
<Lock className="h-4 w-4" />
|
||||
密码(如有)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="如果房间有密码请输入"
|
||||
className="w-full rounded-lg bg-gray-700 px-4 py-3 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
maxLength={20}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{error && (
|
||||
<div className="rounded-lg bg-red-500/20 px-4 py-3 text-sm text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 按钮 */}
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex-1 rounded-lg bg-gray-700 py-3 font-medium text-white transition-colors hover:bg-gray-600"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex-1 rounded-lg bg-green-500 py-3 font-medium text-white transition-colors hover:bg-green-600 disabled:opacity-50"
|
||||
>
|
||||
{loading ? '加入中...' : '加入房间'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* 提示 */}
|
||||
<p className="mt-4 text-center text-xs text-gray-400">
|
||||
加入后将跟随房主的播放内容
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user