feat(admin): 补全 admin 模块核心功能与产品体验优化

修复 v4 报告中的 13 个产品体验问题:新增用户管理列表页和系统设置页,重组导航菜单并补充缺失入口,增加角色切换机制,Dashboard 增加快捷操作和 recharts 趋势图表,考勤增加统计概览,排课增加课表网格视图,统一 Toast 操作反馈,同步更新架构文档
This commit is contained in:
SpecialX
2026-06-22 13:38:07 +08:00
parent 978d9a8309
commit c45b3488c5
23 changed files with 3112 additions and 213 deletions

View File

@@ -11,6 +11,13 @@ import {
CollapsibleTrigger,
} from "@/shared/components/ui/collapsible"
import { ScrollArea } from "@/shared/components/ui/scroll-area"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/components/ui/select"
import {
Tooltip,
TooltipContent,
@@ -19,6 +26,7 @@ import {
} from "@/shared/components/ui/tooltip"
import { cn } from "@/shared/lib/utils"
import { usePermission } from "@/shared/hooks"
import { UnreadMessageBadge } from "@/modules/messaging/components/unread-message-badge"
import { useSidebar } from "./sidebar-provider"
import { NAV_CONFIG, Role } from "../config/navigation"
@@ -27,21 +35,28 @@ interface AppSidebarProps {
}
export function AppSidebar({ mode }: AppSidebarProps) {
const { expanded, toggleSidebar, isMobile } = useSidebar()
const { expanded, toggleSidebar, isMobile, currentRole, setCurrentRole } = useSidebar()
const pathname = usePathname()
const { permissions, hasRole } = usePermission()
const { permissions, roles, hasRole } = usePermission()
// Determine which role's nav config to use based on session roles
let currentRole: Role = "teacher"
if (hasRole("admin")) {
currentRole = "admin"
} else if (hasRole("student")) {
currentRole = "student"
} else if (hasRole("parent")) {
currentRole = "parent"
// 自动检测当前角色(优先级 admin > student > parent > teacher
function detectAutoRole(): Role {
if (hasRole("admin")) return "admin"
if (hasRole("student")) return "student"
if (hasRole("parent")) return "parent"
return "teacher"
}
const allNavItems = NAV_CONFIG[currentRole] ?? NAV_CONFIG.teacher ?? []
// 用户在 NAV_CONFIG 中实际可用的角色(过滤掉未配置的角色)
const availableRoles = roles.filter((r) => NAV_CONFIG[r] !== undefined)
// 如果 context 中有 currentRole 且用户拥有该角色,使用 currentRole否则自动检测
const effectiveRole: Role =
currentRole !== null && availableRoles.includes(currentRole)
? currentRole
: detectAutoRole()
const allNavItems = NAV_CONFIG[effectiveRole] ?? NAV_CONFIG.teacher ?? []
// Filter nav items by permission
const navItems = allNavItems.filter((item) => {
@@ -154,6 +169,7 @@ export function AppSidebar({ mode }: AppSidebarProps) {
>
<item.icon className="size-4" />
<span>{item.title}</span>
{item.href === "/messages" ? <UnreadMessageBadge /> : null}
</Link>
)
})}
@@ -163,12 +179,26 @@ export function AppSidebar({ mode }: AppSidebarProps) {
{/* Sidebar Footer */}
<div className="p-4">
{availableRoles.length > 1 && (expanded || isMobile) && (
<div className="px-2 pb-2">
<Select value={effectiveRole} onValueChange={(v) => setCurrentRole(v as Role)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="切换角色" />
</SelectTrigger>
<SelectContent>
{availableRoles.map((r) => (
<SelectItem key={r} value={r}>{r}</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{!isMobile && (
<button
<button
onClick={toggleSidebar}
className="hover:bg-sidebar-accent text-sidebar-foreground flex w-full items-center justify-center rounded-md border p-2 text-sm transition-colors"
>
{expanded ? "Collapse" : <ChevronRight className="size-4" />}
{expanded ? "收起" : <ChevronRight className="size-4" />}
</button>
)}
</div>

View File

@@ -9,12 +9,15 @@ import {
SheetTitle,
} from "@/shared/components/ui/sheet"
import { cn } from "@/shared/lib/utils"
import type { Role } from "@/shared/types/permissions"
type SidebarContextType = {
expanded: boolean
setExpanded: (expanded: boolean) => void
isMobile: boolean
toggleSidebar: () => void
currentRole: Role | null
setCurrentRole: (role: Role | null) => void
}
const SidebarContext = React.createContext<SidebarContextType | undefined>(
@@ -38,6 +41,8 @@ export function SidebarProvider({ children, sidebar }: SidebarProviderProps) {
const [expanded, setExpanded] = React.useState(true)
const [isMobile, setIsMobile] = React.useState(false)
const [openMobile, setOpenMobile] = React.useState(false)
// null 表示自动检测(按现有优先级 admin > student > parent > teacher
const [currentRole, setCurrentRole] = React.useState<Role | null>(null)
React.useEffect(() => {
const checkMobile = () => {
@@ -62,7 +67,7 @@ export function SidebarProvider({ children, sidebar }: SidebarProviderProps) {
return (
<SidebarContext.Provider
value={{ expanded, setExpanded, isMobile, toggleSidebar }}
value={{ expanded, setExpanded, isMobile, toggleSidebar, currentRole, setCurrentRole }}
>
<div className="flex h-screen overflow-hidden w-full flex-col md:flex-row bg-background">
{/* Mobile Trigger & Sheet */}