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:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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" },
|
||||
}
|
||||
|
||||
/** 变体优先级映射(数值越小优先级越高),V4(P3-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
|
||||
|
||||
Reference in New Issue
Block a user