diff --git a/chuan-next/src/app/HomePage.tsx b/chuan-next/src/app/HomePage.tsx index d221efb..5dc0e74 100644 --- a/chuan-next/src/app/HomePage.tsx +++ b/chuan-next/src/app/HomePage.tsx @@ -3,17 +3,30 @@ import React, { useEffect, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Upload, MessageSquare, Monitor, TestTube } from 'lucide-react'; +import { Upload, MessageSquare, Monitor, Users } from 'lucide-react'; import Hero from '@/components/Hero'; import { WebRTCFileTransfer } from '@/components/WebRTCFileTransfer'; import { WebRTCTextImageTransfer } from '@/components/WebRTCTextImageTransfer'; import DesktopShare from '@/components/DesktopShare'; +import WeChatGroup from '@/components/WeChatGroup'; +import { WebRTCUnsupportedModal } from '@/components/WebRTCUnsupportedModal'; +import { useWebRTCSupport } from '@/hooks/useWebRTCSupport'; export default function HomePage() { const searchParams = useSearchParams(); const [activeTab, setActiveTab] = useState('webrtc'); const [hasInitialized, setHasInitialized] = useState(false); + // WebRTC 支持检测 + const { + webrtcSupport, + isSupported, + isChecked, + showUnsupportedModal, + closeUnsupportedModal, + showUnsupportedModalManually, + } = useWebRTCSupport(); + // 根据URL参数设置初始标签(仅首次加载时) useEffect(() => { if (!hasInitialized) { @@ -50,56 +63,123 @@ export default function HomePage() { - {/* Main Content */} -
- - {/* Tabs Navigation - 横向布局 */} -
- - - - 文件传输 - 文件 - - - - 文本消息 - 消息 - - - - 共享桌面 - 桌面 - - + {/* WebRTC 支持检测加载状态 */} + {!isChecked && ( +
+
+
+
+

正在检测浏览器支持...

+
+ )} - {/* Tab Content */} -
- - - + {/* 主要内容 - 只有在检测完成后才显示 */} + {isChecked && ( +
+ {/* WebRTC 不支持时的警告横幅 */} + {!isSupported && ( +
+
+
+
+ + 当前浏览器不支持 WebRTC,功能可能无法正常使用 + +
+ +
+
+ )} - - - + + {/* Tabs Navigation - 横向布局 */} +
+ + + + 文件传输 + 文件 + {!isSupported && *} + + + + 文本消息 + 消息 + {!isSupported && *} + + + + 共享桌面 + 桌面 + {!isSupported && *} + + + + 微信群 + 微信 + + + + {/* WebRTC 不支持时的提示 */} + {!isSupported && ( +

+ * 需要 WebRTC 支持才能使用 +

+ )} +
- - - -
- -
+ {/* Tab Content */} +
+ + + + + + + + + + + + + + + +
+ +
+ )}
+ + {/* WebRTC 不支持提示模态框 */} + {webrtcSupport && ( + + )} ); } diff --git a/chuan-next/src/app/api/create-room/route.ts b/chuan-next/src/app/api/create-room/route.ts index edbc000..db26742 100644 --- a/chuan-next/src/app/api/create-room/route.ts +++ b/chuan-next/src/app/api/create-room/route.ts @@ -6,14 +6,14 @@ export async function POST(request: NextRequest) { try { console.log('API Route: Creating room, proxying to:', `${GO_BACKEND_URL}/api/create-room`); - const body = await request.json(); - + // 不再需要解析和转发请求体,因为后端会忽略它们 const response = await fetch(`${GO_BACKEND_URL}/api/create-room`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify(body), + // 发送空body即可 + body: JSON.stringify({}), }); const data = await response.json(); diff --git a/chuan-next/src/components/WeChatGroup.tsx b/chuan-next/src/components/WeChatGroup.tsx new file mode 100644 index 0000000..17d873f --- /dev/null +++ b/chuan-next/src/components/WeChatGroup.tsx @@ -0,0 +1,68 @@ +"use client"; + +import React from 'react'; +import { Users } from 'lucide-react'; + +export default function WeChatGroup() { + return ( +
+
+ {/* 标题 */} +
+
+ +
+

加入微信交流群

+

+ 佬们有意见/建议/bug反馈或者奇思妙想想来交流,可以扫码加入 +

+
+ + {/* 二维码区域 */} +
+
+ {/* 微信群二维码 - 请将此区域替换为实际的二维码图片 */} +
+ 微信群二维码 +
+
+
+ + {/* 说明文字 */} +
+
+

🎉 欢迎加入我们的交流群!

+ +
+
+ 💬 + 分享使用心得和建议 +
+
+ 🐛 + 反馈问题和bug +
+
+ 💡 + 提出新功能想法 +
+
+ 🤝 + 与其他用户交流技术 +
+
+
+
+ + {/* 额外信息 */} +
+

群内禁止广告和无关内容,专注技术交流

+
+
+
+ ); +} diff --git a/chuan-next/src/components/WebRTCConnectionStatus.tsx b/chuan-next/src/components/WebRTCConnectionStatus.tsx new file mode 100644 index 0000000..aed1ffc --- /dev/null +++ b/chuan-next/src/components/WebRTCConnectionStatus.tsx @@ -0,0 +1,185 @@ +import React from 'react'; +import { AlertCircle, Wifi, WifiOff, Loader2, RotateCcw } from 'lucide-react'; +import { WebRTCConnection } from '@/hooks/webrtc/useSharedWebRTCManager'; + +interface Props { + webrtc: WebRTCConnection; + className?: string; +} + +/** + * WebRTC连接状态显示组件 + * 显示详细的连接状态、错误信息和重试按钮 + */ +export function WebRTCConnectionStatus({ webrtc, className = '' }: Props) { + const { + isConnected, + isConnecting, + isWebSocketConnected, + isPeerConnected, + error, + canRetry, + retry + } = webrtc; + + // 状态图标 + const getStatusIcon = () => { + if (isConnecting) { + return ; + } + + if (error) { + // 区分信息提示和错误 + if (error.includes('对方已离开房间') || error.includes('已离开房间')) { + return ; + } + return ; + } + + if (isPeerConnected) { + return ; + } + + if (isWebSocketConnected) { + return ; + } + + return ; + }; + + // 状态文本 + const getStatusText = () => { + if (error) { + return error; + } + + if (isConnecting) { + return '正在连接...'; + } + + if (isPeerConnected) { + return 'P2P连接已建立'; + } + + if (isWebSocketConnected) { + return '信令服务器已连接'; + } + + return '未连接'; + }; + + // 状态颜色 + const getStatusColor = () => { + if (error) { + // 区分信息提示和错误 + if (error.includes('对方已离开房间') || error.includes('已离开房间')) { + return 'text-yellow-600'; + } + return 'text-red-600'; + } + if (isConnecting) return 'text-blue-600'; + if (isPeerConnected) return 'text-green-600'; + if (isWebSocketConnected) return 'text-yellow-600'; + return 'text-gray-600'; + }; + + const handleRetry = async () => { + try { + await retry(); + } catch (error) { + console.error('重试连接失败:', error); + } + }; + + return ( +
+
+ {getStatusIcon()} + + {getStatusText()} + +
+ + {/* 连接详细状态指示器 */} +
+ {/* WebSocket状态 */} +
+ + {/* P2P状态 */} +
+ + {/* 重试按钮 */} + {canRetry && ( + + )} +
+
+ ); +} + +/** + * 简单的连接状态指示器(用于空间受限的地方) + */ +export function WebRTCStatusIndicator({ webrtc, className = '' }: Props) { + const { isPeerConnected, isConnecting, error } = webrtc; + + if (error) { + // 区分信息提示和错误 + if (error.includes('对方已离开房间') || error.includes('已离开房间')) { + return ( +
+
+ 对方已离开 +
+ ); + } + return ( +
+
+ 连接错误 +
+ ); + } + + if (isConnecting) { + return ( +
+
+ 连接中 +
+ ); + } + + if (isPeerConnected) { + return ( +
+
+ 已连接 +
+ ); + } + + return ( +
+
+ 未连接 +
+ ); +} diff --git a/chuan-next/src/components/WebRTCFileTransfer.tsx b/chuan-next/src/components/WebRTCFileTransfer.tsx index 0e7067d..1046e4f 100644 --- a/chuan-next/src/components/WebRTCFileTransfer.tsx +++ b/chuan-next/src/components/WebRTCFileTransfer.tsx @@ -309,21 +309,14 @@ export const WebRTCFileTransfer: React.FC = () => { console.log('=== 创建房间 ==='); console.log('选中文件数:', selectedFiles.length); - // 创建后端房间 + // 创建后端房间 - 简化版本,不发送无用的文件信息 const response = await fetch('/api/create-room', { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ - type: 'file', - files: selectedFiles.map(file => ({ - name: file.name, - size: file.size, - type: file.type, - lastModified: file.lastModified - })) - }), + // 不再发送文件列表,因为后端不使用这些信息 + body: JSON.stringify({}), }); const data = await response.json(); diff --git a/chuan-next/src/components/WebRTCUnsupportedModal.tsx b/chuan-next/src/components/WebRTCUnsupportedModal.tsx new file mode 100644 index 0000000..0ebb342 --- /dev/null +++ b/chuan-next/src/components/WebRTCUnsupportedModal.tsx @@ -0,0 +1,186 @@ +import React from 'react'; +import { AlertTriangle, Download, X, Chrome, Monitor } from 'lucide-react'; +import { WebRTCSupport, getBrowserInfo, getRecommendedBrowsers } from '@/lib/webrtc-support'; + +interface Props { + isOpen: boolean; + onClose: () => void; + webrtcSupport: WebRTCSupport; +} + +/** + * WebRTC 不支持提示模态框 + */ +export function WebRTCUnsupportedModal({ isOpen, onClose, webrtcSupport }: Props) { + const browserInfo = getBrowserInfo(); + const recommendedBrowsers = getRecommendedBrowsers(); + + if (!isOpen) return null; + + const handleBrowserDownload = (url: string) => { + window.open(url, '_blank', 'noopener,noreferrer'); + }; + + return ( +
+
+ {/* 头部 */} +
+
+ +

+ 浏览器不支持 WebRTC +

+
+ +
+ + {/* 内容 */} +
+ {/* 当前浏览器信息 */} +
+

当前浏览器状态

+
+
+ 浏览器: {browserInfo.name} {browserInfo.version} +
+
+ WebRTC 支持: + + 不支持 + +
+
+
+ + {/* 缺失的功能 */} +
+

缺失的功能:

+
+ {webrtcSupport.missing.map((feature, index) => ( +
+
+ {feature} +
+ ))} +
+
+ + {/* 功能说明 */} +
+

为什么需要 WebRTC?

+
+
+ +
+ 屏幕共享: 实时共享您的桌面屏幕 +
+
+
+ +
+ 文件传输: 点对点直接传输文件,快速且安全 +
+
+
+ +
+ 文本传输: 实时文本和图像传输 +
+
+
+
+ + {/* 浏览器推荐 */} +
+

推荐使用以下浏览器:

+
+ {recommendedBrowsers.map((browser, index) => ( +
handleBrowserDownload(browser.downloadUrl)} + > +
+
+

{browser.name}

+

版本 {browser.minVersion}

+
+ +
+
+ ))} +
+
+ + {/* 浏览器特定建议 */} + {browserInfo.recommendations && ( +
+

建议

+
    + {browserInfo.recommendations.map((recommendation, index) => ( +
  • +
    + {recommendation} +
  • + ))} +
+
+ )} + + {/* 技术详情(可折叠) */} +
+ + 技术详情 + +
+
+
+ RTCPeerConnection: + + {webrtcSupport.details.rtcPeerConnection ? '支持' : '不支持'} + +
+
+ DataChannel: + + {webrtcSupport.details.dataChannel ? '支持' : '不支持'} + +
+
+
+
+
+ + {/* 底部按钮 */} +
+ + +
+
+
+ ); +} diff --git a/chuan-next/src/components/webrtc/WebRTCDesktopReceiver.tsx b/chuan-next/src/components/webrtc/WebRTCDesktopReceiver.tsx index 1f812bf..152772e 100644 --- a/chuan-next/src/components/webrtc/WebRTCDesktopReceiver.tsx +++ b/chuan-next/src/components/webrtc/WebRTCDesktopReceiver.tsx @@ -18,6 +18,7 @@ interface WebRTCDesktopReceiverProps { export default function WebRTCDesktopReceiver({ className, initialCode, onConnectionChange }: WebRTCDesktopReceiverProps) { const [inputCode, setInputCode] = useState(initialCode || ''); const [isLoading, setIsLoading] = useState(false); + const [isJoiningRoom, setIsJoiningRoom] = useState(false); // 添加加入房间状态 const [showDebug, setShowDebug] = useState(false); const hasTriedAutoJoin = React.useRef(false); // 添加 ref 来跟踪是否已尝试自动加入 const { showToast } = useToast(); @@ -34,27 +35,82 @@ export default function WebRTCDesktopReceiver({ className, initialCode, onConnec // 加入观看 const handleJoinViewing = useCallback(async () => { - if (!inputCode.trim()) { - showToast('请输入房间代码', 'error'); + const trimmedCode = inputCode.trim(); + + // 检查房间代码格式 + if (!trimmedCode || trimmedCode.length !== 6) { + showToast('请输入正确的6位房间代码', "error"); return; } + // 防止重复调用 - 检查是否已经在连接或已连接 + if (desktopShare.isConnecting || desktopShare.isViewing || isJoiningRoom) { + console.log('已在连接中或已连接,跳过重复的房间状态检查'); + return; + } + + setIsJoiningRoom(true); + try { - setIsLoading(true); - console.log('[DesktopShareReceiver] 用户加入观看房间:', inputCode); + console.log('[DesktopShareReceiver] 开始验证房间状态...'); - await desktopShare.joinSharing(inputCode.trim().toUpperCase()); + // 先检查房间状态 + const response = await fetch(`/api/room-info?code=${trimmedCode}`); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: 无法检查房间状态`); + } + + const result = await response.json(); + + if (!result.success) { + let errorMessage = result.message || '房间不存在或已过期'; + if (result.message?.includes('expired')) { + errorMessage = '房间已过期,请联系发送方重新创建'; + } else if (result.message?.includes('not found')) { + errorMessage = '房间不存在,请检查房间代码是否正确'; + } + showToast(errorMessage, "error"); + return; + } + + // 检查发送方是否在线 + if (!result.sender_online) { + showToast('发送方不在线,请确认房间代码是否正确或联系发送方', "error"); + return; + } + + console.log('[DesktopShareReceiver] 房间状态检查通过,开始连接...'); + setIsLoading(true); + + await desktopShare.joinSharing(trimmedCode.toUpperCase()); console.log('[DesktopShareReceiver] 加入观看成功'); showToast('已加入桌面共享', 'success'); } catch (error) { console.error('[DesktopShareReceiver] 加入观看失败:', error); - const errorMessage = error instanceof Error ? error.message : '加入观看失败'; + + let errorMessage = '加入观看失败'; + if (error instanceof Error) { + if (error.message.includes('network') || error.message.includes('fetch')) { + errorMessage = '网络连接失败,请检查网络状况'; + } else if (error.message.includes('timeout')) { + errorMessage = '请求超时,请重试'; + } else if (error.message.includes('HTTP 404')) { + errorMessage = '房间不存在,请检查房间代码'; + } else if (error.message.includes('HTTP 500')) { + errorMessage = '服务器错误,请稍后重试'; + } else { + errorMessage = error.message; + } + } + showToast(errorMessage, 'error'); } finally { setIsLoading(false); + setIsJoiningRoom(false); // 重置加入房间状态 } - }, [desktopShare, inputCode, showToast]); + }, [desktopShare, inputCode, isJoiningRoom, showToast]); // 停止观看 const handleStopViewing = useCallback(async () => { @@ -77,38 +133,94 @@ export default function WebRTCDesktopReceiver({ className, initialCode, onConnec initialCode, isViewing: desktopShare.isViewing, isConnecting: desktopShare.isConnecting, + isJoiningRoom, hasTriedAutoJoin: hasTriedAutoJoin.current }); const autoJoin = async () => { - if (initialCode && !desktopShare.isViewing && !desktopShare.isConnecting && !hasTriedAutoJoin.current) { + if (initialCode && !desktopShare.isViewing && !desktopShare.isConnecting && !isJoiningRoom && !hasTriedAutoJoin.current) { hasTriedAutoJoin.current = true; - console.log('[WebRTCDesktopReceiver] 检测到初始代码,自动加入观看:', initialCode); + const trimmedCode = initialCode.trim(); + + // 检查房间代码格式 + if (!trimmedCode || trimmedCode.length !== 6) { + showToast('房间代码格式不正确', "error"); + return; + } + + setIsJoiningRoom(true); + console.log('[WebRTCDesktopReceiver] 检测到初始代码,开始验证并自动加入:', trimmedCode); try { + // 先检查房间状态 + console.log('[WebRTCDesktopReceiver] 验证房间状态...'); + const response = await fetch(`/api/room-info?code=${trimmedCode}`); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: 无法检查房间状态`); + } + + const result = await response.json(); + + if (!result.success) { + let errorMessage = result.message || '房间不存在或已过期'; + if (result.message?.includes('expired')) { + errorMessage = '房间已过期,请联系发送方重新创建'; + } else if (result.message?.includes('not found')) { + errorMessage = '房间不存在,请检查房间代码是否正确'; + } + showToast(errorMessage, "error"); + return; + } + + // 检查发送方是否在线 + if (!result.sender_online) { + showToast('发送方不在线,请确认房间代码是否正确或联系发送方', "error"); + return; + } + + console.log('[WebRTCDesktopReceiver] 房间验证通过,开始自动连接...'); setIsLoading(true); - await desktopShare.joinSharing(initialCode.trim().toUpperCase()); + + await desktopShare.joinSharing(trimmedCode.toUpperCase()); console.log('[WebRTCDesktopReceiver] 自动加入观看成功'); showToast('已加入桌面共享', 'success'); } catch (error) { console.error('[WebRTCDesktopReceiver] 自动加入观看失败:', error); - const errorMessage = error instanceof Error ? error.message : '加入观看失败'; + + let errorMessage = '自动加入观看失败'; + if (error instanceof Error) { + if (error.message.includes('network') || error.message.includes('fetch')) { + errorMessage = '网络连接失败,请检查网络状况'; + } else if (error.message.includes('timeout')) { + errorMessage = '请求超时,请重试'; + } else if (error.message.includes('HTTP 404')) { + errorMessage = '房间不存在,请检查房间代码'; + } else if (error.message.includes('HTTP 500')) { + errorMessage = '服务器错误,请稍后重试'; + } else { + errorMessage = error.message; + } + } + showToast(errorMessage, 'error'); } finally { setIsLoading(false); + setIsJoiningRoom(false); } } else { console.log('[WebRTCDesktopReceiver] 不满足自动加入条件:', { hasInitialCode: !!initialCode, notViewing: !desktopShare.isViewing, notConnecting: !desktopShare.isConnecting, + notJoiningRoom: !isJoiningRoom, notTriedBefore: !hasTriedAutoJoin.current }); } }; autoJoin(); - }, [initialCode, desktopShare.isViewing, desktopShare.isConnecting]); // 移除了 desktopShare.joinSharing 和 showToast + }, [initialCode, desktopShare.isViewing, desktopShare.isConnecting, isJoiningRoom]); // 添加isJoiningRoom依赖 return (
@@ -138,11 +250,11 @@ export default function WebRTCDesktopReceiver({ className, initialCode, onConnec
setInputCode(e.target.value.replace(/[^A-Z0-9]/g, '').toUpperCase())} + onChange={(e) => setInputCode(e.target.value.replace(/[^123456789ABCDEFGHIJKLMNPQRSTUVWXYZabcdefghijklmnpqrstuvwxyz]/g, ''))} 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} + disabled={isLoading || isJoiningRoom} />

@@ -153,10 +265,15 @@ export default function WebRTCDesktopReceiver({ className, initialCode, onConnec