feat: UI分离

This commit is contained in:
MatrixSeven
2025-08-15 14:15:51 +08:00
parent 3b7fa7c653
commit 2abf7bdf42
8 changed files with 943 additions and 737 deletions

View File

@@ -3,15 +3,13 @@
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, 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';
import { Share, Monitor } from 'lucide-react';
import WebRTCDesktopReceiver from '@/components/webrtc/WebRTCDesktopReceiver';
import WebRTCDesktopSender from '@/components/webrtc/WebRTCDesktopSender';
interface DesktopShareProps {
// 保留向后兼容性的props
// 保留向后兼容性的props(已废弃,但保留接口)
onStartSharing?: () => Promise<string>;
onStopSharing?: () => Promise<void>;
onJoinSharing?: (code: string) => Promise<void>;
@@ -25,24 +23,19 @@ export default function DesktopShare({
const searchParams = useSearchParams();
const router = useRouter();
const [mode, setMode] = useState<'share' | 'view'>('share');
const [inputCode, setInputCode] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [showDebug, setShowDebug] = useState(false);
const { showToast } = useToast();
// 使用桌面共享业务逻辑
const desktopShare = useDesktopShareBusiness();
// 从URL参数中获取初始模式
// 从URL参数中获取初始模式和房间代码
useEffect(() => {
const urlMode = searchParams.get('mode');
const type = searchParams.get('type');
const urlCode = searchParams.get('code');
if (type === 'desktop' && urlMode) {
if (urlMode === 'send') {
setMode('share');
} else if (urlMode === 'receive') {
setMode('view');
// 如果URL中有房间代码将在DesktopShareReceiver组件中自动加入
}
}
}, [searchParams]);
@@ -53,144 +46,26 @@ export default function DesktopShare({
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set('type', 'desktop');
currentUrl.searchParams.set('mode', newMode === 'share' ? 'send' : 'receive');
// 清除代码参数,避免模式切换时的混乱
currentUrl.searchParams.delete('code');
router.replace(currentUrl.pathname + currentUrl.search);
}, [router]);
// 复制房间代码
const copyCode = useCallback(async (code: string) => {
try {
await navigator.clipboard.writeText(code);
showToast('房间代码已复制到剪贴板', 'success');
} catch (error) {
console.error('复制失败:', error);
showToast('复制失败,请手动复制', 'error');
// 获取初始房间代码(用于接收者模式)
const getInitialCode = useCallback(() => {
const urlMode = searchParams.get('mode');
const type = searchParams.get('type');
const code = searchParams.get('code');
console.log('[DesktopShare] getInitialCode 调用, URL参数:', { type, urlMode, code });
if (type === 'desktop' && urlMode === 'receive') {
const result = code || '';
console.log('[DesktopShare] getInitialCode 返回:', result);
return result;
}
}, [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();
console.log('[DesktopShare] getInitialCode 返回空字符串');
return '';
}, [searchParams]);
return (
<div className="space-y-4 sm:space-y-6">
@@ -216,425 +91,14 @@ export default function DesktopShare({
</div>
</div>
{/* 根据模式渲染对应的组件 */}
{mode === 'share' ? (
/* 共享模式 */
<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="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-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="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
...
</>
) : (
<>
<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>
{/* 桌面共享控制区域 */}
{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 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={() => 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"
>
</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>
<WebRTCDesktopSender />
) : (
/* 观看模式 */
<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>
{/* 桌面显示区域 */}
{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>
)}
</div>
</div>
<WebRTCDesktopReceiver
initialCode={getInitialCode()}
/>
)}
{/* 错误显示 */}
{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

@@ -1,7 +1,7 @@
"use client";
import React, { useRef, useEffect, useState, useCallback } from 'react';
import { Monitor, Maximize, Minimize, Volume2, VolumeX, Settings, X } from 'lucide-react';
import { Monitor, Maximize, Minimize, Volume2, VolumeX, Settings, X, Play } from 'lucide-react';
import { Button } from '@/components/ui/button';
interface DesktopViewerProps {
@@ -22,6 +22,9 @@ export default function DesktopViewer({
const [isFullscreen, setIsFullscreen] = useState(false);
const [isMuted, setIsMuted] = useState(false);
const [showControls, setShowControls] = useState(true);
const [isPlaying, setIsPlaying] = useState(false);
const [needsUserInteraction, setNeedsUserInteraction] = useState(false);
const hasAttemptedAutoplayRef = useRef(false);
const [videoStats, setVideoStats] = useState<{
resolution: string;
fps: number;
@@ -39,9 +42,69 @@ export default function DesktopViewer({
videoRef.current.srcObject = stream;
console.log('[DesktopViewer] ✅ 视频元素已设置流');
// 重置状态
hasAttemptedAutoplayRef.current = false;
setNeedsUserInteraction(false);
setIsPlaying(false);
// 添加事件监听器来调试视频加载
const video = videoRef.current;
const handleLoadStart = () => console.log('[DesktopViewer] 📹 视频开始加载');
const handleLoadedMetadata = () => {
console.log('[DesktopViewer] 📹 视频元数据已加载');
console.log('[DesktopViewer] 📹 视频尺寸:', video.videoWidth, 'x', video.videoHeight);
};
const handleCanPlay = () => {
console.log('[DesktopViewer] 📹 视频可以开始播放');
// 只在还未尝试过自动播放时才尝试
if (!hasAttemptedAutoplayRef.current) {
hasAttemptedAutoplayRef.current = true;
video.play()
.then(() => {
console.log('[DesktopViewer] ✅ 视频自动播放成功');
setIsPlaying(true);
setNeedsUserInteraction(false);
})
.catch(e => {
console.log('[DesktopViewer] 📹 自动播放被阻止,需要用户交互:', e.message);
setIsPlaying(false);
setNeedsUserInteraction(true);
});
}
};
const handlePlay = () => {
console.log('[DesktopViewer] 📹 视频开始播放');
setIsPlaying(true);
setNeedsUserInteraction(false);
};
const handlePause = () => {
console.log('[DesktopViewer] 📹 视频暂停');
setIsPlaying(false);
};
const handleError = (e: Event) => console.error('[DesktopViewer] 📹 视频播放错误:', e);
video.addEventListener('loadstart', handleLoadStart);
video.addEventListener('loadedmetadata', handleLoadedMetadata);
video.addEventListener('canplay', handleCanPlay);
video.addEventListener('play', handlePlay);
video.addEventListener('pause', handlePause);
video.addEventListener('error', handleError);
return () => {
video.removeEventListener('loadstart', handleLoadStart);
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
video.removeEventListener('canplay', handleCanPlay);
video.removeEventListener('play', handlePlay);
video.removeEventListener('pause', handlePause);
video.removeEventListener('error', handleError);
};
} else if (videoRef.current && !stream) {
console.log('[DesktopViewer] ❌ 清除视频流');
videoRef.current.srcObject = null;
setIsPlaying(false);
setNeedsUserInteraction(false);
hasAttemptedAutoplayRef.current = false;
}
}, [stream]);
@@ -176,6 +239,21 @@ export default function DesktopViewer({
}
}, []);
// 手动播放视频
const handleManualPlay = useCallback(() => {
if (videoRef.current) {
videoRef.current.play()
.then(() => {
console.log('[DesktopViewer] ✅ 手动播放成功');
setIsPlaying(true);
setNeedsUserInteraction(false);
})
.catch(e => {
console.error('[DesktopViewer] ❌ 手动播放失败:', e);
});
}
}, []);
// 清理定时器
useEffect(() => {
return () => {
@@ -223,6 +301,19 @@ export default function DesktopViewer({
}}
/>
{/* 需要用户交互的播放覆盖层 - 只在自动播放尝试失败后显示 */}
{hasAttemptedAutoplayRef.current && needsUserInteraction && !isPlaying && (
<div className="absolute inset-0 bg-black/50 flex flex-col items-center justify-center text-white z-10">
<div className="text-center">
<div className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-full flex items-center justify-center hover:bg-white/30 transition-colors cursor-pointer" onClick={handleManualPlay}>
<Play className="w-10 h-10 text-white ml-1" />
</div>
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-sm opacity-75"></p>
</div>
</div>
)}
{/* 连接状态覆盖层 */}
{!isConnected && (
<div className="absolute inset-0 bg-black/80 flex flex-col items-center justify-center text-white">
@@ -244,8 +335,8 @@ export default function DesktopViewer({
{/* 左侧信息 */}
<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 className={`w-2 h-2 rounded-full ${isPlaying ? 'bg-green-500 animate-pulse' : 'bg-yellow-500'}`}></div>
<span>{isPlaying ? '桌面共享中' : needsUserInteraction ? '等待播放' : '连接中'}</span>
</div>
{videoStats.resolution !== '0x0' && (
<>

View File

@@ -0,0 +1,123 @@
"use client";
import React from 'react';
import { Button } from '@/components/ui/button';
import QRCodeDisplay from '@/components/QRCodeDisplay';
import { LucideIcon } from 'lucide-react';
interface RoomInfoDisplayProps {
// 房间信息
code: string;
link: string;
// 显示配置
icon: LucideIcon;
iconColor?: string; // 图标背景渐变色,如 'from-emerald-500 to-teal-500'
codeColor?: string; // 代码文字渐变色,如 'from-emerald-600 to-teal-600'
// 文案配置
title: string; // 如 "取件码生成成功!" 或 "房间码生成成功!"
subtitle: string; // 如 "分享以下信息给接收方" 或 "分享以下信息给观看方"
codeLabel: string; // 如 "取件码" 或 "房间代码"
qrLabel: string; // 如 "扫码传输" 或 "扫码观看"
copyButtonText: string; // 如 "复制取件码" 或 "复制房间代码"
copyButtonColor?: string; // 复制按钮颜色,如 'bg-emerald-500 hover:bg-emerald-600'
qrButtonText: string; // 如 "使用手机扫码快速访问" 或 "使用手机扫码快速观看"
linkButtonText: string; // 如 "复制取件链接" 或 "复制观看链接"
// 事件回调
onCopyCode: () => void;
onCopyLink: () => void;
// 样式配置
className?: string;
}
export default function RoomInfoDisplay({
code,
link,
icon: Icon,
iconColor = 'from-emerald-500 to-teal-500',
codeColor = 'from-emerald-600 to-teal-600',
title,
subtitle,
codeLabel,
qrLabel,
copyButtonText,
copyButtonColor = 'bg-emerald-500 hover:bg-emerald-600',
qrButtonText,
linkButtonText,
onCopyCode,
onCopyLink,
className = ''
}: RoomInfoDisplayProps) {
return (
<div className={`border-t border-slate-200 pt-6 ${className}`}>
{/* 左上角状态提示 */}
<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 ${iconColor} rounded-xl flex items-center justify-center`}>
<Icon className="w-5 h-5 text-white" />
</div>
<div>
<h3 className="text-lg font-semibold text-slate-800">{title}</h3>
<p className="text-sm text-slate-600">{subtitle}</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">{codeLabel}</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 ${codeColor} bg-clip-text text-transparent tracking-wider`}>
{code}
</div>
</div>
<Button
onClick={onCopyCode}
className={`w-full px-4 py-2.5 ${copyButtonColor} text-white rounded-lg font-medium shadow transition-all duration-200 mt-3`}
>
{copyButtonText}
</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">{qrLabel}</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={link}
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">
{qrButtonText}
</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">
{link}
</div>
</div>
<Button
onClick={onCopyLink}
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"
>
{linkButtonText}
</Button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,274 @@
"use client";
import React, { useState, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Monitor, Square } from 'lucide-react';
import { useToast } from '@/components/ui/toast-simple';
import { useDesktopShareBusiness } from '@/hooks/webrtc/useDesktopShareBusiness';
import DesktopViewer from '@/components/DesktopViewer';
interface WebRTCDesktopReceiverProps {
className?: string;
initialCode?: string; // 支持从URL参数传入的房间代码
}
export default function WebRTCDesktopReceiver({ className, initialCode }: WebRTCDesktopReceiverProps) {
const [inputCode, setInputCode] = useState(initialCode || '');
const [isLoading, setIsLoading] = useState(false);
const [showDebug, setShowDebug] = useState(false);
const hasTriedAutoJoin = React.useRef(false); // 添加 ref 来跟踪是否已尝试自动加入
const { showToast } = useToast();
// 使用桌面共享业务逻辑
const desktopShare = useDesktopShareBusiness();
// 加入观看
const handleJoinViewing = useCallback(async () => {
if (!inputCode.trim()) {
showToast('请输入房间代码', 'error');
return;
}
try {
setIsLoading(true);
console.log('[DesktopShareReceiver] 用户加入观看房间:', inputCode);
await desktopShare.joinSharing(inputCode.trim().toUpperCase());
console.log('[DesktopShareReceiver] 加入观看成功');
showToast('已加入桌面共享', 'success');
} catch (error) {
console.error('[DesktopShareReceiver] 加入观看失败:', 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('[DesktopShareReceiver] 停止观看失败:', error);
showToast('退出失败', 'error');
} finally {
setIsLoading(false);
}
}, [desktopShare, showToast]);
// 如果有初始代码且还未加入观看,自动尝试加入
React.useEffect(() => {
console.log('[WebRTCDesktopReceiver] useEffect 触发, 参数:', {
initialCode,
isViewing: desktopShare.isViewing,
isConnecting: desktopShare.isConnecting,
hasTriedAutoJoin: hasTriedAutoJoin.current
});
const autoJoin = async () => {
if (initialCode && !desktopShare.isViewing && !desktopShare.isConnecting && !hasTriedAutoJoin.current) {
hasTriedAutoJoin.current = true;
console.log('[WebRTCDesktopReceiver] 检测到初始代码,自动加入观看:', initialCode);
try {
setIsLoading(true);
await desktopShare.joinSharing(initialCode.trim().toUpperCase());
console.log('[WebRTCDesktopReceiver] 自动加入观看成功');
showToast('已加入桌面共享', 'success');
} catch (error) {
console.error('[WebRTCDesktopReceiver] 自动加入观看失败:', error);
const errorMessage = error instanceof Error ? error.message : '加入观看失败';
showToast(errorMessage, 'error');
} finally {
setIsLoading(false);
}
} else {
console.log('[WebRTCDesktopReceiver] 不满足自动加入条件:', {
hasInitialCode: !!initialCode,
notViewing: !desktopShare.isViewing,
notConnecting: !desktopShare.isConnecting,
notTriedBefore: !hasTriedAutoJoin.current
});
}
};
autoJoin();
}, [initialCode, desktopShare.isViewing, desktopShare.isConnecting]); // 移除了 desktopShare.joinSharing 和 showToast
return (
<div className={`space-y-4 sm:space-y-6 ${className || ''}`}>
<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>
{/* 桌面显示区域 */}
{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>
)}
</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.isViewing ? '观看中' : '未观看'}</div>
<div>: {desktopShare.remoteStream ? '已接收' : '无'}</div>
{desktopShare.remoteStream && (
<div>
<div>: {desktopShare.remoteStream.getTracks().length}</div>
<div>: {desktopShare.remoteStream.getVideoTracks().length}</div>
<div>: {desktopShare.remoteStream.getAudioTracks().length}</div>
{desktopShare.remoteStream.getVideoTracks().map((track, index) => (
<div key={index}>
{index}: {track.readyState}, enabled: {track.enabled ? '是' : '否'}
</div>
))}
{desktopShare.remoteStream.getAudioTracks().map((track, index) => (
<div key={index}>
{index}: {track.readyState}, enabled: {track.enabled ? '是' : '否'}
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,353 @@
"use client";
import React, { useState, useCallback } from 'react';
import { Button } from '@/components/ui/button';
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 QRCodeDisplay from '@/components/QRCodeDisplay';
import RoomInfoDisplay from '@/components/RoomInfoDisplay';
interface WebRTCDesktopSenderProps {
className?: string;
}
export default function WebRTCDesktopSender({ className }: WebRTCDesktopSenderProps) {
const [isLoading, setIsLoading] = useState(false);
const [showDebug, setShowDebug] = useState(false);
const { showToast } = useToast();
// 使用桌面共享业务逻辑
const desktopShare = useDesktopShareBusiness();
// 复制房间代码
const copyCode = useCallback(async (code: string) => {
try {
await navigator.clipboard.writeText(code);
showToast('房间代码已复制到剪贴板', 'success');
} catch (error) {
console.error('复制失败:', error);
showToast('复制失败,请手动复制', 'error');
}
}, [showToast]);
// 创建房间
const handleCreateRoom = useCallback(async () => {
try {
setIsLoading(true);
console.log('[DesktopShareSender] 用户点击创建房间');
const roomCode = await desktopShare.createRoom();
console.log('[DesktopShareSender] 房间创建成功:', roomCode);
showToast(`房间创建成功!代码: ${roomCode}`, 'success');
} catch (error) {
console.error('[DesktopShareSender] 创建房间失败:', 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('[DesktopShareSender] 用户点击开始桌面共享');
await desktopShare.startSharing();
console.log('[DesktopShareSender] 桌面共享开始成功');
showToast('桌面共享已开始', 'success');
} catch (error) {
console.error('[DesktopShareSender] 开始桌面共享失败:', 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('[DesktopShareSender] 用户点击切换桌面');
await desktopShare.switchDesktop();
console.log('[DesktopShareSender] 桌面切换成功');
showToast('桌面切换成功', 'success');
} catch (error) {
console.error('[DesktopShareSender] 切换桌面失败:', 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('[DesktopShareSender] 用户点击停止桌面共享');
await desktopShare.stopSharing();
console.log('[DesktopShareSender] 桌面共享停止成功');
showToast('桌面共享已停止', 'success');
} catch (error) {
console.error('[DesktopShareSender] 停止桌面共享失败:', error);
const errorMessage = error instanceof Error ? error.message : '停止桌面共享失败';
showToast(errorMessage, 'error');
} finally {
setIsLoading(false);
}
}, [desktopShare, showToast]);
return (
<div className={`space-y-4 sm:space-y-6 ${className || ''}`}>
<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="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-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="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
...
</>
) : (
<>
<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>
{/* 桌面共享控制区域 */}
{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>
)}
{/* 房间信息显示 */}
<RoomInfoDisplay
code={desktopShare.connectionCode}
link={`${typeof window !== 'undefined' ? window.location.origin : ''}?type=desktop&mode=receive&code=${desktopShare.connectionCode}`}
icon={Monitor}
iconColor="from-emerald-500 to-teal-500"
codeColor="from-purple-600 to-indigo-600"
title="房间码生成成功!"
subtitle="分享以下信息给观看方"
codeLabel="房间代码"
qrLabel="扫码观看"
copyButtonText="复制房间代码"
copyButtonColor="bg-purple-500 hover:bg-purple-600"
qrButtonText="使用手机扫码快速观看"
linkButtonText="复制链接"
onCopyCode={() => copyCode(desktopShare.connectionCode)}
onCopyLink={() => {
const link = `${window.location.origin}?type=desktop&mode=receive&code=${desktopShare.connectionCode}`;
navigator.clipboard.writeText(link);
showToast('观看链接已复制', 'success');
}}
/>
</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.isWaitingForPeer ? '是' : '否'}</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/toast-simple';
import { Upload, FileText, Image, Video, Music, Archive, X } from 'lucide-react';
import QRCodeDisplay from '@/components/QRCodeDisplay';
import RoomInfoDisplay from '@/components/RoomInfoDisplay';
interface FileInfo {
id: string;
@@ -397,80 +398,24 @@ export function WebRTCFileUpload({
</div>
{/* 取件码展示 */}
{pickupCode && (
<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">
<FileText 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-emerald-600 to-teal-600 bg-clip-text text-transparent tracking-wider">
{pickupCode}
</div>
</div>
<Button
onClick={onCopyCode}
className="w-full px-4 py-2.5 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg font-medium shadow transition-all duration-200 mt-3"
>
</Button>
</div>
{/* 分隔线 - 大屏幕显示竖线,移动端隐藏 */}
<div className="hidden lg:block w-px bg-slate-200 h-64 mt-6"></div>
{/* 右侧:二维码 */}
{pickupLink && (
<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={pickupLink}
size={120}
title=""
className="w-auto"
/>
</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>
{/* 底部:取件链接 */}
{pickupLink && (
<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">
{pickupLink}
</div>
</div>
<Button
onClick={onCopyLink}
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>
{pickupCode && pickupLink && (
<RoomInfoDisplay
code={pickupCode}
link={pickupLink}
icon={FileText}
iconColor="from-emerald-500 to-teal-500"
codeColor="from-emerald-600 to-teal-600"
title="取件码生成成功!"
subtitle="分享以下信息给接收方"
codeLabel="取件码"
qrLabel="扫码传输"
copyButtonText="复制取件码"
copyButtonColor="bg-emerald-500 hover:bg-emerald-600"
qrButtonText="使用手机扫码快速访问"
linkButtonText="复制链接"
onCopyCode={onCopyCode || (() => {})}
onCopyLink={onCopyLink || (() => {})}
/>
)}
</div>
);

View File

@@ -8,6 +8,7 @@ import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/toast-simple';
import { MessageSquare, Image, Send, Copy } from 'lucide-react';
import QRCodeDisplay from '@/components/QRCodeDisplay';
import RoomInfoDisplay from '@/components/RoomInfoDisplay';
interface WebRTCTextSenderProps {
onRestart?: () => void;
@@ -471,74 +472,24 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
</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">
<MessageSquare 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-emerald-600 to-teal-600 bg-clip-text text-transparent tracking-wider">
{pickupCode}
</div>
</div>
<Button
onClick={copyCode}
className="w-full px-4 py-2.5 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg font-medium shadow transition-all duration-200 mt-3"
>
</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={pickupLink}
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">
{pickupLink}
</div>
</div>
<Button
onClick={copyShareLink}
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>
{/* 取件码显示 */}
<RoomInfoDisplay
code={pickupCode}
link={pickupLink}
icon={MessageSquare}
iconColor="from-emerald-500 to-teal-500"
codeColor="from-emerald-600 to-teal-600"
title="取件码生成成功!"
subtitle="分享以下信息给接收方"
codeLabel="取件码"
qrLabel="扫码传输"
copyButtonText="复制取件码"
copyButtonColor="bg-emerald-500 hover:bg-emerald-600"
qrButtonText="使用手机扫码快速访问"
linkButtonText="复制链接"
onCopyCode={copyCode}
onCopyLink={copyShareLink}
/>
</div>
)}

View File

@@ -29,6 +29,37 @@ export function useDesktopShareBusiness() {
setState(prev => ({ ...prev, ...updates }));
}, []);
// 处理远程流
const handleRemoteStream = useCallback((stream: MediaStream) => {
console.log('[DesktopShare] 收到远程流:', stream.getTracks().length, '个轨道');
updateState({ remoteStream: stream });
// 如果有视频元素引用,设置流
if (remoteVideoRef.current) {
remoteVideoRef.current.srcObject = stream;
}
}, [updateState]);
// 设置远程轨道处理器(始终监听)
useEffect(() => {
console.log('[DesktopShare] 🎧 设置远程轨道处理器');
webRTC.onTrack((event: RTCTrackEvent) => {
console.log('[DesktopShare] 🎥 收到远程轨道:', event.track.kind, event.track.id);
console.log('[DesktopShare] 远程流数量:', event.streams.length);
if (event.streams.length > 0) {
const remoteStream = event.streams[0];
console.log('[DesktopShare] 🎬 设置远程流,轨道数量:', remoteStream.getTracks().length);
remoteStream.getTracks().forEach(track => {
console.log('[DesktopShare] 远程轨道:', track.kind, track.id, track.enabled, track.readyState);
});
handleRemoteStream(remoteStream);
} else {
console.warn('[DesktopShare] ⚠️ 收到轨道但没有关联的流');
}
});
}, [webRTC, handleRemoteStream]);
// 生成6位房间代码
const generateRoomCode = useCallback(() => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
@@ -141,17 +172,6 @@ export function useDesktopShareBusiness() {
};
}, [webRTC]);
// 处理远程流
const handleRemoteStream = useCallback((stream: MediaStream) => {
console.log('[DesktopShare] 收到远程流:', stream.getTracks().length, '个轨道');
updateState({ remoteStream: stream });
// 如果有视频元素引用,设置流
if (remoteVideoRef.current) {
remoteVideoRef.current.srcObject = stream;
}
}, [updateState]);
// 创建房间(只建立连接,等待对方加入)
const createRoom = useCallback(async (): Promise<string> => {
try {
@@ -313,21 +333,6 @@ export function useDesktopShareBusiness() {
console.log('[DesktopShare] ⏳ 等待连接稳定...');
await new Promise(resolve => setTimeout(resolve, 1000));
// 设置远程流处理 - 在连接建立后设置
console.log('[DesktopShare] 📡 设置远程流处理器...');
webRTC.onTrack((event: RTCTrackEvent) => {
console.log('[DesktopShare] 🎥 收到远程轨道:', event.track.kind, event.track.id);
console.log('[DesktopShare] 远程流数量:', event.streams.length);
if (event.streams.length > 0) {
const remoteStream = event.streams[0];
console.log('[DesktopShare] 🎬 设置远程流,轨道数量:', remoteStream.getTracks().length);
handleRemoteStream(remoteStream);
} else {
console.warn('[DesktopShare] ⚠️ 收到轨道但没有关联的流');
}
});
updateState({ isViewing: true });
console.log('[DesktopShare] 👁️ 已进入桌面共享观看模式,等待接收流...');
} catch (error) {
@@ -336,7 +341,7 @@ export function useDesktopShareBusiness() {
updateState({ error: errorMessage, isViewing: false });
throw error;
}
}, [webRTC, handleRemoteStream, updateState]);
}, [webRTC, updateState]);
// 停止观看桌面共享
const stopViewing = useCallback(async (): Promise<void> => {