feat:状态组件同步,UI细节处理

This commit is contained in:
MatrixSeven
2025-08-15 19:24:55 +08:00
parent 2abf7bdf42
commit 720f808ed6
16 changed files with 717 additions and 677 deletions

View File

@@ -0,0 +1,263 @@
import React, { useEffect, useState, useMemo } from 'react';
import { cn } from '@/lib/utils';
import { useWebRTCStore } from '@/hooks/webrtc/webRTCStore';
interface ConnectionStatusProps {
// 房间信息 - 只需要这个基本信息
currentRoom?: { code: string; role: 'sender' | 'receiver' } | null;
// 样式类名
className?: string;
// 紧凑模式
compact?: boolean;
// 内联模式 - 只返回状态文本不包含UI结构
inline?: boolean;
}
// 连接状态枚举
const getConnectionStatus = (connection: any, currentRoom: any) => {
const isWebSocketConnected = connection?.isWebSocketConnected || false;
const isPeerConnected = connection?.isPeerConnected || false;
const isConnecting = connection?.isConnecting || false;
const error = connection?.error || null;
if (error) {
return {
type: 'error' as const,
message: '连接失败',
detail: error,
};
}
if (isConnecting) {
return {
type: 'connecting' as const,
message: '正在连接',
detail: '建立房间连接中...',
};
}
if (!currentRoom) {
return {
type: 'disconnected' as const,
message: '未连接',
detail: '尚未创建房间',
};
}
// 如果有房间信息但WebSocket未连接且不是正在连接状态
// 可能是状态更新的时序问题,显示连接中状态
if (!isWebSocketConnected && !isConnecting) {
return {
type: 'connecting' as const,
message: '连接中',
detail: '正在建立WebSocket连接...',
};
}
if (isWebSocketConnected && !isPeerConnected) {
return {
type: 'room-ready' as const,
message: '房间已创建',
detail: '等待对方加入并建立P2P连接...',
};
}
if (isWebSocketConnected && isPeerConnected) {
return {
type: 'connected' as const,
message: 'P2P连接成功',
detail: '可以开始传输',
};
}
return {
type: 'unknown' as const,
message: '状态未知',
detail: '',
};
};
// 状态颜色映射
const getStatusColor = (type: string) => {
switch (type) {
case 'connected':
return 'text-green-600';
case 'connecting':
case 'room-ready':
return 'text-yellow-600';
case 'error':
return 'text-red-600';
case 'disconnected':
case 'unknown':
default:
return 'text-gray-500';
}
};
// 状态图标
const StatusIcon = ({ type, className = 'w-3 h-3' }: { type: string; className?: string }) => {
const iconClass = cn('inline-block', className);
switch (type) {
case 'connected':
return <div className={cn(iconClass, 'bg-green-500 rounded-full')} />;
case 'connecting':
case 'room-ready':
return (
<div className={cn(iconClass, 'bg-yellow-500 rounded-full animate-pulse')} />
);
case 'error':
return <div className={cn(iconClass, 'bg-red-500 rounded-full')} />;
case 'disconnected':
case 'unknown':
default:
return <div className={cn(iconClass, 'bg-gray-400 rounded-full')} />;
}
};
// 获取连接状态文字描述
const getConnectionStatusText = (connection: any) => {
const isWebSocketConnected = connection?.isWebSocketConnected || false;
const isPeerConnected = connection?.isPeerConnected || false;
const isConnecting = connection?.isConnecting || false;
const error = connection?.error || null;
const wsStatus = isWebSocketConnected ? 'WS已连接' : 'WS未连接';
const rtcStatus = isPeerConnected ? 'RTC已连接' :
isWebSocketConnected ? 'RTC等待连接' : 'RTC未连接';
if (error) {
return `${wsStatus} ${rtcStatus} - 连接失败`;
}
if (isConnecting) {
return `${wsStatus} ${rtcStatus} - 连接中`;
}
if (isPeerConnected) {
return `${wsStatus} ${rtcStatus} - P2P连接成功`;
}
return `${wsStatus} ${rtcStatus}`;
};
export function ConnectionStatus(props: ConnectionStatusProps) {
const { currentRoom, className, compact = false, inline = false } = props;
// 使用全局WebRTC状态
const webrtcState = useWebRTCStore();
// 创建connection对象以兼容现有代码
const connection = {
isWebSocketConnected: webrtcState.isWebSocketConnected,
isPeerConnected: webrtcState.isPeerConnected,
isConnecting: webrtcState.isConnecting,
error: webrtcState.error,
};
// 如果是内联模式,只返回状态文字
if (inline) {
return <span className={cn('text-sm text-slate-600', className)}>{getConnectionStatusText(connection)}</span>;
}
const status = getConnectionStatus(connection, currentRoom);
if (compact) {
return (
<div className={cn('flex items-center', className)}>
{/* 竖线分割 */}
<div className="w-px h-12 bg-slate-200 mx-4"></div>
{/* 连接状态指示器 */}
<div className="flex items-center gap-3 text-sm">
<div className="flex items-center gap-1.5">
<StatusIcon
type={connection.isWebSocketConnected ? 'connected' : 'disconnected'}
className="w-2.5 h-2.5"
/>
<span className="text-sm text-slate-600 font-medium">WS</span>
</div>
<span className="text-slate-300 font-medium">|</span>
<div className="flex items-center gap-1.5">
<StatusIcon
type={connection.isPeerConnected ? 'connected' : 'disconnected'}
className="w-2.5 h-2.5"
/>
<span className="text-sm text-slate-600 font-medium">RTC</span>
</div>
</div>
</div>
);
}
return (
<div className={cn(className)}>
<div className="space-y-2">
{/* 主要状态 */}
<div className={cn('font-medium text-sm', getStatusColor(status.type))}>
{status.message}
</div>
<div className="text-xs text-muted-foreground">
{status.detail}
</div>
{/* 详细连接状态 */}
<div className="flex items-center gap-4 text-xs">
<div className="flex items-center gap-2">
<span className="text-slate-500 font-medium">WS</span>
<StatusIcon
type={connection.isWebSocketConnected ? 'connected' : 'disconnected'}
className="w-2.5 h-2.5"
/>
<span className={cn(
connection.isWebSocketConnected ? 'text-green-600' : 'text-slate-500'
)}>
{connection.isWebSocketConnected ? '已连接' : '未连接'}
</span>
</div>
<span className="text-slate-300">|</span>
<div className="flex items-center gap-2">
<span className="text-slate-500 font-medium">RTC</span>
<StatusIcon
type={connection.isPeerConnected ? 'connected' : 'disconnected'}
className="w-2.5 h-2.5"
/>
<span className={cn(
connection.isPeerConnected ? 'text-green-600' : 'text-slate-500'
)}>
{connection.isPeerConnected ? '已连接' : '未连接'}
</span>
</div>
</div>
{/* 错误信息
{connection.error && (
<div className="text-xs text-red-600 bg-red-50 rounded p-2">
{connection.error}
</div>
)} */}
</div>
</div>
);
}
// 简化版本的 Hook用于快速集成 - 现在已经不需要了,但保留兼容性
export function useConnectionStatus(webrtcConnection?: any) {
// 这个hook现在不再需要因为ConnectionStatus组件直接使用底层连接
// 但为了向后兼容,保留这个接口
return useMemo(() => ({
isWebSocketConnected: webrtcConnection?.isWebSocketConnected || false,
isPeerConnected: webrtcConnection?.isPeerConnected || false,
isConnecting: webrtcConnection?.isConnecting || false,
currentRoom: webrtcConnection?.currentRoom || null,
error: webrtcConnection?.error || null,
}), [
webrtcConnection?.isWebSocketConnected,
webrtcConnection?.isPeerConnected,
webrtcConnection?.isConnecting,
webrtcConnection?.currentRoom,
webrtcConnection?.error,
]);
}

View File

@@ -6,6 +6,8 @@ import { Button } from '@/components/ui/button';
import { Share, Monitor } from 'lucide-react';
import WebRTCDesktopReceiver from '@/components/webrtc/WebRTCDesktopReceiver';
import WebRTCDesktopSender from '@/components/webrtc/WebRTCDesktopSender';
import { ConnectionStatus } from '@/components/ConnectionStatus';
import { useWebRTCStore } from '@/hooks/webrtc/webRTCStore';
interface DesktopShareProps {
@@ -23,6 +25,9 @@ export default function DesktopShare({
const searchParams = useSearchParams();
const router = useRouter();
const [mode, setMode] = useState<'share' | 'view'>('share');
// 使用全局WebRTC状态
const webrtcState = useWebRTCStore();
// 从URL参数中获取初始模式和房间代码
useEffect(() => {
@@ -67,6 +72,12 @@ export default function DesktopShare({
return '';
}, [searchParams]);
// 连接状态变化处理 - 现在不需要了,因为使用全局状态
const handleConnectionChange = useCallback((connection: any) => {
// 这个函数现在可能不需要了,但为了兼容现有的子组件接口,保留它
console.log('桌面共享连接状态变化:', connection);
}, []);
return (
<div className="space-y-4 sm:space-y-6">
{/* 模式选择器 */}
@@ -92,13 +103,16 @@ export default function DesktopShare({
</div>
{/* 根据模式渲染对应的组件 */}
{mode === 'share' ? (
<WebRTCDesktopSender />
) : (
<WebRTCDesktopReceiver
initialCode={getInitialCode()}
/>
)}
<div>
{mode === 'share' ? (
<WebRTCDesktopSender onConnectionChange={handleConnectionChange} />
) : (
<WebRTCDesktopReceiver
initialCode={getInitialCode()}
onConnectionChange={handleConnectionChange}
/>
)}
</div>
</div>
);
}

View File

@@ -332,12 +332,12 @@ export const WebRTCFileTransfer: React.FC = () => {
}
const code = data.code;
setPickupCode(code);
console.log('房间创建成功,取件码:', code);
// 连接WebRTC作为发送方
connect(code, 'sender');
// 连接WebRTC作为发送方,再设置取件码
// 这样可以确保UI状态与连接状态同步
await connect(code, 'sender');
setPickupCode(code);
showToast(`房间创建成功,取件码: ${code}`, "success");
} catch (error) {
@@ -846,6 +846,8 @@ export const WebRTCFileTransfer: React.FC = () => {
{mode === 'send' ? (
<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">
{/* 连接状态显示 */}
<WebRTCFileUpload
selectedFiles={selectedFiles}
fileList={fileList}
@@ -860,12 +862,12 @@ export const WebRTCFileTransfer: React.FC = () => {
onClearFiles={clearFiles}
onReset={resetRoom}
disabled={!!currentTransferFile}
isConnected={isConnected}
isWebSocketConnected={isWebSocketConnected}
/>
</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">
<WebRTCFileReceive
onJoinRoom={joinRoom}
files={fileList}

View File

@@ -1,11 +1,12 @@
"use client";
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Send, Download, X } from 'lucide-react';
import { WebRTCTextSender } from '@/components/webrtc/WebRTCTextSender';
import { WebRTCTextReceiver } from '@/components/webrtc/WebRTCTextReceiver';
import { useWebRTCStore } from '@/hooks/webrtc/webRTCStore';
export const WebRTCTextImageTransfer: React.FC = () => {
const searchParams = useSearchParams();
@@ -15,6 +16,9 @@ export const WebRTCTextImageTransfer: React.FC = () => {
const [mode, setMode] = useState<'send' | 'receive'>('send');
const [hasProcessedInitialUrl, setHasProcessedInitialUrl] = useState(false);
const [previewImage, setPreviewImage] = useState<string | null>(null);
// 使用全局WebRTC状态
const webrtcState = useWebRTCStore();
// 从URL参数中获取初始模式
useEffect(() => {
@@ -32,7 +36,7 @@ export const WebRTCTextImageTransfer: React.FC = () => {
}, [searchParams, hasProcessedInitialUrl]);
// 更新URL参数
const updateMode = (newMode: 'send' | 'receive') => {
const updateMode = useCallback((newMode: 'send' | 'receive') => {
console.log('=== 切换模式 ===', newMode);
setMode(newMode);
@@ -45,10 +49,10 @@ export const WebRTCTextImageTransfer: React.FC = () => {
}
router.push(`?${params.toString()}`, { scroll: false });
};
}, [searchParams, router]);
// 重新开始函数
const handleRestart = () => {
const handleRestart = useCallback(() => {
setPreviewImage(null);
const params = new URLSearchParams(searchParams.toString());
@@ -56,10 +60,21 @@ export const WebRTCTextImageTransfer: React.FC = () => {
params.set('mode', mode);
params.delete('code');
router.push(`?${params.toString()}`, { scroll: false });
};
}, [searchParams, mode, router]);
const code = searchParams.get('code') || '';
// 连接状态变化处理 - 现在不需要了,因为使用全局状态
const handleConnectionChange = useCallback((connection: any) => {
// 这个函数现在可能不需要了,但为了兼容现有的子组件接口,保留它
console.log('连接状态变化:', connection);
}, []);
// 关闭图片预览
const closePreview = useCallback(() => {
setPreviewImage(null);
}, []);
return (
<div className="space-y-4 sm:space-y-6">
{/* 模式切换 */}
@@ -85,24 +100,31 @@ export const WebRTCTextImageTransfer: React.FC = () => {
</div>
<div className="bg-white/80 backdrop-blur-sm rounded-xl shadow-lg p-4 sm:p-6 animate-fade-in-up">
{mode === 'send' ? (
<WebRTCTextSender onRestart={handleRestart} onPreviewImage={setPreviewImage} />
<WebRTCTextSender
onRestart={handleRestart}
onPreviewImage={setPreviewImage}
onConnectionChange={handleConnectionChange}
/>
) : (
<WebRTCTextReceiver
initialCode={code}
onPreviewImage={setPreviewImage}
onRestart={handleRestart}
onConnectionChange={handleConnectionChange}
/>
)}
</div>
{/* 图片预览模态框 */}
{previewImage && (
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50" onClick={() => setPreviewImage(null)}>
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50" onClick={closePreview}>
<div className="relative max-w-4xl max-h-4xl">
<img src={previewImage} alt="预览" className="max-w-full max-h-full" />
<Button
onClick={() => setPreviewImage(null)}
onClick={closePreview}
className="absolute top-4 right-4 bg-white text-black hover:bg-gray-200"
size="sm"
>

View File

@@ -1,19 +1,21 @@
"use client";
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useEffect } 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';
import { ConnectionStatus } from '@/components/ConnectionStatus';
interface WebRTCDesktopReceiverProps {
className?: string;
initialCode?: string; // 支持从URL参数传入的房间代码
onConnectionChange?: (connection: any) => void;
}
export default function WebRTCDesktopReceiver({ className, initialCode }: WebRTCDesktopReceiverProps) {
export default function WebRTCDesktopReceiver({ className, initialCode, onConnectionChange }: WebRTCDesktopReceiverProps) {
const [inputCode, setInputCode] = useState(initialCode || '');
const [isLoading, setIsLoading] = useState(false);
const [showDebug, setShowDebug] = useState(false);
@@ -23,6 +25,13 @@ export default function WebRTCDesktopReceiver({ className, initialCode }: WebRTC
// 使用桌面共享业务逻辑
const desktopShare = useDesktopShareBusiness();
// 通知父组件连接状态变化
useEffect(() => {
if (onConnectionChange && desktopShare.webRTCConnection) {
onConnectionChange(desktopShare.webRTCConnection);
}
}, [onConnectionChange, desktopShare.isWebSocketConnected, desktopShare.isPeerConnected, desktopShare.isConnecting]);
// 加入观看
const handleJoinViewing = useCallback(async () => {
if (!inputCode.trim()) {
@@ -108,8 +117,8 @@ export default function WebRTCDesktopReceiver({ className, initialCode }: WebRTC
{!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="flex items-center justify-between mb-6 sm:mb-8">
<div className="flex items-center space-x-3">
<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>
@@ -118,6 +127,10 @@ export default function WebRTCDesktopReceiver({ className, initialCode }: WebRTC
<p className="text-sm text-slate-600">6</p>
</div>
</div>
<ConnectionStatus
currentRoom={desktopShare.connectionCode ? { code: desktopShare.connectionCode, role: 'receiver' } : null}
/>
</div>
<form onSubmit={(e) => { e.preventDefault(); handleJoinViewing(); }} className="space-y-4 sm:space-y-6">
@@ -161,24 +174,21 @@ export default function WebRTCDesktopReceiver({ className, initialCode }: WebRTC
) : (
// 已连接,显示桌面观看界面
<div className="space-y-6">
<div className="flex items-center mb-6">
<div className="flex items-center space-x-3 flex-1">
<div className="flex items-center justify-between 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-500">
<span className="text-emerald-600"> </span>
</p>
<p className="text-sm text-slate-600">: {inputCode}</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>
{/* 连接状态 */}
<ConnectionStatus
currentRoom={{ code: inputCode, role: 'receiver' }}
/>
</div>
{/* 观看中的控制面板 */}
@@ -226,49 +236,6 @@ export default function WebRTCDesktopReceiver({ className, initialCode }: WebRTC
)}
</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

@@ -1,25 +1,32 @@
"use client";
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useEffect } 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';
import { ConnectionStatus } from '@/components/ConnectionStatus';
interface WebRTCDesktopSenderProps {
className?: string;
onConnectionChange?: (connection: any) => void;
}
export default function WebRTCDesktopSender({ className }: WebRTCDesktopSenderProps) {
export default function WebRTCDesktopSender({ className, onConnectionChange }: WebRTCDesktopSenderProps) {
const [isLoading, setIsLoading] = useState(false);
const [showDebug, setShowDebug] = useState(false);
const { showToast } = useToast();
// 使用桌面共享业务逻辑
const desktopShare = useDesktopShareBusiness();
// 通知父组件连接状态变化
useEffect(() => {
if (onConnectionChange && desktopShare.webRTCConnection) {
onConnectionChange(desktopShare.webRTCConnection);
}
}, [onConnectionChange, desktopShare.isWebSocketConnected, desktopShare.isPeerConnected, desktopShare.isConnecting]);
// 复制房间代码
const copyCode = useCallback(async (code: string) => {
try {
@@ -114,8 +121,8 @@ export default function WebRTCDesktopSender({ className }: WebRTCDesktopSenderPr
// 创建房间前的界面
<div className="space-y-6">
{/* 功能标题和状态 */}
<div className="flex items-center mb-6">
<div className="flex items-center space-x-3 flex-1">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-3">
<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>
@@ -125,36 +132,16 @@ export default function WebRTCDesktopSender({ className }: WebRTCDesktopSenderPr
</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>
<ConnectionStatus
currentRoom={desktopShare.connectionCode ? { code: desktopShare.connectionCode, role: 'sender' } : null}
/>
</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>
<h3 className="text-lg font-semibold text-slate-800 mb-4"></h3>
<p className="text-slate-600 mb-8"></p>
<Button
@@ -180,44 +167,20 @@ export default function WebRTCDesktopSender({ className }: WebRTCDesktopSenderPr
// 房间已创建,显示取件码和等待界面
<div className="space-y-6">
{/* 功能标题和状态 */}
<div className="flex items-center mb-6">
<div className="flex items-center space-x-3 flex-1">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-3">
<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>
<p className="text-sm text-slate-600">: {desktopShare.connectionCode}</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>
<ConnectionStatus
currentRoom={{ code: desktopShare.connectionCode, role: 'sender' }}
/>
</div>
{/* 桌面共享控制区域 */}
@@ -322,32 +285,6 @@ export default function WebRTCDesktopSender({ className }: WebRTCDesktopSenderPr
)}
</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 { Input } from '@/components/ui/input';
import { Download, FileText, Image, Video, Music, Archive } from 'lucide-react';
import { useToast } from '@/components/ui/toast-simple';
import { ConnectionStatus } from '@/components/ConnectionStatus';
interface FileInfo {
id: string;
@@ -133,63 +134,31 @@ export function WebRTCFileReceive({
return (
<div>
{/* 功能标题和状态 */}
<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-blue-500 to-indigo-500 rounded-xl flex items-center justify-center">
<Download 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">
{isConnected ? '已连接到房间,等待发送方选择文件...' : '正在连接到房间...'}
</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">
{isWebSocketConnected ? (
<>
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
<span className="text-emerald-600">WS</span>
</>
) : (
<>
<div className="w-2 h-2 rounded-full bg-orange-500 animate-pulse"></div>
<span className="text-orange-600">WS</span>
</>
)}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-cyan-500 rounded-xl flex items-center justify-center">
<Download className="w-5 h-5 text-white" />
</div>
{/* 分隔符 */}
<div className="text-slate-300">|</div>
{/* WebRTC状态 */}
<div className="flex items-center space-x-1">
{isConnected ? (
<>
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
<span className="text-emerald-600">RTC</span>
</>
) : (
<>
<div className="w-2 h-2 rounded-full bg-orange-500 animate-pulse"></div>
<span className="text-orange-600">RTC</span>
</>
)}
<div>
<h3 className="text-lg font-semibold text-slate-800"></h3>
<p className="text-sm text-slate-600">: {pickupCode}</p>
</div>
</div>
</div>
</div>
<div className="text-center">
<div className="flex items-center space-x-4">
<ConnectionStatus
currentRoom={pickupCode ? { code: pickupCode, role: 'receiver' } : null}
/>
<Button
onClick={onReset}
variant="outline"
className="text-slate-600 hover:text-slate-800 border-slate-200 hover:border-slate-300"
>
</Button>
</div>
</div> <div className="text-center">
{/* 连接状态指示器 */}
<div className="flex items-center justify-center space-x-4 mb-6">
<div className="flex items-center">
@@ -226,67 +195,22 @@ export function WebRTCFileReceive({
return (
<div className="space-y-4 sm:space-y-6">
{/* 功能标题和状态 */}
<div className="flex items-center">
<div className="flex items-center space-x-3 flex-1">
<div className="flex items-center justify-between 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">
<Download 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">
{isConnected ? (
<span className="text-emerald-600"> </span>
) : (
<span className="text-amber-600"> ...</span>
)}
</p>
<p className="text-sm text-slate-600">: {pickupCode}</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">
{isWebSocketConnected ? (
<>
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
<span className="text-emerald-600">WS</span>
</>
) : (
<>
<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">
{isConnected ? (
<>
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
<span className="text-emerald-600">RTC</span>
</>
) : (
<>
<div className="w-2 h-2 rounded-full bg-orange-500 animate-pulse"></div>
<span className="text-orange-600">RTC</span>
</>
)}
</div>
</div>
<div className="mt-1 text-xs text-slate-400">
{files.length}
</div>
</div>
{/* 连接状态 */}
<ConnectionStatus
currentRoom={{ code: pickupCode, role: 'receiver' }}
/>
</div>
<div>
@@ -371,8 +295,8 @@ export function WebRTCFileReceive({
return (
<div>
{/* 功能标题和状态 */}
<div className="flex items-center mb-6 sm:mb-8">
<div className="flex items-center space-x-3 flex-1">
<div className="flex items-center justify-between mb-6 sm:mb-8">
<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">
<Download className="w-5 h-5 text-white" />
</div>
@@ -382,57 +306,10 @@ export function WebRTCFileReceive({
</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">
{isConnecting ? (
<>
<div className="w-2 h-2 rounded-full bg-orange-500 animate-pulse"></div>
<span className="text-orange-600">WS</span>
</>
) : isWebSocketConnected ? (
<>
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
<span className="text-emerald-600">WS</span>
</>
) : (
<>
<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">
{isConnected ? (
<>
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
<span className="text-emerald-600">RTC</span>
</>
) : isConnecting ? (
<>
<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>
</div>
{/* 连接状态 */}
<ConnectionStatus
currentRoom={null}
/>
</div>
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6">

View File

@@ -2,10 +2,10 @@
import React, { useState, useRef, useCallback } from 'react';
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';
import { ConnectionStatus } from '@/components/ConnectionStatus';
interface FileInfo {
id: string;
@@ -46,8 +46,6 @@ interface WebRTCFileUploadProps {
onClearFiles?: () => void;
onReset?: () => void;
disabled?: boolean;
isConnected?: boolean;
isWebSocketConnected?: boolean;
}
export function WebRTCFileUpload({
@@ -63,9 +61,7 @@ export function WebRTCFileUpload({
onRemoveFile,
onClearFiles,
onReset,
disabled = false,
isConnected = false,
isWebSocketConnected = false
disabled = false
}: WebRTCFileUploadProps) {
const [isDragOver, setIsDragOver] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -116,9 +112,9 @@ export function WebRTCFileUpload({
if (selectedFiles.length === 0 && !pickupCode) {
return (
<div className="space-y-6">
{/* 功能标题和状态 */}
<div className="flex items-center">
<div className="flex items-center space-x-3 flex-1">
{/* 功能标题 */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-indigo-500 rounded-xl flex items-center justify-center">
<Upload className="w-5 h-5 text-white" />
</div>
@@ -128,29 +124,9 @@ export function WebRTCFileUpload({
</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">
<div className="w-2 h-2 rounded-full bg-slate-400"></div>
<span className="text-slate-600">RTC</span>
</div>
</div>
</div>
<ConnectionStatus
currentRoom={null}
/>
</div>
<div
@@ -198,63 +174,22 @@ export function WebRTCFileUpload({
{/* 文件列表 */}
<div>
{/* 功能标题和状态 */}
<div className="flex items-center mb-4 sm: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">
<div className="flex items-center justify-between 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 flex-shrink-0">
<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-500">{selectedFiles.length} </p>
<p className="text-sm text-slate-600">{selectedFiles.length} </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">
{isWebSocketConnected ? (
<>
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
<span className="text-emerald-600">WS</span>
</>
) : (
<>
<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">
{isConnected ? (
<>
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
<span className="text-emerald-600">RTC</span>
</>
) : pickupCode ? (
<>
<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>
</div>
{/* 使用 ConnectionStatus 组件 */}
<ConnectionStatus
currentRoom={pickupCode ? { code: pickupCode, role: 'sender' } : null}
/>
</div>
<div className="space-y-3 mb-4 sm:mb-6">

View File

@@ -8,17 +8,20 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useToast } from '@/components/ui/toast-simple';
import { MessageSquare, Image, Download } from 'lucide-react';
import { ConnectionStatus } from '@/components/ConnectionStatus';
interface WebRTCTextReceiverProps {
initialCode?: string;
onPreviewImage: (imageUrl: string) => void;
onRestart?: () => void;
onConnectionChange?: (connection: any) => void;
}
export const WebRTCTextReceiver: React.FC<WebRTCTextReceiverProps> = ({
initialCode = '',
onPreviewImage,
onRestart
onRestart,
onConnectionChange
}) => {
const { showToast } = useToast();
@@ -29,12 +32,13 @@ export const WebRTCTextReceiver: React.FC<WebRTCTextReceiverProps> = ({
const [receivedImages, setReceivedImages] = useState<Array<{ id: string, content: string, fileName?: string }>>([]);
const [isTyping, setIsTyping] = useState(false);
const [isValidating, setIsValidating] = useState(false);
// Ref用于防止重复自动连接
const hasTriedAutoConnect = useRef(false);
// 创建共享连接 [需要优化]
// 创建共享连接
const connection = useSharedWebRTCManager();
// 使用共享连接创建业务层
const textTransfer = useTextTransferBusiness(connection);
const fileTransfer = useFileTransferBusiness(connection);
@@ -42,116 +46,49 @@ export const WebRTCTextReceiver: React.FC<WebRTCTextReceiverProps> = ({
// 连接所有传输通道
const connectAll = useCallback(async (code: string, role: 'sender' | 'receiver') => {
console.log('=== 连接所有传输通道 ===', { code, role });
// 只需要连接一次,因为使用的是共享连接
await connection.connect(code, role);
// await Promise.all([
// textTransfer.connect(code, role),
// fileTransfer.connect(code, role)
// ]);
}, [textTransfer, fileTransfer]);
}, [connection]);
// 是否有任何连接
const hasAnyConnection = textTransfer.isConnected || fileTransfer.isConnected;
// 是否正在连接
const isAnyConnecting = textTransfer.isConnecting || fileTransfer.isConnecting;
// 通知父组件连接状态变化
useEffect(() => {
if (onConnectionChange) {
onConnectionChange(connection);
}
}, [onConnectionChange, connection.isConnected, connection.isConnecting, connection.isPeerConnected]);
// 是否有任何错误
const hasAnyError = textTransfer.connectionError || fileTransfer.connectionError;
// 监听连接错误并显示 toast
useEffect(() => {
if (hasAnyError) {
console.error('[WebRTCTextReceiver] 连接错误:', hasAnyError);
showToast(hasAnyError, 'error');
}
}, [hasAnyError, showToast]);
// 验证取件码是否存在
const validatePickupCode = async (code: string): Promise<boolean> => {
try {
setIsValidating(true);
console.log('开始验证取件码:', code);
const response = await fetch(`/api/room-info?code=${code}`);
const data = await response.json();
console.log('验证响应:', { status: response.status, data });
if (!response.ok || !data.success) {
const errorMessage = data.message || '取件码验证失败';
showToast(errorMessage, 'error');
console.log('验证失败:', errorMessage);
return false;
}
console.log('取件码验证成功:', data.room);
return true;
} catch (error) {
console.error('验证取件码时发生错误:', error);
const errorMessage = '网络错误,请检查连接后重试';
showToast(errorMessage, 'error');
return false;
} finally {
setIsValidating(false);
}
};
// 重新开始
const restart = () => {
setPickupCode('');
setInputCode('');
setReceivedText('');
setReceivedImages([]);
setIsTyping(false);
// 断开连接
// 清理接收的图片URL
receivedImages.forEach(img => {
if (img.content.startsWith('blob:')) {
URL.revokeObjectURL(img.content);
}
});
setReceivedImages([]);
// 断开连接(只需要断开一次)
connection.disconnect();
if (onRestart) {
onRestart();
}
};
// 加入房间
const joinRoom = useCallback(async (code: string) => {
const trimmedCode = code.trim().toUpperCase();
if (!trimmedCode || trimmedCode.length !== 6) {
showToast('请输入正确的6位取件码', "error");
return;
}
if (isAnyConnecting || isValidating) {
console.log('已经在连接中,跳过重复请求');
return;
}
if (hasAnyConnection) {
console.log('已经连接,跳过重复请求');
return;
}
try {
console.log('=== 开始验证和连接房间 ===', trimmedCode);
const isValid = await validatePickupCode(trimmedCode);
if (!isValid) {
return;
}
setPickupCode(trimmedCode);
await connectAll(trimmedCode, 'receiver');
console.log('=== 房间连接成功 ===', trimmedCode);
showToast(`成功加入消息房间: ${trimmedCode}`, "success");
} catch (error) {
console.error('加入房间失败:', error);
showToast(error instanceof Error ? error.message : '加入房间失败', "error");
setPickupCode('');
}
}, [isAnyConnecting, hasAnyConnection, connectAll, showToast, isValidating, validatePickupCode]);
// 监听实时文本同步
useEffect(() => {
const cleanup = textTransfer.onTextSync((text: string) => {
@@ -174,25 +111,66 @@ export const WebRTCTextReceiver: React.FC<WebRTCTextReceiverProps> = ({
useEffect(() => {
const cleanup = fileTransfer.onFileReceived((fileData) => {
if (fileData.file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const imageData = e.target?.result as string;
setReceivedImages(prev => [...prev, {
id: fileData.id,
content: imageData,
fileName: fileData.file.name
}]);
};
reader.readAsDataURL(fileData.file);
const imageUrl = URL.createObjectURL(fileData.file);
const imageId = Date.now().toString();
setReceivedImages(prev => [...prev, {
id: imageId,
content: imageUrl,
fileName: fileData.file.name
}]);
showToast(`收到图片: ${fileData.file.name}`, "success");
}
});
return cleanup;
}, [fileTransfer.onFileReceived]);
// 验证并加入房间
const joinRoom = useCallback(async (code: string) => {
if (!code || code.length !== 6) return;
setIsValidating(true);
try {
console.log('=== 开始加入房间 ===', code);
// 验证房间
const response = await fetch(`/api/room-info?code=${code}`);
const roomData = await response.json();
if (!response.ok) {
throw new Error(roomData.error || '房间不存在或已过期');
}
console.log('=== 房间验证成功 ===', roomData);
setPickupCode(code);
// 连接到房间
await connectAll(code, 'receiver');
} catch (error: any) {
console.error('加入房间失败:', error);
showToast(error.message || '加入房间失败', "error");
} finally {
setIsValidating(false);
}
}, [connectAll, showToast]);
// 复制文本到剪贴板
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
showToast('已复制到剪贴板', "success");
} catch (error) {
console.error('复制失败:', error);
showToast('复制失败', "error");
}
};
// 处理初始代码连接
useEffect(() => {
// initialCode isAutoConnected
console.log(`initialCode: ${initialCode}, hasTriedAutoConnect: ${hasTriedAutoConnect.current}`);
if (initialCode && initialCode.length === 6 && !hasTriedAutoConnect.current) {
console.log('=== 自动连接初始代码 ===', initialCode);
@@ -201,15 +179,15 @@ export const WebRTCTextReceiver: React.FC<WebRTCTextReceiverProps> = ({
joinRoom(initialCode);
return;
}
}, [initialCode]);
}, [initialCode, joinRoom]);
return (
<div className="space-y-6">
{!hasAnyConnection ? (
// 输入取件码界面
<div>
<div className="flex items-center mb-6 sm:mb-8">
<div className="flex items-center space-x-3 flex-1">
<div className="flex items-center justify-between mb-6 sm:mb-8">
<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">
<Download className="w-5 h-5 text-white" />
</div>
@@ -218,6 +196,12 @@ export const WebRTCTextReceiver: React.FC<WebRTCTextReceiverProps> = ({
<p className="text-sm text-slate-600">6</p>
</div>
</div>
<div className="text-left">
<ConnectionStatus
currentRoom={pickupCode ? { code: pickupCode, role: 'receiver' } : null}
/>
</div>
</div>
<form onSubmit={(e) => { e.preventDefault(); joinRoom(inputCode); }} className="space-y-4 sm:space-y-6">
@@ -266,94 +250,112 @@ export const WebRTCTextReceiver: React.FC<WebRTCTextReceiverProps> = ({
) : (
// 已连接,显示实时文本
<div className="space-y-6">
<div className="flex items-center mb-6">
<div className="flex items-center space-x-3 flex-1">
<div className="flex items-center justify-between 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-500">
<span className="text-emerald-600"> </span>
</p>
<p className="text-sm text-slate-600">: {pickupCode}</p>
</div>
</div>
</div>
<div className="flex items-center space-x-4">
<ConnectionStatus
{/* 连接成功状态 */}
<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">: {pickupCode}</p>
</div>
{/* 实时文本显示区域 */}
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-4 border border-slate-200">
<div className="flex items-center justify-between mb-3">
<h4 className="text-lg font-medium text-slate-800 flex items-center">
<MessageSquare className="w-5 h-5 mr-2" />
</h4>
<div className="flex items-center space-x-3 text-sm">
<span className="text-slate-500">
{receivedText.length} / 50,000
</span>
{textTransfer.isConnected && (
<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">WebRTC实时同步</span>
</div>
)}
</div>
</div>
<div className="relative">
<textarea
value={receivedText}
readOnly
placeholder="等待对方发送文字内容...&#10;&#10;💡 实时同步显示,对方的编辑会立即显示在这里"
className="w-full h-40 px-4 py-3 border border-slate-300 rounded-lg bg-slate-50 text-slate-700 placeholder-slate-400 resize-none"
currentRoom={pickupCode ? { code: pickupCode, role: 'receiver' } : null}
/>
{!receivedText && (
<div className="absolute inset-0 flex items-center justify-center bg-slate-50 rounded-lg border border-slate-300">
<div className="text-center">
<MessageSquare className="w-12 h-12 text-slate-400 mx-auto mb-4" />
<p className="text-slate-600">...</p>
<p className="text-sm text-slate-500 mt-2"></p>
</div>
</div>
<Button
onClick={restart}
variant="outline"
className="text-slate-600 hover:text-slate-800 border-slate-200 hover:border-slate-300"
>
</Button>
</div>
</div>
{/* 文本显示区域 */}
<div className="bg-white/90 backdrop-blur-sm border border-slate-200 rounded-2xl p-6 space-y-4">
<div className="flex items-center justify-between">
<h4 className="font-medium text-slate-800 flex items-center space-x-2">
<MessageSquare className="w-4 h-4" />
<span></span>
</h4>
{receivedText && (
<Button
onClick={() => copyToClipboard(receivedText)}
size="sm"
variant="ghost"
className="text-slate-600 hover:text-slate-800 h-8 px-3"
>
<span></span>
</Button>
)}
</div>
{/* 打字状态提示 */}
{isTyping && (
<div className="flex items-center space-x-2 mt-3 text-sm text-slate-500">
<div className="flex space-x-1">
{[...Array(3)].map((_, i) => (
<div
key={i}
className="w-1 h-1 bg-slate-400 rounded-full animate-bounce"
style={{ animationDelay: `${i * 0.1}s` }}
></div>
))}
<div className="min-h-[200px] bg-slate-50/50 rounded-xl p-4 border border-slate-100">
{receivedText ? (
<div className="space-y-2">
<pre className="whitespace-pre-wrap text-slate-700 text-sm leading-relaxed font-sans">
{receivedText}
</pre>
{isTyping && (
<div className="flex items-center space-x-2 text-slate-500 text-sm">
<div className="flex space-x-1">
<div className="w-1.5 h-1.5 bg-slate-400 rounded-full animate-bounce" style={{animationDelay: '0ms'}}></div>
<div className="w-1.5 h-1.5 bg-slate-400 rounded-full animate-bounce" style={{animationDelay: '150ms'}}></div>
<div className="w-1.5 h-1.5 bg-slate-400 rounded-full animate-bounce" style={{animationDelay: '300ms'}}></div>
</div>
<span>...</span>
</div>
)}
</div>
<span className="italic">...</span>
</div>
)}
) : (
<div className="flex flex-col items-center justify-center h-full text-slate-400 space-y-3">
<MessageSquare className="w-12 h-12 text-slate-300" />
<p className="text-center">
{connection.isPeerConnected ?
'等待对方发送文字内容...' :
'等待连接建立...'}
</p>
</div>
)}
</div>
</div>
{/* 接收到的图片 */}
{/* 图片显示区域 */}
{receivedImages.length > 0 && (
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-6 border border-slate-200">
<h4 className="text-lg font-semibold text-slate-800 mb-4"></h4>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
<div className="bg-white/90 backdrop-blur-sm border border-slate-200 rounded-2xl p-6 space-y-4">
<h4 className="font-medium text-slate-800 flex items-center space-x-2">
<Image className="w-4 h-4" />
<span> ({receivedImages.length})</span>
</h4>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
{receivedImages.map((image) => (
<img
<div
key={image.id}
src={image.content}
alt={image.fileName}
className="w-full h-32 object-cover rounded-lg border cursor-pointer hover:opacity-80 transition-opacity"
className="group relative aspect-square bg-slate-50 rounded-xl overflow-hidden border border-slate-200 hover:border-slate-300 transition-all duration-200 cursor-pointer"
onClick={() => onPreviewImage(image.content)}
/>
>
<img
src={image.content}
alt={image.fileName || '接收的图片'}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
loading="lazy"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-all duration-200 flex items-center justify-center">
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<div className="bg-white/90 rounded-lg px-3 py-1">
<span className="text-sm text-slate-700"></span>
</div>
</div>
</div>
</div>
))}
</div>
</div>
@@ -362,4 +364,4 @@ export const WebRTCTextReceiver: React.FC<WebRTCTextReceiverProps> = ({
)}
</div>
);
};
};

View File

@@ -7,15 +7,16 @@ import { useFileTransferBusiness } from '@/hooks/webrtc/useFileTransferBusiness'
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';
import { ConnectionStatus } from '@/components/ConnectionStatus';
interface WebRTCTextSenderProps {
onRestart?: () => void;
onPreviewImage?: (imageUrl: string) => void;
onConnectionChange?: (connection: any) => void;
}
export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, onPreviewImage }) => {
export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, onPreviewImage, onConnectionChange }) => {
const { showToast } = useToast();
// 状态管理
@@ -49,6 +50,13 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
// 是否正在连接
const isAnyConnecting = textTransfer.isConnecting || fileTransfer.isConnecting;
// 通知父组件连接状态变化
useEffect(() => {
if (onConnectionChange) {
onConnectionChange(connection);
}
}, [onConnectionChange, connection.isConnected, connection.isConnecting, connection.isPeerConnected]);
// 是否有任何错误
const hasAnyError = textTransfer.connectionError || fileTransfer.connectionError;
@@ -287,36 +295,17 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
</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 ${textTransfer.isWebSocketConnected ? 'bg-blue-500 animate-pulse' : 'bg-slate-400'}`}></div>
<span className={textTransfer.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 ${textTransfer.isConnected ? 'bg-emerald-500 animate-pulse' : 'bg-slate-400'}`}></div>
<span className={textTransfer.isConnected ? 'text-emerald-600' : 'text-slate-600'}>RTC</span>
</div>
</div>
</div>
{/* 连接状态 */}
<ConnectionStatus
currentRoom={pickupCode ? { code: pickupCode, role: 'sender' } : null}
/>
</div>
<div className="text-center py-12">
<div className="w-20 h-20 mx-auto mb-6 bg-gradient-to-br from-blue-100 to-indigo-100 rounded-full flex items-center justify-center">
<MessageSquare className="w-10 h-10 text-blue-500" />
</div>
<h3 className="text-xl font-semibold text-slate-800 mb-4"></h3>
<h3 className="text-lg font-semibold text-slate-800 mb-4"></h3>
<p className="text-slate-600 mb-8"></p>
<Button
@@ -342,45 +331,22 @@ export const WebRTCTextSender: React.FC<WebRTCTextSenderProps> = ({ onRestart, o
// 房间已创建,显示取件码和文本传输界面
<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-blue-500 to-indigo-500 rounded-xl flex items-center justify-center">
<MessageSquare 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">
{hasAnyConnection ? '实时编辑,对方可以同步看到' : '等待对方连接'}
</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 ${textTransfer.isWebSocketConnected ? 'bg-blue-500 animate-pulse' : 'bg-red-500'}`}></div>
<span className={textTransfer.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 ${textTransfer.isConnected ? 'bg-emerald-500 animate-pulse' : 'bg-orange-400'}`}></div>
<span className={textTransfer.isConnected ? 'text-emerald-600' : 'text-orange-600'}>RTC</span>
</div>
</div>
</div>
{/* 功能标题和状态 */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-gradient-to-br from-green-500 to-teal-500 rounded-xl flex items-center justify-center">
<MessageSquare 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>
<ConnectionStatus
currentRoom={pickupCode ? { code: pickupCode, role: 'sender' } : null}
/>
</div> {/* 文字编辑区域 - 移到最上面 */}
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-4 border border-slate-200">
<div className="flex items-center justify-between mb-3">
<h4 className="text-lg font-medium text-slate-800 flex items-center">

View File

@@ -408,5 +408,8 @@ export function useDesktopShareBusiness() {
// WebRTC连接状态
webRTCError: webRTC.error,
// 暴露WebRTC连接对象
webRTCConnection: webRTC,
};
}

View File

@@ -1,5 +1,6 @@
import { useState, useRef, useCallback } from 'react';
import { getWsUrl } from '@/lib/config';
import { useWebRTCStore } from './webRTCStore';
// 基础连接状态
interface WebRTCState {
@@ -60,13 +61,8 @@ export interface WebRTCConnection {
* 创建单一的 WebRTC 连接实例,供多个业务模块共享使用
*/
export function useSharedWebRTCManager(): WebRTCConnection {
const [state, setState] = useState<WebRTCState>({
isConnected: false,
isConnecting: false,
isWebSocketConnected: false,
isPeerConnected: false,
error: null,
});
// 使用全局状态 store
const webrtcStore = useWebRTCStore();
const wsRef = useRef<WebSocket | null>(null);
const pcRef = useRef<RTCPeerConnection | null>(null);
@@ -89,8 +85,8 @@ export function useSharedWebRTCManager(): WebRTCConnection {
];
const updateState = useCallback((updates: Partial<WebRTCState>) => {
setState(prev => ({ ...prev, ...updates }));
}, []);
webrtcStore.updateState(updates);
}, [webrtcStore]);
// 清理连接
const cleanup = useCallback(() => {
@@ -203,7 +199,7 @@ export function useSharedWebRTCManager(): WebRTCConnection {
console.log('[SharedWebRTC] 🚀 开始连接到房间:', roomCode, role);
// 如果正在连接中,避免重复连接
if (state.isConnecting) {
if (webrtcStore.isConnecting) {
console.warn('[SharedWebRTC] ⚠️ 正在连接中,跳过重复连接请求');
return;
}
@@ -211,6 +207,7 @@ export function useSharedWebRTCManager(): WebRTCConnection {
// 清理之前的连接
cleanup();
currentRoom.current = { code: roomCode, role };
webrtcStore.setCurrentRoom({ code: roomCode, role });
updateState({ isConnecting: true, error: null });
// 注意不在这里设置超时因为WebSocket连接很快
@@ -466,20 +463,14 @@ export function useSharedWebRTCManager(): WebRTCConnection {
isConnecting: false
});
}
}, [updateState, cleanup, createOffer, handleDataChannelMessage, state.isConnecting, state.isConnected]);
}, [updateState, cleanup, createOffer, handleDataChannelMessage, webrtcStore.isConnecting, webrtcStore.isConnected]);
// 断开连接
const disconnect = useCallback(() => {
console.log('[SharedWebRTC] 断开连接');
cleanup();
setState({
isConnected: false,
isConnecting: false,
isWebSocketConnected: false,
isPeerConnected: false,
error: null,
});
}, [cleanup]);
webrtcStore.reset();
}, [cleanup, webrtcStore]);
// 发送消息
const sendMessage = useCallback((message: WebRTCMessage, channel?: string) => {
@@ -549,8 +540,8 @@ export function useSharedWebRTCManager(): WebRTCConnection {
const isConnectedToRoom = useCallback((roomCode: string, role: 'sender' | 'receiver') => {
return currentRoom.current?.code === roomCode &&
currentRoom.current?.role === role &&
state.isConnected;
}, [state.isConnected]);
webrtcStore.isConnected;
}, [webrtcStore.isConnected]);
// 添加媒体轨道
const addTrack = useCallback((track: MediaStreamTrack, stream: MediaStream) => {
@@ -632,11 +623,11 @@ export function useSharedWebRTCManager(): WebRTCConnection {
return {
// 状态
isConnected: state.isConnected,
isConnecting: state.isConnecting,
isWebSocketConnected: state.isWebSocketConnected,
isPeerConnected: state.isPeerConnected,
error: state.error,
isConnected: webrtcStore.isConnected,
isConnecting: webrtcStore.isConnecting,
isWebSocketConnected: webrtcStore.isWebSocketConnected,
isPeerConnected: webrtcStore.isPeerConnected,
error: webrtcStore.error,
// 操作方法
connect,

View File

@@ -0,0 +1,41 @@
import { create } from 'zustand';
interface WebRTCState {
isConnected: boolean;
isConnecting: boolean;
isWebSocketConnected: boolean;
isPeerConnected: boolean;
error: string | null;
currentRoom: { code: string; role: 'sender' | 'receiver' } | null;
}
interface WebRTCStore extends WebRTCState {
updateState: (updates: Partial<WebRTCState>) => void;
setCurrentRoom: (room: { code: string; role: 'sender' | 'receiver' } | null) => void;
reset: () => void;
}
const initialState: WebRTCState = {
isConnected: false,
isConnecting: false,
isWebSocketConnected: false,
isPeerConnected: false,
error: null,
currentRoom: null,
};
export const useWebRTCStore = create<WebRTCStore>((set) => ({
...initialState,
updateState: (updates) => set((state) => ({
...state,
...updates,
})),
setCurrentRoom: (room) => set((state) => ({
...state,
currentRoom: room,
})),
reset: () => set(initialState),
}));