Commit 23e3ae0b authored by qionghong liu's avatar qionghong liu
Browse files

initial commit

parent 3bf09937
// 文件位置: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
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
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: '获取消息列表失败'
});
}
}
"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>
);
}
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>
); );
} }
.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;
}
// 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>
);
}
"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>
);
}
// 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}</>;
}
"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 */
/* 主容器 - 使用 Grid 布局 */
.chatroom-main {
display: grid;
grid-template-columns: 320px 1fr;
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: 1fr;
grid-template-rows: auto 1fr;
}
.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
"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 */
.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
// 引入 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 */
.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
// 引入 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>
);
}
.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
"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>
);
}
.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;
}
// 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>
);
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment