mirror of
https://github.com/MatrixSeven/file-transfer-go.git
synced 2026-02-15 09:44:45 +08:00
feat: 文字图片传输支持,去掉无用交互和接口
This commit is contained in:
@@ -6,7 +6,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Upload, MessageSquare, Monitor } from 'lucide-react';
|
||||
import Hero from '@/components/Hero';
|
||||
import { WebRTCFileTransfer } from '@/components/WebRTCFileTransfer';
|
||||
import { WebRTCTextImageTransfer } from '@/components/WebRTCTextImageTransfer';
|
||||
import TextTransferWrapper from '@/components/TextTransferWrapper';
|
||||
|
||||
export default function HomePage() {
|
||||
const searchParams = useSearchParams();
|
||||
@@ -90,7 +90,7 @@ export default function HomePage() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="message" className="mt-0 animate-fade-in-up">
|
||||
<WebRTCTextImageTransfer />
|
||||
<TextTransferWrapper />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="desktop" className="mt-0 animate-fade-in-up">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
@@ -6,12 +6,14 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { MessageSquare, Copy, Send, Download, Image, Users, Link, Eye } from 'lucide-react';
|
||||
import { useToast } from '@/components/ui/toast-simple';
|
||||
import QRCodeDisplay from './QRCodeDisplay';
|
||||
|
||||
interface TextTransferProps {
|
||||
onSendText?: (text: string) => Promise<string>; // 返回取件码
|
||||
onReceiveText?: (code: string) => Promise<string>; // 返回文本内容
|
||||
websocket?: WebSocket | null;
|
||||
isConnected?: boolean;
|
||||
isConnected?: boolean; // WebRTC数据通道连接状态
|
||||
isWebSocketConnected?: boolean; // WebSocket信令连接状态
|
||||
currentRole?: 'sender' | 'receiver';
|
||||
pickupCode?: string;
|
||||
onCreateWebSocket?: (code: string, role: 'sender' | 'receiver') => void; // 创建WebSocket连接
|
||||
@@ -21,7 +23,8 @@ export default function TextTransfer({
|
||||
onSendText,
|
||||
onReceiveText,
|
||||
websocket,
|
||||
isConnected = false,
|
||||
isConnected = false, // WebRTC数据通道连接状态
|
||||
isWebSocketConnected = false, // WebSocket信令连接状态
|
||||
currentRole,
|
||||
pickupCode,
|
||||
onCreateWebSocket
|
||||
@@ -38,13 +41,28 @@ export default function TextTransfer({
|
||||
const [sentImages, setSentImages] = useState<string[]>([]); // 发送的图片
|
||||
const [receivedImages, setReceivedImages] = useState<string[]>([]); // 接收的图片
|
||||
const [imagePreview, setImagePreview] = useState<string | null>(null); // 图片预览状态
|
||||
const [currentWebSocketConnected, setCurrentWebSocketConnected] = useState(false); // 本地WebSocket连接状态
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null); // 图片预览弹窗状态
|
||||
const [hasShownJoinSuccess, setHasShownJoinSuccess] = useState(false); // 防止重复显示加入成功消息
|
||||
const [lastToastMessage, setLastToastMessage] = useState<string>(''); // 防止重复Toast
|
||||
const [lastToastTime, setLastToastTime] = useState<number>(0); // 上次Toast时间
|
||||
const { showToast } = useToast();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const updateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const connectionTimeoutRef = useRef<NodeJS.Timeout | null>(null); // 连接超时定时器
|
||||
|
||||
// 优化的Toast显示函数,避免重复消息
|
||||
const showOptimizedToast = useCallback((message: string, type: 'success' | 'error' | 'info') => {
|
||||
const now = Date.now();
|
||||
// 如果是相同消息且在3秒内,不重复显示
|
||||
if (lastToastMessage === message && now - lastToastTime < 3000) {
|
||||
return;
|
||||
}
|
||||
setLastToastMessage(message);
|
||||
setLastToastTime(now);
|
||||
showToast(message, type);
|
||||
}, [lastToastMessage, lastToastTime, showToast]);
|
||||
|
||||
// 从URL参数中获取初始模式
|
||||
useEffect(() => {
|
||||
const urlMode = searchParams.get('mode') as 'send' | 'receive';
|
||||
@@ -65,20 +83,46 @@ export default function TextTransfer({
|
||||
useEffect(() => {
|
||||
const handleWebSocketMessage = (event: CustomEvent) => {
|
||||
const message = event.detail;
|
||||
console.log('TextTransfer收到WebSocket消息:', message);
|
||||
console.log('TextTransfer收到消息:', message);
|
||||
|
||||
switch (message.type) {
|
||||
case 'websocket-signaling-connected':
|
||||
console.log('收到WebSocket信令连接成功事件:', message);
|
||||
|
||||
// 立即更新本地信令连接状态
|
||||
setCurrentWebSocketConnected(true);
|
||||
|
||||
// 只对接收方显示信令连接提示,发送方不需要
|
||||
if (currentRole === 'receiver') {
|
||||
showOptimizedToast('正在建立连接...', 'success');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'webrtc-connecting':
|
||||
console.log('收到WebRTC数据通道连接中事件:', message);
|
||||
// 显示数据通道连接中状态
|
||||
break;
|
||||
|
||||
case 'webrtc-connected':
|
||||
console.log('收到WebRTC数据通道连接成功事件:', message);
|
||||
|
||||
// 清除连接超时定时器
|
||||
if (connectionTimeoutRef.current) {
|
||||
clearTimeout(connectionTimeoutRef.current);
|
||||
connectionTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
// 只显示一个简洁的连接成功提示
|
||||
showOptimizedToast('连接成功!', 'success');
|
||||
break;
|
||||
|
||||
case 'text-content':
|
||||
// 接收到文字房间的初始内容或同步内容
|
||||
if (message.payload?.text !== undefined) {
|
||||
setReceivedText(message.payload.text);
|
||||
if (currentRole === 'receiver') {
|
||||
setTextContent(message.payload.text);
|
||||
// 只在第一次收到文字内容且处于loading状态时显示成功消息
|
||||
if (!hasShownJoinSuccess && isLoading) {
|
||||
setHasShownJoinSuccess(true);
|
||||
showToast('成功加入文字房间!', 'success');
|
||||
}
|
||||
// 移除重复的成功消息,因为连接成功时已经显示了
|
||||
}
|
||||
// 清除连接超时定时器
|
||||
if (connectionTimeoutRef.current) {
|
||||
@@ -103,10 +147,9 @@ export default function TextTransfer({
|
||||
break;
|
||||
|
||||
case 'text-send':
|
||||
// 接收到发送的文字
|
||||
// 接收到发送的文字,不显示Toast,因为UI已经更新了
|
||||
if (message.payload?.text) {
|
||||
setReceivedText(message.payload.text);
|
||||
showToast('收到新的文字内容!', 'success');
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -117,10 +160,11 @@ export default function TextTransfer({
|
||||
// 验证图片数据格式
|
||||
if (message.payload.imageData.startsWith('data:image/')) {
|
||||
setReceivedImages(prev => [...prev, message.payload.imageData]);
|
||||
showToast('收到新的图片!', 'success');
|
||||
// 只在有实际图片时显示提示
|
||||
showOptimizedToast('收到图片', 'success');
|
||||
} else {
|
||||
console.error('无效的图片数据格式:', message.payload.imageData.substring(0, 50));
|
||||
showToast('收到的图片格式不正确', 'error');
|
||||
showOptimizedToast('图片格式错误', 'error');
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -131,6 +175,38 @@ export default function TextTransfer({
|
||||
setConnectedUsers(message.payload.sender_count + message.payload.receiver_count);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'webrtc-error':
|
||||
console.error('收到WebRTC错误事件:', message.payload);
|
||||
// 清除连接超时定时器
|
||||
if (connectionTimeoutRef.current) {
|
||||
clearTimeout(connectionTimeoutRef.current);
|
||||
connectionTimeoutRef.current = null;
|
||||
}
|
||||
// 结束loading状态
|
||||
if (isLoading) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
// 显示错误消息
|
||||
if (message.payload?.message) {
|
||||
showOptimizedToast(message.payload.message, 'error');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'websocket-close':
|
||||
console.log('收到WebSocket关闭事件:', message.payload);
|
||||
// 更新本地连接状态
|
||||
setCurrentWebSocketConnected(false);
|
||||
// 清除连接超时定时器
|
||||
if (connectionTimeoutRef.current) {
|
||||
clearTimeout(connectionTimeoutRef.current);
|
||||
connectionTimeoutRef.current = null;
|
||||
}
|
||||
// 结束loading状态
|
||||
if (isLoading) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -142,27 +218,40 @@ export default function TextTransfer({
|
||||
if (isLoading) {
|
||||
setIsLoading(false);
|
||||
if (code !== 1000) { // 不是正常关闭
|
||||
showToast('取件码不存在或已过期', 'error');
|
||||
showOptimizedToast('房间已关闭', 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleWebSocketConnecting = (event: CustomEvent) => {
|
||||
console.log('WebSocket正在连接:', event.detail);
|
||||
// 可以在这里显示连接中的状态
|
||||
};
|
||||
|
||||
const handleWebSocketError = (event: CustomEvent) => {
|
||||
console.error('WebSocket连接错误:', event.detail);
|
||||
|
||||
// 如果是在loading状态下出现错误,结束loading并显示错误
|
||||
if (isLoading) {
|
||||
setIsLoading(false);
|
||||
showToast('取件码不存在或已过期', 'error');
|
||||
showOptimizedToast('连接失败', 'error');
|
||||
}
|
||||
|
||||
// 清除连接超时定时器
|
||||
if (connectionTimeoutRef.current) {
|
||||
clearTimeout(connectionTimeoutRef.current);
|
||||
connectionTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('websocket-message', handleWebSocketMessage as EventListener);
|
||||
window.addEventListener('websocket-connecting', handleWebSocketConnecting as EventListener);
|
||||
window.addEventListener('websocket-close', handleWebSocketClose as EventListener);
|
||||
window.addEventListener('websocket-error', handleWebSocketError as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('websocket-message', handleWebSocketMessage as EventListener);
|
||||
window.removeEventListener('websocket-connecting', handleWebSocketConnecting as EventListener);
|
||||
window.removeEventListener('websocket-close', handleWebSocketClose as EventListener);
|
||||
window.removeEventListener('websocket-error', handleWebSocketError as EventListener);
|
||||
|
||||
@@ -171,7 +260,7 @@ export default function TextTransfer({
|
||||
clearTimeout(connectionTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [currentRole, showToast, hasShownJoinSuccess, isLoading]);
|
||||
}, [currentRole, showOptimizedToast, hasShownJoinSuccess, isLoading]);
|
||||
|
||||
// 更新URL参数
|
||||
const updateMode = useCallback((newMode: 'send' | 'receive') => {
|
||||
@@ -184,7 +273,11 @@ export default function TextTransfer({
|
||||
|
||||
// 发送实时文字更新
|
||||
const sendTextUpdate = useCallback((text: string) => {
|
||||
if (!websocket || !isConnected) return;
|
||||
// 必须通过WebRTC数据通道发送,不能通过WebSocket信令
|
||||
if (!websocket || !isConnected) {
|
||||
console.log('WebRTC数据通道未连接,无法发送实时更新。信令状态:', isWebSocketConnected, '数据通道状态:', isConnected);
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除之前的定时器
|
||||
if (updateTimeoutRef.current) {
|
||||
@@ -193,12 +286,13 @@ export default function TextTransfer({
|
||||
|
||||
// 设置新的定时器,防抖动
|
||||
updateTimeoutRef.current = setTimeout(() => {
|
||||
// 通过WebRTC数据通道发送实时更新
|
||||
websocket.send(JSON.stringify({
|
||||
type: 'text-update',
|
||||
payload: { text }
|
||||
}));
|
||||
}, 300); // 300ms防抖
|
||||
}, [websocket, isConnected]);
|
||||
}, [websocket, isConnected, isWebSocketConnected]);
|
||||
|
||||
// 处理文字输入
|
||||
const handleTextChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
@@ -213,38 +307,45 @@ export default function TextTransfer({
|
||||
|
||||
// 创建文字传输房间
|
||||
const handleCreateRoom = useCallback(async () => {
|
||||
if (!textContent.trim()) {
|
||||
showToast('请输入要传输的文字内容', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (onSendText) {
|
||||
const code = await onSendText(textContent);
|
||||
if (code) { // 只有在成功创建房间时才设置状态和显示成功消息
|
||||
setRoomCode(code);
|
||||
setIsRoomCreated(true);
|
||||
showToast('房间创建成功!', 'success');
|
||||
|
||||
// 创建WebSocket连接用于实时同步
|
||||
if (onCreateWebSocket) {
|
||||
onCreateWebSocket(code, 'sender');
|
||||
}
|
||||
}
|
||||
// 使用统一的API创建房间(不区分类型)
|
||||
const response = await fetch('/api/create-room', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({}), // 空对象即可
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.message || '创建房间失败');
|
||||
}
|
||||
|
||||
const code = data.code;
|
||||
setRoomCode(code);
|
||||
setIsRoomCreated(true);
|
||||
setIsLoading(false); // 立即结束loading,显示UI
|
||||
// 移除创建成功Toast,UI变化已经足够明显
|
||||
|
||||
// 立即创建WebSocket连接用于实时同步
|
||||
if (onCreateWebSocket) {
|
||||
console.log('房间创建成功,立即建立WebRTC连接:', code);
|
||||
onCreateWebSocket(code, 'sender');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建房间失败:', error);
|
||||
// 错误信息已经在HomePage中处理了,这里不再重复显示
|
||||
} finally {
|
||||
showOptimizedToast(error instanceof Error ? error.message : '创建失败', 'error');
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [textContent, onSendText, onCreateWebSocket, showToast]);
|
||||
}, [onCreateWebSocket, showOptimizedToast]);
|
||||
|
||||
// 加入房间
|
||||
const handleJoinRoom = useCallback(async () => {
|
||||
if (!roomCode.trim() || roomCode.length !== 6) {
|
||||
showToast('请输入正确的6位房间码', 'error');
|
||||
showOptimizedToast('请输入6位房间码', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -254,7 +355,6 @@ export default function TextTransfer({
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setHasShownJoinSuccess(false); // 重置加入成功消息标志
|
||||
|
||||
try {
|
||||
// 先查询房间信息,确认房间存在
|
||||
@@ -262,42 +362,28 @@ export default function TextTransfer({
|
||||
const roomData = await roomInfoResponse.json();
|
||||
|
||||
if (!roomInfoResponse.ok || !roomData.success) {
|
||||
showToast(roomData.message || '房间不存在或已过期', 'error');
|
||||
showOptimizedToast(roomData.message || '房间不存在', 'error');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 房间存在,创建WebSocket连接
|
||||
// 房间存在,立即显示界面和文本框
|
||||
setHasShownJoinSuccess(true);
|
||||
setReceivedText(''); // 立即设置为空字符串以显示文本框
|
||||
setIsLoading(false); // 立即结束loading,显示UI
|
||||
// 移除加入成功Toast,UI变化已经足够明显
|
||||
|
||||
// 创建WebSocket连接用于实时同步
|
||||
if (onCreateWebSocket) {
|
||||
console.log('房间验证成功,手动加入房间:', roomCode);
|
||||
console.log('房间验证成功,开始建立WebRTC连接:', roomCode);
|
||||
onCreateWebSocket(roomCode, 'receiver');
|
||||
|
||||
// 设置连接超时,如果8秒内没有收到消息就认为连接失败
|
||||
connectionTimeoutRef.current = setTimeout(() => {
|
||||
if (isLoading) {
|
||||
setIsLoading(false);
|
||||
showToast('取件码不存在或已过期', 'error');
|
||||
}
|
||||
}, 8000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加入房间失败:', error);
|
||||
showToast('网络错误,请稍后重试', 'error');
|
||||
showOptimizedToast('网络错误', 'error');
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [roomCode, onCreateWebSocket, showToast, isLoading]);
|
||||
|
||||
// 发送文字
|
||||
const handleSendText = useCallback(() => {
|
||||
if (!websocket || !isConnected || !textContent.trim()) return;
|
||||
|
||||
websocket.send(JSON.stringify({
|
||||
type: 'text-send',
|
||||
payload: { text: textContent }
|
||||
}));
|
||||
|
||||
showToast('文字已发送!', 'success');
|
||||
}, [websocket, isConnected, textContent, showToast]);
|
||||
}, [roomCode, onCreateWebSocket, showOptimizedToast, isLoading]);
|
||||
|
||||
// 压缩图片
|
||||
const compressImage = useCallback((file: File): Promise<string> => {
|
||||
@@ -377,35 +463,37 @@ export default function TextTransfer({
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
try {
|
||||
showToast('正在处理图片...', 'info');
|
||||
showOptimizedToast('处理中...', 'info');
|
||||
const compressedImageData = await compressImage(file);
|
||||
setSentImages(prev => [...prev, compressedImageData]);
|
||||
|
||||
// 发送图片给其他用户
|
||||
// 必须通过WebRTC数据通道发送图片
|
||||
if (websocket && isConnected) {
|
||||
websocket.send(JSON.stringify({
|
||||
type: 'image-send',
|
||||
payload: { imageData: compressedImageData }
|
||||
}));
|
||||
showToast('图片已发送!', 'success');
|
||||
// 移除发送成功Toast,视觉反馈已经足够
|
||||
} else {
|
||||
showOptimizedToast('连接断开', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('图片处理失败:', error);
|
||||
showToast('图片处理失败,请重试', 'error');
|
||||
showOptimizedToast('处理失败', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [websocket, isConnected, showToast, compressImage]);
|
||||
}, [websocket, isConnected, showOptimizedToast, compressImage]);
|
||||
|
||||
const copyToClipboard = useCallback(async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
showToast('已复制到剪贴板!', 'success');
|
||||
showOptimizedToast('已复制', 'success');
|
||||
} catch (err) {
|
||||
showToast('复制失败', 'error');
|
||||
showOptimizedToast('复制失败', 'error');
|
||||
}
|
||||
}, [showToast]);
|
||||
}, [showOptimizedToast]);
|
||||
|
||||
// 复制传输链接
|
||||
const copyTransferLink = useCallback(async (code: string) => {
|
||||
@@ -422,8 +510,8 @@ export default function TextTransfer({
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
showToast('图片已下载!', 'success');
|
||||
}, [showToast]);
|
||||
showOptimizedToast('已保存', 'success');
|
||||
}, [showOptimizedToast]);
|
||||
|
||||
// 图片预览组件
|
||||
const ImagePreviewModal = ({ src, onClose }: { src: string; onClose: () => void }) => (
|
||||
@@ -534,13 +622,13 @@ export default function TextTransfer({
|
||||
<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状态 */}
|
||||
{/* WebSocket信令状态 */}
|
||||
<div className="flex items-center space-x-1">
|
||||
{isRoomCreated ? (
|
||||
isConnected ? (
|
||||
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-blue-500 animate-pulse"></div>
|
||||
<span className="text-blue-600">WS</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -559,10 +647,26 @@ export default function TextTransfer({
|
||||
{/* 分隔符 */}
|
||||
<div className="text-slate-300">|</div>
|
||||
|
||||
{/* WebRTC状态 */}
|
||||
{/* 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>
|
||||
{isRoomCreated ? (
|
||||
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-400"></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>
|
||||
{connectedUsers > 0 && (
|
||||
@@ -574,91 +678,149 @@ export default function TextTransfer({
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="relative">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={textContent}
|
||||
onChange={handleTextChange}
|
||||
onPaste={handlePaste}
|
||||
placeholder="在这里输入要传输的文本内容... 💡 提示:支持实时同步编辑,可以直接粘贴图片 (Ctrl+V)"
|
||||
className="w-full min-h-[150px] p-4 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:ring-blue-500 bg-white/80 backdrop-blur-sm resize-none"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{isRoomCreated && isConnected && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<div className="flex items-center space-x-1 bg-emerald-100 text-emerald-700 px-2 py-1 rounded-lg text-xs">
|
||||
<div className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
|
||||
<span>实时同步</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isRoomCreated && !isConnected && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<div className="flex items-center space-x-1 bg-orange-100 text-orange-700 px-2 py-1 rounded-lg text-xs">
|
||||
<div className="w-2 h-2 bg-orange-500 rounded-full"></div>
|
||||
<span>连接中...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-sm text-slate-500">
|
||||
<span>{textContent.length} 字符</span>
|
||||
<span>最大 50,000 字符</span>
|
||||
</div>
|
||||
|
||||
{!isRoomCreated ? (
|
||||
<Button
|
||||
onClick={handleCreateRoom}
|
||||
disabled={!textContent.trim() || textContent.length > 50000 || isLoading}
|
||||
className="w-full h-12 bg-gradient-to-r from-blue-500 to-indigo-500 hover:from-blue-600 hover:to-indigo-600 text-white text-lg font-medium rounded-xl shadow-lg"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
||||
创建房间...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="w-5 h-5 mr-2" />
|
||||
创建文字传输房间
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<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>
|
||||
<p className="text-slate-600 mb-8">创建房间后可以实时同步文字内容</p>
|
||||
|
||||
<Button
|
||||
onClick={handleCreateRoom}
|
||||
disabled={isLoading}
|
||||
className="px-8 py-3 bg-gradient-to-r from-blue-500 to-indigo-500 hover:from-blue-600 hover:to-indigo-600 text-white text-lg font-medium rounded-xl shadow-lg"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
||||
创建中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="w-5 h-5 mr-2" />
|
||||
创建文字传输房间
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-gradient-to-r from-emerald-50 to-teal-50 rounded-xl border border-emerald-200">
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-emerald-700 mb-2">房间码</p>
|
||||
<div className="text-2xl font-bold font-mono text-emerald-600 mb-3">{roomCode}</div>
|
||||
<div className="flex flex-wrap gap-2 justify-center">
|
||||
<div className="space-y-6">
|
||||
{/* 文字编辑区域 - 移到最上面 */}
|
||||
<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">{textContent.length} / 50,000 字符</span>
|
||||
{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>
|
||||
)}
|
||||
{isWebSocketConnected && !isConnected && (
|
||||
<div className="flex items-center space-x-1 bg-blue-100 text-blue-700 px-2 py-1 rounded-md">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
|
||||
<span className="font-medium">建立数据通道中</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={textContent}
|
||||
onChange={handleTextChange}
|
||||
onPaste={handlePaste}
|
||||
placeholder="在这里编辑文字内容... 💡 支持实时同步编辑,对方可以看到你的修改 💡 可以直接粘贴图片 (Ctrl+V)"
|
||||
className="w-full min-h-[200px] p-4 border-2 border-slate-200 rounded-xl focus:border-emerald-500 focus:ring-emerald-500 bg-white/80 backdrop-blur-sm resize-none"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<div className="text-xs text-slate-500">
|
||||
💡 文字会自动保存并实时同步给接收方
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 房间信息卡片 - 类似文件传输的布局 */}
|
||||
<div className="space-y-6">
|
||||
{/* 左上角状态提示 - 类似已选择文件的风格 */}
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-emerald-500 to-teal-500 rounded-xl flex items-center justify-center">
|
||||
<MessageSquare className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-800">取件码生成成功!</h3>
|
||||
<p className="text-sm text-slate-600">分享以下信息给接收方,支持实时文本同步</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 中间区域:取件码 + 分隔线 + 二维码 */}
|
||||
<div className="flex flex-col lg:flex-row lg:items-start gap-6 lg:gap-8">
|
||||
{/* 左侧:取件码 */}
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-3">取件码</label>
|
||||
<div className="flex flex-col items-center rounded-xl border border-slate-200 p-6 h-40 justify-center bg-slate-50">
|
||||
<div className="text-2xl font-bold font-mono bg-gradient-to-r from-emerald-600 to-teal-600 bg-clip-text text-transparent tracking-wider">
|
||||
{roomCode}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => copyToClipboard(roomCode)}
|
||||
size="sm"
|
||||
className="bg-emerald-500 hover:bg-emerald-600 text-white"
|
||||
className="w-full px-4 py-2.5 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg font-medium shadow transition-all duration-200 mt-3"
|
||||
>
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
复制房间码
|
||||
复制取件码
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 分隔线 - 大屏幕显示竖线,移动端隐藏 */}
|
||||
<div className="hidden lg:block w-px bg-slate-200 h-64 mt-6"></div>
|
||||
|
||||
{/* 右侧:二维码 */}
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-3">扫码传输</label>
|
||||
<div className="flex flex-col items-center rounded-xl border border-slate-200 p-6 h-40 justify-center bg-slate-50">
|
||||
<QRCodeDisplay
|
||||
value={`${typeof window !== 'undefined' ? window.location.origin + window.location.pathname : ''}?type=text&mode=receive&code=${roomCode}`}
|
||||
size={120}
|
||||
title=""
|
||||
className="w-auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full px-4 py-2.5 bg-blue-500 text-white rounded-lg font-medium shadow transition-all duration-200 mt-3 text-center">
|
||||
使用手机扫码快速访问
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部:取件链接 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-3">
|
||||
<div className="flex-1 code-display rounded-lg p-3 bg-slate-50 border border-slate-200">
|
||||
<div className="text-sm text-slate-700 break-all font-mono leading-relaxed">
|
||||
{`${typeof window !== 'undefined' ? window.location.origin + window.location.pathname : ''}?type=text&mode=receive&code=${roomCode}`}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => copyTransferLink(roomCode)}
|
||||
size="sm"
|
||||
className="bg-purple-500 hover:bg-purple-600 text-white"
|
||||
className="px-4 py-2.5 bg-blue-500 hover:bg-blue-600 text-white rounded-lg font-medium shadow transition-all duration-200 shrink-0"
|
||||
>
|
||||
<Link className="w-4 h-4 mr-2" />
|
||||
复制链接
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSendText}
|
||||
size="sm"
|
||||
className="bg-blue-500 hover:bg-blue-600 text-white"
|
||||
disabled={!textContent.trim()}
|
||||
>
|
||||
<Send className="w-4 h-4 mr-2" />
|
||||
发送文字
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -682,9 +844,7 @@ export default function TextTransfer({
|
||||
console.error('图片加载失败:', img);
|
||||
e.currentTarget.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-20 transition-opacity rounded-lg"></div>
|
||||
|
||||
/>
|
||||
{/* 悬浮按钮组 */}
|
||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
|
||||
<button
|
||||
@@ -731,7 +891,7 @@ export default function TextTransfer({
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-800">加入房间</h2>
|
||||
<p className="text-sm text-slate-600">
|
||||
{(receivedText || textContent) ?
|
||||
{(receivedText !== '' || textContent || hasShownJoinSuccess) ?
|
||||
(isConnected ? '已连接,可以实时查看和编辑' : '连接断开,等待重连') :
|
||||
'输入6位房间码来获取文字内容'
|
||||
}
|
||||
@@ -746,13 +906,13 @@ export default function TextTransfer({
|
||||
<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状态 */}
|
||||
{/* WebSocket信令状态 */}
|
||||
<div className="flex items-center space-x-1">
|
||||
{(receivedText || textContent) ? (
|
||||
isConnected ? (
|
||||
{(receivedText !== '' || textContent || hasShownJoinSuccess) ? (
|
||||
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-blue-500 animate-pulse"></div>
|
||||
<span className="text-blue-600">WS</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -771,10 +931,26 @@ export default function TextTransfer({
|
||||
{/* 分隔符 */}
|
||||
<div className="text-slate-300">|</div>
|
||||
|
||||
{/* WebRTC状态 */}
|
||||
{/* 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>
|
||||
{(receivedText !== '' || textContent || hasShownJoinSuccess) ? (
|
||||
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-400"></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>
|
||||
{connectedUsers > 0 && (
|
||||
@@ -786,51 +962,33 @@ export default function TextTransfer({
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
value={roomCode}
|
||||
onChange={(e) => setRoomCode(e.target.value.toUpperCase().slice(0, 6))}
|
||||
placeholder="请输入房间码"
|
||||
className="text-center text-2xl sm:text-3xl tracking-[0.3em] sm:tracking-[0.5em] font-mono h-12 sm:h-16 border-2 border-slate-200 rounded-xl focus:border-emerald-500 focus:ring-emerald-500 bg-white/80 backdrop-blur-sm"
|
||||
maxLength={6}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={handleJoinRoom}
|
||||
disabled={roomCode.length !== 6 || isLoading}
|
||||
className="w-full h-12 bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white text-lg font-medium rounded-xl shadow-lg"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
||||
连接中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-5 h-5 mr-2" />
|
||||
加入房间
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{(receivedText || textContent) && (
|
||||
<div className="mt-6 space-y-4">
|
||||
{/* 如果已经加入房间(hasShownJoinSuccess)或获取到文字内容,将文字输入框显示在上方 */}
|
||||
{(receivedText !== '' || textContent || hasShownJoinSuccess) && (
|
||||
<div className="space-y-4">
|
||||
<div className="relative">
|
||||
<textarea
|
||||
value={receivedText || textContent}
|
||||
readOnly={currentRole !== 'receiver'}
|
||||
onChange={currentRole === 'receiver' ? handleTextChange : undefined}
|
||||
className="w-full min-h-[150px] p-4 border-2 border-emerald-200 rounded-xl bg-emerald-50/50 backdrop-blur-sm resize-none"
|
||||
readOnly={true}
|
||||
placeholder={receivedText === '' && textContent === '' ? '等待接收文本内容...' : ''}
|
||||
className="w-full min-h-[200px] p-4 border-2 border-emerald-200 rounded-xl bg-emerald-50/50 backdrop-blur-sm resize-none cursor-default"
|
||||
/>
|
||||
{currentRole === 'receiver' && isConnected && (
|
||||
{isConnected && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<div className="flex items-center space-x-1 bg-emerald-100 text-emerald-700 px-2 py-1 rounded-lg text-xs">
|
||||
<div className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div>
|
||||
<span>实时同步</span>
|
||||
<span>WebRTC实时同步</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{currentRole === 'receiver' && !isConnected && (
|
||||
{isWebSocketConnected && !isConnected && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<div className="flex items-center space-x-1 bg-blue-100 text-blue-700 px-2 py-1 rounded-lg text-xs">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
<span>建立数据通道中</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isWebSocketConnected && !isConnected && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<div className="flex items-center space-x-1 bg-orange-100 text-orange-700 px-2 py-1 rounded-lg text-xs">
|
||||
<div className="w-2 h-2 bg-orange-500 rounded-full"></div>
|
||||
@@ -848,6 +1006,38 @@ export default function TextTransfer({
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 只有在未加入房间时才显示输入框和加入按钮 */}
|
||||
{!(receivedText !== '' || textContent || hasShownJoinSuccess) && (
|
||||
<>
|
||||
<Input
|
||||
value={roomCode}
|
||||
onChange={(e) => setRoomCode(e.target.value.toUpperCase().slice(0, 6))}
|
||||
placeholder="请输入房间码"
|
||||
className="text-center text-2xl sm:text-3xl tracking-[0.3em] sm:tracking-[0.5em] font-mono h-12 sm:h-16 border-2 border-slate-200 rounded-xl focus:border-emerald-500 focus:ring-emerald-500 bg-white/80 backdrop-blur-sm"
|
||||
maxLength={6}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={handleJoinRoom}
|
||||
disabled={roomCode.length !== 6 || isLoading}
|
||||
className="w-full h-12 bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600 hover:to-teal-600 text-white text-lg font-medium rounded-xl shadow-lg"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
||||
连接中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-5 h-5 mr-2" />
|
||||
加入房间
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 接收方显示接收到的图片 */}
|
||||
{mode === 'receive' && receivedImages.length > 0 && (
|
||||
@@ -875,9 +1065,7 @@ export default function TextTransfer({
|
||||
e.currentTarget.style.justifyContent = 'center';
|
||||
e.currentTarget.innerHTML = `<span style="color: #64748b; font-size: 12px;">图片加载失败</span>`;
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-20 transition-opacity rounded-lg"></div>
|
||||
|
||||
/>
|
||||
{/* 悬浮按钮组 */}
|
||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
|
||||
<button
|
||||
|
||||
192
chuan-next/src/components/TextTransferWrapper.tsx
Normal file
192
chuan-next/src/components/TextTransferWrapper.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useWebRTCTransfer } from '@/hooks/useWebRTCTransfer';
|
||||
import TextTransfer from './TextTransfer';
|
||||
|
||||
export default function TextTransferWrapper() {
|
||||
const webrtc = useWebRTCTransfer();
|
||||
const [currentRole, setCurrentRole] = useState<'sender' | 'receiver' | undefined>();
|
||||
const [pickupCode, setPickupCode] = useState<string>();
|
||||
|
||||
// 创建房间并建立连接
|
||||
const handleCreateWebSocket = useCallback(async (code: string, role: 'sender' | 'receiver') => {
|
||||
console.log('=== TextTransferWrapper: 开始建立WebRTC连接 ===');
|
||||
console.log('房间码:', code, '角色:', role);
|
||||
|
||||
setCurrentRole(role);
|
||||
setPickupCode(code);
|
||||
|
||||
try {
|
||||
// 建立WebRTC连接
|
||||
await webrtc.text.connect(code, role);
|
||||
console.log('WebRTC连接请求已发送');
|
||||
} catch (error) {
|
||||
console.error('建立WebRTC连接失败:', error);
|
||||
}
|
||||
}, [webrtc.text.connect]);
|
||||
|
||||
// 处理文字消息接收
|
||||
useEffect(() => {
|
||||
if (!webrtc.text.onMessageReceived) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unsubscribeMessage = webrtc.text.onMessageReceived((message) => {
|
||||
console.log('收到文字消息:', message);
|
||||
|
||||
// 检查是否是图片消息
|
||||
if (message.text && message.text.startsWith('[IMAGE]')) {
|
||||
const imageData = message.text.substring(7); // 移除 '[IMAGE]' 前缀
|
||||
window.dispatchEvent(new CustomEvent('websocket-message', {
|
||||
detail: {
|
||||
type: 'image-send',
|
||||
payload: { imageData }
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
// 普通文字消息
|
||||
window.dispatchEvent(new CustomEvent('websocket-message', {
|
||||
detail: {
|
||||
type: 'text-content',
|
||||
payload: { text: message.text }
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribeMessage;
|
||||
}, [webrtc.text.onMessageReceived]);
|
||||
|
||||
// 处理实时文本更新接收
|
||||
useEffect(() => {
|
||||
if (!webrtc.text.onRealTimeText) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unsubscribeRealTime = webrtc.text.onRealTimeText((text: string) => {
|
||||
console.log('收到实时文本更新:', text);
|
||||
window.dispatchEvent(new CustomEvent('websocket-message', {
|
||||
detail: {
|
||||
type: 'text-update',
|
||||
payload: { text }
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
return unsubscribeRealTime;
|
||||
}, [webrtc.text.onRealTimeText]);
|
||||
|
||||
// 监听连接状态变化并发送事件
|
||||
useEffect(() => {
|
||||
console.log('WebRTC文字传输状态:', {
|
||||
isConnected: webrtc.text.isConnected,
|
||||
isConnecting: webrtc.text.isConnecting,
|
||||
isWebSocketConnected: webrtc.text.isWebSocketConnected,
|
||||
error: webrtc.text.connectionError
|
||||
});
|
||||
|
||||
// WebSocket信令连接成功时发送事件
|
||||
if (webrtc.text.isWebSocketConnected) {
|
||||
console.log('WebSocket信令连接成功,通知TextTransfer组件');
|
||||
window.dispatchEvent(new CustomEvent('websocket-message', {
|
||||
detail: {
|
||||
type: 'websocket-signaling-connected',
|
||||
payload: { code: pickupCode, role: currentRole }
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// WebRTC数据通道连接中
|
||||
if (webrtc.text.isConnecting) {
|
||||
console.log('WebRTC数据通道连接中,通知TextTransfer组件');
|
||||
window.dispatchEvent(new CustomEvent('websocket-message', {
|
||||
detail: {
|
||||
type: 'webrtc-connecting',
|
||||
payload: { code: pickupCode, role: currentRole }
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// WebRTC数据通道连接成功
|
||||
if (webrtc.text.isConnected) {
|
||||
console.log('WebRTC数据通道连接成功,通知TextTransfer组件');
|
||||
window.dispatchEvent(new CustomEvent('websocket-message', {
|
||||
detail: {
|
||||
type: 'webrtc-connected',
|
||||
payload: { code: pickupCode, role: currentRole }
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (webrtc.text.connectionError) {
|
||||
console.error('WebRTC连接错误:', webrtc.text.connectionError);
|
||||
window.dispatchEvent(new CustomEvent('websocket-message', {
|
||||
detail: {
|
||||
type: 'webrtc-error',
|
||||
payload: { message: webrtc.text.connectionError }
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, [webrtc.text.isConnected, webrtc.text.isConnecting, webrtc.text.isWebSocketConnected, webrtc.text.connectionError, pickupCode, currentRole]);
|
||||
|
||||
// 模拟WebSocket对象来保持与TextTransfer的兼容性
|
||||
const mockWebSocket = {
|
||||
send: (data: string) => {
|
||||
// 数据必须通过WebRTC数据通道发送,不能通过WebSocket
|
||||
if (!webrtc.text.isConnected) {
|
||||
console.warn('WebRTC数据通道未建立,无法发送数据。当前状态:', {
|
||||
isWebSocketConnected: webrtc.text.isWebSocketConnected,
|
||||
isConnected: webrtc.text.isConnected
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const message = JSON.parse(data);
|
||||
console.log('通过WebRTC数据通道发送消息:', message.type);
|
||||
|
||||
switch (message.type) {
|
||||
case 'text-update':
|
||||
// 通过WebRTC数据通道发送实时文本更新
|
||||
if (webrtc.text.sendRealTimeText) {
|
||||
webrtc.text.sendRealTimeText(message.payload.text);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text-send':
|
||||
// 通过WebRTC数据通道发送完整文字消息
|
||||
webrtc.text.sendMessage(message.payload.text);
|
||||
break;
|
||||
|
||||
case 'image-send':
|
||||
// 通过WebRTC数据通道发送图片数据
|
||||
const imageMessage = `[IMAGE]${message.payload.imageData}`;
|
||||
webrtc.text.sendMessage(imageMessage);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('未处理的消息类型:', message.type);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('通过WebRTC发送消息失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
readyState: webrtc.text.isConnected ? 1 : 0, // WebSocket.OPEN = 1,但实际是WebRTC状态
|
||||
close: () => {
|
||||
webrtc.text.disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TextTransfer
|
||||
websocket={mockWebSocket as any}
|
||||
isConnected={webrtc.text.isConnected} // WebRTC连接状态
|
||||
isWebSocketConnected={webrtc.text.isWebSocketConnected} // WebSocket信令状态
|
||||
currentRole={currentRole}
|
||||
pickupCode={pickupCode}
|
||||
onCreateWebSocket={handleCreateWebSocket}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -139,9 +139,11 @@ export function useWebRTCTransfer() {
|
||||
disconnect: textTransfer.disconnect,
|
||||
sendMessage: textTransfer.sendMessage,
|
||||
sendTypingStatus: textTransfer.sendTypingStatus,
|
||||
sendRealTimeText: textTransfer.sendRealTimeText,
|
||||
clearMessages: textTransfer.clearMessages,
|
||||
onMessageReceived: textTransfer.onMessageReceived,
|
||||
onTypingStatus: textTransfer.onTypingStatus,
|
||||
onRealTimeText: textTransfer.onRealTimeText,
|
||||
},
|
||||
|
||||
// 整体状态(用于 UI 显示)
|
||||
|
||||
@@ -180,6 +180,12 @@ export function useTextTransferBusiness() {
|
||||
});
|
||||
}, [webrtcCore.getChannelState, webrtcCore.sendMessage]);
|
||||
|
||||
// 注册实时文本回调
|
||||
const onRealTimeText = useCallback((callback: RealTimeTextCallback) => {
|
||||
realTimeTextCallbacks.current.add(callback);
|
||||
return () => { realTimeTextCallbacks.current.delete(callback); };
|
||||
}, []);
|
||||
|
||||
// 清空消息
|
||||
const clearMessages = useCallback(() => {
|
||||
updateState({ messages: [] });
|
||||
@@ -220,10 +226,12 @@ export function useTextTransferBusiness() {
|
||||
disconnect: webrtcCore.disconnect,
|
||||
sendMessage,
|
||||
sendTypingStatus,
|
||||
sendRealTimeText,
|
||||
clearMessages,
|
||||
|
||||
// 回调注册
|
||||
onMessageReceived,
|
||||
onTypingStatus,
|
||||
onRealTimeText,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,11 +12,13 @@ interface ApiResponse {
|
||||
}
|
||||
|
||||
interface CreateRoomData {
|
||||
roomType?: string;
|
||||
type?: string;
|
||||
content?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
interface CreateTextRoomData {
|
||||
type: string;
|
||||
content: string;
|
||||
password?: string;
|
||||
}
|
||||
@@ -92,7 +94,7 @@ export class ClientAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建房间
|
||||
* 创建房间(统一接口)
|
||||
*/
|
||||
async createRoom(data: CreateRoomData): Promise<ApiResponse> {
|
||||
return this.post('/api/create-room', data);
|
||||
@@ -101,29 +103,42 @@ export class ClientAPI {
|
||||
/**
|
||||
* 创建文本房间
|
||||
*/
|
||||
async createTextRoom(data: CreateTextRoomData): Promise<ApiResponse> {
|
||||
return this.post('/api/create-text-room', data);
|
||||
async createTextRoom(content: string): Promise<ApiResponse> {
|
||||
return this.post('/api/create-room', {
|
||||
type: 'text',
|
||||
content: content
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文本内容
|
||||
*/
|
||||
async getTextContent(roomId: string): Promise<ApiResponse> {
|
||||
return this.get(`/api/get-text-content?roomId=${roomId}`);
|
||||
async getTextContent(code: string): Promise<ApiResponse> {
|
||||
return this.get(`/api/get-text-content?code=${code}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文本内容
|
||||
*/
|
||||
async updateTextContent(code: string, content: string): Promise<ApiResponse> {
|
||||
return this.post('/api/update-text-content', {
|
||||
code: code,
|
||||
content: content
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间信息
|
||||
*/
|
||||
async getRoomInfo(roomId: string): Promise<ApiResponse> {
|
||||
return this.get(`/api/room-info?roomId=${roomId}`);
|
||||
async getRoomInfo(code: string): Promise<ApiResponse> {
|
||||
return this.get(`/api/room-info?code=${code}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间状态
|
||||
* 获取WebRTC房间状态
|
||||
*/
|
||||
async getRoomStatus(roomId: string): Promise<ApiResponse> {
|
||||
return this.get(`/api/room-status?roomId=${roomId}`);
|
||||
async getWebRTCRoomStatus(code: string): Promise<ApiResponse> {
|
||||
return this.get(`/api/webrtc-room-status?code=${code}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -62,7 +62,8 @@ func main() {
|
||||
|
||||
// WebRTC房间API
|
||||
r.Post("/api/create-room", h.CreateRoomHandler)
|
||||
r.Get("/api/room-info", h.RoomStatusHandler)
|
||||
r.Get("/api/room-info", h.WebRTCRoomStatusHandler)
|
||||
r.Get("/api/webrtc-room-status", h.WebRTCRoomStatusHandler)
|
||||
|
||||
// 构建服务器地址
|
||||
addr := fmt.Sprintf(":%d", *port)
|
||||
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"chuan/internal/services"
|
||||
@@ -37,6 +38,7 @@ func (h *Handler) CreateRoomHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 创建新房间
|
||||
code := h.webrtcService.CreateNewRoom()
|
||||
log.Printf("创建房间成功: %s", code)
|
||||
|
||||
// 构建响应
|
||||
response := map[string]interface{}{
|
||||
@@ -48,8 +50,8 @@ func (h *Handler) CreateRoomHandler(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// RoomStatusHandler 获取WebRTC房间状态API
|
||||
func (h *Handler) RoomStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// WebRTCRoomStatusHandler WebRTC房间状态API
|
||||
func (h *Handler) WebRTCRoomStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// 设置响应为JSON格式
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@@ -61,35 +63,45 @@ func (h *Handler) RoomStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// 从查询参数获取房间代码
|
||||
code := r.URL.Query().Get("code")
|
||||
if code == "" || len(code) != 6 {
|
||||
if code == "" {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "请提供正确的6位房间码",
|
||||
"message": "缺少房间代码",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取WebRTC房间状态
|
||||
webrtcStatus := h.webrtcService.GetRoomStatus(code)
|
||||
// 获取房间状态
|
||||
status := h.webrtcService.GetRoomStatus(code)
|
||||
|
||||
// 如果房间不存在,返回不存在状态
|
||||
if !webrtcStatus["exists"].(bool) {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "房间不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
response := map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "房间状态获取成功",
|
||||
"sender_online": webrtcStatus["sender_online"],
|
||||
"receiver_online": webrtcStatus["receiver_online"],
|
||||
"created_at": webrtcStatus["created_at"],
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
json.NewEncoder(w).Encode(status)
|
||||
}
|
||||
|
||||
// GetRoomStatusHandler 获取房间状态API
|
||||
func (h *Handler) GetRoomStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if r.Method != http.MethodGet {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "方法不允许",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取房间码
|
||||
code := r.URL.Query().Get("code")
|
||||
if code == "" {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "房间码不能为空",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取房间状态
|
||||
status := h.webrtcService.GetRoomStatus(code)
|
||||
json.NewEncoder(w).Encode(status)
|
||||
}
|
||||
|
||||
@@ -237,8 +237,9 @@ func (ws *WebRTCService) CreateNewRoom() string {
|
||||
|
||||
// generatePickupCode 生成6位取件码
|
||||
func (ws *WebRTCService) generatePickupCode() string {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
code := rand.Intn(900000) + 100000
|
||||
source := rand.NewSource(time.Now().UnixNano())
|
||||
rng := rand.New(source)
|
||||
code := rng.Intn(900000) + 100000
|
||||
return fmt.Sprintf("%d", code)
|
||||
}
|
||||
|
||||
@@ -312,11 +313,14 @@ func (ws *WebRTCService) GetRoomStatus(code string) map[string]interface{} {
|
||||
room := ws.rooms[code]
|
||||
if room == nil {
|
||||
return map[string]interface{}{
|
||||
"exists": false,
|
||||
"success": false,
|
||||
"exists": false,
|
||||
"message": "房间不存在或已过期",
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"success": true,
|
||||
"exists": true,
|
||||
"sender_online": room.Sender != nil,
|
||||
"receiver_online": room.Receiver != nil,
|
||||
|
||||
Reference in New Issue
Block a user