用户个人详情与侧边栏

This commit is contained in:
majiti606 2025-06-19 10:39:48 +08:00
parent 52055de597
commit fe1466025e
7 changed files with 464 additions and 94 deletions

View File

@ -0,0 +1,113 @@
"use client"
import { useState } from "react";
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 = (e: React.FormEvent) => {
e.preventDefault();
if (newPassword !== confirmPassword) {
alert("两次输入的密码不一致!");
return;
}
setShowSuccess(true);
setTimeout(() => setShowSuccess(false), 3000);
console.log("提交修改密码", { oldPassword, newPassword });
};
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,91 @@
"use client"
import React, { useState } from "react"
import { AppSidebar } from "@/components/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,56 @@
"use client"
export default function ProfilePage() {
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>
<h2 className="text-xl font-semibold"></h2>
<p className="text-gray-500"></p>
<p className="text-gray-500">2025-04-05 14:30</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"></label>
<p className="mt-1 text-lg font-medium text-gray-900">zhangsan123</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">zhangsan@example.com</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">2022-03-12</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700"></label>
<p className="mt-1 text-lg font-medium text-green-600"></p>
</div>
</div>
<div className="pt-4 flex justify-end">
<button
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

@ -26,140 +26,61 @@ const data = {
versions: ["1.0.1", "1.1.0-alpha", "2.0.0-beta1"],
navMain: [
{
title: "Getting Started",
title: "学生",
url: "#",
items: [
{
title: "Installation",
title: "学生列表",
url: "#",
},
{
title: "Project Structure",
title: "学生详情",
url: "#",
},
{
title: "学生仪表盘",
url: "#",
},
],
},
{
title: "Building Your Application",
title: "教师",
url: "#",
items: [
{
title: "Routing",
title: "教师列表",
url: "#",
},
{
title: "Data Fetching",
title: "教师详情",
url: "#",
isActive: true,
},
{
title: "Rendering",
url: "#",
},
{
title: "Caching",
url: "#",
},
{
title: "Styling",
url: "#",
},
{
title: "Optimizing",
url: "#",
},
{
title: "Configuring",
url: "#",
},
{
title: "Testing",
url: "#",
},
{
title: "Authentication",
url: "#",
},
{
title: "Deploying",
url: "#",
},
{
title: "Upgrading",
url: "#",
},
{
title: "Examples",
title: "教师仪表盘",
url: "#",
},
],
},
{
title: "API Reference",
title: "管理员",
url: "#",
items: [
{
title: "Components",
title: "管理员列表",
url: "#",
},
{
title: "File Conventions",
title: "管理员详情",
url: "#",
},
{
title: "Functions",
url: "#",
},
{
title: "next.config.js Options",
url: "#",
},
{
title: "CLI",
url: "#",
},
{
title: "Edge Runtime",
url: "#",
},
],
},
{
title: "Architecture",
url: "#",
items: [
{
title: "Accessibility",
url: "#",
},
{
title: "Fast Refresh",
url: "#",
},
{
title: "Next.js Compiler",
url: "#",
},
{
title: "Supported Browsers",
url: "#",
},
{
title: "Turbopack",
url: "#",
},
],
},
{
title: "Community",
url: "#",
items: [
{
title: "Contribution Guide",
url: "#",
},
],
},
],
}

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