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

@@ -9,8 +9,9 @@ import { requirePermission, getAuthContext } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { getSearchParam, type SearchParams } from "@/shared/lib/utils"
import { getAdminClasses } from "@/modules/classes/data-access"
import { getAttendanceRecords } from "@/modules/attendance/data-access"
import { getAttendanceRecords, getAttendanceStats } from "@/modules/attendance/data-access"
import { AttendanceFilters } from "@/modules/attendance/components/attendance-filters"
import { AttendanceStatsCards } from "@/modules/attendance/components/attendance-stats-cards"
import { AttendanceRecordList } from "@/modules/attendance/components/attendance-record-list"
import type { AttendanceStatus } from "@/modules/attendance/types"
@@ -50,6 +51,13 @@ export default async function AdminAttendancePage({
date: date && date.length > 0 ? date : undefined,
})
const stats = await getAttendanceStats({
scope: ctx.dataScope,
currentUserId: ctx.userId,
classId: classId && classId !== "all" ? classId : undefined,
date: date && date.length > 0 ? date : undefined,
})
return (
<div className="h-full flex-1 flex-col space-y-8 p-8 md:flex">
<div className="flex items-center justify-between space-y-2">
@@ -65,6 +73,8 @@ export default async function AdminAttendancePage({
</Button>
</div>
<AttendanceStatsCards stats={stats} />
<AttendanceFilters classes={classOptions} />
{result.items.length === 0 && !classId && !status && !date ? (

View File

@@ -3,15 +3,19 @@ import { PlusCircle, ClipboardList } from "lucide-react"
import type { Metadata } from "next"
import type { JSX } from "react"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { Button } from "@/shared/components/ui/button"
import { EmptyState } from "@/shared/components/ui/empty-state"
import { getSearchParam, type SearchParams } from "@/shared/lib/utils"
import {
getAdminClassesForScheduling,
getScheduleChanges,
getScheduleEntriesForAdmin,
} from "@/modules/scheduling/data-access"
import { ScheduleChangeList } from "@/modules/scheduling/components/schedule-change-list"
import { ScheduleConflictsView } from "@/modules/scheduling/components/schedule-conflicts-view"
import { ScheduleGridView } from "@/modules/scheduling/components/schedule-grid-view"
import type { ScheduleChangeStatus } from "@/modules/scheduling/types"
export const metadata: Metadata = {
@@ -29,15 +33,17 @@ export default async function AdminSchedulingChangesPage({
}: {
searchParams: Promise<SearchParams>
}): Promise<JSX.Element> {
await requirePermission(Permissions.SCHEDULE_ADJUST)
const sp = await searchParams
const statusParam = getSearchParam(sp, "status")
const status = isValidStatus(statusParam) ? statusParam : undefined
const classIdParam = getSearchParam(sp, "classId")
const classId = classIdParam && classIdParam !== "all" ? classIdParam : undefined
const [classes, items] = await Promise.all([
const [classes, items, scheduleEntries] = await Promise.all([
getAdminClassesForScheduling(),
getScheduleChanges({ status, classId }),
getScheduleEntriesForAdmin(),
])
const classOptions = classes.map((c) => ({ id: c.id, name: c.name, grade: c.grade }))
@@ -87,6 +93,14 @@ export default async function AdminSchedulingChangesPage({
<ScheduleConflictsView classes={classOptions} />
)}
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold"></h3>
<p className="text-sm text-muted-foreground">
</p>
<ScheduleGridView entries={scheduleEntries} classes={classOptions} />
</div>
</div>
)
}

View File

@@ -0,0 +1,22 @@
import type { Metadata } from "next"
import type { JSX } from "react"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { AdminSettingsView } from "@/modules/settings/components/admin-settings-view"
export const metadata: Metadata = {
title: "系统设置 - Next_Edu",
description: "管理系统基础信息与运行参数",
}
export const dynamic = "force-dynamic"
export default async function AdminSettingsPage(): Promise<JSX.Element> {
await requirePermission(Permissions.SETTINGS_ADMIN)
return (
<div className="flex h-full flex-col p-8">
<AdminSettingsView />
</div>
)
}

View File

@@ -0,0 +1,48 @@
import type { Metadata } from "next"
import type { JSX } from "react"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { getSearchParam, type SearchParams } from "@/shared/lib/utils"
import { getAdminUsers, getAdminUserRoles } from "@/modules/users/data-access"
import { AdminUsersView } from "@/modules/users/components/admin-users-view"
export const metadata: Metadata = {
title: "用户管理 - Next_Edu",
description: "管理系统所有用户",
}
export const dynamic = "force-dynamic"
export default async function AdminUsersPage({
searchParams,
}: {
searchParams: Promise<SearchParams>
}): Promise<JSX.Element> {
await requirePermission(Permissions.USER_MANAGE)
const sp = await searchParams
const page = Number(getSearchParam(sp, "page") ?? "1") || 1
const search = getSearchParam(sp, "search") ?? ""
const role = getSearchParam(sp, "role") ?? ""
const [result, roleOptions] = await Promise.all([
getAdminUsers({ page, search: search || undefined, role: role || undefined }),
getAdminUserRoles(),
])
return (
<div className="flex h-full flex-col space-y-6 p-8">
<AdminUsersView
users={result.items}
roleOptions={roleOptions}
page={result.page}
pageSize={result.pageSize}
total={result.total}
totalPages={result.totalPages}
search={search}
roleFilter={role}
/>
</div>
)
}