diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 43eb85c..b492286 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,8 +3,8 @@ import { createRoot } from 'react-dom/client' import './index.css' import {createBrowserRouter, RouterProvider} from "react-router-dom"; import {Login} from "./page/login.tsx"; -import {FileSharing} from "./page/share.tsx"; -import {LocalSharing} from "./page/local.tsx"; +import {FileSharing} from "./page/share/share.tsx"; +import {LocalSharing} from "./page/local/local.tsx"; const container = document.getElementById('root') const root = createRoot(container!) diff --git a/frontend/src/page/local/hook/register.tsx b/frontend/src/page/local/hook/register.tsx new file mode 100644 index 0000000..0089be6 --- /dev/null +++ b/frontend/src/page/local/hook/register.tsx @@ -0,0 +1,46 @@ +import {useState, useCallback,} from 'react'; + +export const useRegister = () => { + const [id, setId] = useState(''); + const [name, setName] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // 封装为可手动触发的异步函数 + const register = useCallback(async () => { + setLoading(true); + setError(null); + + try { + const response = await fetch('/api/ulocal/register', { + method: 'POST' + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = (await response.json()).data; + setId(data.id); + setName(data.name); + + return {id: data.id, name: data.name}; + } catch (err) { + setError(err instanceof Error ? err : new Error('Request failed')); + } finally { + setLoading(false); + } + + }, []); + + // 如果需要自动执行(组件挂载时自动注册) + // useEffect(() => { register() }, [register]); + + return { + id, + name, + register, // 手动触发函数 + loading, + error + }; +}; \ No newline at end of file diff --git a/frontend/src/page/local.tsx b/frontend/src/page/local/local.tsx similarity index 54% rename from frontend/src/page/local.tsx rename to frontend/src/page/local/local.tsx index 6d55e93..32b8dbc 100644 --- a/frontend/src/page/local.tsx +++ b/frontend/src/page/local/local.tsx @@ -1,9 +1,9 @@ -import {CloudBackground} from "../component/fluid/cloud.tsx"; +import {CloudBackground} from "../../component/fluid/cloud.tsx"; import {useEffect, useState} from "react"; import {createUseStyles} from "react-jss"; -import {useWebsocket} from "../hook/websocket/u-ws.tsx"; -import {Resp} from "../interface/response.ts"; -import {message} from "../hook/message/u-message.tsx"; +import {useWebsocket} from "../../hook/websocket/u-ws.tsx"; +import {Resp} from "../../interface/response.ts"; +import {useRegister} from "./hook/register.tsx"; const useClass = createUseStyles({ '@global': { @@ -86,15 +86,6 @@ interface Client { candidate: RTCIceCandidateInit; } -interface Store { - client: Client | null - clients: Client[] - rtc: RTCPeerConnection | null - ch: RTCDataChannel | null - offer: RTCSessionDescription | null - candidate: RTCIceCandidate | null -} - function setupDataChannel(ch: RTCDataChannel) { ch.onopen = () => console.log('通道已打开!'); ch.onmessage = (e) => handleFileChunk(e.data); @@ -109,7 +100,10 @@ function handleFileChunk(chunk: any) { export const LocalSharing: React.FC = () => { const classes = useClass(); - const [rtcStore, setRTCStore] = useState({} as Store) + const {id, name, register } = useRegister() + const [rtc, setRTC] = useState(null); + const [client, setClient] = useState(null); + const [clients, setClients] = useState([]); const {connect, close} = useWebsocket({}) // 生成随机颜色 @@ -121,8 +115,8 @@ export const LocalSharing: React.FC = () => { }; // 防碰撞位置生成 - const generateBubbles = (cs: Client[]) => { - if (!cs) return [] + const generateBubbles = () => { + if (!clients) return [] const BUBBLE_SIZE = 100; const centerX = window.innerWidth / 2; @@ -132,11 +126,11 @@ export const LocalSharing: React.FC = () => { let currentRadius = 0; let angleStep = (2 * Math.PI) / 6; // 初始6个位置 - for (let index = 0; index < cs.length; index++) { + for (let index = 0; index < clients.length; index++) { let attempt = 0; let validPosition = false; - if (cs[index].id == rtcStore.client?.id) { + if (clients[index].id == id) { continue } @@ -164,8 +158,8 @@ export const LocalSharing: React.FC = () => { if (inBounds && !collision) { bubbles.push({ - id: cs[index].id, - name: cs[index].name, + id: clients[index].id, + name: clients[index].name, x: x, y: y, color: generateColor(), @@ -181,6 +175,7 @@ export const LocalSharing: React.FC = () => { } } + console.log('[D] generated bubbles =', bubbles); return bubbles; }; @@ -188,9 +183,8 @@ export const LocalSharing: React.FC = () => { setTimeout(async () => { const res = await fetch(`/api/ulocal/clients`) const jes = await res.json() as Resp - setRTCStore(val => { - return {...val, clients: jes.data} - }) + console.log('[D] update clients =', jes) + setClients(jes.data) }, 500) } @@ -200,9 +194,7 @@ export const LocalSharing: React.FC = () => { switch (msg.type) { case "register": const reg_resp = JSON.parse(e.data) as { body: Client } - setRTCStore(val => { - return {...val, client: reg_resp.body} - }) + setClient(reg_resp.body) break case "enter": await updateClients() @@ -211,126 +203,51 @@ export const LocalSharing: React.FC = () => { await updateClients() break case "offer": - const res_offer = JSON.parse(e.data) as { - offer: RTCSessionDescriptionInit, - id: number, - candidate: RTCIceCandidateInit - } - console.log('[D] offer res =', res_offer) - await rtcStore.rtc?.setRemoteDescription(res_offer.offer) - const answer = await rtcStore.rtc?.createAnswer() - await rtcStore.rtc?.setLocalDescription(answer) - await fetch('/api/ulocal/answer', { - method: 'POST', - headers: {"Content-Type": "application/json"}, - body: JSON.stringify({id: rtcStore.client?.id, answer: answer}) - }) break case "answer": - const res_answer = JSON.parse(e.data) as { answer: RTCSessionDescriptionInit, id: number } - await rtcStore.rtc?.setRemoteDescription(res_answer.answer) break } } - // useEffect(() => { - // const fn = async () => { - // // const rtc = new RTCPeerConnection({iceServers: [{urls: "stun:stun.qq.com:3478"}]}) - // const rtc = new RTCPeerConnection({}) - // rtc.onconnectionstatechange = () => { - // console.log('rtc connection state =', rtc.connectionState) - // } - // const dataChannel = rtc.createDataChannel('fileTransfer', {ordered: true}); - // setupDataChannel(dataChannel); - // const waitCandidate = (): Promise => { - // return new Promise(resolve => { - // rtc.onicecandidate = (e) => { - // resolve(e.candidate) - // } - // }) - // } - // - // const waitNegotiation = (): Promise => { - // return new Promise(resolve => { - // rtc.onnegotiationneeded = async () => { - // const _offer = await rtc.createOffer() - // await rtc.setLocalDescription(_offer) - // resolve() - // } - // }) - // } - // - // await waitNegotiation() - // const candidate: RTCIceCandidate | null = await waitCandidate(); - // - // const res = await fetch("/api/ulocal/register", { - // method: "POST", - // headers: {"Content-Type": "application/json"}, - // body: JSON.stringify({candidate: candidate}) - // }) - // const jes = await res.json() as Resp; - // - // console.log('[D] register resp =', jes) - // - // if (!jes.data.id) { - // message.error("注册失败") - // throw new Error("register failed") - // } - // - // setRTCStore(val => { - // return { - // ...val, - // client: jes.data, - // candidate: candidate, - // offer: rtc.localDescription, - // rtc: rtc, - // } - // }) - // - // const api = `${window.location.protocol === 'https' ? 'wss' : 'ws'}://${window.location.host}/api/ulocal/ws?id=${jes.data.id}` - // console.log('[D] websocket url =', api) - // connect(api, {fn: handleWSEvent}) - // - // await updateClients() - // } - // - // fn() - // - // return () => close(); - // }, [] - // ); - // 气泡点击处理 const handleBubbleClick = async (bubble: Bubble) => { - const offer = await rtcStore.rtc?.createOffer() - await rtcStore.rtc?.setLocalDescription(offer) - // await fetch('/api/ulocal/offer', { - // method: 'POST', - // headers: {"Content-Type": "application/json"}, - // body: JSON.stringify({ - // id: bubble.id, - // offer: rtcStore.offer, - // candidate: rtcStore.candidate, - // }) - // }) + const offer = await rtc?.createOffer() + await rtc?.setLocalDescription(offer) + const data = { + id: bubble.id, + offer: offer, + } + await fetch('/api/ulocal/offer', { + method: 'POST', + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(data) + }) }; useEffect(() => { const fn = async () => { - connect("/api/ulocal/ws", {fn: handleWSEvent}) + const reg_data = await register(); + console.log(`[D] register id = ${reg_data?.id}`); + connect(`/api/ulocal/ws?id=${reg_data?.id}`, {fn: handleWSEvent}) await updateClients() const rtc = new RTCPeerConnection({iceServers: [{urls: "stun:stun.qq.com:3478"}]}) - setRTCStore(val => {return {...val, rtc: rtc}}) + rtc.onicecandidate = async (e) => { + if(e.candidate) { + await fetch('/api/ulocal/candidate') + } + } + setRTC(rtc) + + return () => {close();} }; fn() }, []) return
-

{rtcStore.client?.name}

- {rtcStore.clients && generateBubbles(rtcStore.clients).map(bubble => { - // const client = clients.find(c => c.id === bubble.id); - return rtcStore.client ? ( +

{name}

+ {clients && generateBubbles().map(bubble => { + return client ? (