feat(dashboard,diagnostic,elective): add widgets, layout, parent dashboard, role-config, services, elective components

dashboard:

- Add comparison-badge, dashboard-notification-widget, dashboard-responsive-layout, dashboard-time-range-filter

- Add parent-dashboard components directory

- Add config, hooks, and services directories

diagnostic:

- Add role-config and services directory

elective:

- Add elective-course-detail, elective-stats-cards, parent-selection-view components

- Add data-access-settings and data-access-stats
This commit is contained in:
SpecialX
2026-07-03 10:25:46 +08:00
parent dfffb61e94
commit 138b6f1b00
58 changed files with 3313 additions and 695 deletions

View File

@@ -64,7 +64,7 @@ async function TeacherDashboardContent({ data }: { data: TeacherDashboardData &
<TeacherDashboardHeader teacherName={data.teacherName} />
</header>
<DashboardSection variant="stats">
<DashboardSection variant="stats" ariaLabel={t("sections.quickStats")}>
<TeacherStats
toGradeCount={metrics.toGradeCount}
activeAssignmentsCount={metrics.activeAssignmentsCount}
@@ -76,19 +76,19 @@ async function TeacherDashboardContent({ data }: { data: TeacherDashboardData &
<div className="flex flex-col gap-6 lg:grid lg:grid-cols-12">
{/* 课表:移动端首位,桌面端右上 — 仅渲染一次P2-9 修复,原为双实例) */}
<div className="order-1 lg:col-start-9 lg:col-span-4 lg:row-start-1">
<DashboardSection variant="card">
<DashboardSection variant="card" ariaLabel={t("sections.todaySchedule")}>
<TeacherSchedule items={metrics.todayScheduleItems} />
</DashboardSection>
</div>
<section aria-label={t("sections.pendingGrading")} className="flex flex-col gap-6 order-2 lg:col-start-1 lg:col-span-8 lg:row-start-1 lg:row-span-2">
<DashboardSection variant="card">
<DashboardSection variant="card" ariaLabel={t("todo.title")}>
<TeacherTodoCard items={todoItems} />
</DashboardSection>
<DashboardSection variant="chart">
<DashboardSection variant="chart" ariaLabel={t("sections.gradeTrends")}>
<TeacherGradeTrends trends={data.gradeTrends} />
</DashboardSection>
<DashboardSection variant="list">
<DashboardSection variant="list" ariaLabel={t("sections.recentSubmissions")}>
<RecentSubmissions
submissions={metrics.submissionsToGrade}
title={t("sections.pendingGrading")}
@@ -99,10 +99,10 @@ async function TeacherDashboardContent({ data }: { data: TeacherDashboardData &
</section>
<aside aria-label={t("sections.myClasses")} className="flex flex-col gap-6 order-3 lg:col-start-9 lg:col-span-4 lg:row-start-2">
<DashboardSection variant="list">
<DashboardSection variant="list" ariaLabel={t("sections.homework")}>
<TeacherHomeworkCard assignments={data.assignments} />
</DashboardSection>
<DashboardSection variant="list">
<DashboardSection variant="list" ariaLabel={t("sections.myClasses")}>
<TeacherClassesCard classes={data.classes} />
</DashboardSection>
</aside>

View File

@@ -6,35 +6,13 @@ import { CalendarDays, CalendarX, MapPin } from "lucide-react"
import { EmptyState } from "@/shared/components/ui/empty-state"
import { cn } from "@/shared/lib/utils"
import { ScrollArea } from "@/shared/components/ui/scroll-area"
type TeacherTodayScheduleItem = {
id: string
classId: string
className: string
course: string
startTime: string
endTime: string
location: string | null
}
import { getScheduleStatus } from "@/modules/dashboard/lib/dashboard-utils"
import type { TeacherTodayScheduleItem } from "@/modules/dashboard/types"
export async function TeacherSchedule({ items }: { items: TeacherTodayScheduleItem[] }) {
const t = await getTranslations("dashboard")
const hasSchedule = items.length > 0
const getStatus = (start: string, end: string): "live" | "upcoming" | "past" => {
const now = new Date()
const currentTime = now.getHours() * 60 + now.getMinutes()
const [startH, startM] = start.split(":").map(Number)
const [endH, endM] = end.split(":").map(Number)
const startTime = (Number.isFinite(startH) ? startH : 0) * 60 + (Number.isFinite(startM) ? startM : 0)
const endTime = (Number.isFinite(endH) ? endH : 0) * 60 + (Number.isFinite(endM) ? endM : 0)
if (currentTime >= startTime && currentTime <= endTime) return "live"
if (currentTime < startTime) return "upcoming"
return "past"
}
return (
<Card>
<CardHeader className="pb-3">
@@ -60,7 +38,7 @@ export async function TeacherSchedule({ items }: { items: TeacherTodayScheduleIt
<div className="absolute left-[11px] -top-3 h-3 w-px bg-gradient-to-t from-border/50 to-transparent" />
{items.map((item, index) => {
const status = getStatus(item.startTime, item.endTime)
const status = getScheduleStatus(item.startTime, item.endTime, new Date())
const isLive = status === "live"
const isPast = status === "past"
const isLast = index === items.length - 1

View File

@@ -27,6 +27,7 @@ export async function TeacherStats({
href="/teacher/homework/submissions?status=submitted"
highlight={toGradeCount > 0}
color="text-amber-500"
valueClassName="tabular-nums"
/>
<StatCard
title={t("stats.activeAssignments")}
@@ -35,6 +36,7 @@ export async function TeacherStats({
icon={PenTool}
href="/teacher/homework/assignments?status=published"
color="text-blue-500"
valueClassName="tabular-nums"
/>
<StatCard
title={t("stats.averageScore")}
@@ -43,6 +45,7 @@ export async function TeacherStats({
icon={TrendingUp}
href="#grade-trends"
color="text-emerald-500"
valueClassName="tabular-nums"
/>
<StatCard
title={t("stats.submissionRate")}
@@ -51,6 +54,7 @@ export async function TeacherStats({
icon={BarChart}
href="#grade-trends"
color="text-purple-500"
valueClassName="tabular-nums"
/>
</div>
)

View File

@@ -21,6 +21,13 @@ const VARIANT_STYLES: Record<TeacherTodoItem["variant"], { icon: typeof AlertCir
info: { icon: CalendarCheck, iconColor: "text-blue-500", badge: "bg-blue-500 text-white" },
}
/** 变体优先级映射数值越小优先级越高V4P3-3优化排序可读性 */
const VARIANT_PRIORITY: Record<TeacherTodoItem["variant"], number> = {
urgent: 0,
normal: 1,
info: 2,
}
export async function TeacherTodoCard({ items }: TeacherTodoCardProps) {
const t = await getTranslations("dashboard")
const hasItems = items.some((item) => item.count > 0)
@@ -49,11 +56,7 @@ export async function TeacherTodoCard({ items }: TeacherTodoCardProps) {
<div className="space-y-1">
{items
.filter((item) => item.count > 0)
.sort((a, b) => {
if (a.variant === "urgent" && b.variant !== "urgent") return -1
if (a.variant !== "urgent" && b.variant === "urgent") return 1
return 0
})
.sort((a, b) => VARIANT_PRIORITY[a.variant] - VARIANT_PRIORITY[b.variant])
.map((item, idx) => {
const style = VARIANT_STYLES[item.variant]
const Icon = style.icon