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

@@ -1,57 +1,13 @@
"use client"
import { Component, type ReactNode, Suspense } from "react"
import { type ReactNode, Suspense } from "react"
import { AlertCircle } from "lucide-react"
import { EmptyState } from "@/shared/components/ui/empty-state"
import { Card, CardContent, CardHeader } from "@/shared/components/ui/card"
import { Skeleton } from "@/shared/components/ui/skeleton"
import { useTranslations } from "next-intl"
/**
* 仪表盘分区 Error Boundary
*
* 包裹每个独立数据区块,避免单个区块崩溃导致整页不可用。
* 与路由级 error.tsx 不同,此组件仅替换出错区块,其余区块继续渲染。
*/
export class DashboardSectionErrorBoundary extends Component<
{ children: ReactNode },
{ hasError: boolean }
> {
state: { hasError: boolean } = { hasError: false }
static getDerivedStateFromError(): { hasError: boolean } {
return { hasError: true }
}
handleRetry = (): void => {
this.setState({ hasError: false })
}
render(): ReactNode {
if (this.state.hasError) {
return <DashboardSectionErrorFallback onRetry={this.handleRetry} />
}
return this.props.children
}
}
function DashboardSectionErrorFallback({
onRetry,
}: {
onRetry: () => void
}): ReactNode {
const t = useTranslations("dashboard.error")
return (
<EmptyState
icon={AlertCircle}
title={t("sectionLoadFailed")}
description={t("sectionLoadFailedDesc")}
action={{ label: t("retry"), onClick: onRetry }}
className="h-auto border-none shadow-none"
/>
)
}
import { SectionErrorBoundary } from "@/shared/components/section-error-boundary"
/**
* 分区骨架屏变体
@@ -148,23 +104,53 @@ export function DashboardSectionSkeleton({
* 组合 Error Boundary + Suspense + 骨架屏,包裹每个独立数据区块。
* 单个区块出错或加载中时,仅影响该区块,不波及整页。
*
* 使用共享 SectionErrorBoundary 替代模块特定的 DashboardSectionErrorBoundary 类。
*
* V4P3-1新增 `ariaLabel` prop传入时渲染 `<section role="region" tabIndex={0}>`
* 使键盘用户可按逻辑顺序遍历各 Widget提升 a11y。
*
* @example
* <DashboardSection variant="stats">
* <DashboardSection variant="stats" ariaLabel={t("sections.userStats")}>
* <TeacherStats ... />
* </DashboardSection>
*/
export function DashboardSection({
children,
variant = "card",
ariaLabel,
}: {
children: ReactNode
variant?: SkeletonVariant
/** 传入时渲染为可聚焦的 region提升键盘导航 a11y */
ariaLabel?: string
}): ReactNode {
return (
<DashboardSectionErrorBoundary>
const t = useTranslations("dashboard.error")
const fallback = (): ReactNode => (
<EmptyState
icon={AlertCircle}
title={t("sectionLoadFailed")}
description={t("sectionLoadFailedDesc")}
action={{ label: t("retry"), onClick: () => window.location.reload() }}
className="h-auto border-none shadow-none"
/>
)
const content = (
<SectionErrorBoundary namespace="common" fallback={fallback}>
<Suspense fallback={<DashboardSectionSkeleton variant={variant} />}>
{children}
</Suspense>
</DashboardSectionErrorBoundary>
</SectionErrorBoundary>
)
if (ariaLabel) {
return (
<section role="region" aria-label={ariaLabel} tabIndex={0} className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-lg">
{content}
</section>
)
}
return content
}