暂时保存

This commit is contained in:
Asuka 2025-06-20 20:18:13 +08:00
parent bcbe2868d5
commit dff0515dbb
18 changed files with 856 additions and 109 deletions

View 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("修改密码失败");
}
}

View 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("获取用户信息失败");
}
}

View File

@ -0,0 +1,4 @@
// index.ts
export { getUserInfo } from "./getUserInfo";
export { updateUserInfo } from "./updateUserInfo";
export { changePassword } from "./changePassword";

View 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("更新用户信息失败");
}
}

View 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>
&nbsp;
<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>
);
}

View 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>
)
}

View 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>
);
}

View File

@ -1,4 +1,4 @@
import { AppSidebar } from "@/components/sidebar/app-sidebar" import { AppSidebar } from "@/components/sidebar/app-sidebar";
import { import {
Breadcrumb, Breadcrumb,
BreadcrumbItem, BreadcrumbItem,
@ -6,18 +6,29 @@ import {
BreadcrumbList, BreadcrumbList,
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator, BreadcrumbSeparator,
} from "@/components/ui/breadcrumb" } from "@/components/ui/breadcrumb";
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator";
import { import {
SidebarInset, SidebarInset,
SidebarProvider, SidebarProvider,
SidebarTrigger, 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 ( return (
<SidebarProvider> <SidebarProvider>
<AppSidebar /> <AppSidebar user={user} />
<SidebarInset> <SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2"> <header className="flex h-16 shrink-0 items-center gap-2">
<div className="flex items-center gap-2 px-4"> <div className="flex items-center gap-2 px-4">
@ -38,15 +49,8 @@ export default function Page() {
</Breadcrumb> </Breadcrumb>
</div> </div>
</header> </header>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0"> <div className="flex flex-1 flex-col gap-4 p-4 pt-0">{children}</div>
<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>
</SidebarInset> </SidebarInset>
</SidebarProvider> </SidebarProvider>
) );
} }

View File

@ -0,0 +1,3 @@
export default function Page() {
return <div className="h-full w-full border bg-blue-200">Dashboard</div>
}

View 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>
)
}

View 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>
);
}

View 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>
)
}

View File

@ -43,6 +43,19 @@ export function NavUser({
const { isMobile } = useSidebar() const { isMobile } = useSidebar()
const router = useRouter() 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 ( return (
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>
@ -90,11 +103,11 @@ export function NavUser({
</DropdownMenuGroup> </DropdownMenuGroup>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem> <DropdownMenuItem onClick={handleAccount}>
<BadgeCheck /> <BadgeCheck />
Account Account
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push("/sign-in")}> <DropdownMenuItem onClick={() => router.push("/sign-in")}>
<UserPen /> <UserPen />
Switch User Switch User
</DropdownMenuItem> </DropdownMenuItem>
@ -104,9 +117,7 @@ export function NavUser({
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={() => { <DropdownMenuItem onClick={handleLogout}>
router.replace("/");
}}>
<LogOut /> <LogOut />
Log out Log out
</DropdownMenuItem> </DropdownMenuItem>

View 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>
)
}

View File

@ -1,6 +1,7 @@
"use client" "use client";
import { siteConfig } from "@/config/site"
import * as React from "react" import { siteConfig } from "@/config/site";
import * as React from "react";
import { import {
BookOpen, BookOpen,
Command, Command,
@ -8,12 +9,12 @@ import {
Send, Send,
Settings2, Settings2,
SquareTerminal, SquareTerminal,
} from "lucide-react" } from "lucide-react";
import { NavMain } from "@/components/nav-main" import { NavMain } from "@/components/nav-main";
import { NavProjects } from "@/components/nav-projects" import { NavProjects } from "@/components/nav-projects";
import { NavSecondary } from "@/components/nav-secondary" import { NavSecondary } from "@/components/nav-secondary";
import { NavUser } from "@/components/nav-user" import { NavUser } from "@/components/nav-user";
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
@ -22,15 +23,13 @@ import {
SidebarMenu, SidebarMenu,
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, 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 = { const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
navMain: [ navMain: [
{ {
title: "页面", title: "页面",
@ -52,7 +51,7 @@ const data = {
}, },
], ],
}, },
{ {
title: "已完成事项", title: "已完成事项",
url: "#", url: "#",
@ -66,7 +65,7 @@ const data = {
title: "错题集", title: "错题集",
url: "#", url: "#",
}, },
{ {
title: "收藏集", title: "收藏集",
url: "#", url: "#",
}, },
@ -77,10 +76,6 @@ const data = {
url: "#", url: "#",
icon: Settings2, icon: Settings2,
items: [ items: [
{
title: "一般设置",
url: "#",
},
{ {
title: "语言", title: "语言",
url: "#", url: "#",
@ -117,11 +112,50 @@ const data = {
status: "TLE", 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 ( return (
<Sidebar variant="inset" {...props}> <Sidebar {...props}>
<SidebarHeader> <SidebarHeader>
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>
@ -145,8 +179,8 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<NavSecondary items={data.navSecondary} className="mt-auto" /> <NavSecondary items={data.navSecondary} className="mt-auto" />
</SidebarContent> </SidebarContent>
<SidebarFooter> <SidebarFooter>
<NavUser user={data.user} /> <NavUser user={userInfo} />
</SidebarFooter> </SidebarFooter>
</Sidebar> </Sidebar>
) );
} }

View File

@ -36,10 +36,6 @@ const data = {
icon: SquareTerminal, icon: SquareTerminal,
isActive: true, isActive: true,
items: [ items: [
{
title: "课程管理",
url: "/teacher/courses",
},
{ {
title: "学生管理", title: "学生管理",
url: "/teacher/students", url: "/teacher/students",
@ -56,11 +52,11 @@ const data = {
icon: PieChart, icon: PieChart,
items: [ items: [
{ {
title: "成绩统计", title: "完成情况",
url: "/teacher/statistics/grades", url: "/teacher/statistics/grades",
}, },
{ {
title: "错题分析", title: "错题统计",
url: "/teacher/statistics/activity", url: "/teacher/statistics/activity",
}, },
], ],

View File

@ -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>
);
}

View File

@ -247,3 +247,8 @@ export const { auth, handlers, signIn, signOut } = NextAuth({
}, },
}, },
}); });
export const getCurrentUser=async ()=>{
const session=await auth();
return session?.user
}