mirror of
https://github.com/massbug/judge4c.git
synced 2025-07-03 15:20:50 +00:00
暂时保存
This commit is contained in:
parent
bcbe2868d5
commit
dff0515dbb
42
src/app/(app)/management/actions/changePassword.ts
Normal file
42
src/app/(app)/management/actions/changePassword.ts
Normal file
@ -0,0 +1,42 @@
|
||||
// changePassword.ts
|
||||
"use server";
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
export async function changePassword(formData: FormData) {
|
||||
const oldPassword = formData.get("oldPassword") as string;
|
||||
const newPassword = formData.get("newPassword") as string;
|
||||
|
||||
if (!oldPassword || !newPassword) {
|
||||
throw new Error("旧密码和新密码不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: '1' },
|
||||
});
|
||||
|
||||
if (!user) throw new Error("用户不存在");
|
||||
|
||||
if (!user.password) {
|
||||
throw new Error("用户密码未设置");
|
||||
}
|
||||
|
||||
const passwordHash: string = user.password as string;
|
||||
const isMatch = await bcrypt.compare(oldPassword, passwordHash);
|
||||
if (!isMatch) throw new Error("旧密码错误");
|
||||
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: '1' },
|
||||
data: { password: hashedPassword },
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("修改密码失败:", error);
|
||||
throw new Error("修改密码失败");
|
||||
}
|
||||
}
|
19
src/app/(app)/management/actions/getUserInfo.ts
Normal file
19
src/app/(app)/management/actions/getUserInfo.ts
Normal file
@ -0,0 +1,19 @@
|
||||
// getUserInfo.ts
|
||||
"use server";
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export async function getUserInfo() {
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: 'user_001' },
|
||||
});
|
||||
|
||||
if (!user) throw new Error("用户不存在");
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
console.error("获取用户信息失败:", error);
|
||||
throw new Error("获取用户信息失败");
|
||||
}
|
||||
}
|
4
src/app/(app)/management/actions/index.ts
Normal file
4
src/app/(app)/management/actions/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
// index.ts
|
||||
export { getUserInfo } from "./getUserInfo";
|
||||
export { updateUserInfo } from "./updateUserInfo";
|
||||
export { changePassword } from "./changePassword";
|
25
src/app/(app)/management/actions/updateUserInfo.ts
Normal file
25
src/app/(app)/management/actions/updateUserInfo.ts
Normal file
@ -0,0 +1,25 @@
|
||||
// updateUserInfo.ts
|
||||
"use server";
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export async function updateUserInfo(formData: FormData) {
|
||||
const name = formData.get("name") as string;
|
||||
const email = formData.get("email") as string;
|
||||
|
||||
if (!name || !email) {
|
||||
throw new Error("缺少必要字段:name, email");
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: 'user_001' },
|
||||
data: { name, email },
|
||||
});
|
||||
|
||||
return updatedUser;
|
||||
} catch (error) {
|
||||
console.error("更新用户信息失败:", error);
|
||||
throw new Error("更新用户信息失败");
|
||||
}
|
||||
}
|
125
src/app/(app)/management/change-password/page.tsx
Normal file
125
src/app/(app)/management/change-password/page.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
// src/app/(app)/management/change-password/page.tsx
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { changePassword } from "@/app/(app)/management/actions";
|
||||
|
||||
export default function ChangePasswordPage() {
|
||||
const [oldPassword, setOldPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
|
||||
const getPasswordStrength = (password: string) => {
|
||||
if (password.length < 6) return "weak";
|
||||
if (/[A-Za-z]/.test(password) && /\d/.test(password)) return "medium";
|
||||
return "strong";
|
||||
};
|
||||
|
||||
const strengthText = getPasswordStrength(newPassword);
|
||||
let strengthColor = "";
|
||||
let strengthLabel = "";
|
||||
|
||||
switch (strengthText) {
|
||||
case "weak":
|
||||
strengthColor = "bg-red-500";
|
||||
strengthLabel = "弱";
|
||||
break;
|
||||
case "medium":
|
||||
strengthColor = "bg-yellow-500";
|
||||
strengthLabel = "中等";
|
||||
break;
|
||||
case "strong":
|
||||
strengthColor = "bg-green-500";
|
||||
strengthLabel = "强";
|
||||
break;
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (newPassword !== confirmPassword) {
|
||||
alert("两次输入的密码不一致!");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("oldPassword", oldPassword);
|
||||
formData.append("newPassword", newPassword);
|
||||
|
||||
try {
|
||||
await changePassword(formData);
|
||||
setShowSuccess(true);
|
||||
setTimeout(() => setShowSuccess(false), 3000);
|
||||
} catch (error: any) {
|
||||
alert(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full p-6">
|
||||
<div className="h-full w-full bg-white shadow-lg rounded-xl p-8 flex flex-col">
|
||||
<h1 className="text-2xl font-bold mb-6">修改密码</h1>
|
||||
<form onSubmit={handleSubmit} className="space-y-5 flex-1 flex flex-col">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">旧密码</label>
|
||||
<input
|
||||
type="password"
|
||||
value={oldPassword}
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">新密码</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
{newPassword && (
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
密码强度:
|
||||
<span className={`inline-block w-12 h-2 rounded ${strengthColor}`}></span>
|
||||
|
||||
<span className="text-sm">{strengthLabel}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">确认新密码</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
{newPassword && confirmPassword && newPassword !== confirmPassword && (
|
||||
<p className="mt-1 text-xs text-red-500">密码不一致</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto">
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-black hover:bg-gray-800 text-white font-semibold py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
提交
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{showSuccess && (
|
||||
<div className="fixed bottom-5 right-5 bg-green-500 text-white px-4 py-2 rounded shadow-lg animate-fade-in-down">
|
||||
✅ 密码修改成功!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
90
src/app/(app)/management/page.tsx
Normal file
90
src/app/(app)/management/page.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
"use client"
|
||||
import React, { useState } from "react"
|
||||
import { AppSidebar } from "@/components/management-sidebar/manage-sidebar"
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from "@/components/ui/sidebar"
|
||||
import ProfilePage from "./profile/page"
|
||||
import ChangePasswordPage from "./change-password/page"
|
||||
|
||||
// 模拟菜单数据
|
||||
const menuItems = [
|
||||
{ title: "登录信息", key: "profile" },
|
||||
{ title: "修改密码", key: "change-password" },
|
||||
]
|
||||
|
||||
export default function ManagementDefaultPage() {
|
||||
const [activePage, setActivePage] = useState("profile")
|
||||
const [isCollapsed, setIsCollapsed] = useState(false)
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activePage) {
|
||||
case "profile":
|
||||
return <ProfilePage />
|
||||
case "change-password":
|
||||
return <ChangePasswordPage />
|
||||
default:
|
||||
return <ProfilePage />
|
||||
}
|
||||
}
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setIsCollapsed((prev) => !prev)
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<div className="flex h-screen w-screen overflow-hidden">
|
||||
{/* 左侧侧边栏 */}
|
||||
{!isCollapsed && (
|
||||
<div className="w-64 border-r bg-background flex-shrink-0 p-4">
|
||||
<AppSidebar onItemClick={setActivePage} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 右侧主内容区域 */}
|
||||
<SidebarInset className="h-full w-full overflow-auto">
|
||||
<header className="bg-background sticky top-0 z-10 flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
||||
{/* 折叠按钮 */}
|
||||
<SidebarTrigger className="-ml-1" onClick={toggleSidebar} />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
|
||||
{/* 面包屑导航 */}
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem className="hidden md:block">
|
||||
<BreadcrumbLink href="/management">管理面板</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>
|
||||
{menuItems.find((item) => item.key === activePage)?.title}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</header>
|
||||
{/* 主体内容:根据 isCollapsed 切换样式 */}
|
||||
<main
|
||||
className={`flex-1 p-6 bg-background transition-all duration-300 ${
|
||||
isCollapsed ? "w-full" : "md:w-[calc(100%-16rem)]"
|
||||
}`}
|
||||
>
|
||||
{renderContent()}
|
||||
</main>
|
||||
</SidebarInset>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
150
src/app/(app)/management/profile/page.tsx
Normal file
150
src/app/(app)/management/profile/page.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
// src/app/(app)/management/profile/page.tsx
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { getUserInfo, updateUserInfo } from "@/app/(app)/management/actions";
|
||||
|
||||
interface User {
|
||||
id: string; // TEXT 类型
|
||||
name: string | null; // 可能为空
|
||||
email: string; // NOT NULL
|
||||
emailVerified: Date | null; // TIMESTAMP 转换为字符串
|
||||
image: string | null;
|
||||
role: "GUEST" | "USER" | "ADMIN"; // 枚举类型
|
||||
createdAt: Date; // TIMESTAMP 转换为字符串
|
||||
updatedAt: Date; // TIMESTAMP 转换为字符串
|
||||
}
|
||||
|
||||
export default function ProfilePage() {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchUser() {
|
||||
try {
|
||||
const data = await getUserInfo();
|
||||
setUser(data);
|
||||
} catch (error) {
|
||||
console.error("获取用户信息失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
fetchUser();
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
const nameInput = document.getElementById("name") as HTMLInputElement | null;
|
||||
const emailInput = document.getElementById("email") as HTMLInputElement | null;
|
||||
|
||||
if (!nameInput || !emailInput) {
|
||||
alert("表单元素缺失");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("name", nameInput.value);
|
||||
formData.append("email", emailInput.value);
|
||||
|
||||
try {
|
||||
const updatedUser = await updateUserInfo(formData);
|
||||
setUser(updatedUser);
|
||||
setIsEditing(false);
|
||||
} catch (error: any) {
|
||||
alert(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
if (!user) return <p>加载中...</p>;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full p-6">
|
||||
<div className="h-full w-full bg-white shadow-lg rounded-xl p-8 flex flex-col">
|
||||
<h1 className="text-2xl font-bold mb-6">用户信息</h1>
|
||||
|
||||
<div className="flex items-center space-x-6 mb-6">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-16 h-16 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 text-2xl font-bold">
|
||||
👤
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{isEditing ? (
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
defaultValue={user?.name || ""}
|
||||
className="mt-1 block w-full border border-gray-300 rounded-md p-2"
|
||||
/>
|
||||
) : (
|
||||
<h2 className="text-xl font-semibold">{user?.name || "未提供"}</h2>
|
||||
)}
|
||||
<p className="text-gray-500">角色:{user?.role}</p>
|
||||
<p className="text-gray-500">邮箱验证时间:{user.emailVerified ? new Date(user.emailVerified).toLocaleString() : "未验证"}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="border-gray-200 mb-6" />
|
||||
|
||||
<div className="space-y-4 flex-1">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">用户ID</label>
|
||||
<p className="mt-1 text-lg font-medium text-gray-900">{user.id}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">邮箱地址</label>
|
||||
{isEditing ? (
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
defaultValue={user.email}
|
||||
className="mt-1 block w-full border border-gray-300 rounded-md p-2"
|
||||
/>
|
||||
) : (
|
||||
<p className="mt-1 text-lg font-medium text-gray-900">{user.email}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">注册时间</label>
|
||||
<p className="mt-1 text-lg font-medium text-gray-900">{new Date(user.createdAt).toLocaleString()}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">最后更新时间</label>
|
||||
<p className="mt-1 text-lg font-medium text-gray-900">{new Date(user.updatedAt).toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex justify-end space-x-2">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setIsEditing(false)}
|
||||
type="button"
|
||||
className="px-4 py-2 bg-gray-300 rounded-md hover:bg-gray-400 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
type="button"
|
||||
className="px-4 py-2 bg-black text-white rounded-md hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setIsEditing(true)}
|
||||
type="button"
|
||||
className="px-4 py-2 bg-black text-white rounded-md hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
编辑信息
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { AppSidebar } from "@/components/sidebar/app-sidebar"
|
||||
import { AppSidebar } from "@/components/sidebar/app-sidebar";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
@ -6,18 +6,29 @@ import {
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from "@/components/ui/sidebar"
|
||||
} from "@/components/ui/sidebar";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default async function Layout({ children }: LayoutProps) {
|
||||
const session = await auth();
|
||||
const user = session?.user;
|
||||
if (!user) {
|
||||
notFound();
|
||||
}
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<AppSidebar user={user} />
|
||||
<SidebarInset>
|
||||
<header className="flex h-16 shrink-0 items-center gap-2">
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
@ -38,15 +49,8 @@ export default function Page() {
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<div className="aspect-video rounded-xl bg-muted/50" />
|
||||
<div className="aspect-video rounded-xl bg-muted/50" />
|
||||
<div className="aspect-video rounded-xl bg-muted/50" />
|
||||
</div>
|
||||
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">{children}</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
)
|
||||
);
|
||||
}
|
3
src/app/(protected)/dashboard/page.tsx
Normal file
3
src/app/(protected)/dashboard/page.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function Page() {
|
||||
return <div className="h-full w-full border bg-blue-200">Dashboard</div>
|
||||
}
|
28
src/components/management-sidebar/manage-form.tsx
Normal file
28
src/components/management-sidebar/manage-form.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
import { Label } from "@/components/ui/label"
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarInput,
|
||||
} from "@/components/ui/sidebar"
|
||||
|
||||
export function SearchForm({ ...props }: React.ComponentProps<"form">) {
|
||||
return (
|
||||
<form {...props}>
|
||||
<SidebarGroup className="py-0">
|
||||
<SidebarGroupContent className="relative">
|
||||
<Label htmlFor="search" className="sr-only">
|
||||
Search
|
||||
</Label>
|
||||
<SidebarInput
|
||||
id="search"
|
||||
placeholder="Search the docs..."
|
||||
className="pl-8"
|
||||
/>
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 select-none opacity-50" />
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</form>
|
||||
)
|
||||
}
|
97
src/components/management-sidebar/manage-sidebar.tsx
Normal file
97
src/components/management-sidebar/manage-sidebar.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import * as React from "react";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
import { VersionSwitcher } from "@/components//management-sidebar/manage-switcher";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
// 自定义数据:包含用户相关菜单项
|
||||
const data = {
|
||||
versions: ["1.0.1", "1.1.0-alpha", "2.0.0-beta1"],
|
||||
navUser: [
|
||||
{
|
||||
title: "个人中心",
|
||||
url: "#",
|
||||
items: [
|
||||
{ title: "登录信息", url: "#", key: "profile" },
|
||||
{ title: "修改密码", url: "#", key: "change-password" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 显式定义 props 类型
|
||||
interface AppSidebarProps {
|
||||
onItemClick?: (key: string) => void;
|
||||
}
|
||||
|
||||
export function AppSidebar({ onItemClick = (key: string) => {}, ...props }: AppSidebarProps) {
|
||||
return (
|
||||
<Sidebar {...props}>
|
||||
<SidebarHeader>
|
||||
<VersionSwitcher
|
||||
versions={data.versions}
|
||||
defaultVersion={data.versions[0]}
|
||||
/>
|
||||
</SidebarHeader>
|
||||
<SidebarContent className="gap-0">
|
||||
{/* 渲染用户相关的侧边栏菜单 */}
|
||||
{data.navUser.map((item) => (
|
||||
<Collapsible
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
defaultOpen
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel
|
||||
asChild
|
||||
className="group/label text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||
>
|
||||
<CollapsibleTrigger>
|
||||
{item.title}
|
||||
<ChevronRight className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90" />
|
||||
</CollapsibleTrigger>
|
||||
</SidebarGroupLabel>
|
||||
<CollapsibleContent>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{item.items.map((subItem) => (
|
||||
<SidebarMenuItem key={subItem.title}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onItemClick(subItem.key);
|
||||
}}
|
||||
>
|
||||
<a href="#">{subItem.title}</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</CollapsibleContent>
|
||||
</SidebarGroup>
|
||||
</Collapsible>
|
||||
))}
|
||||
</SidebarContent>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
64
src/components/management-sidebar/manage-switcher.tsx
Normal file
64
src/components/management-sidebar/manage-switcher.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Check, ChevronsUpDown, GalleryVerticalEnd } from "lucide-react"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar"
|
||||
|
||||
export function VersionSwitcher({
|
||||
versions,
|
||||
defaultVersion,
|
||||
}: {
|
||||
versions: string[]
|
||||
defaultVersion: string
|
||||
}) {
|
||||
const [selectedVersion, setSelectedVersion] = React.useState(defaultVersion)
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
||||
<GalleryVerticalEnd className="size-4" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 leading-none">
|
||||
<span className="font-semibold">Documentation</span>
|
||||
<span className="">v{selectedVersion}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[--radix-dropdown-menu-trigger-width]"
|
||||
align="start"
|
||||
>
|
||||
{versions.map((version) => (
|
||||
<DropdownMenuItem
|
||||
key={version}
|
||||
onSelect={() => setSelectedVersion(version)}
|
||||
>
|
||||
v{version}{" "}
|
||||
{version === selectedVersion && <Check className="ml-auto" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
}
|
@ -43,6 +43,19 @@ export function NavUser({
|
||||
const { isMobile } = useSidebar()
|
||||
const router = useRouter()
|
||||
|
||||
async function handleLogout() {
|
||||
await fetch("/api/auth/signout", { method: "POST" });
|
||||
router.replace("/sign-in");
|
||||
}
|
||||
|
||||
function handleAccount() {
|
||||
if (user && user.email) {
|
||||
router.replace("/user/profile");
|
||||
} else {
|
||||
router.replace("/sign-in");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
@ -90,7 +103,7 @@ export function NavUser({
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleAccount}>
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
@ -104,9 +117,7 @@ export function NavUser({
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => {
|
||||
router.replace("/");
|
||||
}}>
|
||||
<DropdownMenuItem onClick={handleLogout}>
|
||||
<LogOut />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
|
108
src/components/sidebar/admin-sidebar.tsx
Normal file
108
src/components/sidebar/admin-sidebar.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
"use client"
|
||||
import { siteConfig } from "@/config/site"
|
||||
import * as React from "react"
|
||||
import {
|
||||
LifeBuoy,
|
||||
Send,
|
||||
Shield,
|
||||
} from "lucide-react"
|
||||
|
||||
import { NavMain } from "@/components/nav-main"
|
||||
import { NavProjects } from "@/components/nav-projects"
|
||||
import { NavSecondary } from "@/components/nav-secondary"
|
||||
import { NavUser } from "@/components/nav-user"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
const adminData = {
|
||||
navMain: [
|
||||
{
|
||||
title: "管理面板",
|
||||
url: "#",
|
||||
icon: Shield,
|
||||
isActive: true,
|
||||
items: [
|
||||
{ title: "管理员管理", url: "/usermanagement/admin" },
|
||||
{ title: "用户管理", url: "/usermanagement/guest" },
|
||||
{ title: "教师管理", url: "/usermanagement/teacher" },
|
||||
{ title: "题目管理", url: "/usermanagement/problem" },
|
||||
],
|
||||
},
|
||||
|
||||
],
|
||||
navSecondary: [
|
||||
{ title: "帮助", url: "/", icon: LifeBuoy },
|
||||
{ title: "反馈", url: siteConfig.url.repo.github, icon: Send },
|
||||
],
|
||||
wrongProblems: [],
|
||||
}
|
||||
|
||||
async function fetchCurrentUser() {
|
||||
try {
|
||||
const res = await fetch("/api/auth/session");
|
||||
if (!res.ok) return null;
|
||||
const session = await res.json();
|
||||
return {
|
||||
name: session?.user?.name ?? "未登录管理员",
|
||||
email: session?.user?.email ?? "",
|
||||
avatar: session?.user?.image ?? "/avatars/default.jpg",
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
name: "未登录管理员",
|
||||
email: "",
|
||||
avatar: "/avatars/default.jpg",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function AdminSidebar(props: React.ComponentProps<typeof Sidebar>) {
|
||||
const [user, setUser] = useState({
|
||||
name: "未登录管理员",
|
||||
email: "",
|
||||
avatar: "/avatars/default.jpg",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchCurrentUser().then(u => u && setUser(u));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Sidebar {...props}>
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size="lg" asChild>
|
||||
<a href="#">
|
||||
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
||||
<Shield className="size-4" />
|
||||
</div>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">Admin</span>
|
||||
<span className="truncate text-xs">管理后台</span>
|
||||
</div>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={adminData.navMain} />
|
||||
<NavProjects projects={adminData.wrongProblems} />
|
||||
<NavSecondary items={adminData.navSecondary} className="mt-auto" />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={user} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
import { siteConfig } from "@/config/site"
|
||||
import * as React from "react"
|
||||
"use client";
|
||||
|
||||
import { siteConfig } from "@/config/site";
|
||||
import * as React from "react";
|
||||
import {
|
||||
BookOpen,
|
||||
Command,
|
||||
@ -8,12 +9,12 @@ import {
|
||||
Send,
|
||||
Settings2,
|
||||
SquareTerminal,
|
||||
} from "lucide-react"
|
||||
} from "lucide-react";
|
||||
|
||||
import { NavMain } from "@/components/nav-main"
|
||||
import { NavProjects } from "@/components/nav-projects"
|
||||
import { NavSecondary } from "@/components/nav-secondary"
|
||||
import { NavUser } from "@/components/nav-user"
|
||||
import { NavMain } from "@/components/nav-main";
|
||||
import { NavProjects } from "@/components/nav-projects";
|
||||
import { NavSecondary } from "@/components/nav-secondary";
|
||||
import { NavUser } from "@/components/nav-user";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@ -22,15 +23,13 @@ import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar"
|
||||
} from "@/components/ui/sidebar";
|
||||
import { User } from "next-auth";
|
||||
|
||||
// import { useEffect, useState } from "react"
|
||||
// import { auth, signIn } from "@/lib/auth"
|
||||
|
||||
const data = {
|
||||
user: {
|
||||
name: "shadcn",
|
||||
email: "m@example.com",
|
||||
avatar: "/avatars/shadcn.jpg",
|
||||
},
|
||||
navMain: [
|
||||
{
|
||||
title: "页面",
|
||||
@ -66,7 +65,7 @@ const data = {
|
||||
title: "错题集",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
{
|
||||
title: "收藏集",
|
||||
url: "#",
|
||||
},
|
||||
@ -77,10 +76,6 @@ const data = {
|
||||
url: "#",
|
||||
icon: Settings2,
|
||||
items: [
|
||||
{
|
||||
title: "一般设置",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
title: "语言",
|
||||
url: "#",
|
||||
@ -117,11 +112,50 @@ const data = {
|
||||
status: "TLE",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// // 获取当前登录用户信息的 API
|
||||
// async function fetchCurrentUser() {
|
||||
// try {
|
||||
// const res = await fetch("/api/auth/session");
|
||||
// if (!res.ok) return null;
|
||||
// const session = await res.json();
|
||||
// return {
|
||||
// name: session?.user?.name ?? "未登录用户",
|
||||
// email: session?.user?.email ?? "",
|
||||
// avatar: session?.user?.image ?? "/avatars/default.jpg",
|
||||
// };
|
||||
// } catch {
|
||||
// return {
|
||||
// name: "未登录用户",
|
||||
// email: "",
|
||||
// avatar: "/avatars/default.jpg",
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
|
||||
interface AppSidebarProps{
|
||||
user:User
|
||||
}
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
export function AppSidebar({ user, ...props }: AppSidebarProps) {
|
||||
// const [user, setUser] = useState({
|
||||
// name: "未登录用户",
|
||||
// email: "",
|
||||
// avatar: "/avatars/default.jpg",
|
||||
// });
|
||||
|
||||
// useEffect(() => {
|
||||
// fetchCurrentUser().then(u => u && setUser(u));
|
||||
// }, []);
|
||||
const userInfo = {
|
||||
name: user.name ?? "",
|
||||
email: user.email ?? "",
|
||||
avatar: user.image ?? "",
|
||||
};
|
||||
|
||||
return (
|
||||
<Sidebar variant="inset" {...props}>
|
||||
<Sidebar {...props}>
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
@ -145,8 +179,8 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<NavSecondary items={data.navSecondary} className="mt-auto" />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={data.user} />
|
||||
<NavUser user={userInfo} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
)
|
||||
);
|
||||
}
|
@ -36,10 +36,6 @@ const data = {
|
||||
icon: SquareTerminal,
|
||||
isActive: true,
|
||||
items: [
|
||||
{
|
||||
title: "课程管理",
|
||||
url: "/teacher/courses",
|
||||
},
|
||||
{
|
||||
title: "学生管理",
|
||||
url: "/teacher/students",
|
||||
@ -56,11 +52,11 @@ const data = {
|
||||
icon: PieChart,
|
||||
items: [
|
||||
{
|
||||
title: "成绩统计",
|
||||
title: "完成情况",
|
||||
url: "/teacher/statistics/grades",
|
||||
},
|
||||
{
|
||||
title: "错题分析",
|
||||
title: "错题统计",
|
||||
url: "/teacher/statistics/activity",
|
||||
},
|
||||
],
|
||||
|
@ -1,58 +0,0 @@
|
||||
// 简单的环形图组件,使用 SVG 实现
|
||||
import React from "react";
|
||||
|
||||
interface DonutChartProps {
|
||||
percent: number; // 完成比例 0-100
|
||||
size?: number; // 图表直径
|
||||
strokeWidth?: number; // 圆环宽度
|
||||
color?: string; // 完成部分颜色
|
||||
bgColor?: string; // 未完成部分颜色
|
||||
}
|
||||
|
||||
export function DonutChart({
|
||||
percent,
|
||||
size = 120,
|
||||
strokeWidth = 16,
|
||||
color = "#3b82f6",
|
||||
bgColor = "#e5e7eb",
|
||||
}: DonutChartProps) {
|
||||
const radius = (size - strokeWidth) / 2;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const offset = circumference * (1 - percent / 100);
|
||||
|
||||
return (
|
||||
<svg width={size} height={size}>
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke={bgColor}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={offset}
|
||||
strokeLinecap="round"
|
||||
transform={`rotate(-90 ${size / 2} ${size / 2})`}
|
||||
/>
|
||||
<text
|
||||
x="50%"
|
||||
y="50%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fontSize={size / 5}
|
||||
fontWeight="bold"
|
||||
fill="#222"
|
||||
>
|
||||
{percent}%
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -247,3 +247,8 @@ export const { auth, handlers, signIn, signOut } = NextAuth({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const getCurrentUser=async ()=>{
|
||||
const session=await auth();
|
||||
return session?.user
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user