diff --git a/.env.development b/.env.development index cfaa1fd..baf245d 100644 --- a/.env.development +++ b/.env.development @@ -1,5 +1,5 @@ -NEXT_PUBLIC_SERVER_URL=http://localhost:8080 -NEXT_PUBLIC_WS_URL=ws://localhost:8080 +NEXT_PUBLIC_SERVER_URL=http://1.117.152.40:8080 +NEXT_PUBLIC_WS_URL=ws://1.117.152.40:8080 MODEL_API_BASE_URL=https://dashscope.aliyuncs.com/api/v1/apps/ diff --git a/.env.production b/.env.production index ed8e9f8..baf245d 100644 --- a/.env.production +++ b/.env.production @@ -1,4 +1,3 @@ -# TODO 生产环境域名 NEXT_PUBLIC_SERVER_URL=http://1.117.152.40:8080 NEXT_PUBLIC_WS_URL=ws://1.117.152.40:8080 diff --git a/src/app/cooperation/layout.tsx b/src/app/cooperation/layout.tsx index 0b61c00..2098b2f 100644 --- a/src/app/cooperation/layout.tsx +++ b/src/app/cooperation/layout.tsx @@ -1,7 +1,35 @@ +'use client'; + +import { useEffect } from 'react'; +import { usePathname, useRouter } from 'next/navigation'; + +import { PATHS, STORAGE_KEY_AUTH } from '@/utils/constants'; +import { useAuthStore } from '@/store/authStore'; + export default function CooperationLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const { setAuth, getAuth } = useAuthStore(); + const router = useRouter(); + const pathname = usePathname(); + useEffect(() => { + let auth = getAuth(); + + if (!auth.access_token) { + const storagedAuth = localStorage.getItem(STORAGE_KEY_AUTH); + + if (storagedAuth) { + auth = JSON.parse(storagedAuth); + setAuth(auth); + } + } + + if (!auth.access_token) { + router.push(`${PATHS.LOGIN}?redirect=${pathname}`); + } + }, [pathname]); + return
{children}
; } diff --git a/src/app/edit/[projectId]/layout.tsx b/src/app/edit/[projectId]/layout.tsx index b43ae78..d87d91d 100644 --- a/src/app/edit/[projectId]/layout.tsx +++ b/src/app/edit/[projectId]/layout.tsx @@ -157,7 +157,7 @@ const Page: React.FC<{ children: React.ReactNode; params: any }> = ({ children,
{/* 侧边栏 */} -
+
= ({ children, -
+
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 36cc59b..3c67ba7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,12 +1,8 @@ 'use client'; import { Inter } from 'next/font/google'; -import { useEffect } from 'react'; -import { usePathname, useRouter } from 'next/navigation'; import NextProcessLoader from '@/components/nextTopLoader'; -import { PATHS, PATHS_SKIPPED_AUTH, STORAGE_KEY_AUTH } from '@/utils/constants'; -import { useAuthStore } from '@/store/authStore'; import './globals.css'; import './xterm.css'; @@ -18,34 +14,6 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const { setAuth, getAuth } = useAuthStore(); - const router = useRouter(); - const pathname = usePathname(); - - useEffect(() => { - // 首先,从zustand获取登录信息 - let auth = getAuth(); - - // TODO 使用localstorage存放登录信息不合适 - // 如果zustand中没有登录信息,则从localStorage中读取 - if (!auth.access_token) { - const storagedAuth = localStorage.getItem(STORAGE_KEY_AUTH); - - if (storagedAuth) { - // 将localStorage中的登录信息同步到zustand - auth = JSON.parse(storagedAuth); - - setAuth(auth); - } - } - - if (!PATHS_SKIPPED_AUTH.includes(pathname) && !auth.access_token) { - // 当前路由需登录但未登录时,跳转到登录页 - // FIXME: 跳转时,会短暂显示目标页,再跳转到登录页 - router.push(`${PATHS.LOGIN}?redirect=${pathname}`); - } - }, [pathname]); - return ( diff --git a/src/components/cooperation/avatarList/index.tsx b/src/components/cooperation/avatarList/index.tsx index d04e908..de846b0 100644 --- a/src/components/cooperation/avatarList/index.tsx +++ b/src/components/cooperation/avatarList/index.tsx @@ -6,13 +6,13 @@ export const AnimatedTooltip = ({ items, }: { items: { - id: number; + id: string; name: string; designation: string; image: string; }[]; }) => { - const [hoveredIndex, setHoveredIndex] = useState(null); + const [hoveredIndex, setHoveredIndex] = useState(null); return (
diff --git a/src/components/cooperation/cooperationEditor/index.tsx b/src/components/cooperation/cooperationEditor/index.tsx index 2f45a57..1e7e963 100644 --- a/src/components/cooperation/cooperationEditor/index.tsx +++ b/src/components/cooperation/cooperationEditor/index.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { memo, useEffect, useMemo, useState, useRef } from 'react'; import dynamic from 'next/dynamic'; import * as monaco from 'monaco-editor'; import * as Y from 'yjs'; @@ -25,60 +25,52 @@ const CooperationEditor: React.FC = ({ roomId, userInfo const [editor, setEditor] = useState(null); const [provider, setProvider] = useState(null); const [, setBinding] = useState(null); - const [awareness, setAwareness] = useState(); - const { setPersons } = useCooperationPerson(); + const [awareness, setAwareness] = useState([]); + const { removePersons, addPersons } = useCooperationPerson(); - useEffect(() => {}, []); + const emailsRef = useRef(new Set()); + const emailMapRef = useRef(new Map()); useEffect(() => { - if (roomId == null) { - return; - } + if (!roomId) return; const provider = new WebsocketProvider( `${process.env.NEXT_PUBLIC_WS_URL}`, 'collaborateDoc', ydoc, { - params: { - record_id: roomId, - }, + params: { record_id: roomId }, }, ); setProvider(provider); const handleBeforeUnload = () => { - provider.awareness.setLocalStateField('cursorLocation', { - x: undefined, - y: undefined, - }); + provider.awareness.setLocalStateField('cursorLocation', { x: undefined, y: undefined }); + provider.destroy(); + ydoc.destroy(); }; window.addEventListener('beforeunload', handleBeforeUnload); return () => { - provider?.destroy(); + provider.destroy(); ydoc.destroy(); + setBinding(null); window.removeEventListener('beforeunload', handleBeforeUnload); }; }, [ydoc, roomId]); useEffect(() => { - if (provider == null || editor == null || roomId === null) { - return; - } + if (!provider || !editor || !roomId) return; - provider.awareness.setLocalStateField('cursorLocation', { - x: undefined, - y: undefined, - }); + provider.awareness.setLocalStateField('cursorLocation', { x: undefined, y: undefined }); provider.awareness.setLocalStateField('userInfo', userInfo); const binding = new MonacoBinding( ydoc.getText(), editor.getModel()!, new Set([editor]), - provider?.awareness, + provider.awareness, ); setBinding(binding); @@ -86,26 +78,20 @@ const CooperationEditor: React.FC = ({ roomId, userInfo const { clientX, clientY } = e; const { innerWidth, innerHeight } = window; - const isNearEdge = - clientX < 10 || clientX > innerWidth - 10 || clientY < 10 || clientY > innerHeight - 10; - - if (isNearEdge) { - provider.awareness.setLocalStateField('cursorLocation', { - x: undefined, - y: undefined, - }); - } else { - provider.awareness.setLocalStateField('cursorLocation', { - x: clientX, - y: clientY, - }); - } - }, 10); - const handleMouseout = () => { provider.awareness.setLocalStateField('cursorLocation', { - x: undefined, - y: undefined, + x: + clientX < 10 || clientX > innerWidth - 10 || clientY < 10 || clientY > innerHeight - 10 + ? undefined + : clientX, + y: + clientX < 10 || clientX > innerWidth - 10 || clientY < 10 || clientY > innerHeight - 10 + ? undefined + : clientY, }); + }, 10); + + const handleMouseout = () => { + provider.awareness.setLocalStateField('cursorLocation', { x: undefined, y: undefined }); }; window.addEventListener('mousemove', handleMouseMove); @@ -114,58 +100,58 @@ const CooperationEditor: React.FC = ({ roomId, userInfo const styleElement = document.createElement('style'); document.head.appendChild(styleElement); - provider.awareness.on( - 'change', - ({ - updated, - added, - removed, - }: { - updated: Array; - added: Array; - removed: Array; - }) => { - type UserAwarenessData = Map>; - - let awarenessState = provider.awareness.getStates() as UserAwarenessData; - setAwareness(Array.from(awarenessState)); - console.log('awarenessState', awarenessState); - - let newStyles = ''; - // 如果一个用户打开2个标签也要处理为一个 - const emails = new Set(); - - const updateUsers = (userIds: Array, action: 'add' | 'remove') => { - userIds.forEach((id) => { - const user = awarenessState.get(id); - - if (user && user.userInfo?.email) { - const email = user.userInfo.email; - - if (action === 'add') { - emails.add(email); - setPersons(Array.from(emails)); - } else { - emails.delete(email); - setPersons(Array.from(emails)); - } - } - }); - }; + const updateUsers = (userIds: Array, action: 'add' | 'remove' | 'update') => { + userIds.forEach((id) => { + const user = provider.awareness.getStates().get(id); + console.log(emailMapRef.current); + + if (!user) { + const email = findKeyContainingElement(emailMapRef.current, id); + + if (email) { + emailsRef.current.delete(email); + removePersons([email]); + } + } else if (user.userInfo?.email) { + const email = user.userInfo.email; + + if (emailMapRef.current.has(email)) { + emailMapRef.current.set( + email, + Array.from(new Set([...emailMapRef.current.get(email), id])), + ); + } else { + emailMapRef.current.set(email, [id]); + } + + if (action === 'add') { + emailsRef.current.add(email); + addPersons(Array.from(emailsRef.current) as string[]); + } else if (action === 'remove') { + emailsRef.current.delete(email); + removePersons([email]); + } + } + }); + }; - updateUsers(added, 'add'); - updateUsers(updated, 'add'); - updateUsers(removed, 'remove'); + const throttledUpdate = throttle((changes: Record) => { + const { updated, added, removed } = changes; + const awarenessState = provider.awareness.getStates(); + setAwareness(Array.from(awarenessState)); - for (let addedUserClientID of updated) { - if (addedUserClientID === ydoc.clientID) return; + updateUsers(updated, 'update'); + updateUsers(added, 'add'); + updateUsers(removed, 'remove'); - let addUserId = ''; - Array.from(awarenessState).forEach(([o, t]) => { - if (o === addedUserClientID) { - addUserId = t.userInfo?.email; - } - }); + let newStyles = ''; + + updated.forEach((addedUserClientID: any) => { + if (addedUserClientID === ydoc.clientID) return; + + const addUserId = awarenessState.get(addedUserClientID)?.userInfo?.email; + + if (addUserId) { newStyles += ` .yRemoteSelection-${addedUserClientID} { background-color: ${createColorFromId(addUserId)}; @@ -173,9 +159,7 @@ const CooperationEditor: React.FC = ({ roomId, userInfo } .yRemoteSelectionHead-${addedUserClientID} { position: relative; - border-left: 2px solid ${createColorFromId(addUserId)}; - border-top: 2px solid ${createColorFromId(addUserId)}; - border-bottom: 2px solid ${createColorFromId(addUserId)}; + border: 2px solid ${createColorFromId(addUserId)}; height: 100%; box-sizing: border-box; } @@ -192,10 +176,12 @@ const CooperationEditor: React.FC = ({ roomId, userInfo } `; } + }); - styleElement.innerHTML += newStyles; - }, - ); + styleElement.innerHTML += newStyles; + }, 250); + + provider.awareness.on('change', throttledUpdate); return () => { binding.destroy(); @@ -203,10 +189,11 @@ const CooperationEditor: React.FC = ({ roomId, userInfo styleElement.remove(); window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseout', handleMouseout); + provider.awareness.off('change', throttledUpdate); // 移除事件监听器 }; - }, [ydoc, provider, editor, roomId]); + }, [provider, editor, roomId, userInfo]); - const handleEditorDidMount = async (editor: monaco.editor.IStandaloneCodeEditor) => { + const handleEditorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => { setEditor(editor); }; @@ -236,22 +223,30 @@ const CooperationEditor: React.FC = ({ roomId, userInfo onMount={handleEditorDidMount} /> {awareness - ?.filter(([id]) => id !== ydoc.clientID) + .filter(([id]) => id !== ydoc.clientID) .filter(([, state]) => state.cursorLocation?.x !== undefined) - .map(([id, state]) => { - return ( - - ); - })} + .map(([id, state]) => ( + + ))}
); }; -export default CooperationEditor; +function findKeyContainingElement(map: Map, element: number) { + for (const [key, valueArray] of map.entries()) { + if (Array.isArray(valueArray) && valueArray.includes(element)) { + return key; + } + } + + return null; +} + +export default memo(CooperationEditor); diff --git a/src/components/cooperation/cursor/index.tsx b/src/components/cooperation/cursor/index.tsx index f38cea8..9ced162 100644 --- a/src/components/cooperation/cursor/index.tsx +++ b/src/components/cooperation/cursor/index.tsx @@ -9,6 +9,7 @@ export const Cursor = React.memo(({ point, color }: { point: number[]; color: st const elm = rCursor.current; if (!elm) return; elm.style.setProperty('transform', `translate(${point[0]}px, ${point[1]}px)`); + elm.style.setProperty('transition', 'transform 0.05s ease'); }, []); const onPointMove = usePerfectCursor(animateCursor); diff --git a/src/components/modals/create-coopertaion-modal/index.tsx b/src/components/modals/create-coopertaion-modal/index.tsx index dc1aee8..62e3e1e 100644 --- a/src/components/modals/create-coopertaion-modal/index.tsx +++ b/src/components/modals/create-coopertaion-modal/index.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { useRouter } from 'next/navigation'; +import { usePathname, useRouter } from 'next/navigation'; import { Button } from '@/components/ui/button'; import { @@ -12,15 +12,36 @@ import { import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useModal } from '@/hooks/useModal'; +import { useAuthStore } from '@/store/authStore'; +import { PATHS, STORAGE_KEY_AUTH } from '@/utils'; export function CreateCoopertaionModal() { + const { setAuth, getAuth } = useAuthStore(); const router = useRouter(); + const pathname = usePathname(); const [docName, setDocName] = useState(''); const { isOpen, onClose, type } = useModal(); const isModalOpen = isOpen && type === 'createCooperation'; const handleCreate = async () => { - // 发送 POST 请求 + let auth = getAuth(); + + if (!auth.access_token) { + const storagedAuth = localStorage.getItem(STORAGE_KEY_AUTH); + + if (storagedAuth) { + auth = JSON.parse(storagedAuth); + + setAuth(auth); + } + } + + if (!auth.access_token) { + router.push(`${PATHS.LOGIN}?redirect=${pathname}`); + + return; + } + const response = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/v1/document/create`, { method: 'POST', headers: { diff --git a/src/components/preview/index.tsx b/src/components/preview/index.tsx index f2967e4..ee3acf4 100644 --- a/src/components/preview/index.tsx +++ b/src/components/preview/index.tsx @@ -53,8 +53,8 @@ export const Preview: FC = memo(function Preview() { }; return ( -
-
+
+
setId(uuidv4())} diff --git a/src/store/cooperationPersonStore.tsx b/src/store/cooperationPersonStore.tsx index dfd001f..9746c3e 100644 --- a/src/store/cooperationPersonStore.tsx +++ b/src/store/cooperationPersonStore.tsx @@ -1,13 +1,13 @@ import { create } from 'zustand'; interface CooperationPersonState { - persons: number[]; + persons: string[]; } interface CooperationPersonActions { - addPersons: (newPersons: number[]) => void; - removePersons: (personsToRemove: number[]) => void; - getPersons: () => number[]; + addPersons: (newPersons: string[]) => void; + removePersons: (personsToRemove: string[]) => void; + getPersons: () => string[]; setPersons: (persons: any[]) => void; }