Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
9fb248dff0 | |||
1b4ba1cb61 | |||
e5f7b5e6dc | |||
21287e0874 | |||
5bc695bde3 | |||
b8645a68ed | |||
013670b78f | |||
16e9d663f4 | |||
becbc137c5 | |||
d41d516f19 | |||
0a24393dcb |
@ -25,7 +25,8 @@ COPY --from=frontend-builder /app/frontend/dist /usr/share/nginx/html
|
||||
COPY --from=backend-builder /app/ushare /usr/local/bin/ushare
|
||||
|
||||
# 配置 Nginx
|
||||
COPY deployment/nginx.conf /etc/nginx/nginx.conf
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
COPY deployment/nginx.conf /etc/nginx/conf.d
|
||||
COPY deployment/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
@ -1,50 +1,23 @@
|
||||
user root;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
location /api {
|
||||
proxy_pass http://localhost:9119;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://localhost:9119;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location /ushare {
|
||||
proxy_pass http://localhost:9119;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_max_temp_file_size 0;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
client_max_body_size 5M;
|
||||
}
|
||||
location /ushare {
|
||||
proxy_pass http://localhost:9119;
|
||||
const rtc = new RTCPeerConnection({iceServers: [{urls: "stun:stun.qq.com:3478"}]})
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ export const useFileUpload = () => {
|
||||
setProgress(0);
|
||||
|
||||
try {
|
||||
console.log(`[D] api.Upload: upload file = ${file.name}, size = ${file.size}`, file);
|
||||
const url = `/api/ushare/${file.name}`;
|
||||
|
||||
// 1. 初始化上传
|
||||
@ -25,7 +26,7 @@ export const useFileUpload = () => {
|
||||
});
|
||||
|
||||
if (!res1.ok) {
|
||||
console.log(`[W] upload: put file not ok, status = ${res1.status}, res = ${await res1.text()}`)
|
||||
console.log(`[D] upload: put file not ok, status = ${res1.status}, res = ${await res1.text()}`)
|
||||
if (res1.status === 401) {
|
||||
window.location.href = "/login?next=/share"
|
||||
return ""
|
||||
|
@ -17,8 +17,7 @@ const useClass = createUseStyles({
|
||||
background: "rgba(0, 0, 0, 0.5)",
|
||||
backdropFilter: "blur(2px)"
|
||||
},
|
||||
background: "rgba(212,212,212,0.85)",
|
||||
backdropFilter: "blur(8px)",
|
||||
background: "rgba(255, 255, 255, 0.4)",
|
||||
},
|
||||
dialog_content: {
|
||||
padding: "1.5rem",
|
||||
|
@ -50,6 +50,7 @@ export const Drawer: React.FC<DrawerProps> = ({
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
console.log('[D] escape close:', close)
|
||||
close()
|
||||
}
|
||||
};
|
||||
|
@ -1,87 +1,6 @@
|
||||
import React, {useState} from 'react';
|
||||
import {Dialog} from "../../../component/dialog/dialog.tsx";
|
||||
import {ReceivedMessage} from "./types.ts";
|
||||
import {createUseStyles} from "react-jss";
|
||||
|
||||
const useStyles = createUseStyles({
|
||||
root: {
|
||||
marginBottom: '1rem',
|
||||
},
|
||||
sender: {
|
||||
margin: '0 0 0.5rem 0',
|
||||
fontSize: '0.9rem',
|
||||
color: '#666',
|
||||
},
|
||||
msgBox: {
|
||||
background: 'rgba(255,255,255,0.96)',
|
||||
color: '#222',
|
||||
padding: '1rem',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ddd',
|
||||
maxHeight: '400px',
|
||||
minHeight: '80px',
|
||||
overflowY: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
},
|
||||
downloadLink: {
|
||||
color: '#007aff',
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
btnRow: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: '0.5rem',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
marginTop: '8px',
|
||||
},
|
||||
copySuccess: {
|
||||
color: '#28a745',
|
||||
fontSize: '0.9rem',
|
||||
marginRight: '0.5rem',
|
||||
animation: 'fadeIn 0.3s ease-in',
|
||||
},
|
||||
copyBtn: {
|
||||
padding: '8px 16px',
|
||||
background: '#007aff',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.9rem',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': {
|
||||
background: '#0056b3',
|
||||
},
|
||||
},
|
||||
copyBtnSuccess: {
|
||||
background: '#28a745',
|
||||
},
|
||||
closeBtn: {
|
||||
padding: '8px 16px',
|
||||
background: '#6c757d',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.9rem',
|
||||
transition: 'all 0.2s',
|
||||
},
|
||||
progressBar: {
|
||||
width: '100%',
|
||||
height: 8,
|
||||
background: '#eee',
|
||||
borderRadius: 4,
|
||||
margin: '12px 0',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
progressInner: {
|
||||
height: '100%',
|
||||
background: '#4dabf7',
|
||||
transition: 'width 0.3s',
|
||||
},
|
||||
});
|
||||
|
||||
interface MessageDialogProps {
|
||||
open: boolean;
|
||||
@ -91,14 +10,16 @@ interface MessageDialogProps {
|
||||
|
||||
export const MessageDialog: React.FC<MessageDialogProps> = ({open, message, onClose}) => {
|
||||
const [copySuccess, setCopySuccess] = useState(false);
|
||||
const classes = useStyles();
|
||||
|
||||
const handleCopyMessage = () => {
|
||||
if (message) {
|
||||
navigator.clipboard.writeText(message.text || '').then(() => {
|
||||
console.log('消息已复制到剪贴板');
|
||||
setCopySuccess(true);
|
||||
// 2秒后隐藏成功提示
|
||||
setTimeout(() => setCopySuccess(false), 2000);
|
||||
}).catch(() => {
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
alert('复制失败,请手动复制');
|
||||
});
|
||||
}
|
||||
@ -111,49 +32,82 @@ export const MessageDialog: React.FC<MessageDialogProps> = ({open, message, onCl
|
||||
onClose={onClose}
|
||||
footer={false}
|
||||
>
|
||||
<div className={classes.root}>
|
||||
<p className={classes.sender}>
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<p style={{ margin: '0 0 0.5rem 0', fontSize: '0.9rem', color: '#666' }}>
|
||||
来自: {message?.sender}
|
||||
</p>
|
||||
<div className={classes.msgBox}>
|
||||
<div style={{
|
||||
background: '#f5f5f5',
|
||||
padding: '1rem',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ddd',
|
||||
maxHeight: '200px',
|
||||
overflowY: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word'
|
||||
}}>
|
||||
{message?.isFile ? (
|
||||
<>
|
||||
<div>📎 文件: {message.fileName} ({message.fileSize ? (message.fileSize/1024).toFixed(1) : ''} KB)</div>
|
||||
{message.receiving ? (
|
||||
<>
|
||||
<div style={{fontSize: '0.95em', color: '#888'}}>正在接收... {Math.round((message.progress||0)*100)}%</div>
|
||||
<div className={classes.progressBar}>
|
||||
<div className={classes.progressInner} style={{width: `${Math.round((message.progress||0)*100)}%`}} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<a href={message.fileBlobUrl} download={message.fileName} className={classes.downloadLink}>
|
||||
点击下载
|
||||
</a>
|
||||
)}
|
||||
<a href={message.fileBlobUrl} download={message.fileName} style={{color:'#007aff',textDecoration:'underline'}}>
|
||||
点击下载
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
message?.text
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.btnRow}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', gap: '0.5rem', justifyContent: 'flex-end', alignItems: 'center', marginTop: '8px' }}>
|
||||
{copySuccess && (
|
||||
<span className={classes.copySuccess}>
|
||||
<span style={{
|
||||
color: '#28a745',
|
||||
fontSize: '0.9rem',
|
||||
marginRight: '0.5rem',
|
||||
animation: 'fadeIn 0.3s ease-in'
|
||||
}}>
|
||||
✓ 已复制
|
||||
</span>
|
||||
)}
|
||||
{!message?.isFile && (
|
||||
<button
|
||||
onClick={handleCopyMessage}
|
||||
className={copySuccess ? `${classes.copyBtn} ${classes.copyBtnSuccess}` : classes.copyBtn}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: copySuccess ? '#28a745' : '#007aff',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.9rem',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!copySuccess) {
|
||||
e.currentTarget.style.backgroundColor = '#0056b3';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!copySuccess) {
|
||||
e.currentTarget.style.backgroundColor = '#007aff';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{copySuccess ? '已复制' : '复制消息'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={classes.closeBtn}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: '#6c757d',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.9rem',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { WSMessage, ReceivedMessage} from "./types.ts";
|
||||
import {Resp} from "../../../interface/response.ts";
|
||||
import {Client, WSMessage, ReceivedMessage} from "./types.ts";
|
||||
import {useLocalStore} from "../../../store/local.ts";
|
||||
|
||||
// 文件接收缓存
|
||||
@ -8,7 +9,7 @@ export const handleFileChunk = (chunk: any, onFileReceived: (msg: import("./type
|
||||
if (chunk.type === 'file') {
|
||||
// 文件元信息,初始化缓存
|
||||
fileReceiveCache[chunk.name + '_' + chunk.timestamp] = {
|
||||
chunks: new Array(chunk.totalChunks),
|
||||
chunks: [],
|
||||
total: chunk.totalChunks,
|
||||
received: 0,
|
||||
name: chunk.name,
|
||||
@ -16,16 +17,6 @@ export const handleFileChunk = (chunk: any, onFileReceived: (msg: import("./type
|
||||
sender: chunk.sender,
|
||||
timestamp: chunk.timestamp
|
||||
};
|
||||
// 首次弹出进度
|
||||
onFileReceived({
|
||||
sender: chunk.sender,
|
||||
timestamp: chunk.timestamp,
|
||||
fileName: chunk.name,
|
||||
fileSize: chunk.size,
|
||||
isFile: true,
|
||||
progress: 0,
|
||||
receiving: true
|
||||
});
|
||||
} else if (chunk.type === 'file-chunk') {
|
||||
const key = chunk.name + '_' + chunk.timestamp;
|
||||
const cache = fileReceiveCache[key];
|
||||
@ -34,49 +25,21 @@ export const handleFileChunk = (chunk: any, onFileReceived: (msg: import("./type
|
||||
const uint8 = new Uint8Array(chunk.data);
|
||||
cache.chunks[chunk.chunkIndex] = uint8.buffer;
|
||||
cache.received++;
|
||||
// 实时回调进度
|
||||
if (cache.received < cache.total) {
|
||||
// 全部收到
|
||||
if (cache.received === cache.total) {
|
||||
const blob = new Blob(cache.chunks);
|
||||
const url = URL.createObjectURL(blob);
|
||||
onFileReceived({
|
||||
sender: cache.sender,
|
||||
timestamp: cache.timestamp,
|
||||
fileName: cache.name,
|
||||
fileSize: cache.size,
|
||||
isFile: true,
|
||||
progress: cache.received / cache.total,
|
||||
receiving: true
|
||||
fileBlobUrl: url,
|
||||
isFile: true
|
||||
});
|
||||
delete fileReceiveCache[key];
|
||||
}
|
||||
}
|
||||
} else if (chunk.type === 'file-end') {
|
||||
const key = chunk.name + '_' + chunk.timestamp;
|
||||
const cache = fileReceiveCache[key];
|
||||
if (cache && cache.received === cache.total) {
|
||||
const blob = new Blob(cache.chunks);
|
||||
const url = URL.createObjectURL(blob);
|
||||
onFileReceived({
|
||||
sender: cache.sender,
|
||||
timestamp: cache.timestamp,
|
||||
fileName: cache.name,
|
||||
fileSize: cache.size,
|
||||
fileBlobUrl: url,
|
||||
isFile: true,
|
||||
progress: 1,
|
||||
receiving: false
|
||||
});
|
||||
delete fileReceiveCache[key];
|
||||
} else if (cache) {
|
||||
// 分块未齐全,提示异常
|
||||
onFileReceived({
|
||||
sender: cache.sender,
|
||||
timestamp: cache.timestamp,
|
||||
fileName: cache.name,
|
||||
fileSize: cache.size,
|
||||
isFile: true,
|
||||
progress: cache.received / cache.total,
|
||||
receiving: true,
|
||||
text: '文件分块未齐全,接收失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -97,18 +60,21 @@ export class RTCHandler {
|
||||
|
||||
// 更新回调函数的方法
|
||||
updateCallbacks = (newCallbacks: RTCHandlerCallbacks) => {
|
||||
console.log('[D] Updating RTC handler callbacks');
|
||||
this.callbacks = newCallbacks;
|
||||
};
|
||||
|
||||
setupDataChannel = async (ch: RTCDataChannel, type: 'sender' | 'receiver') => {
|
||||
console.log(`[D] Setting up data channel for type: ${type}`);
|
||||
ch.onopen = () => {
|
||||
console.log(`[D] 通道已打开!类型: ${type}`);
|
||||
console.log('[D] Calling onChannelOpen callback with type:', type);
|
||||
this.callbacks.onChannelOpen(type);
|
||||
useLocalStore.getState().setChannel(ch);
|
||||
};
|
||||
|
||||
ch.onmessage = (e) => {
|
||||
// console.log('[D] Received message:', e.data);
|
||||
console.log('[D] Received message:', e.data);
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'message') {
|
||||
@ -118,9 +84,10 @@ export class RTCHandler {
|
||||
timestamp: Date.now(),
|
||||
sender: data.sender || '未知用户'
|
||||
};
|
||||
console.log('[D] Calling onMessageReceived callback with message:', message);
|
||||
this.callbacks.onMessageReceived(message);
|
||||
} else if (data.type === 'file' || data.type === 'file-chunk' || data.type === 'file-end') {
|
||||
// 处理文件相关消息
|
||||
} else if (data.type === 'file') {
|
||||
// 处理文件消息
|
||||
handleFileChunk(data, this.callbacks.onMessageReceived);
|
||||
}
|
||||
} catch (error) {
|
||||
@ -130,23 +97,27 @@ export class RTCHandler {
|
||||
timestamp: Date.now(),
|
||||
sender: '未知用户'
|
||||
};
|
||||
console.log('[D] Calling onMessageReceived callback with plain text message:', message);
|
||||
this.callbacks.onMessageReceived(message);
|
||||
}
|
||||
};
|
||||
|
||||
ch.onclose = () => {
|
||||
console.log('[D] 通道关闭');
|
||||
console.log('[D] Calling onChannelClose callback');
|
||||
this.callbacks.onChannelClose();
|
||||
useLocalStore.getState().setChannel();
|
||||
};
|
||||
};
|
||||
|
||||
handleBubbleClick = async (bubbleId: string, currentUserId: string) => {
|
||||
console.log(`[D] click id = ${bubbleId}`);
|
||||
const current_rtc = this.rtcRef.current;
|
||||
if (!current_rtc) return;
|
||||
|
||||
current_rtc.onnegotiationneeded = async () => {
|
||||
const offer = await current_rtc.createOffer();
|
||||
console.log('[D] offer created', offer);
|
||||
await current_rtc.setLocalDescription(offer);
|
||||
const data = {
|
||||
id: bubbleId,
|
||||
@ -161,6 +132,7 @@ export class RTCHandler {
|
||||
};
|
||||
|
||||
current_rtc.onicecandidate = async (e) => {
|
||||
console.log('[D] on candidate ', e);
|
||||
await fetch('/api/ulocal/candidate', {
|
||||
method: 'POST',
|
||||
headers: {"Content-Type": "application/json"},
|
||||
@ -176,7 +148,7 @@ export class RTCHandler {
|
||||
let current_id: string;
|
||||
let current_rtc: RTCPeerConnection | null;
|
||||
const msg = JSON.parse(e.data) as WSMessage;
|
||||
// console.log('[D] ws event msg =', msg);
|
||||
console.log('[D] ws event msg =', msg);
|
||||
|
||||
switch (msg.type) {
|
||||
case "enter":
|
||||
@ -241,6 +213,7 @@ export class RTCHandler {
|
||||
return;
|
||||
}
|
||||
if (!candidate_data.candidate) {
|
||||
console.log('[W] candidate data null');
|
||||
return;
|
||||
}
|
||||
await current_rtc.addIceCandidate(candidate_data.candidate);
|
||||
@ -250,8 +223,10 @@ export class RTCHandler {
|
||||
|
||||
sendMessage = (msg: string, files: File[], senderName: string) => {
|
||||
const ch = useLocalStore.getState().channel;
|
||||
console.log('[D] ready to send:', msg, files, ch);
|
||||
const CHUNK_SIZE = 64 * 1024; // 64KB
|
||||
const BUFFERED_AMOUNT_THRESHOLD = 1 * 1024 * 1024; // 1MB
|
||||
|
||||
if (ch && ch.readyState === 'open') {
|
||||
if (msg.trim()) {
|
||||
// 发送文本消息
|
||||
@ -279,28 +254,23 @@ export class RTCHandler {
|
||||
};
|
||||
ch.send(JSON.stringify(fileData));
|
||||
|
||||
ch.bufferedAmountLowThreshold = BUFFERED_AMOUNT_THRESHOLD;
|
||||
|
||||
let offset = 0;
|
||||
let chunkIndex = 0;
|
||||
const reader = new FileReader();
|
||||
|
||||
const sendNextChunk = () => {
|
||||
if (offset >= file.size) {
|
||||
// 分块全部发送完毕,发送 file-end
|
||||
ch.send(JSON.stringify({
|
||||
type: 'file-end',
|
||||
name: file.name,
|
||||
timestamp,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (ch.bufferedAmount > BUFFERED_AMOUNT_THRESHOLD) {
|
||||
function sendNextChunk() {
|
||||
if (offset >= file.size) return;
|
||||
if (ch && ch.bufferedAmount && ch.bufferedAmount > BUFFERED_AMOUNT_THRESHOLD) {
|
||||
// 等待缓冲区变低
|
||||
ch.addEventListener('bufferedamountlow', sendNextChunk, { once: true });
|
||||
return;
|
||||
}
|
||||
const slice = file.slice(offset, offset + CHUNK_SIZE);
|
||||
reader.onload = (e) => {
|
||||
if (e.target?.result) {
|
||||
ch.send(JSON.stringify({
|
||||
ch?.send(JSON.stringify({
|
||||
type: 'file-chunk',
|
||||
name: file.name,
|
||||
timestamp,
|
||||
@ -314,7 +284,7 @@ export class RTCHandler {
|
||||
}
|
||||
};
|
||||
reader.readAsArrayBuffer(slice);
|
||||
};
|
||||
}
|
||||
sendNextChunk();
|
||||
});
|
||||
}
|
||||
|
@ -6,12 +6,11 @@ interface SendDialogProps {
|
||||
open: boolean;
|
||||
onSend: (msg: string, files: File[]) => void;
|
||||
onClose: () => void;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const SendDialog: React.FC<SendDialogProps> = ({open, onSend, onClose, name}) => {
|
||||
export const SendDialog: React.FC<SendDialogProps> = ({open, onSend, onClose}) => {
|
||||
return (
|
||||
<Dialog open={open} title={`发送消息给${name}`} onClose={onClose}>
|
||||
<Dialog open={open} title="发送消息" onClose={onClose}>
|
||||
<Sender onSend={onSend} />
|
||||
</Dialog>
|
||||
);
|
||||
|
@ -150,10 +150,17 @@ export const Sender: React.FC<SenderProps> = ({onSend}) => {
|
||||
</div>
|
||||
<div className={classes.buttons}>
|
||||
<input type="file" ref={fileInputRef} hidden onChange={handleFileSelect}/>
|
||||
<button className={classes.action_btn} onClick={() => {
|
||||
<button
|
||||
className={classes.action_btn}
|
||||
style={{ opacity: 0.5, cursor: 'not-allowed' }}
|
||||
onClick={() => { message.warning('暂不支持文件发送') }}
|
||||
>
|
||||
📁 选择文件
|
||||
</button>
|
||||
{/* <button className={classes.action_btn} onClick={() => {
|
||||
fileInputRef.current && fileInputRef.current.click()
|
||||
}}>📁 选择文件
|
||||
</button>
|
||||
</button> */}
|
||||
<button className={classes.action_btn} onClick={handleSubmit}>✈️ 发送</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -33,6 +33,4 @@ export interface ReceivedMessage {
|
||||
fileSize?: number;
|
||||
fileBlobUrl?: string;
|
||||
isFile?: boolean;
|
||||
progress?: number; // 0-1
|
||||
receiving?: boolean; // 是否正在接收
|
||||
}
|
@ -55,15 +55,14 @@ const useClass = createUseStyles({
|
||||
|
||||
export const LocalSharing: React.FC = () => {
|
||||
const classes = useClass();
|
||||
const {id, name, set, } = useLocalStore();
|
||||
const [_rtc, setRTC] = useState<RTCPeerConnection>();
|
||||
const {id, name, set, setChannel} = useLocalStore();
|
||||
const [rtc, setRTC] = useState<RTCPeerConnection>();
|
||||
const rtcRef = useRef<RTCPeerConnection | null>(null);
|
||||
const [clients, setClients] = useState<Client[]>([]);
|
||||
const {connect, close} = useWebsocket({});
|
||||
const [open, setOpen] = useState<{ send: boolean; receive: boolean }>({send: false, receive: false});
|
||||
const [receivedMessage, setReceivedMessage] = useState<ReceivedMessage | null>(null);
|
||||
const [showMessageDialog, setShowMessageDialog] = useState(false);
|
||||
const [receivingFile, setReceivingFile] = useState<ReceivedMessage | null>(null);
|
||||
|
||||
// RTC处理器的回调函数 - 使用useCallback确保稳定性
|
||||
const onChannelOpen = useCallback((type: 'sender' | 'receiver') => {
|
||||
@ -72,16 +71,9 @@ export const LocalSharing: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
const onMessageReceived = useCallback((message: ReceivedMessage) => {
|
||||
if (message.isFile && message.receiving) {
|
||||
setReceivingFile(message);
|
||||
} else if (message.isFile && !message.receiving) {
|
||||
setReceivingFile(null);
|
||||
setReceivedMessage(message);
|
||||
setShowMessageDialog(true);
|
||||
} else {
|
||||
setReceivedMessage(message);
|
||||
setShowMessageDialog(true);
|
||||
}
|
||||
console.log('[D] Message received:', message);
|
||||
setReceivedMessage(message);
|
||||
setShowMessageDialog(true);
|
||||
}, []);
|
||||
|
||||
const onChannelClose = useCallback(() => {
|
||||
@ -101,6 +93,7 @@ export const LocalSharing: React.FC = () => {
|
||||
// 更新RTC处理器的回调函数
|
||||
useEffect(() => {
|
||||
if (rtcHandlerRef.current) {
|
||||
console.log('[D] Updating RTC handler callbacks');
|
||||
rtcHandlerRef.current.updateCallbacks(rtcCallbacks);
|
||||
}
|
||||
}, [rtcCallbacks]);
|
||||
@ -115,6 +108,7 @@ export const LocalSharing: React.FC = () => {
|
||||
|
||||
const handleWSEvent = async (e: MessageEvent) => {
|
||||
const msgData = JSON.parse(e.data);
|
||||
console.log('[D] ws event msg =', msgData);
|
||||
|
||||
if (msgData.type === "enter" || msgData.type === "leave") {
|
||||
await updateClients();
|
||||
@ -135,6 +129,7 @@ export const LocalSharing: React.FC = () => {
|
||||
|
||||
const handleBubbleClick = async (bubble: any) => {
|
||||
setOpen({send: true, receive: false});
|
||||
console.log('[D] Bubble clicked:', bubble.id, 'Current RTC handler:', rtcHandlerRef.current);
|
||||
if (rtcHandlerRef.current) {
|
||||
try {
|
||||
await rtcHandlerRef.current.handleBubbleClick(bubble.id, id);
|
||||
@ -170,6 +165,7 @@ export const LocalSharing: React.FC = () => {
|
||||
const response = await fetch('/api/ulocal/register', {method: 'POST'});
|
||||
const data = ((await response.json()) as Resp<{ id: string; name: string }>).data;
|
||||
set(data.id, data.name);
|
||||
console.log(`[D] register id = ${data.id}`);
|
||||
connect(`/api/ulocal/ws?id=${data.id}`, {fn: handleWSEvent});
|
||||
await updateClients();
|
||||
|
||||
@ -178,6 +174,7 @@ export const LocalSharing: React.FC = () => {
|
||||
setRTC(_rtc);
|
||||
|
||||
// 在RTC连接创建后立即创建处理器实例
|
||||
console.log('[D] Creating RTC handler after connection setup');
|
||||
rtcHandlerRef.current = new RTCHandler(rtcRef, rtcCallbacks);
|
||||
|
||||
return () => {
|
||||
@ -192,14 +189,6 @@ export const LocalSharing: React.FC = () => {
|
||||
|
||||
const bubbles = generateBubbles(clients, id);
|
||||
|
||||
useEffect(() => {
|
||||
if (receivingFile && receivingFile.isFile && !receivingFile.receiving) {
|
||||
setReceivingFile(null);
|
||||
setReceivedMessage(receivingFile);
|
||||
setShowMessageDialog(true);
|
||||
}
|
||||
}, [receivingFile]);
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<CloudBackground/>
|
||||
@ -220,17 +209,8 @@ export const LocalSharing: React.FC = () => {
|
||||
open={open.send}
|
||||
onSend={handleSend}
|
||||
onClose={() => setOpen({send: false, receive: false})}
|
||||
name={name}
|
||||
/>
|
||||
|
||||
{/* 文件接收进度弹窗 */}
|
||||
{receivingFile && (
|
||||
<MessageDialog
|
||||
open={true}
|
||||
message={receivingFile}
|
||||
onClose={() => setReceivingFile(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<MessageDialog
|
||||
open={showMessageDialog}
|
||||
|
@ -136,6 +136,7 @@ const PanelLeftUpload: React.FC<{ set_code: (code:string) => void }> = ({set_cod
|
||||
}
|
||||
|
||||
function onFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
console.log('[D] onFileChange: e =', e)
|
||||
setFile(e.currentTarget.files ? e.currentTarget.files[0] : null)
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,7 @@ export const PanelRight = () => {
|
||||
|
||||
async function onFetchFile() {
|
||||
const url = `/ushare/${code}`
|
||||
console.log('[D] onFetchFile: url =', url)
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
document.body.appendChild(link);
|
||||
|
@ -1,12 +1,13 @@
|
||||
import {useState} from "react";
|
||||
import {createUseStyles} from "react-jss";
|
||||
import {Dialog} from "../../component/dialog/dialog.tsx";
|
||||
|
||||
const useClass = createUseStyles({
|
||||
container: {}
|
||||
})
|
||||
export const TestPage = () => {
|
||||
const classes = useClass()
|
||||
const [_open, setOpen] = useState<boolean>(false)
|
||||
const [open, setOpen] = useState<boolean>(false)
|
||||
|
||||
const handleOpen = () => {
|
||||
setOpen(true)
|
||||
@ -14,5 +15,8 @@ export const TestPage = () => {
|
||||
|
||||
return <div className={classes.container}>
|
||||
<button onClick={handleOpen}>open</button>
|
||||
<Dialog open={open} title="hello world" >
|
||||
<input />
|
||||
</Dialog>
|
||||
</div>
|
||||
}
|
@ -8,7 +8,7 @@ export interface LocalStore {
|
||||
setChannel: (chan?: RTCDataChannel) => void;
|
||||
}
|
||||
|
||||
export const useLocalStore = create<LocalStore>()((_set, _get) => ({
|
||||
export const useLocalStore = create<LocalStore>()((_set, get) => ({
|
||||
id: '',
|
||||
name: '',
|
||||
set: (id: string, name: string) => {
|
||||
|
@ -167,6 +167,8 @@
|
||||
const text = inputBox.value;
|
||||
const files = fileInput.files;
|
||||
|
||||
console.log('文本内容:', text);
|
||||
console.log('发送文件:', Array.from(files));
|
||||
|
||||
// 清空内容
|
||||
inputBox.value = '';
|
||||
|
Reference in New Issue
Block a user