Commit 5b05db40 authored by 成尧 江's avatar 成尧 江
Browse files

completed

parent 1a0c27d8
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
\ No newline at end of file
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function POST(req: Request) {
const { username, password } = await req.json();
if (!username || !password) {
return NextResponse.json({ code: 1, error: "缺少参数" });
}
try {
const user = await prisma.user.findUnique({ where: { username } });
if (!user || user.password !== password) {
return NextResponse.json({ code: 1, error: "用户名或密码错误" });
}
// ✅ 返回用户名,前端才能存
return NextResponse.json({ code: 0, username: user.username });
} catch (err) {
console.error(err);
return NextResponse.json({ code: 1, error: "登录失败" });
}
}
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function POST(req: Request) {
const { username, password } = await req.json();
if (!username || !password) {
return NextResponse.json({ code: 1, error: "缺少参数" });
}
try {
const exist = await prisma.user.findUnique({ where: { username } });
if (exist) {
return NextResponse.json({ code: 1, error: "用户名已存在" });
}
const newUser = await prisma.user.create({
data: { username, password },
});
// ✅ 注册成功后返回 username,保证和 login 一致
return NextResponse.json({ code: 0, username: newUser.username });
} catch (err) {
console.error(err);
return NextResponse.json({ code: 1, error: "注册失败" });
}
}
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function POST(req: Request) {
try {
const body = await req.json();
// roomId 可能是 string,要转成 number
const roomIdRaw = body.roomId;
const roomId =
typeof roomIdRaw === "string" ? parseInt(roomIdRaw, 10) : roomIdRaw;
const content: string = body.content;
const sender: string = body.sender;
const created = await prisma.message.create({
data: {
roomId,
content,
sender, // 只存字符串
time: new Date() // 统一用 time 记录时间(也可省略,schema 有 default(now()) 就会自动填)
},
});
return NextResponse.json({
code: 0,
data: {
messageId: created.id,
time: created.time,
},
});
} catch (err: any) {
console.error("MessageAdd error:", err);
return NextResponse.json({ code: 1, error: err.message || "Server error" });
}
}
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
interface RoomAddArgs {
user: string;
roomName: string;
}
export async function POST(req: Request) {
try {
const { user, roomName }: RoomAddArgs = await req.json();
if (!user || !roomName) {
return NextResponse.json({ code: 1, error: "Missing arguments" });
}
const creator = await prisma.user.findFirst({ where: { username: user } });
if (!creator) {
return NextResponse.json({ code: 1, error: "User not found" });
}
const room = await prisma.room.create({
data: {
roomName,
createdBy: creator.id, // 记录谁是创建者
users: { connect: { id: creator.id } }, // 创建者自动加入房间
},
});
return NextResponse.json({ code: 0, data: { roomId: room.id } });
} catch (err: any) {
console.error("RoomAdd error:", err);
return NextResponse.json({ code: 1, error: err.message || "Server error" });
}
}
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function POST(req: Request) {
try {
const { roomId } = await req.json();
if (!roomId) {
return NextResponse.json({
code: 1,
error: "缺少 roomId 参数",
});
}
const roomIdInt = Number(roomId); // ✅ 统一转成 Int
// 1️⃣ 先清空房间的用户关联
await prisma.room.update({
where: { id: roomIdInt },
data: {
users: { set: [] }, // 删除所有用户关联
},
});
// 2️⃣ 删除房间的消息
await prisma.message.deleteMany({
where: { roomId: roomIdInt }, // ✅ 注意这里
});
// 3️⃣ 删除房间本身
await prisma.room.delete({
where: { id: roomIdInt },
});
return NextResponse.json({
code: 0,
message: "房间删除成功(用户保留)",
});
} catch (err: any) {
console.error("DeleteRoom error:", err);
return NextResponse.json({
code: 1,
error: err.message || "删除房间失败",
});
}
}
import { prisma } from "@/lib/prisma";
import { NextResponse } from "next/server";
export async function GET() {
try {
const rooms = await prisma.room.findMany({
include: {
users: { // ✅ 一定要 include users
select: {
id: true,
username: true, // 避免把 password 返回出去
},
},
messages: {
orderBy: { time: "desc" },
take: 1, // 只取最新一条消息
},
},
});
const data = rooms.map((room) => ({
id: room.id,
roomName: room.roomName,
users: room.users,
createdBy: room.createdBy,
createdAt: room.createdAt.getTime(),
lastMessage: room.messages[0]
? {
messageId: room.messages[0].id,
roomId: room.id,
sender: room.messages[0].sender,
content: room.messages[0].content,
time: room.messages[0].time.getTime(),
}
: null,
}));
return NextResponse.json({ code: 0, data: { rooms: data } });
} catch (err: any) {
console.error("Room list error:", err);
return NextResponse.json({ code: 1, error: "获取房间列表失败" });
}
}
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function GET(req: Request) {
try {
const { searchParams } = new URL(req.url);
const roomIdRaw = searchParams.get("roomId");
const sinceRaw = searchParams.get("sinceMessageId");
const roomId = roomIdRaw ? parseInt(roomIdRaw, 10) : NaN;
const sinceMessageId = sinceRaw ? parseInt(sinceRaw, 10) : NaN;
if (!Number.isFinite(roomId) || !Number.isFinite(sinceMessageId)) {
return NextResponse.json({ code: 1, error: "参数缺失或无效" });
}
const messages = await prisma.message.findMany({
where: { roomId, id: { gt: sinceMessageId } },
orderBy: { time: "asc" }, // ✅ 用 time
});
return NextResponse.json({ code: 0, data: { messages } });
} catch (err: any) {
console.error("RoomMessageGetUpdate error:", err);
return NextResponse.json({ code: 1, error: err.message || "Server error" });
}
}
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function GET(req: Request) {
try {
const { searchParams } = new URL(req.url);
const roomIdRaw = searchParams.get("roomId");
const roomId = roomIdRaw ? parseInt(roomIdRaw, 10) : NaN;
if (!Number.isFinite(roomId)) {
return NextResponse.json({ code: 1, error: "参数缺失或无效" });
}
const messages = await prisma.message.findMany({
where: { roomId },
orderBy: { time: "asc" }, // ✅ 用 time
});
return NextResponse.json({ code: 0, data: { messages } });
} catch (err: any) {
console.error("RoomMessageList error:", err);
return NextResponse.json({ code: 1, error: err.message || "Server error" });
}
}
"use client";
import React, { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { RoomPreviewInfo, Message } from "@/types/chat";
import RoomEntry from "@/components/RoomEntry";
import MessageItem from "@/components/MessageItem";
import { addRoom, getRooms, deleteRoom } from "@/lib/api";
export default function ChatRoomPage() {
const [rooms, setRooms] = useState<RoomPreviewInfo[]>([]);
const [currentRoomId, setCurrentRoomId] = useState<number | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [newMessage, setNewMessage] = useState("");
const [currentUser, setCurrentUser] = useState<string | null>(null);
const [mounted, setMounted] = useState(false);
const router = useRouter();
// ✅ 挂载时获取用户名 & 房间列表
useEffect(() => {
setMounted(true);
const username = localStorage.getItem("username");
if (username) {
setCurrentUser(username);
}
async function fetchRooms() {
const res = await getRooms();
if (res.code === 0) {
setRooms(
res.data.rooms.map((r: any) => ({
roomId: String(r.id), // 确保是字符串
roomName: r.roomName,
lastMessage: {
roomid: r.id,
messageId: r.lastMessage?.id || null,
content: r.lastMessage?.content || "",
sender: r.lastMessage?.sender || "",
time: r.lastMessage?.time || new Date().toISOString(),
},
}))
);
}
}
fetchRooms();
}, []);
// ✅ 房间消息更新轮询
useEffect(() => {
if (!currentRoomId) return;
const interval = setInterval(async () => {
if (messages.length === 0) return;
const lastId = messages[messages.length - 1].messageId;
try {
const res = await fetch(
`/api/room/message/getUpdate?roomId=${currentRoomId}&sinceMessageId=${lastId}`
);
const data = await res.json();
if (data.code === 0 && data.data.messages.length > 0) {
setMessages((prev) => [...prev, ...data.data.messages]);
}
} catch (err) {
console.error("更新消息失败:", err);
}
}, 3000);
return () => clearInterval(interval);
}, [currentRoomId, messages]);
// ✅ 添加房间
const handleAddRoom = async () => {
if (!currentUser) return alert("请先登录!");
const name = prompt("请输入新房间名称");
if (!name) return;
const res = await addRoom(currentUser, name);
if (res.code === 0) {
const listRes = await getRooms();
if (listRes.code === 0) {
setRooms(listRes.data.rooms);
}
}
};
// ✅ 删除房间
const handleDeleteRoom = async (roomId: number) => {
const confirmDelete = confirm("确定要删除这个房间吗?");
if (!confirmDelete) return;
const res = await deleteRoom(roomId);
if (res.code === 0) {
const listRes = await getRooms();
if (listRes.code === 0) {
setRooms(listRes.data.rooms);
}
if (currentRoomId === roomId) {
setCurrentRoomId(null);
setMessages([]);
}
} else {
alert("删除失败:" + res.error);
}
};
// ✅ 选择房间
const handleSelectRoom = async (roomId: number) => {
setCurrentRoomId(roomId);
const res = await fetch(`/api/room/message/list?roomId=${roomId}`);
const data = await res.json();
if (data.code === 0) {
setMessages(data.data.messages);
} else {
setMessages([]);
}
};
// ✅ 发送消息
const handleSendMessage = async () => {
if (!newMessage.trim() || currentRoomId === null || !currentUser) return;
const now = Date.now();
const optimisticMsg: Message = {
messageId: now,
roomId: currentRoomId,
sender: currentUser,
content: newMessage,
time: now,
};
setMessages((prev) => [...prev, optimisticMsg]);
setRooms((prev) =>
prev.map((room) =>
room.roomId === currentRoomId ? { ...room, lastMessage: optimisticMsg } : room
)
);
setNewMessage("");
try {
const res = await fetch("/api/message/add", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
roomId: Number(currentRoomId),
content: newMessage,
sender: currentUser,
}),
});
const data = await res.json();
if (data.code !== 0) {
console.error("发送失败:", data.error);
}
} catch (err) {
console.error("发送消息异常:", err);
}
};
if (!mounted) return <div>加载中...</div>;
return (
<div className="flex flex-col h-screen bg-gray-50">
{/* 顶部用户栏 */}
<div className="flex justify-between items-center px-6 py-3 border-b border-gray-200 bg-white shadow-sm">
<div className="flex items-center">
<span className="text-gray-700 font-medium">当前用户:</span>
<span className="ml-2 text-indigo-600 font-semibold">
{currentUser ?? <span className="text-red-500">未登录</span>}
</span>
</div>
{currentUser && (
<button
onClick={() => {
localStorage.removeItem("username");
setCurrentUser(null);
router.push("/login");
}}
className="px-3 py-1.5 bg-red-500 text-white rounded-md hover:bg-red-600 transition duration-200 flex items-center text-sm"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
退出
</button>
)}
</div>
{/* 主体内容 */}
<div className="flex flex-1 overflow-hidden">
{/* 左侧房间列表 */}
<div className="w-64 border-r border-gray-200 bg-white flex flex-col">
<div className="p-3 border-b border-gray-200">
<button
onClick={handleAddRoom}
className="w-full px-3 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition duration-200 flex items-center justify-center text-sm"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
添加房间
</button>
</div>
<div className="flex-1 overflow-y-auto">
{rooms.map((room) => (
<div
key={room.roomId}
className={`flex items-center p-2 border-b border-gray-100 hover:bg-gray-50 transition duration-150 ${
room.roomId === currentRoomId ? "bg-blue-50 border-l-4 border-l-blue-500" : ""
}`}
>
<div className="flex-1 min-w-0">
<RoomEntry
room={room}
isActive={room.roomId === currentRoomId}
onClick={() => handleSelectRoom(room.roomId)}
onDelete={() => handleDeleteRoom(room.roomId)}
/>
</div>
<button
onClick={() => handleDeleteRoom(room.roomId)}
className="ml-1 p-1 text-gray-400 hover:text-red-500 transition duration-150 rounded-full hover:bg-red-50"
title="删除房间"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
))}
</div>
</div>
{/* 右侧聊天窗口 */}
<div className="flex-1 flex flex-col bg-white">
<div className="flex-1 p-4 overflow-y-auto bg-gray-50">
{messages.length === 0 ? (
<div className="h-full flex items-center justify-center">
<div className="text-center text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" className="h-12 w-12 mx-auto mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
</svg>
<p className="text-sm">选择一个房间开始聊天</p>
</div>
</div>
) : (
messages.map((msg) => (
<MessageItem key={msg.messageId} message={msg} />
))
)}
</div>
{currentRoomId && (
<div className="p-3 border-t border-gray-200 bg-white">
<div className="flex space-x-2">
<input
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm"
placeholder="输入消息..."
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleSendMessage();
}
}}
/>
<button
onClick={handleSendMessage}
disabled={!newMessage.trim()}
className={`px-3 py-2 rounded-full flex items-center text-sm ${
newMessage.trim()
? "bg-indigo-600 text-white hover:bg-indigo-700"
: "bg-gray-200 text-gray-400 cursor-not-allowed"
} transition duration-200`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
发送
</button>
</div>
</div>
)}
</div>
</div>
</div>
);
}
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
"use client";
import React, { useState } from "react";
import { useRouter } from "next/navigation";
export default function LoginPage() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError("");
try {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
const data = await res.json();
if (data.code === 0) {
localStorage.setItem("username", data.username);
router.push("/chat");
} else {
setError(data.error || "登录失败,请检查用户名和密码");
}
} catch (err) {
setError("网络错误,请稍后重试");
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8 bg-white p-10 rounded-xl shadow-lg">
<div className="text-center">
<h2 className="mt-6 text-3xl font-extrabold text-gray-900">欢迎回来</h2>
<p className="mt-2 text-sm text-gray-600">登录您的账号继续使用服务</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleLogin}>
<div className="rounded-md shadow-sm space-y-4">
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-1">
用户名
</label>
<input
id="username"
name="username"
type="text"
required
className="appearance-none relative block w-full px-4 py-3 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="请输入用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
密码
</label>
<input
id="password"
name="password"
type="password"
required
className="appearance-none relative block w-full px-4 py-3 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="请输入密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
{error && (
<div className="rounded-md bg-red-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">登录失败</h3>
<div className="mt-2 text-sm text-red-700">
<p>{error}</p>
</div>
</div>
</div>
</div>
)}
<div>
<button
type="submit"
disabled={isLoading}
className={`group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-medium rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition duration-200 ${
isLoading
? "bg-indigo-400 cursor-not-allowed"
: "bg-indigo-600 hover:bg-indigo-700"
}`}
>
{isLoading ? (
<>
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
登录中...
</>
) : "登录"}
</button>
</div>
<div className="text-center">
<p className="text-sm text-gray-600">
还没有账号?{" "}
<a href="/register" className="font-medium text-indigo-600 hover:text-indigo-500 transition duration-200">
去注册
</a>
</p>
</div>
</form>
</div>
</div>
);
}
\ No newline at end of file
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
export default function HomePage() {
const router = useRouter();
useEffect(() => {
const username = localStorage.getItem("username");
if (username) {
router.replace("/chat"); // ✅ 已登录,去聊天室
} else {
router.replace("/login"); // ❌ 未登录,去登录页
}
}, [router]);
return (
<div className="flex h-screen items-center justify-center">
<p className="text-gray-500">正在跳转...</p>
</div>
);
}
"use client";
import { useState } from "react";
export default function RegisterPage() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
const data = await res.json();
if (data.code === 0) {
localStorage.setItem("username", data.username);
window.location.href = "/chat";
}
else {
setError(data.error);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8 bg-white p-10 rounded-xl shadow-lg">
<div className="text-center">
<h2 className="mt-6 text-3xl font-extrabold text-gray-900">创建账户</h2>
<p className="mt-2 text-sm text-gray-600">注册新账号开始使用我们的服务</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleRegister}>
<div className="rounded-md shadow-sm space-y-4">
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-1">
用户名
</label>
<input
id="username"
name="username"
type="text"
required
className="appearance-none relative block w-full px-4 py-3 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="请输入用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
密码
</label>
<input
id="password"
name="password"
type="password"
required
className="appearance-none relative block w-full px-4 py-3 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder="请输入密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
{error && (
<div className="rounded-md bg-red-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">注册失败</h3>
<div className="mt-2 text-sm text-red-700">
<p>{error}</p>
</div>
</div>
</div>
</div>
)}
<div>
<button
type="submit"
className="group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition duration-200"
>
注册
</button>
</div>
<div className="text-center">
<p className="text-sm text-gray-600">
已有账号?{" "}
<a href="/login" className="font-medium text-indigo-600 hover:text-indigo-500 transition duration-200">
去登录
</a>
</p>
</div>
</form>
</div>
</div>
);
}
\ No newline at end of file
"use client";
import React from "react";
import { Message } from "@/types/chat";
interface MessageItemProps {
message: Message;
}
export default function MessageItem({ message }: MessageItemProps) {
// 格式化时间 -> "14:50:22"
const timeStr = new Date(message.time).toLocaleTimeString("zh-CN", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false, // ✅ 使用24小时制
});
// 检查是否是用户自己的消息(假设用户名为"You"或"User")
const isOwnMessage = message.sender === "You" || message.sender === "User";
return (
<div className={`flex mb-4 ${isOwnMessage ? "justify-end" : "justify-start"}`}>
<div className={`max-w-xs md:max-w-md lg:max-w-lg px-4 py-2 rounded-lg shadow-sm ${isOwnMessage ? "bg-blue-500 text-white" : "bg-gray-100 text-gray-800"}`}>
<div className="flex items-baseline justify-between mb-1">
<span className={`text-sm font-semibold ${isOwnMessage ? "text-blue-100" : "text-blue-600"}`}>
{message.sender}
</span>
<span className={`text-xs ml-2 ${isOwnMessage ? "text-blue-200" : "text-gray-500"}`}>
{timeStr}
</span>
</div>
<p className="text-sm break-words">{message.content}</p>
</div>
</div>
);
}
\ No newline at end of file
"use client";
import React from "react";
import { RoomPreviewInfo } from "@/types/chat";
interface RoomEntryProps {
room: RoomPreviewInfo;
isActive: boolean;
onClick: () => void;
onDelete: () => void;
}
export default function RoomEntry({ room, isActive, onClick, onDelete }: RoomEntryProps) {
return (
<div
onClick={onClick}
onContextMenu={(e) => {
e.preventDefault();
onDelete();
}}
className={`w-full cursor-pointer transition-colors duration-150 ${
isActive ? "bg-blue-50" : "hover:bg-gray-50"
}`}
>
<div className="p-2">
{/* 房间名称 */}
<div className="font-semibold text-gray-800 truncate mb-1">
{room.roomName}
</div>
{/* 最后消息或空状态 */}
{room.lastMessage ? (
<div className="flex items-center space-x-1">
{/* 发送者名称 - 美化样式 */}
<div className="text-xs font-bold text-indigo-700 truncate tracking-wide flex-shrink-0">
{room.lastMessage.sender}:
</div>
{/* 消息内容 - 改为黑色 */}
<div className="text-xs text-gray-900 truncate flex-1">
{room.lastMessage.content}
</div>
</div>
) : (
<div className="text-xs text-gray-400 italic">暂无消息</div>
)}
</div>
</div>
);
}
import { RoomPreviewInfo, Message } from "@/types/chat";
const apiFetch = async (url: string, options?: RequestInit) => {
const res = await fetch(url, {
...options,
headers: {
"Content-Type": "application/json",
...(options?.headers || {}),
},
});
return res.json();
};
// 房间相关
export async function addRoom(user: string, roomName: string) {
const res = await fetch("/api/room/add", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user, roomName }),
});
return res.json();
}
export async function getRooms() {
return apiFetch("/api/room/list");
}
export async function deleteRoom(roomId: number) {
const res = await fetch("/api/room/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ roomId }),
});
return res.json();
}
// 消息相关
export async function addMessage(roomId: number, content: string, sender: string) {
return apiFetch("/api/message/add", {
method: "POST",
body: JSON.stringify({ roomId, content, sender }),
});
}
export async function getRoomMessages(roomId: number) {
return apiFetch(`/api/room/message/list?roomId=${roomId}`);
}
export async function getMessageUpdates(roomId: number, sinceMessageId: number) {
return apiFetch(`/api/room/message/getUpdate?roomId=${roomId}&sinceMessageId=${sinceMessageId}`);
}
import jwt from "jsonwebtoken";
import { prisma } from "./prisma";
import { cookies } from "next/headers";
const JWT_SECRET = process.env.JWT_SECRET || "dev_secret";
export function signToken(payload: object) {
return jwt.sign(payload, JWT_SECRET, { expiresIn: "1h" });
}
export async function getUserFromToken(req?: Request) {
const cookieStore = await cookies();
const token = cookieStore.get("token")?.value;
if (!token) return null;
try {
const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
return await prisma.user.findUnique({ where: { id: Number(decoded.id) } });
} catch {
return null;
}
}
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