From 720f808ed64c7463ca1ed9db99f8880df73a7d4a Mon Sep 17 00:00:00 2001 From: MatrixSeven Date: Fri, 15 Aug 2025 19:24:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E7=8A=B6=E6=80=81=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=90=8C=E6=AD=A5,UI=E7=BB=86=E8=8A=82=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 22 +- chuan-next/package.json | 3 +- .../src/components/ConnectionStatus.tsx | 263 +++++++++++++ chuan-next/src/components/DesktopShare.tsx | 28 +- .../src/components/WebRTCFileTransfer.tsx | 14 +- .../components/WebRTCTextImageTransfer.tsx | 38 +- .../webrtc/WebRTCDesktopReceiver.tsx | 81 ++-- .../components/webrtc/WebRTCDesktopSender.tsx | 109 ++---- .../components/webrtc/WebRTCFileReceive.tsx | 197 ++-------- .../components/webrtc/WebRTCFileUpload.tsx | 101 +---- .../components/webrtc/WebRTCTextReceiver.tsx | 352 +++++++++--------- .../components/webrtc/WebRTCTextSender.tsx | 94 ++--- .../hooks/webrtc/useDesktopShareBusiness.ts | 3 + .../hooks/webrtc/useSharedWebRTCManager.ts | 43 +-- chuan-next/src/hooks/webrtc/webRTCStore.ts | 41 ++ chuan-next/yarn.lock | 5 + 16 files changed, 717 insertions(+), 677 deletions(-) create mode 100644 chuan-next/src/components/ConnectionStatus.tsx create mode 100644 chuan-next/src/hooks/webrtc/webRTCStore.ts diff --git a/README.md b/README.md index 7644417..4b04aa5 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,22 @@ ## ✨ 核心功能 - 📁 **文件传输** - 支持多文件同时传输,基于WebRTC的P2P直连 -- 📝 **文字传输** - 快速分享文本内容 -- 🖥️ **桌面共享** - 实时屏幕共享(开发中) +- 📝 **文字传输** - 快速分享文本内容 ✅ +- 🖥️ **桌面共享** - 实时屏幕共享 ✅ +- 🔗 **连接状态同步** - 实时连接状态UI同步 ✅ - 🔒 **端到端加密** - 数据传输安全,服务器不存储文件 - 📱 **响应式设计** - 完美适配手机、平板、电脑 - 🖥️ **多平台支持** - 支持linux/macos/win 单文件部署 + +## 🔄 开发进度 + +- ✅ 文件传输功能 +- ✅ 文字传输功能 +- ✅ 桌面共享功能 +- ✅ 连接状态UI同步 +- 🔄 大文件传输数据安全保证(进行中) +- 🔄 docker镜像发布(进行中) + ## 🚀 技术栈 **前端** - Next.js 15 + React 18 + TypeScript + Tailwind CSS @@ -38,8 +49,11 @@ cd file-transfer-go **发送文件** 1. 选择文件 → 生成取件码 → 分享6位码 -**接收文件** -1. 输入取件码 → 自动连接 → 下载文件 +**文字传输** +1. 输入文字内容 → 生成取件码 → 分享给对方 + +**桌面共享** +1. 点击共享桌面 → 生成取件码 → 对方输入码观看 ## 📊 项目架构 diff --git a/chuan-next/package.json b/chuan-next/package.json index bc30b4b..79388a2 100644 --- a/chuan-next/package.json +++ b/chuan-next/package.json @@ -35,7 +35,8 @@ "react": "19.1.0", "react-dom": "19.1.0", "tailwind-merge": "^3.3.1", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zustand": "^5.0.7" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/chuan-next/src/components/ConnectionStatus.tsx b/chuan-next/src/components/ConnectionStatus.tsx new file mode 100644 index 0000000..8f644fb --- /dev/null +++ b/chuan-next/src/components/ConnectionStatus.tsx @@ -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
; + case 'connecting': + case 'room-ready': + return ( +
+ ); + case 'error': + return
; + case 'disconnected': + case 'unknown': + default: + return
; + } +}; + +// 获取连接状态文字描述 +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 {getConnectionStatusText(connection)}; + } + + const status = getConnectionStatus(connection, currentRoom); + + if (compact) { + return ( +
+ {/* 竖线分割 */} +
+ + {/* 连接状态指示器 */} +
+
+ + WS +
+ | +
+ + RTC +
+
+
+ ); + } + + return ( +
+
+ {/* 主要状态 */} +
+ {status.message} +
+
+ {status.detail} +
+ + {/* 详细连接状态 */} +
+
+ WS + + + {connection.isWebSocketConnected ? '已连接' : '未连接'} + +
+ + | + +
+ RTC + + + {connection.isPeerConnected ? '已连接' : '未连接'} + +
+
+ + {/* 错误信息 + {connection.error && ( +
+ {connection.error} +
+ )} */} +
+
+ ); +} + +// 简化版本的 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, + ]); +} diff --git a/chuan-next/src/components/DesktopShare.tsx b/chuan-next/src/components/DesktopShare.tsx index dd9f815..f05c697 100644 --- a/chuan-next/src/components/DesktopShare.tsx +++ b/chuan-next/src/components/DesktopShare.tsx @@ -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 (
{/* 模式选择器 */} @@ -92,13 +103,16 @@ export default function DesktopShare({
{/* 根据模式渲染对应的组件 */} - {mode === 'share' ? ( - - ) : ( - - )} +
+ {mode === 'share' ? ( + + ) : ( + + )} +
); } diff --git a/chuan-next/src/components/WebRTCFileTransfer.tsx b/chuan-next/src/components/WebRTCFileTransfer.tsx index fe07ece..330fb21 100644 --- a/chuan-next/src/components/WebRTCFileTransfer.tsx +++ b/chuan-next/src/components/WebRTCFileTransfer.tsx @@ -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' ? (
+ {/* 连接状态显示 */} + { onClearFiles={clearFiles} onReset={resetRoom} disabled={!!currentTransferFile} - isConnected={isConnected} - isWebSocketConnected={isWebSocketConnected} />
) : (
+ + { 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(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 (
{/* 模式切换 */} @@ -85,24 +100,31 @@ export const WebRTCTextImageTransfer: React.FC = () => {
+ + {mode === 'send' ? ( - + ) : ( )}
{/* 图片预览模态框 */} {previewImage && ( -
setPreviewImage(null)}> +
预览 - - {showDebug && ( -
-
WebSocket连接: {desktopShare.isWebSocketConnected ? '✅' : '❌'}
-
P2P连接: {desktopShare.isPeerConnected ? '✅' : '❌'}
-
观看状态: {desktopShare.isViewing ? '观看中' : '未观看'}
-
远程流: {desktopShare.remoteStream ? '已接收' : '无'}
- {desktopShare.remoteStream && ( -
-
流轨道数量: {desktopShare.remoteStream.getTracks().length}
-
视频轨道: {desktopShare.remoteStream.getVideoTracks().length}
-
音频轨道: {desktopShare.remoteStream.getAudioTracks().length}
- {desktopShare.remoteStream.getVideoTracks().map((track, index) => ( -
- 视频轨道{index}: {track.readyState}, enabled: {track.enabled ? '是' : '否'} -
- ))} - {desktopShare.remoteStream.getAudioTracks().map((track, index) => ( -
- 音频轨道{index}: {track.readyState}, enabled: {track.enabled ? '是' : '否'} -
- ))} -
- )} -
- )} -
); } diff --git a/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx b/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx index dedbfc6..4742b37 100644 --- a/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx +++ b/chuan-next/src/components/webrtc/WebRTCDesktopSender.tsx @@ -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 // 创建房间前的界面
{/* 功能标题和状态 */} -
-
+
+
@@ -125,36 +132,16 @@ export default function WebRTCDesktopSender({ className }: WebRTCDesktopSenderPr
- {/* 竖线分割 */} -
- - {/* 状态显示 */} -
-
连接状态
-
- {/* WebSocket状态 */} -
-
- WS -
- - {/* 分隔符 */} -
|
- - {/* WebRTC状态 */} -
-
- RTC -
-
-
+
-

创建桌面共享房间

+

创建桌面共享房间

创建房间后将生成分享码,等待接收方加入后即可开始桌面共享

- - {showDebug && ( -
-
WebSocket连接: {desktopShare.isWebSocketConnected ? '✅' : '❌'}
-
P2P连接: {desktopShare.isPeerConnected ? '✅' : '❌'}
-
房间代码: {desktopShare.connectionCode || '未创建'}
-
共享状态: {desktopShare.isSharing ? '进行中' : '未共享'}
-
等待对方: {desktopShare.isWaitingForPeer ? '是' : '否'}
-
- )} -
); } diff --git a/chuan-next/src/components/webrtc/WebRTCFileReceive.tsx b/chuan-next/src/components/webrtc/WebRTCFileReceive.tsx index 689d18b..d791e08 100644 --- a/chuan-next/src/components/webrtc/WebRTCFileReceive.tsx +++ b/chuan-next/src/components/webrtc/WebRTCFileReceive.tsx @@ -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 (
{/* 功能标题和状态 */} -
-
-
- -
-
-

等待文件

-

- {isConnected ? '已连接到房间,等待发送方选择文件...' : '正在连接到房间...'} -

-
-
- - {/* 竖线分割 */} -
- - {/* 状态显示 */} -
-
连接状态
-
- {/* WebSocket状态 */} -
- {isWebSocketConnected ? ( - <> -
- WS - - ) : ( - <> -
- WS - - )} +
+
+
+
- - {/* 分隔符 */} -
|
- - {/* WebRTC状态 */} -
- {isConnected ? ( - <> -
- RTC - - ) : ( - <> -
- RTC - - )} +
+

文件接收中

+

取件码: {pickupCode}

-
-
- -
+ +
+ + + +
+
{/* 连接状态指示器 */}
@@ -226,67 +195,22 @@ export function WebRTCFileReceive({ return (
{/* 功能标题和状态 */} -
-
+
+

可下载文件

-

- {isConnected ? ( - ✅ 已连接,可以下载文件 - ) : ( - ⏳ 正在建立连接... - )} -

+

房间代码: {pickupCode}

- {/* 竖线分割 */} -
- - {/* 状态显示 */} -
-
连接状态
-
- {/* WebSocket状态 */} -
- {isWebSocketConnected ? ( - <> -
- WS - - ) : ( - <> -
- WS - - )} -
- - {/* 分隔符 */} -
|
- - {/* WebRTC状态 */} -
- {isConnected ? ( - <> -
- RTC - - ) : ( - <> -
- RTC - - )} -
-
-
- {files.length} 个文件 -
-
+ {/* 连接状态 */} +
@@ -371,8 +295,8 @@ export function WebRTCFileReceive({ return (
{/* 功能标题和状态 */} -
-
+
+
@@ -382,57 +306,10 @@ export function WebRTCFileReceive({
- {/* 竖线分割 */} -
- - {/* 状态显示 */} -
-
连接状态
-
- {/* WebSocket状态 */} -
- {isConnecting ? ( - <> -
- WS - - ) : isWebSocketConnected ? ( - <> -
- WS - - ) : ( - <> -
- WS - - )} -
- - {/* 分隔符 */} -
|
- - {/* WebRTC状态 */} -
- {isConnected ? ( - <> -
- RTC - - ) : isConnecting ? ( - <> -
- RTC - - ) : ( - <> -
- RTC - - )} -
-
-
+ {/* 连接状态 */} +
diff --git a/chuan-next/src/components/webrtc/WebRTCFileUpload.tsx b/chuan-next/src/components/webrtc/WebRTCFileUpload.tsx index 85ac9a1..01ddb0b 100644 --- a/chuan-next/src/components/webrtc/WebRTCFileUpload.tsx +++ b/chuan-next/src/components/webrtc/WebRTCFileUpload.tsx @@ -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(null); @@ -116,9 +112,9 @@ export function WebRTCFileUpload({ if (selectedFiles.length === 0 && !pickupCode) { return (
- {/* 功能标题和状态 */} -
-
+ {/* 功能标题 */} +
+
@@ -128,29 +124,9 @@ export function WebRTCFileUpload({
- {/* 竖线分割 */} -
- - {/* 状态显示 */} -
-
连接状态
-
- {/* WebSocket状态 */} -
-
- WS -
- - {/* 分隔符 */} -
|
- - {/* WebRTC状态 */} -
-
- RTC -
-
-
+
{/* 功能标题和状态 */} -
-
-
+
+ {/* 标题部分 */} +
+

已选择文件

-

{selectedFiles.length} 个文件准备传输

+

{selectedFiles.length} 个文件准备传输

- {/* 竖线分割 */} -
- - {/* 状态显示 */} -
-
连接状态
-
- {/* WebSocket状态 */} -
- {isWebSocketConnected ? ( - <> -
- WS - - ) : ( - <> -
- WS - - )} -
- - {/* 分隔符 */} -
|
- - {/* WebRTC状态 */} -
- {isConnected ? ( - <> -
- RTC - - ) : pickupCode ? ( - <> -
- RTC - - ) : ( - <> -
- RTC - - )} -
-
-
+ {/* 使用 ConnectionStatus 组件 */} +
diff --git a/chuan-next/src/components/webrtc/WebRTCTextReceiver.tsx b/chuan-next/src/components/webrtc/WebRTCTextReceiver.tsx index 6f87ad4..007820f 100644 --- a/chuan-next/src/components/webrtc/WebRTCTextReceiver.tsx +++ b/chuan-next/src/components/webrtc/WebRTCTextReceiver.tsx @@ -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 = ({ initialCode = '', onPreviewImage, - onRestart + onRestart, + onConnectionChange }) => { const { showToast } = useToast(); @@ -29,12 +32,13 @@ export const WebRTCTextReceiver: React.FC = ({ const [receivedImages, setReceivedImages] = useState>([]); 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 = ({ // 连接所有传输通道 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 => { - 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 = ({ 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 = ({ joinRoom(initialCode); return; } - }, [initialCode]); + }, [initialCode, joinRoom]); return (
{!hasAnyConnection ? ( // 输入取件码界面
-
-
+
+
@@ -218,6 +196,12 @@ export const WebRTCTextReceiver: React.FC = ({

请输入6位取件码来获取实时文字内容

+ +
+ +
{ e.preventDefault(); joinRoom(inputCode); }} className="space-y-4 sm:space-y-6"> @@ -266,94 +250,112 @@ export const WebRTCTextReceiver: React.FC = ({ ) : ( // 已连接,显示实时文本
-
-
+
+

实时文字内容

-

- ✅ 已连接,正在实时接收文字 -

+

取件码: {pickupCode}

-
+ +
+ -

已连接到文字房间

-

取件码: {pickupCode}

-
- - {/* 实时文本显示区域 */} -
-
-

- - 实时文字内容 -

-
- - {receivedText.length} / 50,000 字符 - - {textTransfer.isConnected && ( -
-
- WebRTC实时同步 -
- )} -
-
- -
-