Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
qionghong liu
xlab_chatroom
Commits
23e3ae0b
Commit
23e3ae0b
authored
Aug 31, 2025
by
qionghong liu
Browse files
initial commit
parent
3bf09937
Changes
48
Hide whitespace changes
Inline
Side-by-side
src/app/api/room/list/route.ts
0 → 100644
View file @
23e3ae0b
// 文件位置:src/app/api/room/list/route.ts
import
{
NextResponse
}
from
'
next/server
'
;
import
{
RoomListRes
}
from
'
@/types/api
'
;
import
{
rooms
}
from
'
@/lib/dataStore
'
;
export
async
function
GET
()
{
try
{
const
response
:
RoomListRes
=
{
rooms
};
return
NextResponse
.
json
({
code
:
0
,
data
:
response
,
});
}
catch
(
error
)
{
return
NextResponse
.
json
({
code
:
1
,
message
:
'
获取房间列表失败
'
});
}
}
src/app/api/room/message/getUpdate/route.ts
0 → 100644
View file @
23e3ae0b
// 文件位置:src/app/api/room/message/getUpdate/route.ts
import
{
NextResponse
}
from
'
next/server
'
;
import
{
RoomMessageGetUpdateRes
}
from
'
@/types/api
'
;
import
{
getRoomUpdateMessages
}
from
'
@/lib/dataStore
'
;
export
async
function
GET
(
request
:
Request
)
{
try
{
const
{
searchParams
}
=
new
URL
(
request
.
url
);
const
roomId
=
parseInt
(
searchParams
.
get
(
'
roomId
'
)
||
'
0
'
);
const
sinceMessageId
=
parseInt
(
searchParams
.
get
(
'
sinceMessageId
'
)
||
'
0
'
);
if
(
!
roomId
)
{
return
NextResponse
.
json
({
code
:
1
,
message
:
'
房间ID必须提供
'
});
}
// 获取指定房间中ID大于sinceMessageId的消息
const
newMessages
=
getRoomUpdateMessages
(
roomId
,
sinceMessageId
);
const
response
:
RoomMessageGetUpdateRes
=
{
messages
:
newMessages
};
return
NextResponse
.
json
({
code
:
0
,
data
:
response
,
});
}
catch
(
error
)
{
return
NextResponse
.
json
({
code
:
1
,
message
:
'
获取消息更新失败
'
});
}
}
src/app/api/room/message/list/route.ts
0 → 100644
View file @
23e3ae0b
// 文件位置:src/app/api/room/message/list/route.ts
import
{
NextResponse
}
from
'
next/server
'
;
import
{
RoomMessageListRes
}
from
'
@/types/api
'
;
import
{
getRoomMessages
}
from
'
@/lib/dataStore
'
;
export
async
function
GET
(
request
:
Request
)
{
try
{
const
{
searchParams
}
=
new
URL
(
request
.
url
);
const
roomId
=
parseInt
(
searchParams
.
get
(
'
roomId
'
)
||
'
0
'
);
if
(
!
roomId
)
{
return
NextResponse
.
json
({
code
:
1
,
message
:
'
房间ID必须提供
'
});
}
const
roomMessages
=
getRoomMessages
(
roomId
);
const
response
:
RoomMessageListRes
=
{
messages
:
roomMessages
};
return
NextResponse
.
json
({
code
:
0
,
data
:
response
,
});
}
catch
(
error
)
{
return
NextResponse
.
json
({
code
:
1
,
message
:
'
获取消息列表失败
'
});
}
}
src/app/chatroom/page.tsx
0 → 100644
View file @
23e3ae0b
"
use client
"
;
import
React
from
"
react
"
;
import
ProtectedRoute
from
"
@/components/Auth/ProtectedRoute
"
;
import
UserInfo
from
"
@/components/UserInfo/UserInfo
"
;
import
ChatRoom
from
"
@/components/ChatRoom/ChatRoom
"
;
export
default
function
ChatRoomPage
()
{
return
(
<
ProtectedRoute
>
<
div
className
=
"app-container"
style
=
{
{
minHeight
:
"
100vh
"
,
background
:
"
#f8f9fa
"
}
}
>
<
UserInfo
/>
<
ChatRoom
/>
</
div
>
</
ProtectedRoute
>
);
}
src/app/page.tsx
View file @
23e3ae0b
import
Image
from
"
next/image
"
;
/*
import
styles
from
"
./page.module.css
"
;
import SetName from "@/components/SetName/SetName";
import Link from "next/link";
export default function Home() {
export default function Home() {
return (
return (
<
div
className
=
{
styles
.
page
}
>
<>
<
main
className
=
{
styles
.
main
}
>
<div>
<
Image
<h1>Welcome to Chatroom!</h1>
className
=
{
styles
.
logo
}
// Your Logic Here
src
=
"/next.svg"
<SetName />
alt
=
"Next.js logo"
width
=
{
180
}
// 添加到聊天室的导航链接
height
=
{
38
}
<Link href="/chatroom">
priority
<button>进入聊天室</button>
</Link>
</div>
</>
);
}
*/
"
use client
"
;
import
React
,
{
useState
}
from
'
react
'
;
import
SetName
from
"
@/components/SetName/SetName
"
;
import
Link
from
"
next/link
"
;
import
{
useAuth
}
from
'
@/hooks/useAuth
'
;
import
AuthModal
from
'
@/components/Auth/AuthModal
'
;
export
default
function
Home
()
{
const
{
isAuthenticated
,
user
}
=
useAuth
();
const
[
showAuthModal
,
setShowAuthModal
]
=
useState
(
false
);
if
(
!
isAuthenticated
)
{
return
(
<
main
style
=
{
{
padding
:
'
2rem
'
,
textAlign
:
'
center
'
,
minHeight
:
'
100vh
'
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
justifyContent
:
'
center
'
}
}
>
<
h1
style
=
{
{
fontSize
:
'
2.5rem
'
,
marginBottom
:
'
1rem
'
,
color
:
'
#333
'
}
}
>
Welcome to Chatroom!
</
h1
>
<
p
style
=
{
{
fontSize
:
'
1.2rem
'
,
color
:
'
#666
'
,
marginBottom
:
'
2rem
'
}
}
>
请登录或注册以开始与其他用户聊天
</
p
>
<
button
onClick
=
{
()
=>
setShowAuthModal
(
true
)
}
style
=
{
{
padding
:
'
0.75rem 2rem
'
,
background
:
'
#007bff
'
,
color
:
'
white
'
,
border
:
'
none
'
,
borderRadius
:
'
8px
'
,
cursor
:
'
pointer
'
,
fontSize
:
'
1.1rem
'
,
fontWeight
:
'
500
'
,
margin
:
'
0 auto
'
,
display
:
'
block
'
,
boxShadow
:
'
0 2px 4px rgba(0, 123, 255, 0.3)
'
,
}
}
>
开始聊天
</
button
>
<
AuthModal
isOpen
=
{
showAuthModal
}
onClose
=
{
()
=>
setShowAuthModal
(
false
)
}
/>
/>
<
ol
>
</
main
>
<
li
>
);
Get started by editing
<
code
>
src/app/page.tsx
</
code
>
.
}
</
li
>
<
li
>
Save and see your changes instantly.
</
li
>
</
ol
>
<
div
className
=
{
styles
.
ctas
}
>
return
(
<
a
<
div
>
className
=
{
styles
.
primary
}
<
div
style
=
{
{
href
=
"https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
background
:
'
#f8f9fa
'
,
target
=
"_blank"
padding
:
'
1rem
'
,
rel
=
"noopener noreferrer"
borderBottom
:
'
1px solid #e9ecef
'
,
>
display
:
'
flex
'
,
<
Image
justifyContent
:
'
space-between
'
,
className
=
{
styles
.
logo
}
alignItems
:
'
center
'
src
=
"/vercel.svg"
}
}
>
alt
=
"Vercel logomark"
<
h1
>
Welcome to Chatroom!
</
h1
>
width
=
{
20
}
<
div
style
=
{
{
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
'
1rem
'
}
}
>
height
=
{
20
}
<
span
>
欢迎,
{
user
?.
username
}
!
</
span
>
/>
Deploy now
</
a
>
<
a
href
=
"https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target
=
"_blank"
rel
=
"noopener noreferrer"
className
=
{
styles
.
secondary
}
>
Read our docs
</
a
>
</
div
>
</
div
>
</
main
>
</
div
>
<
footer
className
=
{
styles
.
footer
}
>
<
a
<
div
style
=
{
{
padding
:
'
2rem
'
}
}
>
href
=
"https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
<
SetName
/>
target
=
"_blank"
rel
=
"noopener noreferrer"
<
Link
href
=
"/chatroom"
>
>
<
button
style
=
{
{
<
Image
padding
:
'
0.75rem 1.5rem
'
,
aria-hidden
background
:
'
#007bff
'
,
src
=
"/file.svg"
color
:
'
white
'
,
alt
=
"File icon"
border
:
'
none
'
,
width
=
{
16
}
borderRadius
:
'
4px
'
,
height
=
{
16
}
cursor
:
'
pointer
'
,
/>
fontSize
:
'
1rem
'
,
Learn
marginTop
:
'
1rem
'
</
a
>
}
}
>
<
a
进入聊天室
href
=
"https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
</
button
>
target
=
"_blank"
</
Link
>
rel
=
"noopener noreferrer"
</
div
>
>
<
Image
aria-hidden
src
=
"/window.svg"
alt
=
"Window icon"
width
=
{
16
}
height
=
{
16
}
/>
Examples
</
a
>
<
a
href
=
"https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target
=
"_blank"
rel
=
"noopener noreferrer"
>
<
Image
aria-hidden
src
=
"/globe.svg"
alt
=
"Globe icon"
width
=
{
16
}
height
=
{
16
}
/>
Go to nextjs.org →
</
a
>
</
footer
>
</
div
>
</
div
>
);
);
}
}
src/components/Auth/Auth.css
0 → 100644
View file @
23e3ae0b
.auth-form
{
max-width
:
400px
;
margin
:
2rem
auto
;
padding
:
2rem
;
border
:
1px
solid
#e0e0e0
;
border-radius
:
8px
;
background
:
#fff
;
box-shadow
:
0
2px
10px
rgba
(
0
,
0
,
0
,
0.1
);
}
.auth-form
h2
{
text-align
:
center
;
margin-bottom
:
1.5rem
;
color
:
#333
;
}
.auth-field
{
margin-bottom
:
1rem
;
}
.auth-field
label
{
display
:
block
;
margin-bottom
:
0.5rem
;
font-weight
:
500
;
color
:
#555
;
}
.auth-field
input
{
width
:
100%
;
padding
:
0.75rem
;
border
:
1px
solid
#ddd
;
border-radius
:
4px
;
font-size
:
1rem
;
transition
:
border-color
0.2s
;
box-sizing
:
border-box
;
}
.auth-field
input
:focus
{
outline
:
none
;
border-color
:
#007bff
;
box-shadow
:
0
0
0
2px
rgba
(
0
,
123
,
255
,
0.25
);
}
.auth-field
input
:disabled
{
background-color
:
#f5f5f5
;
cursor
:
not-allowed
;
}
.auth-button
{
width
:
100%
;
padding
:
0.75rem
;
background
:
#007bff
;
color
:
white
;
border
:
none
;
border-radius
:
4px
;
font-size
:
1rem
;
font-weight
:
500
;
cursor
:
pointer
;
transition
:
background-color
0.2s
;
}
.auth-button
:hover:not
(
:disabled
)
{
background
:
#0056b3
;
}
.auth-button
:disabled
{
background
:
#6c757d
;
cursor
:
not-allowed
;
}
.auth-error
{
background
:
#f8d7da
;
color
:
#721c24
;
padding
:
0.75rem
;
border-radius
:
4px
;
margin-bottom
:
1rem
;
border
:
1px
solid
#f5c6cb
;
}
.auth-switch
{
text-align
:
center
;
margin-top
:
1.5rem
;
color
:
#666
;
}
.auth-link
{
background
:
none
;
border
:
none
;
color
:
#007bff
;
cursor
:
pointer
;
text-decoration
:
underline
;
font-size
:
inherit
;
margin-left
:
0.5rem
;
}
.auth-link
:hover
{
color
:
#0056b3
;
}
/* Modal styles */
.auth-modal-overlay
{
position
:
fixed
;
top
:
0
;
left
:
0
;
right
:
0
;
bottom
:
0
;
background
:
rgba
(
0
,
0
,
0
,
0.5
);
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
z-index
:
1000
;
}
.auth-modal-content
{
position
:
relative
;
background
:
white
;
border-radius
:
8px
;
max-width
:
90vw
;
max-height
:
90vh
;
overflow-y
:
auto
;
}
.auth-modal-close
{
position
:
absolute
;
top
:
1rem
;
right
:
1rem
;
background
:
none
;
border
:
none
;
font-size
:
1.5rem
;
cursor
:
pointer
;
color
:
#666
;
z-index
:
1
;
}
.auth-modal-close
:hover
{
color
:
#333
;
}
src/components/Auth/AuthModal.tsx
0 → 100644
View file @
23e3ae0b
// React 弹窗组件(AuthModal),用于显示登录或注册表单
// 声明这是一个客户端组件,告诉 Next.js 这个组件只在浏览器端运行
"
use client
"
;
// 引入 React 和 useState 钩子,用于管理组件的状态
//useState 是 React 的函数式组件中用于声明和管理状态的钩子函数。它返回一个数组,包含当前状态值和更新状态的函数。
import
React
,
{
useState
}
from
"
react
"
;
// 引入 LoginForm 和 RegisterForm 两个子组件,分别处理登录和注册的表单
import
LoginForm
from
"
./LoginForm
"
;
import
RegisterForm
from
"
./RegisterForm
"
;
import
"
./Auth.css
"
;
// 定义 AuthModalProps 接口,描述组件接收的属性(props)
interface
AuthModalProps
{
isOpen
:
boolean
;
onClose
:
()
=>
void
;
onSuccess
?:
()
=>
void
;
}
// 定义 AuthMode 类型,表示弹窗的两种模式:登录或注册
type
AuthMode
=
"
login
"
|
"
register
"
;
// 定义 AuthModal 组件,接收 isOpen、onClose、onSuccess 三个属性
export
default
function
AuthModal
({
isOpen
,
onClose
,
onSuccess
,
}:
AuthModalProps
)
{
// 使用 useState 钩子管理弹窗的模式,初始值是 'login'
const
[
mode
,
setMode
]
=
useState
<
AuthMode
>
(
"
login
"
);
// 定义 handleSuccess 函数,处理登录或注册成功后的逻辑
const
handleSuccess
=
()
=>
{
onSuccess
?.();
onClose
();
};
if
(
!
isOpen
)
return
null
;
// 渲染弹窗的 HTML 结构
return
(
<
div
className
=
"auth-modal-overlay"
onClick
=
{
onClose
}
>
<
div
className
=
"auth-modal-content"
onClick
=
{
(
e
)
=>
e
.
stopPropagation
()
}
>
<
button
className
=
"auth-modal-close"
onClick
=
{
onClose
}
>
×
</
button
>
{
mode
===
"
login
"
?
(
<
LoginForm
onSuccess
=
{
handleSuccess
}
switchToRegister
=
{
()
=>
setMode
(
"
register
"
)
}
/>
)
:
(
<
RegisterForm
onSuccess
=
{
handleSuccess
}
switchToLogin
=
{
()
=>
setMode
(
"
login
"
)
}
/>
)
}
</
div
>
</
div
>
);
}
src/components/Auth/LoginForm.tsx
0 → 100644
View file @
23e3ae0b
"
use client
"
;
import
React
,
{
useState
}
from
"
react
"
;
// 引入 useAuth 自定义钩子,用于处理登录相关的逻辑
// useAuth 是一个自定义钩子,封装了认证逻辑,遵循 React 钩子命名规范(以 use 开头)
import
{
useAuth
}
from
"
@/hooks/useAuth
"
;
import
"
./Auth.css
"
;
// 定义 LoginFormProps 接口,描述组件接收的属性
interface
LoginFormProps
{
onSuccess
?:
()
=>
void
;
switchToRegister
:
()
=>
void
;
// 切换到注册模式的函数
}
// 定义 LoginForm 组件,接收 onSuccess 和 switchToRegister 属性
export
default
function
LoginForm
({
onSuccess
,
switchToRegister
,
}:
LoginFormProps
)
{
// 定义状态变量:用户名、密码、加载状态和错误信息
const
[
username
,
setUsername
]
=
useState
(
""
);
const
[
password
,
setPassword
]
=
useState
(
""
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
""
);
// 从 useAuth 钩子中获取 login 函数,用于处理登录
const
{
login
}
=
useAuth
();
// 定义 handleSubmit 函数,处理表单提交
// 当用户点击“登录”按钮时,这个函数会被调用,负责检查输入、发送登录请求。
const
handleSubmit
=
async
(
e
:
React
.
FormEvent
)
=>
{
e
.
preventDefault
();
// 检查用户名和密码是否为空
if
(
!
username
.
trim
()
||
!
password
.
trim
())
{
setError
(
"
请输入用户名和密码
"
);
return
;
}
setLoading
(
true
);
setError
(
""
);
try
{
// 发送 POST 请求到登录 API
// 使用 fetch 函数发送 HTTP POST 请求到 /api/auth/login 接口,携带用户名和密码。
const
response
=
await
fetch
(
"
/api/auth/login
"
,
{
method
:
"
POST
"
,
headers
:
{
"
Content-Type
"
:
"
application/json
"
,
},
body
:
JSON
.
stringify
({
username
,
password
}),
});
// 解析服务器返回的 JSON 数据
const
data
=
await
response
.
json
();
// 检查服务器返回的数据:如果 data.code 是 0 且 data.data 存在,说明登录成功
if
(
data
.
code
===
0
&&
data
.
data
)
{
login
(
data
.
data
);
onSuccess
?.();
}
else
{
setError
(
data
.
message
||
"
登录失败
"
);
}
}
catch
(
error
)
{
setError
(
"
网络错误,请重试
"
);
console
.
error
(
"
Login error:
"
,
error
);
}
finally
{
setLoading
(
false
);
}
};
return
(
<
form
onSubmit
=
{
handleSubmit
}
className
=
"auth-form"
>
<
h2
>
用户登录
</
h2
>
{
error
&&
<
div
className
=
"auth-error"
>
{
error
}
</
div
>
}
<
div
className
=
"auth-field"
>
<
label
htmlFor
=
"username"
>
用户名
</
label
>
<
input
id
=
"username"
type
=
"text"
value
=
{
username
}
onChange
=
{
(
e
)
=>
setUsername
(
e
.
target
.
value
)
}
placeholder
=
"请输入用户名"
disabled
=
{
loading
}
/>
</
div
>
<
div
className
=
"auth-field"
>
<
label
htmlFor
=
"password"
>
密码
</
label
>
<
input
id
=
"password"
type
=
"password"
value
=
{
password
}
onChange
=
{
(
e
)
=>
setPassword
(
e
.
target
.
value
)
}
placeholder
=
"请输入密码"
disabled
=
{
loading
}
/>
</
div
>
<
button
type
=
"submit"
className
=
"auth-button"
disabled
=
{
loading
}
>
{
loading
?
"
登录中...
"
:
"
登录
"
}
</
button
>
<
div
className
=
"auth-switch"
>
还没有账户?
<
button
type
=
"button"
onClick
=
{
switchToRegister
}
className
=
"auth-link"
>
立即注册
</
button
>
</
div
>
</
form
>
);
}
src/components/Auth/ProtectedRoute.tsx
0 → 100644
View file @
23e3ae0b
// ProtectedRoute 是一个 React 组件,作用是保护某些页面,只有登录用户才能访问。
"
use client
"
;
import
React
,
{
useState
,
useEffect
}
from
"
react
"
;
import
{
useAuth
}
from
"
@/hooks/useAuth
"
;
// 引入 AuthModal 组件,用于显示登录/注册弹窗
// AuthModal 是一个现成的弹窗工具,当用户没登录时,会弹出让用户登录或注册
import
AuthModal
from
"
./AuthModal
"
;
// 用 TypeScript 定义了一个 ProtectedRouteProps 接口,规定了 ProtectedRoute 组件可以接收的“参数”(props)。
interface
ProtectedRouteProps
{
children
:
React
.
ReactNode
;
fallback
?:
React
.
ReactNode
;
}
export
default
function
ProtectedRoute
({
children
,
fallback
,
}:
ProtectedRouteProps
)
{
// 从 useAuth 钩子中获取 isAuthenticated,判断用户是否已登录
const
{
isAuthenticated
}
=
useAuth
();
const
[
showAuthModal
,
setShowAuthModal
]
=
useState
(
false
);
const
[
isClient
,
setIsClient
]
=
useState
(
false
);
useEffect
(()
=>
{
setIsClient
(
true
);
},
[]);
useEffect
(()
=>
{
if
(
isClient
&&
!
isAuthenticated
)
{
setShowAuthModal
(
true
);
}
else
{
setShowAuthModal
(
false
);
}
},
[
isAuthenticated
,
isClient
]);
// 服务端渲染时不显示任何内容
if
(
!
isClient
)
{
return
null
;
}
if
(
!
isAuthenticated
)
{
return
(
<>
{
fallback
||
(
<
div
style
=
{
{
textAlign
:
"
center
"
,
padding
:
"
2rem
"
}
}
>
<
h2
>
请先登录
</
h2
>
<
p
>
您需要登录才能访问聊天室
</
p
>
</
div
>
)
}
<
AuthModal
isOpen
=
{
showAuthModal
}
onClose
=
{
()
=>
setShowAuthModal
(
false
)
}
/>
</>
);
}
return
<>
{
children
}
</>;
}
src/components/Auth/RegisterForm.tsx
0 → 100644
View file @
23e3ae0b
"
use client
"
;
import
React
,
{
useState
}
from
"
react
"
;
import
{
useAuth
}
from
"
@/hooks/useAuth
"
;
import
"
./Auth.css
"
;
interface
RegisterFormProps
{
onSuccess
?:
()
=>
void
;
switchToLogin
:
()
=>
void
;
}
export
default
function
RegisterForm
({
onSuccess
,
switchToLogin
,
}:
RegisterFormProps
)
{
// 定义状态变量:用户名、邮箱、密码、确认密码、加载状态和错误信息
const
[
username
,
setUsername
]
=
useState
(
""
);
const
[
email
,
setEmail
]
=
useState
(
""
);
const
[
password
,
setPassword
]
=
useState
(
""
);
const
[
confirmPassword
,
setConfirmPassword
]
=
useState
(
""
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
error
,
setError
]
=
useState
(
""
);
// 从 useAuth 钩子中获取 login 函数,用于处理注册后的登录
const
{
login
}
=
useAuth
();
const
handleSubmit
=
async
(
e
:
React
.
FormEvent
)
=>
{
e
.
preventDefault
();
if
(
!
username
.
trim
()
||
!
password
.
trim
())
{
setError
(
"
请输入用户名和密码
"
);
return
;
}
// 检查两次输入的密码是否一致
if
(
password
!==
confirmPassword
)
{
setError
(
"
两次输入的密码不一致
"
);
return
;
}
if
(
username
.
length
<
3
)
{
setError
(
"
用户名至少需要3个字符
"
);
return
;
}
if
(
password
.
length
<
6
)
{
setError
(
"
密码至少需要6个字符
"
);
return
;
}
setLoading
(
true
);
setError
(
""
);
try
{
// 使用 fetch 函数发送 HTTP POST 请求到 /api/auth/register 接口,携带用户名、密码和邮箱(可选)。
const
response
=
await
fetch
(
"
/api/auth/register
"
,
{
method
:
"
POST
"
,
headers
:
{
"
Content-Type
"
:
"
application/json
"
,
},
body
:
JSON
.
stringify
({
username
,
password
,
email
:
email
.
trim
()
||
undefined
,
}),
});
const
data
=
await
response
.
json
();
//检查服务器返回的数据:如果 data.code 是 0 且 data.data 存在,说明注册成功。
if
(
data
.
code
===
0
&&
data
.
data
)
{
login
(
data
.
data
);
onSuccess
?.();
}
else
{
setError
(
data
.
message
||
"
注册失败
"
);
}
}
catch
(
error
)
{
setError
(
"
网络错误,请重试
"
);
console
.
error
(
"
Register error:
"
,
error
);
}
finally
{
setLoading
(
false
);
}
};
return
(
<
form
onSubmit
=
{
handleSubmit
}
className
=
"auth-form"
>
<
h2
>
用户注册
</
h2
>
{
error
&&
<
div
className
=
"auth-error"
>
{
error
}
</
div
>
}
<
div
className
=
"auth-field"
>
<
label
htmlFor
=
"username"
>
用户名 *
</
label
>
<
input
id
=
"username"
type
=
"text"
value
=
{
username
}
onChange
=
{
(
e
)
=>
setUsername
(
e
.
target
.
value
)
}
placeholder
=
"请输入用户名(至少3位)"
disabled
=
{
loading
}
/>
</
div
>
<
div
className
=
"auth-field"
>
<
label
htmlFor
=
"email"
>
邮箱 (可选)
</
label
>
<
input
id
=
"email"
type
=
"email"
value
=
{
email
}
onChange
=
{
(
e
)
=>
setEmail
(
e
.
target
.
value
)
}
placeholder
=
"请输入邮箱地址"
disabled
=
{
loading
}
/>
</
div
>
<
div
className
=
"auth-field"
>
<
label
htmlFor
=
"password"
>
密码 *
</
label
>
<
input
id
=
"password"
type
=
"password"
value
=
{
password
}
onChange
=
{
(
e
)
=>
setPassword
(
e
.
target
.
value
)
}
placeholder
=
"请输入密码(至少6位)"
disabled
=
{
loading
}
/>
</
div
>
<
div
className
=
"auth-field"
>
<
label
htmlFor
=
"confirmPassword"
>
确认密码 *
</
label
>
<
input
id
=
"confirmPassword"
type
=
"password"
value
=
{
confirmPassword
}
onChange
=
{
(
e
)
=>
setConfirmPassword
(
e
.
target
.
value
)
}
placeholder
=
"请再次输入密码"
disabled
=
{
loading
}
/>
</
div
>
<
button
type
=
"submit"
className
=
"auth-button"
disabled
=
{
loading
}
>
{
loading
?
"
注册中...
"
:
"
注册
"
}
</
button
>
<
div
className
=
"auth-switch"
>
已有账户?
<
button
type
=
"button"
onClick
=
{
switchToLogin
}
className
=
"auth-link"
>
立即登录
</
button
>
</
div
>
</
form
>
);
}
src/components/ChatRoom/ChatRoom.css
0 → 100644
View file @
23e3ae0b
/* src/components/ChatRoom/ChatRoom.css */
/* 主容器 - 使用 Grid 布局 */
.chatroom-main
{
display
:
grid
;
grid-template-columns
:
320px
1
fr
;
height
:
100vh
;
max-height
:
100vh
;
background-color
:
#f8f9fa
;
font-family
:
-apple-system
,
BlinkMacSystemFont
,
"Segoe UI"
,
Roboto
,
sans-serif
;
overflow
:
hidden
;
}
/* 响应式设计 */
@media
(
max-width
:
768px
)
{
.chatroom-main
{
grid-template-columns
:
1
fr
;
grid-template-rows
:
auto
1
fr
;
}
.room-sidebar
{
height
:
auto
;
max-height
:
40vh
;
}
}
/* ============ 左侧边栏样式 ============ */
.room-sidebar
{
background
:
linear-gradient
(
135deg
,
#667eea
0%
,
#764ba2
100%
);
color
:
white
;
display
:
flex
;
flex-direction
:
column
;
border-right
:
1px
solid
#e1e5e9
;
overflow
:
hidden
;
}
.sidebar-header
{
padding
:
1.5rem
1rem
;
background
:
rgba
(
0
,
0
,
0
,
0.1
);
backdrop-filter
:
blur
(
10px
);
border-bottom
:
1px
solid
rgba
(
255
,
255
,
255
,
0.2
);
}
.sidebar-title
{
margin
:
0
0
0.5rem
0
;
font-size
:
1.25rem
;
font-weight
:
600
;
letter-spacing
:
-0.025em
;
}
.user-status
{
display
:
flex
;
align-items
:
center
;
gap
:
0.5rem
;
font-size
:
0.875rem
;
opacity
:
0.9
;
}
.current-user-indicator
{
font-weight
:
500
;
}
.online-status
{
width
:
8px
;
height
:
8px
;
background-color
:
#10b981
;
border-radius
:
50%
;
animation
:
pulse
2s
infinite
;
}
@keyframes
pulse
{
0
%,
100
%
{
opacity
:
1
;
}
50
%
{
opacity
:
0.5
;
}
}
.room-navigation
{
flex
:
1
;
overflow
:
hidden
;
}
.room-list
{
list-style
:
none
;
margin
:
0
;
padding
:
0
;
height
:
100%
;
overflow-y
:
auto
;
scrollbar-width
:
thin
;
scrollbar-color
:
rgba
(
255
,
255
,
255
,
0.3
)
transparent
;
}
.room-list
::-webkit-scrollbar
{
width
:
6px
;
}
.room-list
::-webkit-scrollbar-track
{
background
:
transparent
;
}
.room-list
::-webkit-scrollbar-thumb
{
background
:
rgba
(
255
,
255
,
255
,
0.3
);
border-radius
:
3px
;
}
.room-list-item
{
border-bottom
:
1px
solid
rgba
(
255
,
255
,
255
,
0.1
);
}
/* ============ 聊天区域样式 ============ */
.chat-section
{
display
:
flex
;
flex-direction
:
column
;
background-color
:
white
;
overflow
:
hidden
;
}
.chat-header
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
padding
:
1rem
1.5rem
;
background
:
white
;
border-bottom
:
1px
solid
#e1e5e9
;
box-shadow
:
0
1px
3px
rgba
(
0
,
0
,
0
,
0.1
);
z-index
:
10
;
}
.room-info
{
flex
:
1
;
min-width
:
0
;
/* 防止 flex 子元素溢出 */
}
.room-title
{
margin
:
0
0
0.25rem
0
;
font-size
:
1.125rem
;
font-weight
:
600
;
color
:
#1f2937
;
/* 防止标题过长 */
white-space
:
nowrap
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
}
.participant-count
{
margin
:
0
;
font-size
:
0.875rem
;
color
:
#6b7280
;
}
.room-actions
{
display
:
flex
;
gap
:
0.5rem
;
}
.action-button
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
width
:
36px
;
height
:
36px
;
border
:
none
;
border-radius
:
8px
;
background-color
:
#f3f4f6
;
cursor
:
pointer
;
transition
:
all
0.2s
ease
;
font-size
:
1rem
;
}
.action-button
:hover
{
background-color
:
#e5e7eb
;
transform
:
translateY
(
-1px
);
}
.action-button
:focus
{
outline
:
2px
solid
#3b82f6
;
outline-offset
:
2px
;
}
/* ============ 消息区域样式 ============ */
.messages-viewport
{
flex
:
1
;
overflow
:
hidden
;
position
:
relative
;
}
.messages-container
{
height
:
100%
;
overflow-y
:
auto
;
padding
:
1rem
1.5rem
;
display
:
flex
;
flex-direction
:
column
;
gap
:
0.75rem
;
scrollbar-width
:
thin
;
scrollbar-color
:
#cbd5e1
transparent
;
}
.messages-container
::-webkit-scrollbar
{
width
:
8px
;
}
.messages-container
::-webkit-scrollbar-track
{
background
:
transparent
;
}
.messages-container
::-webkit-scrollbar-thumb
{
background
:
#cbd5e1
;
border-radius
:
4px
;
}
.messages-container
::-webkit-scrollbar-thumb:hover
{
background
:
#94a3b8
;
}
/* ============ 消息输入区域样式 ============ */
.message-input-section
{
padding
:
1rem
1.5rem
;
background
:
white
;
border-top
:
1px
solid
#e1e5e9
;
}
.message-form
{
width
:
100%
;
}
.input-wrapper
{
display
:
flex
;
gap
:
0.75rem
;
align-items
:
flex-end
;
}
.message-textarea
{
flex
:
1
;
min-height
:
40px
;
max-height
:
120px
;
padding
:
0.75rem
1rem
;
border
:
2px
solid
#e1e5e9
;
border-radius
:
12px
;
font-family
:
inherit
;
font-size
:
0.875rem
;
line-height
:
1.5
;
resize
:
none
;
transition
:
all
0.2s
ease
;
background-color
:
#f8f9fa
;
/* 防止文本溢出 */
word-wrap
:
break-word
;
overflow-wrap
:
break-word
;
}
.message-textarea
:focus
{
outline
:
none
;
border-color
:
#3b82f6
;
background-color
:
white
;
box-shadow
:
0
0
0
3px
rgba
(
59
,
130
,
246
,
0.1
);
}
.message-textarea
::placeholder
{
color
:
#9ca3af
;
}
.send-button
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
width
:
40px
;
height
:
40px
;
border
:
none
;
border-radius
:
12px
;
background
:
linear-gradient
(
135deg
,
#3b82f6
0%
,
#1d4ed8
100%
);
color
:
white
;
cursor
:
pointer
;
transition
:
all
0.2s
ease
;
font-size
:
1rem
;
}
.send-button
:hover:not
(
:disabled
)
{
transform
:
translateY
(
-2px
);
box-shadow
:
0
4px
12px
rgba
(
59
,
130
,
246
,
0.4
);
}
.send-button
:active
{
transform
:
translateY
(
0
);
}
.send-button
:disabled
{
background
:
#d1d5db
;
cursor
:
not-allowed
;
transform
:
none
;
box-shadow
:
none
;
}
.send-button
:focus
{
outline
:
2px
solid
#3b82f6
;
outline-offset
:
2px
;
}
/* ============ 空状态样式 ============ */
.empty-state
{
flex
:
1
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
padding
:
2rem
;
}
.empty-content
{
text-align
:
center
;
max-width
:
400px
;
}
.empty-icon
{
font-size
:
4rem
;
margin-bottom
:
1rem
;
opacity
:
0.6
;
}
.empty-title
{
margin
:
0
0
0.5rem
0
;
font-size
:
1.25rem
;
font-weight
:
600
;
color
:
#374151
;
}
.empty-description
{
margin
:
0
;
color
:
#6b7280
;
line-height
:
1.6
;
}
/* ============ 通用交互效果 ============ */
.chatroom-main
*
{
box-sizing
:
border-box
;
}
/* 平滑滚动 */
.messages-container
,
.room-list
{
scroll-behavior
:
smooth
;
}
/* 选择文本样式 */
::selection
{
background-color
:
#3b82f6
;
color
:
white
;
}
/* 焦点管理 */
.chatroom-main
:focus
{
outline-offset
:
2px
;
}
/* ============ 创建房间功能样式 ============ */
.create-room-section
{
padding
:
1rem
;
border-bottom
:
1px
solid
rgba
(
255
,
255
,
255
,
0.2
);
}
.create-room-button
{
width
:
100%
;
padding
:
0.75rem
1rem
;
background
:
rgba
(
255
,
255
,
255
,
0.1
);
border
:
1px
solid
rgba
(
255
,
255
,
255
,
0.3
);
border-radius
:
8px
;
color
:
white
;
font-size
:
0.875rem
;
cursor
:
pointer
;
transition
:
all
0.2s
ease
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
0.5rem
;
}
.create-room-button
:hover
{
background
:
rgba
(
255
,
255
,
255
,
0.2
);
transform
:
translateY
(
-1px
);
}
.create-room-form
{
display
:
flex
;
flex-direction
:
column
;
gap
:
0.75rem
;
}
.room-name-input
{
width
:
100%
;
padding
:
0.75rem
;
border
:
1px
solid
rgba
(
255
,
255
,
255
,
0.3
);
border-radius
:
6px
;
background
:
rgba
(
255
,
255
,
255
,
0.1
);
color
:
white
;
font-size
:
0.875rem
;
}
.room-name-input
::placeholder
{
color
:
rgba
(
255
,
255
,
255
,
0.7
);
}
.room-name-input
:focus
{
outline
:
none
;
border-color
:
rgba
(
255
,
255
,
255
,
0.6
);
background
:
rgba
(
255
,
255
,
255
,
0.15
);
}
.form-buttons
{
display
:
flex
;
gap
:
0.5rem
;
}
.confirm-button
,
.cancel-button
{
flex
:
1
;
padding
:
0.5rem
;
border
:
none
;
border-radius
:
4px
;
font-size
:
0.875rem
;
cursor
:
pointer
;
transition
:
all
0.2s
ease
;
}
.confirm-button
{
background
:
#10b981
;
color
:
white
;
}
.confirm-button
:hover:not
(
:disabled
)
{
background
:
#059669
;
}
.confirm-button
:disabled
{
background
:
rgba
(
255
,
255
,
255
,
0.3
);
cursor
:
not-allowed
;
}
.cancel-button
{
background
:
rgba
(
255
,
255
,
255
,
0.2
);
color
:
white
;
}
.cancel-button
:hover
{
background
:
rgba
(
255
,
255
,
255
,
0.3
);
}
/* ============ 右键菜单样式 ============ */
.context-menu-overlay
{
position
:
fixed
;
top
:
0
;
left
:
0
;
width
:
100vw
;
height
:
100vh
;
z-index
:
1000
;
background
:
transparent
;
}
.context-menu
{
position
:
fixed
;
background
:
white
;
border
:
1px
solid
#e1e5e9
;
border-radius
:
8px
;
box-shadow
:
0
4px
12px
rgba
(
0
,
0
,
0
,
0.15
);
z-index
:
1001
;
min-width
:
120px
;
padding
:
0.25rem
0
;
}
.context-menu-item
{
width
:
100%
;
padding
:
0.75rem
1rem
;
border
:
none
;
background
:
white
;
text-align
:
left
;
cursor
:
pointer
;
font-size
:
0.875rem
;
color
:
#374151
;
display
:
flex
;
align-items
:
center
;
gap
:
0.5rem
;
transition
:
background-color
0.2s
ease
;
}
.context-menu-item
:hover
{
background-color
:
#f3f4f6
;
}
.context-menu-item.delete
{
color
:
#dc2626
;
}
.context-menu-item.delete
:hover
{
background-color
:
#fef2f2
;
}
src/components/ChatRoom/ChatRoom.tsx
0 → 100644
View file @
23e3ae0b
// 文件位置:src/components/ChatRoom/ChatRoom.tsx
"
use client
"
;
import
{
useState
,
useEffect
,
useRef
}
from
"
react
"
;
import
useSWR
from
"
swr
"
;
import
useSWRMutation
from
"
swr/mutation
"
;
import
{
Message
,
RoomPreviewInfo
,
ChatRoomData
}
from
"
@/types
"
;
import
{
RoomListRes
,
RoomMessageListRes
,
RoomAddRes
,
RoomMessageGetUpdateRes
,
}
from
"
@/types/api
"
;
import
{
getFetcher
,
postFetcher
}
from
"
@/lib/api
"
;
import
RoomEntry
from
"
@/components/RoomEntry/RoomEntry
"
;
import
MessageItem
from
"
@/components/MessageItem/MessageItem
"
;
import
"
./ChatRoom.css
"
;
export
default
function
ChatRoom
()
{
// 从localStorage获取用户名,如果没有则默认为"我"
const
[
currentUser
]
=
useState
(()
=>
{
if
(
typeof
window
!==
"
undefined
"
)
{
return
localStorage
.
getItem
(
"
userName
"
)
||
"
我
"
;
}
return
"
我
"
;
});
const
[
message
,
setMessage
]
=
useState
(
""
);
const
[
roomId
,
setRoomId
]
=
useState
<
number
|
null
>
(
null
);
const
[
newRoomName
,
setNewRoomName
]
=
useState
(
""
);
const
[
showCreateForm
,
setShowCreateForm
]
=
useState
(
false
);
// 添加一个标记来跟踪是否已经初始化了房间选择
const
[
hasInitializedRoom
,
setHasInitializedRoom
]
=
useState
(
false
);
// 用于跟踪最后一条消息ID,实现增量更新
const
lastMessageIdRef
=
useRef
<
number
>
(
0
);
// 右键菜单状态
const
[
contextMenu
,
setContextMenu
]
=
useState
<
{
visible
:
boolean
;
x
:
number
;
y
:
number
;
roomId
:
number
|
null
;
}
>
({
visible
:
false
,
x
:
0
,
y
:
0
,
roomId
:
null
,
});
// 获取房间列表
const
{
data
:
roomListData
,
error
:
roomError
,
isLoading
:
roomIsLoading
,
mutate
:
mutateRooms
,
}
=
useSWR
<
RoomListRes
>
(
"
/api/room/list
"
,
getFetcher
,
{
refreshInterval
:
5000
,
revalidateOnFocus
:
false
,
revalidateOnReconnect
:
true
,
});
// 获取当前房间的消息列表
const
{
data
:
messageListData
,
error
:
messageListError
,
isLoading
:
messageListIsLoading
,
mutate
:
mutateMessages
,
}
=
useSWR
<
RoomMessageListRes
>
(
()
=>
{
if
(
roomId
===
null
)
return
null
;
return
`/api/room/message/list?roomId=
${
roomId
}
`
;
},
getFetcher
,
{
refreshInterval
:
3000
,
revalidateOnFocus
:
false
,
onSuccess
:
(
data
)
=>
{
if
(
data
.
messages
.
length
>
0
)
{
const
maxId
=
Math
.
max
(...
data
.
messages
.
map
((
m
)
=>
m
.
messageId
));
lastMessageIdRef
.
current
=
maxId
;
}
},
}
);
// 创建房间的mutation - 更新类型定义
const
{
trigger
:
createRoomTrigger
,
isMutating
:
isCreatingRoom
}
=
useSWRMutation
<
RoomAddRes
,
Error
,
string
,
{
roomName
:
string
}
>
(
"
/api/room/add
"
,
postFetcher
);
// 发送消息的mutation - 更新类型定义
const
{
trigger
:
sendMessageTrigger
,
isMutating
:
isSendingMessage
}
=
useSWRMutation
<
void
,
Error
,
string
,
{
roomId
:
number
;
content
:
string
}
>
(
"
/api/message/add
"
,
postFetcher
);
// 删除房间的mutation - 更新类型定义
const
{
trigger
:
deleteRoomTrigger
,
isMutating
:
isDeletingRoom
}
=
useSWRMutation
<
void
,
Error
,
string
,
{
roomId
:
number
}
>
(
"
/api/room/delete
"
,
postFetcher
);
// 自动选择房间的逻辑 - 修改为确保始终有房间被选中
useEffect
(()
=>
{
if
(
typeof
window
===
"
undefined
"
)
return
;
if
(
!
roomListData
?.
rooms
||
roomListData
.
rooms
.
length
===
0
)
return
;
// 如果已经有选中的房间且房间仍然存在,不重复设置
if
(
roomId
!==
null
)
{
const
roomExists
=
roomListData
.
rooms
.
some
(
(
room
)
=>
room
.
roomId
===
roomId
);
if
(
roomExists
)
return
;
}
// 尝试从localStorage恢复上次选择的房间
const
savedRoomId
=
localStorage
.
getItem
(
"
lastSelectedRoomId
"
);
if
(
savedRoomId
&&
!
hasInitializedRoom
)
{
const
savedId
=
parseInt
(
savedRoomId
);
const
roomExists
=
roomListData
.
rooms
.
some
(
(
room
)
=>
room
.
roomId
===
savedId
);
if
(
roomExists
)
{
setRoomId
(
savedId
);
setHasInitializedRoom
(
true
);
return
;
}
}
// 如果没有保存的房间ID或房间不存在,选择第一个房间
console
.
log
(
"
自动选择第一个房间:
"
,
roomListData
.
rooms
[
0
].
roomId
);
setRoomId
(
roomListData
.
rooms
[
0
].
roomId
);
setHasInitializedRoom
(
true
);
},
[
roomListData
,
hasInitializedRoom
]);
// 移除roomId依赖,添加hasInitializedRoom
// 保存房间选择到localStorage
useEffect
(()
=>
{
if
(
typeof
window
!==
"
undefined
"
&&
roomId
!==
null
)
{
localStorage
.
setItem
(
"
lastSelectedRoomId
"
,
roomId
.
toString
());
}
},
[
roomId
]);
// 处理房间点击
const
handleRoomClick
=
(
newRoomId
:
number
)
=>
{
setRoomId
(
newRoomId
);
lastMessageIdRef
.
current
=
0
;
};
// 发送消息 - 移除sender字段
const
sendMessage
=
async
()
=>
{
const
messageContent
=
message
.
trim
();
if
(
!
messageContent
||
!
roomId
||
isSendingMessage
)
{
return
;
}
console
.
log
(
"
准备发送消息:
"
,
{
roomId
,
content
:
messageContent
,
});
try
{
await
sendMessageTrigger
({
roomId
,
content
:
messageContent
,
});
console
.
log
(
"
消息发送成功
"
);
setMessage
(
""
);
// 立即刷新消息列表
mutateMessages
();
}
catch
(
error
)
{
console
.
error
(
"
发送消息失败:
"
,
error
);
alert
(
`发送消息失败:
${
error
instanceof
Error
?
error
.
message
:
"
未知错误
"
}
`
);
}
};
// 创建新房间 - 移除user字段
const
createNewRoom
=
async
()
=>
{
if
(
newRoomName
.
trim
()
&&
!
isCreatingRoom
)
{
try
{
const
result
=
await
createRoomTrigger
({
roomName
:
newRoomName
.
trim
(),
});
console
.
log
(
"
房间创建成功:
"
,
result
);
setNewRoomName
(
""
);
setShowCreateForm
(
false
);
// 刷新房间列表
await
mutateRooms
();
// 自动切换到新建的房间
setRoomId
(
result
.
roomId
);
}
catch
(
error
)
{
console
.
error
(
"
创建房间失败:
"
,
error
);
alert
(
`创建房间失败:
${
error
instanceof
Error
?
error
.
message
:
"
未知错误
"
}
`
);
}
}
};
// 处理右键菜单
const
handleRoomContextMenu
=
(
e
:
React
.
MouseEvent
,
roomId
:
number
)
=>
{
e
.
preventDefault
();
setContextMenu
({
visible
:
true
,
x
:
e
.
clientX
,
y
:
e
.
clientY
,
roomId
:
roomId
,
});
};
// 关闭右键菜单
const
closeContextMenu
=
()
=>
{
setContextMenu
({
visible
:
false
,
x
:
0
,
y
:
0
,
roomId
:
null
,
});
};
// 删除房间 - 移除user字段,添加更多调试信息和错误处理
const
deleteRoom
=
async
(
roomIdToDelete
:
number
)
=>
{
if
(
isDeletingRoom
)
{
console
.
log
(
"
正在删除房间,跳过重复请求
"
);
return
;
}
if
(
roomListData
?.
rooms
&&
roomListData
.
rooms
.
length
<=
1
)
{
alert
(
"
至少需要保留一个房间!
"
);
closeContextMenu
();
return
;
}
const
currentRooms
=
roomListData
?.
rooms
||
[];
const
roomToDelete
=
currentRooms
.
find
((
r
)
=>
r
.
roomId
===
roomIdToDelete
);
if
(
!
roomToDelete
)
{
console
.
log
(
"
房间不在当前列表中,roomIdToDelete:
"
,
roomIdToDelete
);
console
.
log
(
"
当前房间列表:
"
,
currentRooms
.
map
((
r
)
=>
({
id
:
r
.
roomId
,
name
:
r
.
roomName
}))
);
alert
(
"
房间不存在或已被删除!
"
);
closeContextMenu
();
await
mutateRooms
();
return
;
}
if
(
!
confirm
(
`确定要删除房间"
${
roomToDelete
.
roomName
}
"吗?`
))
{
closeContextMenu
();
return
;
}
closeContextMenu
();
try
{
console
.
log
(
"
开始删除房间:
"
,
{
roomIdToDelete
,
roomName
:
roomToDelete
.
roomName
,
currentRoomsCount
:
currentRooms
.
length
,
});
// 如果要删除的是当前房间,先切换到其他房间
if
(
roomId
===
roomIdToDelete
)
{
const
remainingRooms
=
currentRooms
.
filter
(
(
r
)
=>
r
.
roomId
!==
roomIdToDelete
);
if
(
remainingRooms
.
length
>
0
)
{
console
.
log
(
"
切换到房间:
"
,
remainingRooms
[
0
].
roomId
);
setRoomId
(
remainingRooms
[
0
].
roomId
);
await
new
Promise
((
resolve
)
=>
setTimeout
(
resolve
,
200
));
// 增加等待时间
}
else
{
setRoomId
(
null
);
}
}
// 调用删除API - 移除user字段
const
deleteParams
=
{
roomId
:
roomIdToDelete
,
};
console
.
log
(
"
调用删除API,参数:
"
,
deleteParams
);
await
deleteRoomTrigger
(
deleteParams
);
console
.
log
(
"
房间删除API调用成功:
"
,
roomIdToDelete
);
// 删除成功后刷新房间列表
await
mutateRooms
();
console
.
log
(
"
房间列表已刷新
"
);
}
catch
(
error
)
{
console
.
error
(
"
删除房间失败 - 详细错误信息:
"
,
{
error
,
errorMessage
:
error
instanceof
Error
?
error
.
message
:
String
(
error
),
errorName
:
error
instanceof
Error
?
error
.
name
:
"
Unknown
"
,
errorStack
:
error
instanceof
Error
?
error
.
stack
:
"
No stack
"
,
roomId
:
roomIdToDelete
,
});
const
errorMessage
=
error
instanceof
Error
?
error
.
message
:
String
(
error
);
// 更精确的错误处理
if
(
errorMessage
.
includes
(
"
房间不存在
"
)
||
errorMessage
.
includes
(
"
不存在
"
)
||
errorMessage
.
includes
(
"
404
"
)
||
errorMessage
.
includes
(
"
请求失败
"
)
)
{
console
.
log
(
"
房间可能已经被删除,刷新房间列表
"
);
alert
(
"
房间可能已经被删除或不存在!正在刷新列表...
"
);
await
mutateRooms
();
}
else
if
(
errorMessage
.
includes
(
"
至少需要保留一个房间
"
))
{
alert
(
"
至少需要保留一个房间!
"
);
}
else
{
alert
(
`删除房间失败:
${
errorMessage
}
`
);
}
}
};
// 获取当前房间数据 - 确保始终显示聊天界面而不是空状态
const
getCurrentRoomData
=
():
ChatRoomData
|
null
=>
{
// 只有在没有房间或房间列表为空时才返回null
if
(
!
roomListData
?.
rooms
||
roomListData
.
rooms
.
length
===
0
)
{
return
null
;
}
// 如果还没选择房间,返回null(但这种情况应该很快就会被useEffect处理)
if
(
roomId
===
null
)
{
return
null
;
}
const
currentRoom
=
roomListData
.
rooms
.
find
((
r
)
=>
r
.
roomId
===
roomId
);
if
(
!
currentRoom
)
{
// 如果当前选择的房间不存在了,自动选择第一个房间
console
.
log
(
"
当前房间不存在,自动切换到第一个房间
"
);
setRoomId
(
roomListData
.
rooms
[
0
].
roomId
);
return
null
;
}
// 即使消息为空也显示聊天界面
const
messages
=
messageListData
?.
messages
||
[];
return
{
roomId
,
roomName
:
currentRoom
.
roomName
,
messages
:
messages
,
participants
:
[],
};
};
const
currentRoomData
=
getCurrentRoomData
();
// 处理全局点击,关闭右键菜单
useEffect
(()
=>
{
const
handleGlobalClick
=
()
=>
{
if
(
contextMenu
.
visible
)
{
closeContextMenu
();
}
};
document
.
addEventListener
(
"
click
"
,
handleGlobalClick
);
return
()
=>
{
document
.
removeEventListener
(
"
click
"
,
handleGlobalClick
);
};
},
[
contextMenu
.
visible
]);
// 加载状态
if
(
roomIsLoading
)
{
return
(
<
main
className
=
"chatroom-main"
>
<
div
className
=
"loading-state"
>
<
div
className
=
"loading-content"
>
<
div
className
=
"loading-spinner"
>
⏳
</
div
>
<
p
>
正在加载聊天室...
</
p
>
</
div
>
</
div
>
</
main
>
);
}
// 错误状态
if
(
roomError
)
{
return
(
<
main
className
=
"chatroom-main"
>
<
div
className
=
"error-state"
>
<
div
className
=
"error-content"
>
<
div
className
=
"error-icon"
>
❌
</
div
>
<
h3
>
加载失败
</
h3
>
<
p
>
无法连接到服务器:
{
roomError
.
message
}
</
p
>
<
button
onClick
=
{
()
=>
mutateRooms
()
}
className
=
"retry-button"
>
重试
</
button
>
</
div
>
</
div
>
</
main
>
);
}
return
(
<
main
className
=
"chatroom-main"
>
{
/* 左侧边栏 - 房间列表 */
}
<
aside
className
=
"room-sidebar"
>
<
header
className
=
"sidebar-header"
>
<
h1
className
=
"sidebar-title"
>
聊天房间
</
h1
>
<
div
className
=
"user-status"
>
<
span
className
=
"current-user-indicator"
>
{
currentUser
}
</
span
>
<
div
className
=
"online-status"
></
div
>
</
div
>
</
header
>
<
nav
className
=
"room-navigation"
>
<
div
className
=
"create-room-section"
>
{
!
showCreateForm
?
(
<
button
className
=
"create-room-button"
onClick
=
{
()
=>
setShowCreateForm
(
true
)
}
disabled
=
{
isCreatingRoom
}
>
➕ 创建新房间
</
button
>
)
:
(
<
div
className
=
"create-room-form"
>
<
input
type
=
"text"
className
=
"room-name-input"
value
=
{
newRoomName
}
onChange
=
{
(
e
)
=>
setNewRoomName
(
e
.
target
.
value
)
}
placeholder
=
"输入房间名称..."
disabled
=
{
isCreatingRoom
}
onKeyDown
=
{
(
e
)
=>
{
if
(
e
.
key
===
"
Enter
"
&&
!
isCreatingRoom
)
{
createNewRoom
();
}
else
if
(
e
.
key
===
"
Escape
"
)
{
setShowCreateForm
(
false
);
setNewRoomName
(
""
);
}
}
}
/>
<
div
className
=
"form-buttons"
>
<
button
className
=
"confirm-button"
onClick
=
{
createNewRoom
}
disabled
=
{
!
newRoomName
.
trim
()
||
isCreatingRoom
}
>
{
isCreatingRoom
?
"
⏳
"
:
"
✓
"
}
</
button
>
<
button
className
=
"cancel-button"
onClick
=
{
()
=>
{
setShowCreateForm
(
false
);
setNewRoomName
(
""
);
}
}
disabled
=
{
isCreatingRoom
}
>
✗
</
button
>
</
div
>
</
div
>
)
}
</
div
>
<
ul
className
=
"room-list"
>
{
roomListData
?.
rooms
?.
map
((
room
)
=>
(
<
li
key
=
{
room
.
roomId
}
className
=
"room-list-item"
>
<
RoomEntry
room
=
{
room
}
isActive
=
{
room
.
roomId
===
roomId
}
onClick
=
{
handleRoomClick
}
onContextMenu
=
{
handleRoomContextMenu
}
/>
</
li
>
))
}
</
ul
>
</
nav
>
</
aside
>
{
/* 右侧主体 - 聊天区域 */
}
<
section
className
=
"chat-section"
>
{
currentRoomData
?
(
<>
<
header
className
=
"chat-header"
>
<
div
className
=
"room-info"
>
<
h2
className
=
"room-title"
>
{
currentRoomData
.
roomName
}
</
h2
>
<
p
className
=
"participant-count"
>
{
currentRoomData
.
messages
.
length
}
条消息
{
messageListIsLoading
&&
"
(更新中...)
"
}
</
p
>
</
div
>
<
div
className
=
"room-actions"
>
<
button
className
=
"action-button"
type
=
"button"
aria-label
=
"房间设置"
>
⚙️
</
button
>
</
div
>
</
header
>
<
div
className
=
"messages-viewport"
>
<
div
className
=
"messages-container"
>
{
messageListError
?
(
<
div
className
=
"error-message"
>
<
p
>
消息加载失败:
{
messageListError
.
message
}
</
p
>
<
button
onClick
=
{
()
=>
mutateMessages
()
}
>
重试
</
button
>
</
div
>
)
:
messageListIsLoading
&&
currentRoomData
.
messages
.
length
===
0
?
(
<
div
className
=
"loading-messages"
>
<
p
>
正在加载消息...
</
p
>
</
div
>
)
:
currentRoomData
.
messages
.
length
===
0
?
(
<
div
className
=
"no-messages"
>
<
p
>
还没有消息,发送第一条消息开始聊天吧!
</
p
>
</
div
>
)
:
(
currentRoomData
.
messages
.
map
((
msg
)
=>
(
<
MessageItem
key
=
{
msg
.
messageId
}
message
=
{
msg
}
isCurrentUser
=
{
msg
.
sender
===
currentUser
}
/>
))
)
}
</
div
>
</
div
>
<
footer
className
=
"message-input-section"
>
<
form
className
=
"message-form"
onSubmit
=
{
(
e
)
=>
{
e
.
preventDefault
();
sendMessage
();
}
}
>
<
div
className
=
"input-wrapper"
>
<
textarea
className
=
"message-textarea"
value
=
{
message
}
onChange
=
{
(
e
)
=>
setMessage
(
e
.
target
.
value
)
}
placeholder
=
"输入消息... (按 Enter 发送,Shift+Enter 换行)"
rows
=
{
1
}
disabled
=
{
isSendingMessage
}
onKeyDown
=
{
(
e
)
=>
{
if
(
e
.
key
===
"
Enter
"
&&
!
e
.
shiftKey
)
{
e
.
preventDefault
();
sendMessage
();
}
}
}
/>
<
button
className
=
"send-button"
type
=
"submit"
disabled
=
{
!
message
.
trim
()
||
isSendingMessage
}
aria-label
=
"发送消息"
>
{
isSendingMessage
?
"
⏳
"
:
"
📤
"
}
</
button
>
</
div
>
</
form
>
</
footer
>
</>
)
:
(
<
div
className
=
"empty-state"
>
<
div
className
=
"empty-content"
>
<
div
className
=
"empty-icon"
>
💬
</
div
>
<
h3
className
=
"empty-title"
>
{
roomListData
?.
rooms
&&
roomListData
.
rooms
.
length
===
0
?
"
还没有聊天房间
"
:
"
正在加载聊天室...
"
}
</
h3
>
<
p
className
=
"empty-description"
>
{
roomListData
?.
rooms
&&
roomListData
.
rooms
.
length
===
0
?
'
点击 "创建新房间" 开始您的第一次聊天吧!
'
:
"
请稍候,正在为您准备聊天环境...
"
}
</
p
>
</
div
>
</
div
>
)
}
</
section
>
{
/* 右键菜单 */
}
{
contextMenu
.
visible
&&
contextMenu
.
roomId
&&
(
<>
<
div
className
=
"context-menu-overlay"
onClick
=
{
closeContextMenu
}
/>
<
div
className
=
"context-menu"
style
=
{
{
left
:
contextMenu
.
x
,
top
:
contextMenu
.
y
,
}
}
onClick
=
{
(
e
)
=>
e
.
stopPropagation
()
}
>
<
button
className
=
"context-menu-item delete"
onClick
=
{
()
=>
{
if
(
contextMenu
.
roomId
&&
!
isDeletingRoom
)
{
deleteRoom
(
contextMenu
.
roomId
);
}
}
}
disabled
=
{
isDeletingRoom
}
>
{
isDeletingRoom
?
"
⏳ 删除中...
"
:
"
🗑️ 删除房间
"
}
</
button
>
</
div
>
</>
)
}
</
main
>
);
}
src/components/MessageItem/MessageItem.css
0 → 100644
View file @
23e3ae0b
/* src/components/MessageItem/MessageItem.css */
.message-item
{
margin
:
10px
0
;
padding
:
10px
;
border-radius
:
8px
;
max-width
:
70%
;
}
.message-item.current-user
{
background-color
:
#007bff
;
color
:
white
;
margin-left
:
auto
;
text-align
:
right
;
}
.message-item.other-user
{
background-color
:
#f1f1f1
;
color
:
#333
;
margin-right
:
auto
;
}
.message-header
{
display
:
flex
;
justify-content
:
space-between
;
margin-bottom
:
5px
;
font-size
:
0.8em
;
opacity
:
0.8
;
}
.message-content
{
font-size
:
1em
;
line-height
:
1.4
;
}
src/components/MessageItem/MessageItem.tsx
0 → 100644
View file @
23e3ae0b
// src/components/MessageItem/MessageItem.tsx
// 引入 Message 类型,用于定义消息的结构
import
{
Message
}
from
"
@/types
"
;
import
"
./MessageItem.css
"
;
// 定义 MessageItemProps 接口,描述组件接收的属
interface
MessageItemProps
{
message
:
Message
;
// 消息对象,包含发送者、内容、时间等
isCurrentUser
:
boolean
;
// 表示消息是否由当前用户发送
}
// 定义 MessageItem 组件,接收 message 和 isCurrentUser 属性
export
default
function
MessageItem
({
message
,
isCurrentUser
,
}:
MessageItemProps
)
{
// 定义 formatTime 函数,将时间戳格式化为本地时间
const
formatTime
=
(
timestamp
:
number
)
=>
{
return
new
Date
(
timestamp
).
toLocaleTimeString
(
"
zh-CN
"
,
{
hour
:
"
2-digit
"
,
minute
:
"
2-digit
"
,
});
};
return
(
<
div
className
=
{
`message-item
${
isCurrentUser
?
"
current-user
"
:
"
other-user
"
}
`
}
>
<
div
className
=
"message-header"
>
<
span
className
=
"sender"
>
{
message
.
sender
}
</
span
>
<
span
className
=
"time"
>
{
formatTime
(
message
.
time
)
}
</
span
>
</
div
>
<
div
className
=
"message-content"
>
{
message
.
content
}
</
div
>
</
div
>
);
}
src/components/RoomEntry/RoomEntry.css
0 → 100644
View file @
23e3ae0b
/* src/components/RoomEntry/RoomEntry.css */
.room-entry
{
padding
:
12px
;
border-bottom
:
1px
solid
#eee
;
cursor
:
pointer
;
transition
:
background-color
0.2s
;
}
.room-entry
:hover
{
background-color
:
#f5f5f5
;
}
.room-entry.active
{
background-color
:
#e3f2fd
;
border-left
:
4px
solid
#007bff
;
}
.room-header
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
margin-bottom
:
5px
;
}
.room-name
{
margin
:
0
;
font-size
:
1.1em
;
font-weight
:
600
;
}
.last-time
{
font-size
:
0.8em
;
color
:
#666
;
}
.room-preview
{
margin
:
0
;
}
.last-message
{
margin
:
0
;
font-size
:
0.9em
;
color
:
#666
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.sender
{
font-weight
:
500
;
margin-right
:
4px
;
}
.no-message
{
margin
:
0
;
font-size
:
0.9em
;
color
:
#999
;
font-style
:
italic
;
}
src/components/RoomEntry/RoomEntry.tsx
0 → 100644
View file @
23e3ae0b
// src/components/RoomEntry/RoomEntry.tsx
// 引入 RoomPreviewInfo 类型,用于定义聊天室预览信息的结构
import
{
RoomPreviewInfo
}
from
"
@/types
"
;
import
"
./RoomEntry.css
"
;
interface
RoomEntryProps
{
room
:
RoomPreviewInfo
;
// 聊天室预览信息
isActive
:
boolean
;
// 表示该聊天室是否当前选中
onClick
:
(
roomId
:
number
)
=>
void
;
// 点击聊天室时的回调函数
onContextMenu
?:
(
e
:
React
.
MouseEvent
,
roomId
:
number
)
=>
void
;
// 可选的右键菜单回调函数
}
export
default
function
RoomEntry
({
room
,
isActive
,
onClick
,
onContextMenu
,
}:
RoomEntryProps
)
{
const
formatTime
=
(
timestamp
?:
number
)
=>
{
if
(
!
timestamp
)
return
""
;
return
new
Date
(
timestamp
).
toLocaleTimeString
(
"
zh-CN
"
,
{
hour
:
"
2-digit
"
,
minute
:
"
2-digit
"
,
});
};
return
(
<
div
className
=
{
`room-entry
${
isActive
?
"
active
"
:
""
}
`
}
onClick
=
{
()
=>
onClick
(
room
.
roomId
)
}
onContextMenu
=
{
(
e
)
=>
onContextMenu
?.(
e
,
room
.
roomId
)
}
>
<
div
className
=
"room-header"
>
<
h3
className
=
"room-name"
>
{
room
.
roomName
}
</
h3
>
{
room
.
lastMessage
&&
(
<
span
className
=
"last-time"
>
{
formatTime
(
room
.
lastMessage
.
time
)
}
</
span
>
)
}
</
div
>
<
div
className
=
"room-preview"
>
{
room
.
lastMessage
?
(
<
p
className
=
"last-message"
>
<
span
className
=
"sender"
>
{
room
.
lastMessage
.
sender
}
:
</
span
>
<
span
className
=
"content"
>
{
room
.
lastMessage
.
content
}
</
span
>
</
p
>
)
:
(
<
p
className
=
"no-message"
>
暂无消息
</
p
>
)
}
</
div
>
</
div
>
);
}
src/components/SetName/SetName.css
0 → 100644
View file @
23e3ae0b
.setname-container
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
justify-content
:
center
;
height
:
100vh
;
gap
:
20px
;
}
.setname-container
input
{
padding
:
10px
;
font-size
:
16px
;
border
:
1px
solid
#ccc
;
border-radius
:
4px
;
width
:
200px
;
}
.setname-container
button
{
padding
:
10px
20px
;
font-size
:
16px
;
background-color
:
#007bff
;
color
:
white
;
border
:
none
;
border-radius
:
4px
;
cursor
:
pointer
;
}
.setname-container
button
:hover
{
background-color
:
#0056b3
;
}
src/components/SetName/SetName.tsx
0 → 100644
View file @
23e3ae0b
// 文件位置:src/components/SetName/SetName.tsx
"
use client
"
;
import
{
useState
,
useEffect
}
from
"
react
"
;
import
{
useRouter
}
from
"
next/navigation
"
;
import
"
./SetName.css
"
;
export
default
function
SetName
()
{
const
[
name
,
setName
]
=
useState
(
""
);
const
router
=
useRouter
();
// 检查是否已经设置了用户名
useEffect
(()
=>
{
if
(
typeof
window
!==
"
undefined
"
)
{
const
savedName
=
localStorage
.
getItem
(
"
userName
"
);
if
(
savedName
)
{
// 如果已经有用户名,直接跳转到聊天室
router
.
push
(
"
/chatroom
"
);
}
}
},
[
router
]);
const
handleSubmit
=
()
=>
{
if
(
name
.
trim
())
{
// 保存用户名到localStorage
localStorage
.
setItem
(
"
userName
"
,
name
.
trim
());
// 跳转到聊天室页面
router
.
push
(
"
/chatroom
"
);
}
};
const
handleKeyDown
=
(
e
:
React
.
KeyboardEvent
)
=>
{
if
(
e
.
key
===
"
Enter
"
&&
name
.
trim
())
{
handleSubmit
();
}
};
return
(
<
div
className
=
"setname-container"
>
<
div
className
=
"setname-card"
>
<
h1
className
=
"setname-title"
>
设置昵称
</
h1
>
<
p
className
=
"setname-subtitle"
>
请输入您的昵称以开始聊天
</
p
>
<
div
className
=
"setname-form"
>
<
input
type
=
"text"
value
=
{
name
}
onChange
=
{
(
e
)
=>
setName
(
e
.
target
.
value
)
}
onKeyDown
=
{
handleKeyDown
}
placeholder
=
"请输入昵称"
className
=
"setname-input"
maxLength
=
{
20
}
autoFocus
/>
<
button
onClick
=
{
handleSubmit
}
disabled
=
{
!
name
.
trim
()
}
className
=
"setname-button"
>
进入聊天室
</
button
>
</
div
>
<
div
className
=
"setname-tips"
>
<
p
>
💡 提示:昵称将作为您在聊天室中的显示名称
</
p
>
</
div
>
</
div
>
</
div
>
);
}
src/components/UserInfo/UserInfo.css
0 → 100644
View file @
23e3ae0b
.user-info
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
padding
:
0.75rem
1rem
;
background
:
#f8f9fa
;
border-bottom
:
1px
solid
#e9ecef
;
gap
:
1rem
;
}
.user-details
{
display
:
flex
;
align-items
:
center
;
gap
:
0.5rem
;
}
.user-avatar
{
font-size
:
1.2rem
;
}
.user-name
{
font-weight
:
500
;
color
:
#495057
;
}
.logout-button
{
padding
:
0.375rem
0.75rem
;
background
:
#dc3545
;
color
:
white
;
border
:
none
;
border-radius
:
4px
;
cursor
:
pointer
;
font-size
:
0.875rem
;
transition
:
background-color
0.2s
;
}
.logout-button
:hover
{
background
:
#c82333
;
}
src/components/UserInfo/UserInfo.tsx
0 → 100644
View file @
23e3ae0b
// UserInfo 是一个 React 组件,负责显示当前登录用户的信息和提供退出登录功能
// 显示用户的头像(占位符 👤)和用户名(user.username),以及一个退出登录按钮。(就是聊天室最上面那一行)
"
use client
"
;
import
React
from
"
react
"
;
import
{
useAuth
}
from
"
@/hooks/useAuth
"
;
import
"
./UserInfo.css
"
;
export
default
function
UserInfo
()
{
const
{
isAuthenticated
,
user
,
logout
}
=
useAuth
();
if
(
!
isAuthenticated
||
!
user
)
{
return
null
;
}
return
(
<
div
className
=
"user-info"
>
<
div
className
=
"user-details"
>
<
span
className
=
"user-avatar"
>
👤
</
span
>
<
span
className
=
"user-name"
>
{
user
.
username
}
</
span
>
</
div
>
<
button
onClick
=
{
logout
}
className
=
"logout-button"
>
退出登录
</
button
>
</
div
>
);
}
Prev
1
2
3
Next
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment