feat:桌面共享支持

This commit is contained in:
MatrixSeven
2025-08-14 15:37:03 +08:00
parent e1d163f80c
commit 3b7fa7c653
14 changed files with 1787 additions and 454 deletions

View File

@@ -4,33 +4,41 @@ import React, { useState, useCallback, useEffect } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Share, Monitor, Copy, Play, Square } from 'lucide-react';
import { Share, Monitor, Copy, Play, Square, Repeat, Users, Wifi, WifiOff } from 'lucide-react';
import { useToast } from '@/components/ui/toast-simple';
import { useDesktopShareBusiness } from '@/hooks/webrtc/useDesktopShareBusiness';
import DesktopViewer from '@/components/DesktopViewer';
import QRCodeDisplay from '@/components/QRCodeDisplay';
interface DesktopShareProps {
onStartSharing?: () => Promise<string>; // 返回连接码
// 保留向后兼容性的props
onStartSharing?: () => Promise<string>;
onStopSharing?: () => Promise<void>;
onJoinSharing?: (code: string) => Promise<void>;
}
export default function DesktopShare({ onStartSharing, onStopSharing, onJoinSharing }: DesktopShareProps) {
export default function DesktopShare({
onStartSharing,
onStopSharing,
onJoinSharing
}: DesktopShareProps) {
const searchParams = useSearchParams();
const router = useRouter();
const [mode, setMode] = useState<'share' | 'view'>('share');
const [connectionCode, setConnectionCode] = useState('');
const [inputCode, setInputCode] = useState('');
const [isSharing, setIsSharing] = useState(false);
const [isViewing, setIsViewing] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [showDebug, setShowDebug] = useState(false);
const { showToast } = useToast();
// 使用桌面共享业务逻辑
const desktopShare = useDesktopShareBusiness();
// 从URL参数中获取初始模式
useEffect(() => {
const urlMode = searchParams.get('mode');
const type = searchParams.get('type');
if (type === 'desktop' && urlMode) {
// 将send映射为sharereceive映射为view
if (urlMode === 'send') {
setMode('share');
} else if (urlMode === 'receive') {
@@ -42,75 +50,151 @@ export default function DesktopShare({ onStartSharing, onStopSharing, onJoinShar
// 更新URL参数
const updateMode = useCallback((newMode: 'share' | 'view') => {
setMode(newMode);
const params = new URLSearchParams(searchParams.toString());
params.set('type', 'desktop');
// 将share映射为sendview映射为receive以保持一致性
params.set('mode', newMode === 'share' ? 'send' : 'receive');
router.push(`?${params.toString()}`, { scroll: false });
}, [searchParams, router]);
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set('type', 'desktop');
currentUrl.searchParams.set('mode', newMode === 'share' ? 'send' : 'receive');
router.replace(currentUrl.pathname + currentUrl.search);
}, [router]);
const handleStartSharing = useCallback(async () => {
if (!onStartSharing) return;
setIsLoading(true);
// 复制房间代码
const copyCode = useCallback(async (code: string) => {
try {
const code = await onStartSharing();
setConnectionCode(code);
setIsSharing(true);
showToast('桌面共享已开始!', 'success');
await navigator.clipboard.writeText(code);
showToast('房间代码已复制到剪贴板', 'success');
} catch (error) {
console.error('开始共享失败:', error);
showToast('开始共享失败,请重试', 'error');
} finally {
setIsLoading(false);
}
}, [onStartSharing, showToast]);
const handleStopSharing = useCallback(async () => {
if (!onStopSharing) return;
setIsLoading(true);
try {
await onStopSharing();
setIsSharing(false);
setConnectionCode('');
showToast('桌面共享已停止', 'success');
} catch (error) {
console.error('停止共享失败:', error);
showToast('停止共享失败', 'error');
} finally {
setIsLoading(false);
}
}, [onStopSharing, showToast]);
const handleJoinSharing = useCallback(async () => {
if (!inputCode.trim() || !onJoinSharing) return;
setIsLoading(true);
try {
await onJoinSharing(inputCode);
setIsViewing(true);
showToast('已连接到桌面共享!', 'success');
} catch (error) {
console.error('连接失败:', error);
showToast('连接失败,请检查连接码', 'error');
} finally {
setIsLoading(false);
}
}, [inputCode, onJoinSharing, showToast]);
const copyToClipboard = useCallback(async (text: string) => {
try {
await navigator.clipboard.writeText(text);
showToast('已复制到剪贴板!', 'success');
} catch (err) {
showToast('复制失败', 'error');
console.error('复制失败:', error);
showToast('复制失败,请手动复制', 'error');
}
}, [showToast]);
// 创建房间
const handleCreateRoom = useCallback(async () => {
try {
setIsLoading(true);
console.log('[DesktopShare] 用户点击创建房间');
const roomCode = await desktopShare.createRoom();
console.log('[DesktopShare] 房间创建成功:', roomCode);
showToast(`房间创建成功!代码: ${roomCode}`, 'success');
} catch (error) {
console.error('[DesktopShare] 创建房间失败:', error);
const errorMessage = error instanceof Error ? error.message : '创建房间失败';
showToast(errorMessage, 'error');
} finally {
setIsLoading(false);
}
}, [desktopShare, showToast]);
// 开始桌面共享
const handleStartSharing = useCallback(async () => {
try {
setIsLoading(true);
console.log('[DesktopShare] 用户点击开始桌面共享');
await desktopShare.startSharing();
console.log('[DesktopShare] 桌面共享开始成功');
showToast('桌面共享已开始', 'success');
} catch (error) {
console.error('[DesktopShare] 开始桌面共享失败:', error);
const errorMessage = error instanceof Error ? error.message : '开始桌面共享失败';
showToast(errorMessage, 'error');
} finally {
setIsLoading(false);
}
}, [desktopShare, showToast]);
// 切换桌面
const handleSwitchDesktop = useCallback(async () => {
try {
setIsLoading(true);
console.log('[DesktopShare] 用户点击切换桌面');
await desktopShare.switchDesktop();
console.log('[DesktopShare] 桌面切换成功');
showToast('桌面切换成功', 'success');
} catch (error) {
console.error('[DesktopShare] 切换桌面失败:', error);
const errorMessage = error instanceof Error ? error.message : '切换桌面失败';
showToast(errorMessage, 'error');
} finally {
setIsLoading(false);
}
}, [desktopShare, showToast]);
// 停止桌面共享
const handleStopSharing = useCallback(async () => {
try {
setIsLoading(true);
console.log('[DesktopShare] 用户点击停止桌面共享');
await desktopShare.stopSharing();
console.log('[DesktopShare] 桌面共享停止成功');
showToast('桌面共享已停止', 'success');
} catch (error) {
console.error('[DesktopShare] 停止桌面共享失败:', error);
const errorMessage = error instanceof Error ? error.message : '停止桌面共享失败';
showToast(errorMessage, 'error');
} finally {
setIsLoading(false);
}
}, [desktopShare, showToast]);
// 加入观看
const handleJoinViewing = useCallback(async () => {
if (!inputCode.trim()) {
showToast('请输入房间代码', 'error');
return;
}
try {
setIsLoading(true);
console.log('[DesktopShare] 用户加入观看房间:', inputCode);
await desktopShare.joinSharing(inputCode.trim().toUpperCase());
console.log('[DesktopShare] 加入观看成功');
showToast('已加入桌面共享', 'success');
} catch (error) {
console.error('[DesktopShare] 加入观看失败:', error);
const errorMessage = error instanceof Error ? error.message : '加入观看失败';
showToast(errorMessage, 'error');
} finally {
setIsLoading(false);
}
}, [desktopShare, inputCode, showToast]);
// 停止观看
const handleStopViewing = useCallback(async () => {
try {
setIsLoading(true);
await desktopShare.stopViewing();
showToast('已退出桌面共享', 'success');
setInputCode('');
} catch (error) {
console.error('[DesktopShare] 停止观看失败:', error);
showToast('退出失败', 'error');
} finally {
setIsLoading(false);
}
}, [desktopShare, showToast]);
// 连接状态指示器
const getConnectionStatus = () => {
if (desktopShare.isConnecting) return { icon: Wifi, text: '连接中...', color: 'text-yellow-600' };
if (desktopShare.isPeerConnected) return { icon: Wifi, text: 'P2P已连接', color: 'text-green-600' };
if (desktopShare.isWebSocketConnected) return { icon: Users, text: '等待对方加入', color: 'text-blue-600' };
return { icon: WifiOff, text: '未连接', color: 'text-gray-600' };
};
const connectionStatus = getConnectionStatus();
return (
<div className="space-y-4 sm:space-y-6">
{/* 模式切换 */}
{/* 模式选择器 */}
<div className="flex justify-center mb-6">
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-1 shadow-lg">
<Button
@@ -133,228 +217,424 @@ export default function DesktopShare({ onStartSharing, onStopSharing, onJoinShar
</div>
{mode === 'share' ? (
<div className="bg-white/80 backdrop-blur-sm rounded-xl shadow-lg p-4 sm:p-6 animate-fade-in-up">
{/* 功能标题和状态 */}
<div className="flex items-center mb-6">
<div className="flex items-center space-x-3 flex-1">
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl flex items-center justify-center">
<Share className="w-5 h-5 text-white" />
</div>
<div>
<h2 className="text-lg font-semibold text-slate-800"></h2>
<p className="text-sm text-slate-600">
{isSharing ? '桌面共享进行中' : '开始共享您的桌面屏幕'}
</p>
</div>
</div>
{/* 竖线分割 */}
<div className="w-px h-12 bg-slate-200 mx-4"></div>
{/* 状态显示 */}
<div className="text-right">
<div className="text-sm text-slate-500 mb-1"></div>
<div className="flex items-center justify-end space-x-3 text-sm">
{/* WebSocket状态 */}
<div className="flex items-center space-x-1">
<div className="w-2 h-2 rounded-full bg-slate-400"></div>
<span className="text-slate-600">WS</span>
/* 共享模式 */
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-4 sm:p-6 shadow-lg border border-white/20 animate-fade-in-up">
{!desktopShare.connectionCode ? (
// 创建房间前的界面
<div className="space-y-6">
{/* 功能标题和状态 */}
<div className="flex items-center mb-6">
<div className="flex items-center space-x-3 flex-1">
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-indigo-500 rounded-xl flex items-center justify-center">
<Monitor className="w-5 h-5 text-white" />
</div>
<div>
<h2 className="text-lg font-semibold text-slate-800"></h2>
<p className="text-sm text-slate-600"></p>
</div>
</div>
{/* 分隔符 */}
<div className="text-slate-300">|</div>
{/* 竖线分割 */}
<div className="w-px h-12 bg-slate-200 mx-4"></div>
{/* WebRTC状态 */}
<div className="flex items-center space-x-1">
{isSharing ? (
{/* 状态显示 */}
<div className="text-right">
<div className="text-sm text-slate-500 mb-1"></div>
<div className="flex items-center justify-end space-x-3 text-sm">
{/* WebSocket状态 */}
<div className="flex items-center space-x-1">
<div className={`w-2 h-2 rounded-full ${desktopShare.isWebSocketConnected ? 'bg-blue-500 animate-pulse' : 'bg-slate-400'}`}></div>
<span className={desktopShare.isWebSocketConnected ? 'text-blue-600' : 'text-slate-600'}>WS</span>
</div>
{/* 分隔符 */}
<div className="text-slate-300">|</div>
{/* WebRTC状态 */}
<div className="flex items-center space-x-1">
<div className={`w-2 h-2 rounded-full ${desktopShare.isPeerConnected ? 'bg-emerald-500 animate-pulse' : 'bg-slate-400'}`}></div>
<span className={desktopShare.isPeerConnected ? 'text-emerald-600' : 'text-slate-600'}>RTC</span>
</div>
</div>
</div>
</div>
<div className="text-center py-12">
<div className="w-20 h-20 mx-auto mb-6 bg-gradient-to-br from-purple-100 to-indigo-100 rounded-full flex items-center justify-center">
<Monitor className="w-10 h-10 text-purple-500" />
</div>
<h3 className="text-xl font-semibold text-slate-800 mb-4"></h3>
<p className="text-slate-600 mb-8"></p>
<Button
onClick={handleCreateRoom}
disabled={isLoading || desktopShare.isConnecting}
className="px-8 py-3 bg-gradient-to-r from-purple-500 to-indigo-500 hover:from-purple-600 hover:to-indigo-600 text-white text-lg font-medium rounded-xl shadow-lg"
>
{isLoading ? (
<>
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
<span className="text-emerald-600">RTC</span>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
...
</>
) : (
<>
<div className="w-2 h-2 rounded-full bg-slate-400"></div>
<span className="text-slate-600">RTC</span>
<Share className="w-5 h-5 mr-2" />
</>
)}
</Button>
</div>
</div>
) : (
// 房间已创建,显示取件码和等待界面
<div className="space-y-6">
{/* 功能标题和状态 */}
<div className="flex items-center mb-6">
<div className="flex items-center space-x-3 flex-1">
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-indigo-500 rounded-xl flex items-center justify-center">
<Monitor className="w-5 h-5 text-white" />
</div>
<div>
<h2 className="text-lg font-semibold text-slate-800"></h2>
<p className="text-sm text-slate-600">
{desktopShare.isPeerConnected ? '✅ 接收方已连接,现在可以开始共享桌面' :
desktopShare.isWebSocketConnected ? '⏳ 房间已创建等待接收方加入建立P2P连接' :
'⚠️ 等待连接'}
</p>
</div>
</div>
{/* 竖线分割 */}
<div className="w-px h-12 bg-slate-200 mx-4"></div>
{/* 状态显示 */}
<div className="text-right">
<div className="text-sm text-slate-500 mb-1"></div>
<div className="flex items-center justify-end space-x-3 text-sm">
{/* WebSocket状态 */}
<div className="flex items-center space-x-1">
<div className={`w-2 h-2 rounded-full ${desktopShare.isWebSocketConnected ? 'bg-blue-500 animate-pulse' : 'bg-red-500'}`}></div>
<span className={desktopShare.isWebSocketConnected ? 'text-blue-600' : 'text-red-600'}>WS</span>
</div>
{/* 分隔符 */}
<div className="text-slate-300">|</div>
{/* WebRTC状态 */}
<div className="flex items-center space-x-1">
<div className={`w-2 h-2 rounded-full ${desktopShare.isPeerConnected ? 'bg-emerald-500 animate-pulse' : 'bg-orange-400'}`}></div>
<span className={desktopShare.isPeerConnected ? 'text-emerald-600' : 'text-orange-600'}>RTC</span>
</div>
</div>
</div>
</div>
{isSharing && connectionCode && (
<div className="mt-1 text-xs text-purple-600">
{connectionCode}
{/* 桌面共享控制区域 */}
{desktopShare.canStartSharing && (
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-4 border border-slate-200 mb-6">
<div className="flex items-center justify-between mb-3">
<h4 className="text-lg font-medium text-slate-800 flex items-center">
<Monitor className="w-5 h-5 mr-2" />
</h4>
{desktopShare.isSharing && (
<div className="flex items-center space-x-1 bg-emerald-100 text-emerald-700 px-2 py-1 rounded-md">
<div className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
<span className="font-medium"></span>
</div>
)}
</div>
<div className="space-y-4">
{!desktopShare.isSharing ? (
<div className="space-y-3">
<Button
onClick={handleStartSharing}
disabled={isLoading || !desktopShare.isPeerConnected}
className={`w-full px-8 py-3 text-lg font-medium rounded-xl shadow-lg ${
desktopShare.isPeerConnected
? 'bg-gradient-to-r from-purple-500 to-indigo-500 hover:from-purple-600 hover:to-indigo-600 text-white'
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
}`}
>
<Play className="w-5 h-5 mr-2" />
{isLoading ? '启动中...' : '选择并开始共享桌面'}
</Button>
{!desktopShare.isPeerConnected && (
<div className="text-center">
<p className="text-sm text-gray-500 mb-2">
P2P连接...
</p>
<div className="flex items-center justify-center space-x-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-purple-500"></div>
<span className="text-sm text-purple-600"></span>
</div>
</div>
)}
</div>
) : (
<div className="space-y-4">
<div className="flex items-center justify-center space-x-2 text-green-600 mb-4">
<Play className="w-5 h-5" />
<span className="font-semibold"></span>
</div>
<div className="flex justify-center space-x-3">
<Button
onClick={handleSwitchDesktop}
disabled={isLoading}
variant="outline"
size="sm"
>
<Repeat className="w-4 h-4 mr-2" />
{isLoading ? '切换中...' : '切换桌面'}
</Button>
<Button
onClick={handleStopSharing}
disabled={isLoading}
variant="destructive"
size="sm"
>
<Square className="w-4 h-4 mr-2" />
{isLoading ? '停止中...' : '停止共享'}
</Button>
</div>
</div>
)}
</div>
</div>
)}
</div>
</div>
<div className="space-y-4">
{!isSharing ? (
<Button
onClick={handleStartSharing}
disabled={isLoading}
className="w-full h-12 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white text-lg font-medium rounded-xl shadow-lg"
>
{isLoading ? (
<>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
...
</>
) : (
<>
<Play className="w-5 h-5 mr-2" />
</>
)}
</Button>
) : (
<div className="space-y-4">
<div className="p-4 bg-gradient-to-r from-purple-50 to-pink-50 rounded-xl border border-purple-200">
<div className="text-center">
<p className="text-sm text-purple-700 mb-2"></p>
<div className="text-2xl font-bold font-mono text-purple-600 mb-3">{connectionCode}</div>
{/* 取件码显示 - 和文件传输一致的风格 */}
<div className="border-t border-slate-200 pt-6">
{/* 左上角状态提示 */}
<div className="flex items-center mb-6">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 to-teal-500 rounded-xl flex items-center justify-center">
<Monitor className="w-5 h-5 text-white" />
</div>
<div>
<h3 className="text-lg font-semibold text-slate-800"></h3>
<p className="text-sm text-slate-600"></p>
</div>
</div>
</div>
{/* 中间区域:取件码 + 分隔线 + 二维码 */}
<div className="flex flex-col lg:flex-row lg:items-start gap-6 lg:gap-8 mb-8">
{/* 左侧:取件码 */}
<div className="flex-1">
<label className="block text-sm font-medium text-slate-700 mb-3"></label>
<div className="flex flex-col items-center rounded-xl border border-slate-200 p-6 h-40 justify-center bg-slate-50">
<div className="text-2xl font-bold font-mono bg-gradient-to-r from-purple-600 to-indigo-600 bg-clip-text text-transparent tracking-wider">
{desktopShare.connectionCode}
</div>
</div>
<Button
onClick={() => copyToClipboard(connectionCode)}
size="sm"
className="bg-purple-500 hover:bg-purple-600 text-white"
onClick={() => copyCode(desktopShare.connectionCode)}
className="w-full px-4 py-2.5 bg-purple-500 hover:bg-purple-600 text-white rounded-lg font-medium shadow transition-all duration-200 mt-3"
>
<Copy className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 分隔线 - 大屏幕显示竖线,移动端隐藏 */}
<div className="hidden lg:block w-px bg-slate-200 h-64 mt-6"></div>
{/* 右侧:二维码 */}
<div className="flex-1">
<label className="block text-sm font-medium text-slate-700 mb-3"></label>
<div className="flex flex-col items-center rounded-xl border border-slate-200 p-6 h-40 justify-center bg-slate-50">
<QRCodeDisplay
value={`${typeof window !== 'undefined' ? window.location.origin : ''}?type=desktop&mode=receive&code=${desktopShare.connectionCode}`}
size={120}
/>
</div>
<div className="w-full px-4 py-2.5 bg-blue-500 text-white rounded-lg font-medium shadow transition-all duration-200 mt-3 text-center">
使
</div>
</div>
</div>
{/* 底部:观看链接 */}
<div className="space-y-3">
<div className="flex gap-3">
<div className="flex-1 code-display rounded-lg p-3 bg-slate-50 border border-slate-200">
<div className="text-sm text-slate-700 break-all font-mono leading-relaxed">
{`${typeof window !== 'undefined' ? window.location.origin : ''}?type=desktop&mode=receive&code=${desktopShare.connectionCode}`}
</div>
</div>
<Button
onClick={() => {
const link = `${window.location.origin}?type=desktop&mode=receive&code=${desktopShare.connectionCode}`;
navigator.clipboard.writeText(link);
showToast('观看链接已复制', 'success');
}}
className="px-4 py-2.5 bg-blue-500 hover:bg-blue-600 text-white rounded-lg font-medium shadow transition-all duration-200 shrink-0"
>
</Button>
</div>
</div>
</div>
</div>
)}
</div>
) : (
/* 观看模式 */
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-4 sm:p-6 shadow-lg border border-white/20 animate-fade-in-up">
<div className="space-y-6">
{!desktopShare.isViewing ? (
// 输入房间代码界面 - 与文本消息风格一致
<div>
<div className="flex items-center mb-6 sm:mb-8">
<div className="flex items-center space-x-3 flex-1">
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-indigo-500 rounded-xl flex items-center justify-center">
<Monitor className="w-5 h-5 text-white" />
</div>
<div>
<h2 className="text-lg font-semibold text-slate-800"></h2>
<p className="text-sm text-slate-600">6</p>
</div>
</div>
</div>
<form onSubmit={(e) => { e.preventDefault(); handleJoinViewing(); }} className="space-y-4 sm:space-y-6">
<div className="space-y-3">
<div className="relative">
<Input
value={inputCode}
onChange={(e) => setInputCode(e.target.value.replace(/[^A-Z0-9]/g, '').toUpperCase())}
placeholder="请输入房间代码"
className="text-center text-2xl sm:text-3xl tracking-[0.3em] sm:tracking-[0.5em] font-mono h-12 sm:h-16 border-2 border-slate-200 rounded-xl focus:border-purple-500 focus:ring-purple-500 bg-white/80 backdrop-blur-sm pb-2 sm:pb-4"
maxLength={6}
disabled={isLoading}
/>
</div>
<p className="text-center text-xs sm:text-sm text-slate-500">
{inputCode.length}/6
</p>
</div>
<div className="flex justify-center">
<Button
type="submit"
disabled={inputCode.length !== 6 || isLoading}
className="w-full h-10 sm:h-12 bg-gradient-to-r from-purple-500 to-indigo-500 hover:from-purple-600 hover:to-indigo-600 text-white text-base sm:text-lg font-medium rounded-xl shadow-lg transition-all duration-200 hover:shadow-xl hover:scale-105 disabled:opacity-50 disabled:scale-100"
>
{isLoading ? (
<div className="flex items-center space-x-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
<span>...</span>
</div>
) : (
<div className="flex items-center space-x-2">
<Monitor className="w-5 h-5" />
<span></span>
</div>
)}
</Button>
</div>
</form>
</div>
) : (
// 已连接,显示桌面观看界面
<div className="space-y-6">
<div className="flex items-center mb-6">
<div className="flex items-center space-x-3 flex-1">
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 to-teal-500 rounded-xl flex items-center justify-center">
<Monitor className="w-5 h-5 text-white" />
</div>
<div>
<h3 className="text-lg font-semibold text-slate-800"></h3>
<p className="text-sm text-slate-500">
<span className="text-emerald-600"> </span>
</p>
</div>
</div>
</div>
{/* 连接成功状态 */}
<div className="bg-emerald-50 border border-emerald-200 rounded-xl p-6">
<h4 className="font-semibold text-emerald-800 mb-1"></h4>
<p className="text-emerald-700">: {inputCode}</p>
</div>
{/* 观看中的控制面板 */}
<div className="flex justify-center mb-4">
<div className="bg-white rounded-lg p-3 shadow-lg border flex items-center space-x-4">
<div className="flex items-center space-x-2 text-green-600">
<Monitor className="w-4 h-4" />
<span className="font-semibold"></span>
</div>
<Button
onClick={handleStopViewing}
disabled={isLoading}
variant="destructive"
size="sm"
>
<Square className="w-4 h-4 mr-2" />
{isLoading ? '退出中...' : '退出观看'}
</Button>
</div>
</div>
<Button
onClick={handleStopSharing}
disabled={isLoading}
className="w-full h-12 bg-gradient-to-r from-red-500 to-pink-500 hover:from-red-600 hover:to-pink-600 text-white text-lg font-medium rounded-xl shadow-lg"
>
{isLoading ? (
<>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
...
</>
) : (
<>
<Square className="w-5 h-5 mr-2" />
</>
)}
</Button>
</div>
)}
</div>
</div>
) : (
<div className="bg-white/80 backdrop-blur-sm rounded-xl shadow-lg p-4 sm:p-6 animate-fade-in-up">
{/* 功能标题和状态 */}
<div className="flex items-center mb-6">
<div className="flex items-center space-x-3 flex-1">
<div className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-500 rounded-xl flex items-center justify-center">
<Monitor className="w-5 h-5 text-white" />
</div>
<div>
<h2 className="text-lg font-semibold text-slate-800"></h2>
<p className="text-sm text-slate-600">
{isViewing ? '正在观看桌面共享' : '输入连接码观看他人的桌面'}
</p>
</div>
</div>
{/* 竖线分割 */}
<div className="w-px h-12 bg-slate-200 mx-4"></div>
{/* 状态显示 */}
<div className="text-right">
<div className="text-sm text-slate-500 mb-1"></div>
<div className="flex items-center justify-end space-x-3 text-sm">
{/* WebSocket状态 */}
<div className="flex items-center space-x-1">
<div className="w-2 h-2 rounded-full bg-slate-400"></div>
<span className="text-slate-600">WS</span>
</div>
{/* 分隔符 */}
<div className="text-slate-300">|</div>
{/* WebRTC状态 */}
<div className="flex items-center space-x-1">
{isViewing ? (
<>
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
<span className="text-emerald-600">RTC</span>
</>
) : isLoading ? (
<>
<div className="w-2 h-2 rounded-full bg-orange-500 animate-pulse"></div>
<span className="text-orange-600">RTC</span>
</>
) : (
<>
<div className="w-2 h-2 rounded-full bg-slate-400"></div>
<span className="text-slate-600">RTC</span>
</>
)}
</div>
</div>
{isViewing && (
<div className="mt-1 text-xs text-indigo-600">
</div>
)}
</div>
</div>
<div className="space-y-4">
{!isViewing ? (
<>
<Input
value={inputCode}
onChange={(e) => setInputCode(e.target.value.toUpperCase().slice(0, 6))}
placeholder="请输入连接码"
className="text-center text-2xl sm:text-3xl tracking-[0.3em] sm:tracking-[0.5em] font-mono h-12 sm:h-16 border-2 border-slate-200 rounded-xl focus:border-indigo-500 focus:ring-indigo-500 bg-white/80 backdrop-blur-sm"
maxLength={6}
disabled={isLoading}
/>
<Button
onClick={handleJoinSharing}
disabled={inputCode.length !== 6 || isLoading}
className="w-full h-12 bg-gradient-to-r from-indigo-500 to-purple-500 hover:from-indigo-600 hover:to-purple-600 text-white text-lg font-medium rounded-xl shadow-lg"
>
{isLoading ? (
<>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
...
</>
) : (
<>
<Monitor className="w-5 h-5 mr-2" />
</>
)}
</Button>
</>
) : (
<div className="space-y-4">
<div className="aspect-video bg-slate-900 rounded-xl flex items-center justify-center text-white">
<div className="text-center">
<Monitor className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p className="text-sm opacity-75"></p>
{/* 桌面显示区域 */}
{desktopShare.remoteStream ? (
<DesktopViewer
stream={desktopShare.remoteStream}
isConnected={desktopShare.isViewing}
connectionCode={inputCode}
onDisconnect={handleStopViewing}
/>
) : (
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-8 border border-slate-200">
<div className="text-center">
<Monitor className="w-16 h-16 mx-auto text-slate-400 mb-4" />
<p className="text-slate-600 mb-2">...</p>
<p className="text-sm text-slate-500"></p>
<div className="flex items-center justify-center space-x-2 mt-4">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-purple-500"></div>
<span className="text-sm text-purple-600">...</span>
</div>
</div>
</div>
</div>
<Button
onClick={() => setIsViewing(false)}
className="w-full h-12 bg-gradient-to-r from-red-500 to-pink-500 hover:from-red-600 hover:to-pink-600 text-white text-lg font-medium rounded-xl shadow-lg"
>
<Square className="w-5 h-5 mr-2" />
</Button>
)}
</div>
)}
</div>
</div>
)}
{/* 错误显示 */}
{desktopShare.error && (
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-600 text-sm">{desktopShare.error}</p>
</div>
)}
{/* 调试信息 */}
<div className="mt-6">
<button
onClick={() => setShowDebug(!showDebug)}
className="text-xs text-gray-500 hover:text-gray-700"
>
{showDebug ? '隐藏' : '显示'}
</button>
{showDebug && (
<div className="mt-2 p-3 bg-gray-50 rounded text-xs text-gray-600 space-y-1">
<div>WebSocket连接: {desktopShare.isWebSocketConnected ? '✅' : '❌'}</div>
<div>P2P连接: {desktopShare.isPeerConnected ? '✅' : '❌'}</div>
<div>: {desktopShare.connectionCode || '未创建'}</div>
<div>: {desktopShare.isSharing ? '进行中' : '未共享'}</div>
<div>: {desktopShare.isViewing ? '观看中' : '未观看'}</div>
<div>: {desktopShare.isWaitingForPeer ? '是' : '否'}</div>
<div>: {desktopShare.remoteStream ? '已接收' : '无'}</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,344 @@
"use client";
import React, { useRef, useEffect, useState, useCallback } from 'react';
import { Monitor, Maximize, Minimize, Volume2, VolumeX, Settings, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
interface DesktopViewerProps {
stream: MediaStream | null;
isConnected: boolean;
connectionCode?: string;
onDisconnect: () => void;
}
export default function DesktopViewer({
stream,
isConnected,
connectionCode,
onDisconnect
}: DesktopViewerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const [isMuted, setIsMuted] = useState(false);
const [showControls, setShowControls] = useState(true);
const [videoStats, setVideoStats] = useState<{
resolution: string;
fps: number;
}>({ resolution: '0x0', fps: 0 });
const hideControlsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// 设置视频流
useEffect(() => {
if (videoRef.current && stream) {
console.log('[DesktopViewer] 🎬 设置视频流,轨道数量:', stream.getTracks().length);
stream.getTracks().forEach(track => {
console.log('[DesktopViewer] 轨道详情:', track.kind, track.id, track.enabled, track.readyState);
});
videoRef.current.srcObject = stream;
console.log('[DesktopViewer] ✅ 视频元素已设置流');
} else if (videoRef.current && !stream) {
console.log('[DesktopViewer] ❌ 清除视频流');
videoRef.current.srcObject = null;
}
}, [stream]);
// 监控视频统计信息
useEffect(() => {
if (!videoRef.current) return;
const video = videoRef.current;
const updateStats = () => {
if (video.videoWidth && video.videoHeight) {
setVideoStats({
resolution: `${video.videoWidth}x${video.videoHeight}`,
fps: 0, // 实际FPS需要更复杂的计算
});
}
};
video.addEventListener('loadedmetadata', updateStats);
video.addEventListener('resize', updateStats);
const interval = setInterval(updateStats, 1000);
return () => {
video.removeEventListener('loadedmetadata', updateStats);
video.removeEventListener('resize', updateStats);
clearInterval(interval);
};
}, []);
// 全屏相关处理
useEffect(() => {
const handleFullscreenChange = () => {
const isCurrentlyFullscreen = !!document.fullscreenElement;
setIsFullscreen(isCurrentlyFullscreen);
if (isCurrentlyFullscreen) {
// 全屏时自动隐藏控制栏,鼠标移动时显示
setShowControls(false);
} else {
setShowControls(true);
}
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange);
};
}, []);
// 鼠标移动处理(全屏时)
const handleMouseMove = useCallback(() => {
if (isFullscreen) {
setShowControls(true);
// 清除之前的定时器
if (hideControlsTimeoutRef.current) {
clearTimeout(hideControlsTimeoutRef.current);
}
// 3秒后自动隐藏控制栏
hideControlsTimeoutRef.current = setTimeout(() => {
setShowControls(false);
}, 3000);
}
}, [isFullscreen]);
// 键盘快捷键
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
switch (event.key) {
case 'Escape':
if (isFullscreen) {
exitFullscreen();
}
break;
case 'f':
case 'F':
if (event.ctrlKey) {
event.preventDefault();
toggleFullscreen();
}
break;
case 'm':
case 'M':
if (event.ctrlKey) {
event.preventDefault();
toggleMute();
}
break;
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [isFullscreen]);
// 切换全屏
const toggleFullscreen = useCallback(async () => {
if (!containerRef.current) return;
try {
if (isFullscreen) {
await document.exitFullscreen();
} else {
await containerRef.current.requestFullscreen();
}
} catch (error) {
console.error('[DesktopViewer] 全屏切换失败:', error);
}
}, [isFullscreen]);
// 退出全屏
const exitFullscreen = useCallback(async () => {
try {
if (document.fullscreenElement) {
await document.exitFullscreen();
}
} catch (error) {
console.error('[DesktopViewer] 退出全屏失败:', error);
}
}, []);
// 切换静音
const toggleMute = useCallback(() => {
if (videoRef.current) {
videoRef.current.muted = !videoRef.current.muted;
setIsMuted(videoRef.current.muted);
}
}, []);
// 清理定时器
useEffect(() => {
return () => {
if (hideControlsTimeoutRef.current) {
clearTimeout(hideControlsTimeoutRef.current);
}
};
}, []);
if (!stream) {
return (
<div className="flex flex-col items-center justify-center h-96 bg-slate-900 rounded-xl text-white">
<Monitor className="w-16 h-16 opacity-50 mb-4" />
<p className="text-lg opacity-75">
{isConnected ? '等待桌面共享流...' : '等待桌面共享连接...'}
</p>
{connectionCode && (
<p className="text-sm opacity-50 mt-2">: {connectionCode}</p>
)}
<div className="mt-4 flex items-center space-x-2 text-sm">
<div className={`w-2 h-2 rounded-full ${isConnected ? 'bg-green-500 animate-pulse' : 'bg-yellow-500 animate-pulse'}`}></div>
<span>{isConnected ? '已连接,等待视频流' : '正在建立连接'}</span>
</div>
</div>
);
}
return (
<div
ref={containerRef}
className={`relative bg-black rounded-xl overflow-hidden ${isFullscreen ? 'fixed inset-0 z-50' : 'w-full'}`}
onMouseMove={handleMouseMove}
onMouseEnter={() => isFullscreen && setShowControls(true)}
>
{/* 主视频显示 */}
<video
ref={videoRef}
autoPlay
playsInline
muted={isMuted}
className={`w-full h-full object-contain ${isFullscreen ? 'cursor-none' : ''}`}
style={{
aspectRatio: isFullscreen ? 'unset' : '16/9',
minHeight: isFullscreen ? '100vh' : '400px'
}}
/>
{/* 连接状态覆盖层 */}
{!isConnected && (
<div className="absolute inset-0 bg-black/80 flex flex-col items-center justify-center text-white">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white mb-4"></div>
<p className="text-lg">...</p>
{connectionCode && (
<p className="text-sm opacity-75 mt-2">: {connectionCode}</p>
)}
</div>
)}
{/* 控制栏 */}
<div
className={`absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent p-4 transition-all duration-300 ${
showControls || !isFullscreen ? 'opacity-100' : 'opacity-0 pointer-events-none'
}`}
>
<div className="flex items-center justify-between">
{/* 左侧信息 */}
<div className="flex items-center space-x-4 text-white text-sm">
<div className="flex items-center space-x-2">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
<span></span>
</div>
{videoStats.resolution !== '0x0' && (
<>
<div className="w-px h-4 bg-white/30"></div>
<span>{videoStats.resolution}</span>
</>
)}
{connectionCode && (
<>
<div className="w-px h-4 bg-white/30"></div>
<span className="font-mono">{connectionCode}</span>
</>
)}
</div>
{/* 右侧控制按钮 */}
<div className="flex items-center space-x-2">
{/* 音频控制 */}
<Button
variant="ghost"
size="sm"
onClick={toggleMute}
className="text-white hover:bg-white/20"
>
{isMuted ? (
<VolumeX className="w-4 h-4" />
) : (
<Volume2 className="w-4 h-4" />
)}
</Button>
{/* 设置 */}
<Button
variant="ghost"
size="sm"
className="text-white hover:bg-white/20"
>
<Settings className="w-4 h-4" />
</Button>
{/* 全屏切换 */}
<Button
variant="ghost"
size="sm"
onClick={toggleFullscreen}
className="text-white hover:bg-white/20"
title={isFullscreen ? "退出全屏 (Esc)" : "全屏 (Ctrl+F)"}
>
{isFullscreen ? (
<Minimize className="w-4 h-4" />
) : (
<Maximize className="w-4 h-4" />
)}
</Button>
{/* 断开连接 */}
<Button
variant="ghost"
size="sm"
onClick={onDisconnect}
className="text-white hover:bg-red-500/30"
title="断开连接"
>
<X className="w-4 h-4" />
</Button>
</div>
</div>
{/* 快捷键提示(仅全屏时显示) */}
{isFullscreen && showControls && (
<div className="mt-2 text-xs text-white/60 text-center">
<p>快捷键: Esc 退 | Ctrl+F | Ctrl+M </p>
</div>
)}
</div>
{/* 加载状态 */}
{stream && !isConnected && (
<div className="absolute top-4 left-4 bg-black/60 text-white px-3 py-2 rounded-lg text-sm flex items-center space-x-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
<span>...</span>
</div>
)}
{/* 网络状态指示器 */}
<div className="absolute top-4 right-4 bg-black/60 text-white px-3 py-2 rounded-lg text-xs">
<div className="flex items-center space-x-2">
<div className={`w-2 h-2 rounded-full ${
isConnected ? 'bg-green-500' : 'bg-yellow-500 animate-pulse'
}`}></div>
<span>{isConnected ? '已连接' : '连接中'}</span>
</div>
</div>
</div>
);
}

View File

@@ -287,8 +287,8 @@ export const WebRTCFileTransfer: React.FC = () => {
const updatedList = [...prev, ...newFileInfos];
console.log('更新后的文件列表:', updatedList);
// 如果已连接,立即同步文件列表
if (isConnected && pickupCode) {
// 如果P2P连接已建立,立即同步文件列表
if (isConnected && connection.isPeerConnected && pickupCode) {
console.log('立即同步文件列表到对端');
setTimeout(() => sendFileList(updatedList), 100);
}
@@ -573,18 +573,23 @@ export const WebRTCFileTransfer: React.FC = () => {
console.log('WebRTC连接状态:', isConnected);
console.log('连接中状态:', isConnecting);
// 如果WebSocket断开但不是主动断开的情况
// 只有在之前已经建立过连接,现在断开的情况下才显示断开提示
// 避免在初始连接时误报断开
if (!isWebSocketConnected && !isConnected && !isConnecting && pickupCode) {
showToast('与服务器的连接已断开,请重新连接', "error");
// 清理传输状态
console.log('WebSocket断开清理传输状态');
setCurrentTransferFile(null);
setFileList(prev => prev.map(item =>
item.status === 'downloading'
? { ...item, status: 'ready' as const, progress: 0 }
: item
));
// 增加额外检查:只有在之前曾经连接成功过的情况下才显示断开提示
// 通过检查是否有文件列表来判断是否曾经连接过
if (fileList.length > 0 || currentTransferFile) {
showToast('与服务器的连接已断开,请重新连接', "error");
// 清理传输状态
console.log('WebSocket断开清理传输状态');
setCurrentTransferFile(null);
setFileList(prev => prev.map(item =>
item.status === 'downloading'
? { ...item, status: 'ready' as const, progress: 0 }
: item
));
}
}
// WebSocket连接成功时的提示
@@ -592,7 +597,7 @@ export const WebRTCFileTransfer: React.FC = () => {
console.log('WebSocket已连接正在建立P2P连接...');
}
}, [isWebSocketConnected, isConnected, isConnecting, pickupCode, showToast]);
}, [isWebSocketConnected, isConnected, isConnecting, pickupCode, showToast, fileList.length, currentTransferFile]);
// 监听连接状态变化,清理传输状态
useEffect(() => {
@@ -646,8 +651,8 @@ export const WebRTCFileTransfer: React.FC = () => {
console.log('正在建立WebRTC连接...');
}
// 只有在连接成功且没有错误时才发送文件列表
if (isConnected && !error && pickupCode && mode === 'send' && selectedFiles.length > 0) {
// 只有在P2P连接建立且没有错误时才发送文件列表
if (isConnected && connection.isPeerConnected && !error && pickupCode && mode === 'send' && selectedFiles.length > 0) {
// 确保有文件列表
if (fileList.length === 0) {
console.log('创建文件列表并发送...');
@@ -662,7 +667,7 @@ export const WebRTCFileTransfer: React.FC = () => {
setFileList(newFileInfos);
// 延迟发送,确保数据通道已准备好
setTimeout(() => {
if (isConnected && !error) { // 再次检查连接状态
if (isConnected && connection.isPeerConnected && !error) { // 再次检查连接状态
sendFileList(newFileInfos);
}
}, 500);
@@ -670,13 +675,26 @@ export const WebRTCFileTransfer: React.FC = () => {
console.log('发送现有文件列表...');
// 延迟发送,确保数据通道已准备好
setTimeout(() => {
if (isConnected && !error) { // 再次检查连接状态
if (isConnected && connection.isPeerConnected && !error) { // 再次检查连接状态
sendFileList(fileList);
}
}, 500);
}
}
}, [isConnected, isConnecting, isWebSocketConnected, pickupCode, mode, selectedFiles.length, error]);
}, [isConnected, connection.isPeerConnected, isConnecting, isWebSocketConnected, pickupCode, mode, selectedFiles.length, error]);
// 监听P2P连接建立自动发送文件列表
useEffect(() => {
if (connection.isPeerConnected && mode === 'send' && fileList.length > 0) {
console.log('P2P连接已建立发送文件列表...');
// 稍微延迟一下,确保数据通道完全准备好
setTimeout(() => {
if (connection.isPeerConnected && connection.getChannelState() === 'open') {
sendFileList(fileList);
}
}, 200);
}
}, [connection.isPeerConnected, mode, fileList.length, sendFileList]);
// 请求下载文件(接收方调用)
const requestFile = (fileId: string) => {
@@ -765,7 +783,8 @@ export const WebRTCFileTransfer: React.FC = () => {
console.log('=== 清空文件 ===');
setSelectedFiles([]);
setFileList([]);
if (isConnected && pickupCode) {
// 只有在P2P连接建立且数据通道准备好时才发送清空消息
if (isConnected && connection.isPeerConnected && connection.getChannelState() === 'open' && pickupCode) {
sendFileList([]);
}
};

View File

@@ -106,8 +106,7 @@ export const WebRTCTextReceiver: React.FC<WebRTCTextReceiverProps> = ({
setIsTyping(false);
// 断开连接
textTransfer.disconnect();
fileTransfer.disconnect();
connection.disconnect();
if (onRestart) {
onRestart();

View File

@@ -38,11 +38,9 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
// 连接所有传输通道
const connectAll = useCallback(async (code: string, role: 'sender' | 'receiver') => {
console.log('=== 连接所有传输通道 ===', { code, role });
await Promise.all([
textTransfer.connect(code, role),
fileTransfer.connect(code, role)
]);
}, [textTransfer, fileTransfer]);
// 只需要连接一次,因为使用的是共享连接
await connection.connect(code, role);
}, [connection]);
// 是否有任何连接
const hasAnyConnection = textTransfer.isConnected || fileTransfer.isConnected;
@@ -63,9 +61,8 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
sentImages.forEach(img => URL.revokeObjectURL(img.url));
setSentImages([]);
// 断开连接
textTransfer.disconnect();
fileTransfer.disconnect();
// 断开连接(只需要断开一次)
connection.disconnect();
if (onRestart) {
onRestart();
@@ -141,7 +138,7 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
// 如果有初始文本,发送它
if (currentText) {
setTimeout(() => {
if (textTransfer.isConnected) {
if (connection.isPeerConnected && textTransfer.isConnected) {
// 发送实时文本同步
textTransfer.sendTextSync(currentText);
@@ -171,8 +168,8 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
const newHeight = Math.min(Math.max(textarea.scrollHeight, 100), 300); // 最小100px最大300px
textarea.style.height = `${newHeight}px`;
// 实时同步文本内容(如果已连接
if (textTransfer.isConnected) {
// 实时同步文本内容(如果P2P连接已建立
if (connection.isPeerConnected && textTransfer.isConnected) {
// 发送实时文本同步
textTransfer.sendTextSync(value);
@@ -215,9 +212,11 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
}]);
// 发送文件
if (fileTransfer.isConnected) {
if (connection.isPeerConnected && fileTransfer.isConnected) {
fileTransfer.sendFile(file);
showToast('图片发送中...', "success");
} else if (!connection.isPeerConnected) {
showToast('等待对方加入P2P网络...', "error");
} else {
showToast('请先连接到房间', "error");
}
@@ -409,8 +408,16 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
value={textInput}
onChange={handleTextInputChange}
onPaste={handlePaste}
placeholder="在这里编辑文字内容...&#10;&#10;💡 支持实时同步编辑,对方可以看到你的修改&#10;💡 可以直接粘贴图片 (Ctrl+V)"
className="w-full h-40 px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none text-slate-700 placeholder-slate-400"
disabled={!connection.isPeerConnected}
placeholder={connection.isPeerConnected
? "在这里编辑文字内容...&#10;&#10;💡 支持实时同步编辑,对方可以看到你的修改&#10;💡 可以直接粘贴图片 (Ctrl+V)"
: "等待对方加入P2P网络...&#10;&#10;📡 建立连接后即可开始输入文字"
}
className={`w-full h-40 px-4 py-3 border rounded-lg resize-none text-slate-700 ${
connection.isPeerConnected
? "border-slate-300 focus:ring-2 focus:ring-blue-500 focus:border-transparent placeholder-slate-400"
: "border-slate-200 bg-slate-50 cursor-not-allowed placeholder-slate-300"
}`}
/>
<div className="flex items-center justify-between mt-3">
@@ -419,7 +426,10 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
onClick={() => fileInputRef.current?.click()}
variant="outline"
size="sm"
className="flex items-center space-x-1"
disabled={!connection.isPeerConnected}
className={`flex items-center space-x-1 ${
!connection.isPeerConnected ? 'cursor-not-allowed opacity-50' : ''
}`}
>
<Image className="w-4 h-4" />
<span></span>