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:
@@ -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 类。
|
||||
*
|
||||
* V4(P3-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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user