From e76c626779f9a9570a30501f4966d5a36139c7e8 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Mon, 6 Jul 2026 22:22:23 +0800 Subject: [PATCH] =?UTF-8?q?refactor(classes,audit,school,settings,adaptive?= =?UTF-8?q?-practice,announcements,messaging,questions,student):=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=8C=96=E9=87=8D=E6=9E=84=E9=81=97=E6=BC=8F?= =?UTF-8?q?=E8=A1=A5=E5=85=A8=20-=2020=20=E4=B8=AA=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补全未被批次覆盖的 9 个模块的重复组件迁移: - classes: 迁移 students-filters/schedule-filters/class-skeleton/class-error-boundary - audit: 迁移 3 个 filters + audit-log-table-skeleton + audit-error-boundary - school: 迁移 grade-insights-filters/school-skeleton/school-error-boundary - settings: 迁移 settings-section-error-boundary - adaptive-practice: 迁移 2 个 practice-stats-cards 到 StatsGrid - announcements: 迁移 announcement-list-skeleton 到 SkeletonCard - messaging: 迁移 message-list-skeleton 到 SkeletonCard - questions: 迁移 question-filters 到 app 层 - student: 迁移 course-filters/student-schedule-filters 到 app 层 - 补充 school/settings 模块 i18n error.boundary* 键 - tsc 零错误 --- .../_components}/question-filters.tsx | 2 +- .../admin/announcements/loading.tsx | 4 +- .../admin/audit-logs/data-changes/loading.tsx | 4 +- .../admin/audit-logs/data-changes/page.tsx | 6 +- .../(dashboard)/admin/audit-logs/loading.tsx | 4 +- .../admin/audit-logs/login-logs/loading.tsx | 4 +- .../admin/audit-logs/login-logs/page.tsx | 6 +- src/app/(dashboard)/admin/audit-logs/page.tsx | 6 +- src/app/(dashboard)/admin/questions/page.tsx | 2 +- .../admin/school/academic-year/page.tsx | 6 +- .../(dashboard)/admin/school/classes/page.tsx | 6 +- .../admin/school/departments/page.tsx | 6 +- .../admin/school/grades/insights/page.tsx | 16 +- .../(dashboard)/admin/school/grades/page.tsx | 6 +- .../(dashboard)/admin/school/schools/page.tsx | 6 +- src/app/(dashboard)/announcements/loading.tsx | 4 +- .../management/grade/dashboard/page.tsx | 21 +-- .../management/grade/insights/page.tsx | 16 +- .../management/grade/practice/page.tsx | 66 ++++++-- src/app/(dashboard)/messages/page.tsx | 5 +- src/app/(dashboard)/parent/practice/page.tsx | 72 ++++++++- src/app/(dashboard)/profile/page.tsx | 14 +- .../learning/courses}/course-filters.tsx | 2 +- .../student/learning/courses/page.tsx | 2 +- src/app/(dashboard)/student/practice/page.tsx | 37 ++++- src/app/(dashboard)/student/schedule/page.tsx | 2 +- .../schedule}/student-schedule-filters.tsx | 30 +++- .../teacher/classes/my/[id]/page.tsx | 6 +- .../(dashboard)/teacher/classes/my/page.tsx | 6 +- .../teacher/classes/schedule/page.tsx | 8 +- .../classes/schedule}/schedule-filters.tsx | 43 +++-- .../teacher/classes/students/page.tsx | 8 +- .../classes/students}/students-filters.tsx | 45 +++--- src/app/(dashboard)/teacher/practice/page.tsx | 49 +++++- .../(dashboard)/teacher/questions/page.tsx | 2 +- .../practice-overview-stats-cards.tsx | 99 ------------ .../components/practice-stats-cards.tsx | 73 --------- .../components/announcement-list-skeleton.tsx | 50 ------ .../audit/components/audit-error-boundary.tsx | 23 --- .../audit/components/audit-log-filters.tsx | 107 ------------- .../components/audit-log-table-skeleton.tsx | 43 ----- .../audit/components/audit-log-view.tsx | 101 +++++++++++- .../audit/components/audit-overview-view.tsx | 22 +-- .../components/data-change-log-filters.tsx | 149 ------------------ .../audit/components/data-change-log-view.tsx | 117 +++++++++++++- .../audit/components/login-log-filters.tsx | 91 ----------- .../audit/components/login-log-view.tsx | 89 ++++++++++- .../components/class-error-boundary.tsx | 57 ------- .../classes/components/class-skeleton.tsx | 120 -------------- .../components/message-list-section.tsx | 2 +- .../components/message-list-skeleton.tsx | 36 ----- .../components/grade-insights-filters.tsx | 48 ------ .../components/school-error-boundary.tsx | 57 ------- .../school/components/school-skeleton.tsx | 69 -------- .../settings-section-error-boundary.tsx | 25 --- .../settings/components/settings-view.tsx | 18 +-- src/shared/i18n/messages/en/classes.json | 4 +- src/shared/i18n/messages/en/school.json | 5 + src/shared/i18n/messages/en/settings.json | 5 + src/shared/i18n/messages/zh-CN/classes.json | 4 +- src/shared/i18n/messages/zh-CN/school.json | 5 + src/shared/i18n/messages/zh-CN/settings.json | 5 + 62 files changed, 699 insertions(+), 1247 deletions(-) rename src/{modules/questions/components => app/(dashboard)/_components}/question-filters.tsx (97%) rename src/{modules/student/components => app/(dashboard)/student/learning/courses}/course-filters.tsx (92%) rename src/{modules/student/components => app/(dashboard)/student/schedule}/student-schedule-filters.tsx (58%) rename src/{modules/classes/components => app/(dashboard)/teacher/classes/schedule}/schedule-filters.tsx (85%) rename src/{modules/classes/components => app/(dashboard)/teacher/classes/students}/students-filters.tsx (86%) delete mode 100644 src/modules/adaptive-practice/components/practice-overview-stats-cards.tsx delete mode 100644 src/modules/adaptive-practice/components/practice-stats-cards.tsx delete mode 100644 src/modules/announcements/components/announcement-list-skeleton.tsx delete mode 100644 src/modules/audit/components/audit-error-boundary.tsx delete mode 100644 src/modules/audit/components/audit-log-filters.tsx delete mode 100644 src/modules/audit/components/audit-log-table-skeleton.tsx delete mode 100644 src/modules/audit/components/data-change-log-filters.tsx delete mode 100644 src/modules/audit/components/login-log-filters.tsx delete mode 100644 src/modules/classes/components/class-error-boundary.tsx delete mode 100644 src/modules/classes/components/class-skeleton.tsx delete mode 100644 src/modules/messaging/components/message-list-skeleton.tsx delete mode 100644 src/modules/school/components/grade-insights-filters.tsx delete mode 100644 src/modules/school/components/school-error-boundary.tsx delete mode 100644 src/modules/school/components/school-skeleton.tsx delete mode 100644 src/modules/settings/components/settings-section-error-boundary.tsx diff --git a/src/modules/questions/components/question-filters.tsx b/src/app/(dashboard)/_components/question-filters.tsx similarity index 97% rename from src/modules/questions/components/question-filters.tsx rename to src/app/(dashboard)/_components/question-filters.tsx index a61ae6b..ba26831 100644 --- a/src/modules/questions/components/question-filters.tsx +++ b/src/app/(dashboard)/_components/question-filters.tsx @@ -11,7 +11,7 @@ import { SelectValue, } from "@/shared/components/ui/select" import { FilterBar, FilterSearchInput } from "@/shared/components/ui/filter-bar" -import { QuestionCascadeFilter } from "./question-cascade-filter" +import { QuestionCascadeFilter } from "@/modules/questions/components/question-cascade-filter" /** * 题库筛选栏。 diff --git a/src/app/(dashboard)/admin/announcements/loading.tsx b/src/app/(dashboard)/admin/announcements/loading.tsx index 7d27fd7..7077a47 100644 --- a/src/app/(dashboard)/admin/announcements/loading.tsx +++ b/src/app/(dashboard)/admin/announcements/loading.tsx @@ -1,5 +1,5 @@ -import { AnnouncementListSkeleton } from "@/modules/announcements/components/announcement-list-skeleton" +import { SkeletonCard } from "@/shared/components/ui/skeleton" export default function AdminAnnouncementsLoading() { - return + return } diff --git a/src/app/(dashboard)/admin/audit-logs/data-changes/loading.tsx b/src/app/(dashboard)/admin/audit-logs/data-changes/loading.tsx index f3534be..5e96d30 100644 --- a/src/app/(dashboard)/admin/audit-logs/data-changes/loading.tsx +++ b/src/app/(dashboard)/admin/audit-logs/data-changes/loading.tsx @@ -1,4 +1,4 @@ -import { AuditLogTableSkeleton } from "@/modules/audit/components/audit-log-table-skeleton" +import { SkeletonCard } from "@/shared/components/ui/skeleton" export default function DataChangeLogsLoading() { return ( @@ -10,7 +10,7 @@ export default function DataChangeLogsLoading() {
- + ) } diff --git a/src/app/(dashboard)/admin/audit-logs/data-changes/page.tsx b/src/app/(dashboard)/admin/audit-logs/data-changes/page.tsx index 4e2d6d9..3a9206d 100644 --- a/src/app/(dashboard)/admin/audit-logs/data-changes/page.tsx +++ b/src/app/(dashboard)/admin/audit-logs/data-changes/page.tsx @@ -12,7 +12,7 @@ import { } from "@/modules/audit/data-access" import { DataChangeLogView } from "@/modules/audit/components/data-change-log-view" import { AuditLogExportButton } from "@/modules/audit/components/audit-log-export-button" -import { AuditErrorBoundary } from "@/modules/audit/components/audit-error-boundary" +import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" import type { DataChangeAction } from "@/modules/audit/types" export async function generateMetadata(): Promise { @@ -67,7 +67,7 @@ export default async function DataChangeLogsPage({ - + - + ) } diff --git a/src/app/(dashboard)/admin/audit-logs/loading.tsx b/src/app/(dashboard)/admin/audit-logs/loading.tsx index 076bdf2..08c2602 100644 --- a/src/app/(dashboard)/admin/audit-logs/loading.tsx +++ b/src/app/(dashboard)/admin/audit-logs/loading.tsx @@ -1,4 +1,4 @@ -import { AuditLogTableSkeleton } from "@/modules/audit/components/audit-log-table-skeleton" +import { SkeletonCard } from "@/shared/components/ui/skeleton" export default function AuditLogsLoading() { return ( @@ -10,7 +10,7 @@ export default function AuditLogsLoading() {
- + ) } diff --git a/src/app/(dashboard)/admin/audit-logs/login-logs/loading.tsx b/src/app/(dashboard)/admin/audit-logs/login-logs/loading.tsx index e6458ce..8de467d 100644 --- a/src/app/(dashboard)/admin/audit-logs/login-logs/loading.tsx +++ b/src/app/(dashboard)/admin/audit-logs/login-logs/loading.tsx @@ -1,4 +1,4 @@ -import { AuditLogTableSkeleton } from "@/modules/audit/components/audit-log-table-skeleton" +import { SkeletonCard } from "@/shared/components/ui/skeleton" export default function LoginLogsLoading() { return ( @@ -10,7 +10,7 @@ export default function LoginLogsLoading() {
- + ) } diff --git a/src/app/(dashboard)/admin/audit-logs/login-logs/page.tsx b/src/app/(dashboard)/admin/audit-logs/login-logs/page.tsx index 636c96c..04e4559 100644 --- a/src/app/(dashboard)/admin/audit-logs/login-logs/page.tsx +++ b/src/app/(dashboard)/admin/audit-logs/login-logs/page.tsx @@ -8,7 +8,7 @@ import { getSearchParam, type SearchParams } from "@/shared/lib/utils" import { getLoginLogs } from "@/modules/audit/data-access" import { LoginLogView } from "@/modules/audit/components/login-log-view" import { AuditLogExportButton } from "@/modules/audit/components/audit-log-export-button" -import { AuditErrorBoundary } from "@/modules/audit/components/audit-error-boundary" +import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" import type { LoginLogAction, LoginLogStatus } from "@/modules/audit/types" export async function generateMetadata(): Promise { @@ -65,7 +65,7 @@ export default async function LoginLogsPage({ - + - + ) } diff --git a/src/app/(dashboard)/admin/audit-logs/page.tsx b/src/app/(dashboard)/admin/audit-logs/page.tsx index e1a5f45..f511c54 100644 --- a/src/app/(dashboard)/admin/audit-logs/page.tsx +++ b/src/app/(dashboard)/admin/audit-logs/page.tsx @@ -8,7 +8,7 @@ import { getSearchParam, type SearchParams } from "@/shared/lib/utils" import { getAuditLogs, getAuditModuleOptions } from "@/modules/audit/data-access" import { AuditLogView } from "@/modules/audit/components/audit-log-view" import { AuditLogExportButton } from "@/modules/audit/components/audit-log-export-button" -import { AuditErrorBoundary } from "@/modules/audit/components/audit-error-boundary" +import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" import type { AuditLogStatus } from "@/modules/audit/types" export async function generateMetadata(): Promise { @@ -64,7 +64,7 @@ export default async function AuditLogsPage({ - + - + ) } diff --git a/src/app/(dashboard)/admin/questions/page.tsx b/src/app/(dashboard)/admin/questions/page.tsx index 529055a..0d49f7b 100644 --- a/src/app/(dashboard)/admin/questions/page.tsx +++ b/src/app/(dashboard)/admin/questions/page.tsx @@ -3,7 +3,7 @@ import { Suspense } from "react" import { ClipboardList } from "lucide-react" import { getTranslations } from "next-intl/server" -import { QuestionFilters } from "@/modules/questions/components/question-filters" +import { QuestionFilters } from "../../_components/question-filters" import { CreateQuestionButton } from "@/modules/questions/components/create-question-button" import { QuestionBankResultsClient } from "@/modules/questions/components/question-bank-results-client" import { ImportExportButtons } from "@/modules/questions/components/import-export-buttons" diff --git a/src/app/(dashboard)/admin/school/academic-year/page.tsx b/src/app/(dashboard)/admin/school/academic-year/page.tsx index 3cb71d5..348489a 100644 --- a/src/app/(dashboard)/admin/school/academic-year/page.tsx +++ b/src/app/(dashboard)/admin/school/academic-year/page.tsx @@ -5,7 +5,7 @@ import { getTranslations } from "next-intl/server" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import { AcademicYearClient } from "@/modules/school/components/academic-year-view" -import { SchoolErrorBoundary } from "@/modules/school/components/school-error-boundary" +import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" import { getAcademicYears } from "@/modules/school/data-access" export const dynamic = "force-dynamic" @@ -28,9 +28,9 @@ export default async function AdminAcademicYearPage(): Promise {

{t("academicYear.title")}

{t("academicYear.description")}

- + - + ) } diff --git a/src/app/(dashboard)/admin/school/classes/page.tsx b/src/app/(dashboard)/admin/school/classes/page.tsx index 357e953..3238406 100644 --- a/src/app/(dashboard)/admin/school/classes/page.tsx +++ b/src/app/(dashboard)/admin/school/classes/page.tsx @@ -5,7 +5,7 @@ import { getTranslations } from "next-intl/server" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import { getAdminClasses, getTeacherOptions } from "@/modules/classes/data-access" -import { ClassErrorBoundary } from "@/modules/classes/components/class-error-boundary" +import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" import { AdminClassesClient } from "@/modules/classes/components/admin-classes-view" import { getGrades, getSchools } from "@/modules/school/data-access" @@ -35,9 +35,9 @@ export default async function AdminSchoolClassesPage(): Promise {

{t("classManagement.title")}

{t("classManagement.description")}

- + - + ) } diff --git a/src/app/(dashboard)/admin/school/departments/page.tsx b/src/app/(dashboard)/admin/school/departments/page.tsx index 5db940e..58679da 100644 --- a/src/app/(dashboard)/admin/school/departments/page.tsx +++ b/src/app/(dashboard)/admin/school/departments/page.tsx @@ -5,7 +5,7 @@ import { getTranslations } from "next-intl/server" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import { DepartmentsClient } from "@/modules/school/components/departments-view" -import { SchoolErrorBoundary } from "@/modules/school/components/school-error-boundary" +import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" import { getDepartments } from "@/modules/school/data-access" export const dynamic = "force-dynamic" @@ -28,9 +28,9 @@ export default async function AdminDepartmentsPage(): Promise {

{t("departments.title")}

{t("departments.description")}

- + - + ) } diff --git a/src/app/(dashboard)/admin/school/grades/insights/page.tsx b/src/app/(dashboard)/admin/school/grades/insights/page.tsx index 9f67c14..395970f 100644 --- a/src/app/(dashboard)/admin/school/grades/insights/page.tsx +++ b/src/app/(dashboard)/admin/school/grades/insights/page.tsx @@ -10,7 +10,8 @@ import { getGrades } from "@/modules/school/data-access" import { getGradeHomeworkInsights } from "@/modules/classes/data-access" import { getSchoolWideGradeSummary } from "@/modules/grades/data-access-analytics" import { SchoolWideSummaryCard } from "@/modules/grades/components/school-wide-summary-card" -import { GradeInsightsFilters } from "@/modules/school/components/grade-insights-filters" +import { FilterBar } from "@/shared/components/ui/filter-bar" +import { ChipNav } from "@/shared/components/ui/chip-nav" import { EmptyState } from "@/shared/components/ui/empty-state" import { StatCard } from "@/shared/components/ui/stat-card" import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" @@ -74,11 +75,14 @@ export default async function AdminGradeInsightsPage({ )} {/* 年级筛选:ChipNav 即时切换,无整页刷新 */} - ({ id: g.id, name: g.name, schoolName: g.school.name }))} - currentGradeId={selected || "all"} - buildHref={buildHref} - /> + + ({ id: g.id, name: `${g.school.name} / ${g.name}` }))} + currentId={selected || "all"} + buildHref={buildHref} + allOption={{ id: "all", label: t("grades.gradeInsights.selectGrade") }} + /> + {!selected ? ( {

{t("grades.title")}

{t("grades.description")}

- + - + ) } diff --git a/src/app/(dashboard)/admin/school/schools/page.tsx b/src/app/(dashboard)/admin/school/schools/page.tsx index 2ae59d4..23ded50 100644 --- a/src/app/(dashboard)/admin/school/schools/page.tsx +++ b/src/app/(dashboard)/admin/school/schools/page.tsx @@ -5,7 +5,7 @@ import { getTranslations } from "next-intl/server" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import { SchoolsClient } from "@/modules/school/components/schools-view" -import { SchoolErrorBoundary } from "@/modules/school/components/school-error-boundary" +import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" import { getSchools } from "@/modules/school/data-access" export const dynamic = "force-dynamic" @@ -28,9 +28,9 @@ export default async function AdminSchoolsPage(): Promise {

{t("schools.title")}

{t("schools.description")}

- + - + ) } diff --git a/src/app/(dashboard)/announcements/loading.tsx b/src/app/(dashboard)/announcements/loading.tsx index 24f5cc1..7c51116 100644 --- a/src/app/(dashboard)/announcements/loading.tsx +++ b/src/app/(dashboard)/announcements/loading.tsx @@ -1,5 +1,5 @@ -import { AnnouncementListSkeleton } from "@/modules/announcements/components/announcement-list-skeleton" +import { SkeletonCard } from "@/shared/components/ui/skeleton" export default function AnnouncementsLoading() { - return + return } diff --git a/src/app/(dashboard)/management/grade/dashboard/page.tsx b/src/app/(dashboard)/management/grade/dashboard/page.tsx index e9f3c6b..b537978 100644 --- a/src/app/(dashboard)/management/grade/dashboard/page.tsx +++ b/src/app/(dashboard)/management/grade/dashboard/page.tsx @@ -10,12 +10,12 @@ import { getGradesForStaff } from "@/modules/school/data-access" import { getGradeDistributionByGradeId } from "@/modules/grades/data-access-analytics" import { getExamsByGradeId } from "@/modules/exams/data-access" import { getGradeCoursePlanProgress } from "@/modules/course-plans/data-access" -import { GradeInsightsFilters } from "@/modules/school/components/grade-insights-filters" +import { FilterBar } from "@/shared/components/ui/filter-bar" import { GradeDistributionPanel } from "@/modules/school/components/grade-dashboard/grade-distribution-panel" import { GradeHomeworkPanel } from "@/modules/school/components/grade-dashboard/grade-homework-panel" import { GradeExamsPanel } from "@/modules/school/components/grade-dashboard/grade-exams-panel" import { GradeProgressPanel } from "@/modules/school/components/grade-dashboard/grade-progress-panel" -import { SchoolErrorBoundary } from "@/modules/school/components/school-error-boundary" +import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" import { EmptyState } from "@/shared/components/ui/empty-state" import { ChipNav } from "@/shared/components/ui/chip-nav" import { BarChart3 } from "lucide-react" @@ -126,12 +126,15 @@ export default async function GradeDashboardPage({

{t("grades.gradeDashboard.description")}

- - ({ id: g.id, name: g.name, schoolName: g.school.name }))} - currentGradeId={selected || "all"} - buildHref={buildHref} - /> + + + ({ id: g.id, name: `${g.school.name} / ${g.name}` }))} + currentId={selected || "all"} + buildHref={buildHref} + allOption={{ id: "all", label: t("grades.gradeInsights.selectGrade") }} + /> + {!selected ? ( )} - + ) } diff --git a/src/app/(dashboard)/management/grade/insights/page.tsx b/src/app/(dashboard)/management/grade/insights/page.tsx index fc536d6..0049df6 100644 --- a/src/app/(dashboard)/management/grade/insights/page.tsx +++ b/src/app/(dashboard)/management/grade/insights/page.tsx @@ -7,7 +7,8 @@ import { Permissions } from "@/shared/types/permissions" import { getTeacherIdForMutations } from "@/modules/classes/data-access" import { getGradeHomeworkInsights } from "@/modules/classes/data-access" import { getGradesForStaff } from "@/modules/school/data-access" -import { GradeInsightsFilters } from "@/modules/school/components/grade-insights-filters" +import { FilterBar } from "@/shared/components/ui/filter-bar" +import { ChipNav } from "@/shared/components/ui/chip-nav" import { EmptyState } from "@/shared/components/ui/empty-state" import { StatCard } from "@/shared/components/ui/stat-card" import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" @@ -72,11 +73,14 @@ export default async function TeacherGradeInsightsPage({ searchParams }: { searc {/* 年级筛选:ChipNav 即时切换,无整页刷新 */} - ({ id: g.id, name: g.name, schoolName: g.school.name }))} - currentGradeId={selected || "all"} - buildHref={buildHref} - /> + + ({ id: g.id, name: `${g.school.name} / ${g.name}` }))} + currentId={selected || "all"} + buildHref={buildHref} + allOption={{ id: "all", label: t("grades.gradeInsights.selectGrade") }} + /> + {!selected ? ( { await requirePermission(Permissions.ADAPTIVE_PRACTICE_READ) const t = await getTranslations("practice") + const tSchool = await getTranslations("school") const params = await searchParams const gradeId = getParam(params, "gradeId") @@ -94,11 +96,14 @@ export default async function GradePracticePage({

{t("grade.description")}

- ({ id: g.id, name: g.name, schoolName: g.school.name }))} - currentGradeId={selected || "all"} - buildHref={buildHref} - /> + + ({ id: g.id, name: `${g.school.name} / ${g.name}` }))} + currentId={selected || "all"} + buildHref={buildHref} + allOption={{ id: "all", label: tSchool("grades.gradeInsights.selectGrade") }} + /> + {!selected ? ( {/* 年级整体统计卡片 */} - {/* 各班级练习对比表 */} diff --git a/src/app/(dashboard)/messages/page.tsx b/src/app/(dashboard)/messages/page.tsx index 334e416..a9f0f21 100644 --- a/src/app/(dashboard)/messages/page.tsx +++ b/src/app/(dashboard)/messages/page.tsx @@ -8,9 +8,8 @@ import { SectionErrorBoundary } from "@/shared/components/section-error-boundary import { getNotifications } from "@/modules/notifications/data-access" import { NotificationList } from "@/modules/notifications/components/notification-list" import { MessageListSection } from "@/modules/messaging/components/message-list-section" -import { MessageListSkeleton } from "@/modules/messaging/components/message-list-skeleton" import { Card, CardContent, CardHeader } from "@/shared/components/ui/card" -import { Skeleton } from "@/shared/components/ui/skeleton" +import { Skeleton, SkeletonCard } from "@/shared/components/ui/skeleton" export const dynamic = "force-dynamic" @@ -63,7 +62,7 @@ export default async function MessagesPage(): Promise { {/* P2-8 + P2-9: 消息列表区块独立 Suspense + ErrorBoundary,局部失败不影响通知区块 */} - }> + }> diff --git a/src/app/(dashboard)/parent/practice/page.tsx b/src/app/(dashboard)/parent/practice/page.tsx index e86e44d..3c1566a 100644 --- a/src/app/(dashboard)/parent/practice/page.tsx +++ b/src/app/(dashboard)/parent/practice/page.tsx @@ -1,6 +1,6 @@ import type { JSX } from "react" import { getTranslations } from "next-intl/server" -import { Target, Users } from "lucide-react" +import { Target, Users, CheckCircle2, TrendingUp, Award } from "lucide-react" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" @@ -9,7 +9,7 @@ import { WidgetBoundary } from "@/shared/components/widget-boundary" import { getPracticeSessions, getPracticeStats } from "@/modules/adaptive-practice/data-access" import { PracticeHistory } from "@/modules/adaptive-practice/components/practice-history" -import { PracticeStatsCards } from "@/modules/adaptive-practice/components/practice-stats-cards" +import { StatsGrid } from "@/shared/components/ui/stats-grid" import { PracticeServiceProvider } from "@/modules/adaptive-practice/services/practice-service" import type { PracticeSessionSummary, PracticeStats } from "@/modules/adaptive-practice/types" import { getUserNamesByIds } from "@/modules/users/data-access" @@ -86,7 +86,39 @@ export default async function ParentPracticePage(): Promise { {isSingleChild ? ( <> - +

{t("parent.history")}

@@ -109,7 +141,39 @@ export default async function ParentPracticePage(): Promise {
- +

{t("parent.history")} diff --git a/src/app/(dashboard)/profile/page.tsx b/src/app/(dashboard)/profile/page.tsx index 4843e05..ad0f182 100644 --- a/src/app/(dashboard)/profile/page.tsx +++ b/src/app/(dashboard)/profile/page.tsx @@ -9,7 +9,7 @@ import { getUserProfile } from "@/modules/users/data-access" import { AvatarUpload } from "@/modules/settings/components/avatar-upload" import { ProfileStudentOverview, ProfileStudentOverviewSkeleton } from "@/modules/settings/components/profile-student-overview" import { ProfileTeacherOverview, ProfileTeacherOverviewSkeleton } from "@/modules/settings/components/profile-teacher-overview" -import { SettingsSectionErrorBoundary } from "@/modules/settings/components/settings-section-error-boundary" +import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" import { resolveProfileOverviewType } from "@/modules/settings/config/profile-overview-config" import { Badge } from "@/shared/components/ui/badge" import { Button } from "@/shared/components/ui/button" @@ -50,13 +50,13 @@ export default async function ProfilePage(): Promise { } /> - + - +
@@ -142,19 +142,19 @@ export default async function ProfilePage(): Promise {
{overviewType === "student" ? ( - + }> - + ) : null} {overviewType === "teacher" ? ( - + }> - + ) : null}

) diff --git a/src/modules/student/components/course-filters.tsx b/src/app/(dashboard)/student/learning/courses/course-filters.tsx similarity index 92% rename from src/modules/student/components/course-filters.tsx rename to src/app/(dashboard)/student/learning/courses/course-filters.tsx index 965ffc0..468f59c 100644 --- a/src/modules/student/components/course-filters.tsx +++ b/src/app/(dashboard)/student/learning/courses/course-filters.tsx @@ -5,7 +5,7 @@ import { useTranslations } from "next-intl" import { FilterBar, FilterSearchInput } from "@/shared/components/ui/filter-bar" -export function CourseFilters() { +export function CourseFilters(): React.ReactNode { const t = useTranslations("student") const [search, setSearch] = useQueryState("q", parseAsString.withDefault("")) diff --git a/src/app/(dashboard)/student/learning/courses/page.tsx b/src/app/(dashboard)/student/learning/courses/page.tsx index 85ad15d..b64195a 100644 --- a/src/app/(dashboard)/student/learning/courses/page.tsx +++ b/src/app/(dashboard)/student/learning/courses/page.tsx @@ -5,7 +5,7 @@ import { getTranslations } from "next-intl/server" import { getStudentClasses } from "@/modules/classes/data-access" import { getCurrentStudentUser } from "@/modules/users/data-access" import { StudentCoursesView } from "@/modules/student/components/student-courses-view" -import { CourseFilters } from "@/modules/student/components/course-filters" +import { CourseFilters } from "./course-filters" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import { EmptyState } from "@/shared/components/ui/empty-state" diff --git a/src/app/(dashboard)/student/practice/page.tsx b/src/app/(dashboard)/student/practice/page.tsx index d585bec..8c1d579 100644 --- a/src/app/(dashboard)/student/practice/page.tsx +++ b/src/app/(dashboard)/student/practice/page.tsx @@ -1,4 +1,5 @@ import type { JSX } from "react" +import { Target, CheckCircle2, TrendingUp, Award } from "lucide-react" import { getTranslations } from "next-intl/server" import { requirePermission } from "@/shared/lib/auth-guard" @@ -7,7 +8,7 @@ import { Permissions } from "@/shared/types/permissions" import { getPracticeSessions, getPracticeStats } from "@/modules/adaptive-practice/data-access" import { PracticeStarterWithNav } from "@/modules/adaptive-practice/components/practice-starter-with-nav" import { PracticeHistory } from "@/modules/adaptive-practice/components/practice-history" -import { PracticeStatsCards } from "@/modules/adaptive-practice/components/practice-stats-cards" +import { StatsGrid } from "@/shared/components/ui/stats-grid" import { PracticeServiceProvider } from "@/modules/adaptive-practice/services/practice-service" import { getKnowledgePointOptions } from "@/modules/questions/data-access" @@ -31,7 +32,39 @@ export default async function StudentPracticePage(): Promise {

{t("page.description")}

- +
diff --git a/src/app/(dashboard)/student/schedule/page.tsx b/src/app/(dashboard)/student/schedule/page.tsx index aea3471..3df850b 100644 --- a/src/app/(dashboard)/student/schedule/page.tsx +++ b/src/app/(dashboard)/student/schedule/page.tsx @@ -4,7 +4,7 @@ import { getTranslations } from "next-intl/server" import { getStudentClasses, getStudentSchedule } from "@/modules/classes/data-access" import { getCurrentStudentUser } from "@/modules/users/data-access" -import { StudentScheduleFilters } from "@/modules/student/components/student-schedule-filters" +import { StudentScheduleFilters } from "./student-schedule-filters" import { StudentScheduleView } from "@/modules/student/components/student-schedule-view" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" diff --git a/src/modules/student/components/student-schedule-filters.tsx b/src/app/(dashboard)/student/schedule/student-schedule-filters.tsx similarity index 58% rename from src/modules/student/components/student-schedule-filters.tsx rename to src/app/(dashboard)/student/schedule/student-schedule-filters.tsx index a387f00..80fcb21 100644 --- a/src/modules/student/components/student-schedule-filters.tsx +++ b/src/app/(dashboard)/student/schedule/student-schedule-filters.tsx @@ -4,17 +4,37 @@ import { useMemo } from "react" import { useQueryState, parseAsString } from "nuqs" import { useTranslations } from "next-intl" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/components/ui/select" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/components/ui/select" +import { FilterBar } from "@/shared/components/ui/filter-bar" import type { StudentEnrolledClass } from "@/modules/classes/types" -export function StudentScheduleFilters({ classes }: { classes: StudentEnrolledClass[] }) { +export function StudentScheduleFilters({ + classes, +}: { + classes: StudentEnrolledClass[] +}): React.ReactNode { const t = useTranslations("student") const [classId, setClassId] = useQueryState("classId", parseAsString.withDefault("all")) - const options = useMemo(() => [{ id: "all", name: t("scheduleFilters.allClasses") }, ...classes], [classes, t]) + const options = useMemo( + () => [{ id: "all", name: t("scheduleFilters.allClasses") }, ...classes], + [classes, t], + ) + + const hasFilters = classId !== "all" return ( -
+ setClassId(null)} + >
-
+ ) } diff --git a/src/app/(dashboard)/teacher/classes/my/[id]/page.tsx b/src/app/(dashboard)/teacher/classes/my/[id]/page.tsx index 07b4f2b..c355107 100644 --- a/src/app/(dashboard)/teacher/classes/my/[id]/page.tsx +++ b/src/app/(dashboard)/teacher/classes/my/[id]/page.tsx @@ -7,7 +7,7 @@ import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import { getClassHomeworkInsights, getClassSchedule, getClassStudentSubjectScoresV2, getClassStudents } from "@/modules/classes/data-access" import { ClassAssignmentsWidget } from "@/modules/classes/components/class-detail/class-assignments-widget" -import { ClassErrorBoundary } from "@/modules/classes/components/class-error-boundary" +import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" import { ClassTrendsWidget } from "@/modules/classes/components/class-detail/class-trends-widget" import { ClassHeader } from "@/modules/classes/components/class-detail/class-header" import { ClassOverviewStats } from "@/modules/classes/components/class-detail/class-overview-stats" @@ -77,7 +77,7 @@ export default async function ClassDetailPage({ return (
- +
- +
) } diff --git a/src/app/(dashboard)/teacher/classes/my/page.tsx b/src/app/(dashboard)/teacher/classes/my/page.tsx index e77b229..5120593 100644 --- a/src/app/(dashboard)/teacher/classes/my/page.tsx +++ b/src/app/(dashboard)/teacher/classes/my/page.tsx @@ -4,7 +4,7 @@ import { getTranslations } from "next-intl/server" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import { getClassSubjects, getTeacherClasses } from "@/modules/classes/data-access" -import { ClassErrorBoundary } from "@/modules/classes/components/class-error-boundary" +import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" import { MyClassesGrid } from "@/modules/classes/components/my-classes-grid" export const dynamic = "force-dynamic" @@ -23,9 +23,9 @@ export default async function MyClassesPage(): Promise { return (
- + - +
) } diff --git a/src/app/(dashboard)/teacher/classes/schedule/page.tsx b/src/app/(dashboard)/teacher/classes/schedule/page.tsx index b99f52b..3c209d0 100644 --- a/src/app/(dashboard)/teacher/classes/schedule/page.tsx +++ b/src/app/(dashboard)/teacher/classes/schedule/page.tsx @@ -7,9 +7,9 @@ import { getTranslations } from "next-intl/server" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import { getClassSchedule, getTeacherClasses } from "@/modules/classes/data-access" -import { ScheduleFilters } from "@/modules/classes/components/schedule-filters" +import { ScheduleFilters } from "./schedule-filters" import { ScheduleView } from "@/modules/classes/components/schedule-view" -import { ClassErrorBoundary } from "@/modules/classes/components/class-error-boundary" +import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" import { EmptyState } from "@/shared/components/ui/empty-state" import { Skeleton } from "@/shared/components/ui/skeleton" import { getParam, type SearchParams } from "@/shared/lib/search-params" @@ -79,7 +79,7 @@ export default async function SchedulePage({ searchParams }: { searchParams: Pro return (
- + }> @@ -87,7 +87,7 @@ export default async function SchedulePage({ searchParams }: { searchParams: Pro }> - +
) diff --git a/src/modules/classes/components/schedule-filters.tsx b/src/app/(dashboard)/teacher/classes/schedule/schedule-filters.tsx similarity index 85% rename from src/modules/classes/components/schedule-filters.tsx rename to src/app/(dashboard)/teacher/classes/schedule/schedule-filters.tsx index f78c908..82d8e2d 100644 --- a/src/modules/classes/components/schedule-filters.tsx +++ b/src/app/(dashboard)/teacher/classes/schedule/schedule-filters.tsx @@ -26,8 +26,9 @@ import { } from "@/shared/components/ui/dialog" import { Input } from "@/shared/components/ui/input" import { Label } from "@/shared/components/ui/label" -import type { TeacherClass } from "../types" -import { createClassScheduleItemAction } from "../actions" +import { FilterBar } from "@/shared/components/ui/filter-bar" +import type { TeacherClass } from "@/modules/classes/types" +import { createClassScheduleItemAction } from "@/modules/classes/actions" export function ScheduleFilters({ classes }: { classes: TeacherClass[] }) { const [classId, setClassId] = useQueryState("classId", parseAsString.withDefault("all").withOptions({ shallow: false })) @@ -73,24 +74,22 @@ export function ScheduleFilters({ classes }: { classes: TeacherClass[] }) { const title = selectedClass ? selectedClass.name : t("filters.allClasses") return ( -
-
- -
+ + -
+
{title}
@@ -102,8 +101,8 @@ export function ScheduleFilters({ classes }: { classes: TeacherClass[] }) { }} > -
+
) } diff --git a/src/app/(dashboard)/teacher/classes/students/page.tsx b/src/app/(dashboard)/teacher/classes/students/page.tsx index 98d724b..dad180b 100644 --- a/src/app/(dashboard)/teacher/classes/students/page.tsx +++ b/src/app/(dashboard)/teacher/classes/students/page.tsx @@ -7,9 +7,9 @@ import { getTranslations } from "next-intl/server" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import { getClassStudents, getTeacherClasses, getStudentsSubjectScores } from "@/modules/classes/data-access" -import { StudentsFilters } from "@/modules/classes/components/students-filters" +import { StudentsFilters } from "./students-filters" import { StudentsTable } from "@/modules/classes/components/students-table" -import { ClassErrorBoundary } from "@/modules/classes/components/class-error-boundary" +import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" import { EmptyState } from "@/shared/components/ui/empty-state" import { Skeleton } from "@/shared/components/ui/skeleton" import { getParam, type SearchParams } from "@/shared/lib/search-params" @@ -100,7 +100,7 @@ export default async function StudentsPage({ searchParams }: { searchParams: Pro return (
- + }> @@ -108,7 +108,7 @@ export default async function StudentsPage({ searchParams }: { searchParams: Pro }> - +
) diff --git a/src/modules/classes/components/students-filters.tsx b/src/app/(dashboard)/teacher/classes/students/students-filters.tsx similarity index 86% rename from src/modules/classes/components/students-filters.tsx rename to src/app/(dashboard)/teacher/classes/students/students-filters.tsx index 6a9c89c..2042797 100644 --- a/src/modules/classes/components/students-filters.tsx +++ b/src/app/(dashboard)/teacher/classes/students/students-filters.tsx @@ -4,7 +4,7 @@ import { useState } from "react" import { useRouter } from "next/navigation" import { useTranslations } from "next-intl" import { useQueryState, parseAsString } from "nuqs" -import { Search, UserPlus, ChevronDown, Check } from "lucide-react" +import { UserPlus, ChevronDown, Check } from "lucide-react" import { toast } from "sonner" import { Input } from "@/shared/components/ui/input" @@ -13,9 +13,9 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuTrigger, DropdownMenuSeparator, DropdownMenuLabel, + DropdownMenuTrigger, } from "@/shared/components/ui/dropdown-menu" import { Dialog, @@ -34,8 +34,9 @@ import { SelectValue, } from "@/shared/components/ui/select" import { Label } from "@/shared/components/ui/label" -import type { TeacherClass } from "../types" -import { enrollStudentByEmailAction } from "../actions" +import { FilterBar, FilterSearchInput } from "@/shared/components/ui/filter-bar" +import type { TeacherClass } from "@/modules/classes/types" +import { enrollStudentByEmailAction } from "@/modules/classes/actions" export function StudentsFilters({ classes, defaultClassId }: { classes: TeacherClass[], defaultClassId?: string }) { const [search, setSearch] = useQueryState("q", parseAsString.withDefault("").withOptions({ shallow: false, throttleMs: 500 })) @@ -83,22 +84,18 @@ export function StudentsFilters({ classes, defaultClassId }: { classes: TeacherC const statusLabel = status === "all" ? t("filters.allStatuses") : (status === "active" ? t("students.status.active") : t("students.status.inactive")) return ( -
+
- {/* Search - Minimal */} -
- - setSearch(e.target.value || null)} - /> -
+ setSearch(value || null)} + placeholder={t("students.searchPlaceholder")} + className="w-auto md:w-[180px]" + inputClassName="h-8 text-xs bg-transparent border-transparent hover:bg-muted/50 focus-visible:bg-background focus-visible:ring-1 focus-visible:ring-ring focus-visible:border-input transition-all" + /> -
+
- {/* Class Filter - Compact */}
+ ) } - diff --git a/src/app/(dashboard)/teacher/practice/page.tsx b/src/app/(dashboard)/teacher/practice/page.tsx index 0a726cb..75abc9d 100644 --- a/src/app/(dashboard)/teacher/practice/page.tsx +++ b/src/app/(dashboard)/teacher/practice/page.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next" import type { JSX } from "react" import { Suspense } from "react" -import { BarChart3 } from "lucide-react" +import { Activity, BarChart3, BookOpen, CheckCircle2, Target, Users } from "lucide-react" import { getTranslations } from "next-intl/server" import { requirePermission } from "@/shared/lib/auth-guard" @@ -23,7 +23,7 @@ import { getStudentsWithoutPractice, getStudentNameMap, } from "@/modules/adaptive-practice/data-access-analytics" -import { PracticeOverviewStatsCards } from "@/modules/adaptive-practice/components/practice-overview-stats-cards" +import { StatsGrid } from "@/shared/components/ui/stats-grid" import { ClassPracticeComparisonTable } from "@/modules/adaptive-practice/components/class-practice-comparison-table" import { PracticeTypeBreakdownChart } from "@/modules/adaptive-practice/components/practice-type-breakdown-chart" import { ClassKnowledgePointWeaknessChart } from "@/modules/adaptive-practice/components/class-knowledge-point-weakness-chart" @@ -183,12 +183,45 @@ async function TeacherPracticeContent({ ) : null} {/* 统计卡片 */} - {/* 班级对比表(仅在"全部班级"视图下显示) */} diff --git a/src/app/(dashboard)/teacher/questions/page.tsx b/src/app/(dashboard)/teacher/questions/page.tsx index 1a59d94..d60de95 100644 --- a/src/app/(dashboard)/teacher/questions/page.tsx +++ b/src/app/(dashboard)/teacher/questions/page.tsx @@ -3,7 +3,7 @@ import { Suspense } from "react" import { ClipboardList } from "lucide-react" import { getTranslations } from "next-intl/server" -import { QuestionFilters } from "@/modules/questions/components/question-filters" +import { QuestionFilters } from "../../_components/question-filters" import { CreateQuestionButton } from "@/modules/questions/components/create-question-button" import { QuestionBankResultsClient } from "@/modules/questions/components/question-bank-results-client" import { ImportExportButtons } from "@/modules/questions/components/import-export-buttons" diff --git a/src/modules/adaptive-practice/components/practice-overview-stats-cards.tsx b/src/modules/adaptive-practice/components/practice-overview-stats-cards.tsx deleted file mode 100644 index 58eb4fc..0000000 --- a/src/modules/adaptive-practice/components/practice-overview-stats-cards.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { Activity, BookOpen, CheckCircle2, Target, Users } from "lucide-react" -import { useTranslations } from "next-intl" - -import { Card, CardContent } from "@/shared/components/ui/card" -import { cn } from "@/shared/lib/utils" - -interface PracticeOverviewStatsCardsProps { - totalClasses: number - totalSessions: number - totalAnswered: number - averageAccuracy: number - participationRate: number - className?: string -} - -/** - * 专项练习概览统计卡片(教师视图) - * - * 5 个卡片:覆盖班级 / 练习总数 / 答题总数 / 平均正确率 / 参与率 - */ -export function PracticeOverviewStatsCards({ - totalClasses, - totalSessions, - totalAnswered, - averageAccuracy, - participationRate, - className, -}: PracticeOverviewStatsCardsProps) { - const t = useTranslations("practice") - - const cards = [ - { - label: t("teacher.overview.totalClasses"), - value: totalClasses, - sub: "", - icon: BookOpen, - color: "text-blue-600 dark:text-blue-400", - bg: "bg-blue-50 dark:bg-blue-950/30", - }, - { - label: t("teacher.overview.totalSessions"), - value: totalSessions, - sub: "", - icon: Activity, - color: "text-purple-600 dark:text-purple-400", - bg: "bg-purple-50 dark:bg-purple-950/30", - }, - { - label: t("teacher.overview.totalAnswered"), - value: totalAnswered, - sub: "", - icon: Target, - color: "text-amber-600 dark:text-amber-400", - bg: "bg-amber-50 dark:bg-amber-950/30", - }, - { - label: t("teacher.overview.averageAccuracy"), - value: `${Math.round(averageAccuracy * 100)}%`, - sub: averageAccuracy >= 0.6 ? "" : "", - icon: CheckCircle2, - color: "text-emerald-600 dark:text-emerald-400", - bg: "bg-emerald-50 dark:bg-emerald-950/30", - }, - { - label: t("teacher.overview.participationRate"), - value: `${Math.round(participationRate * 100)}%`, - sub: participationRate >= 0.5 ? "" : "", - icon: Users, - color: "text-rose-600 dark:text-rose-400", - bg: "bg-rose-50 dark:bg-rose-950/30", - }, - ] - - return ( -
- {cards.map((card) => { - const Icon = card.icon - return ( - - -
-
-
{card.label}
-
- {card.value} -
- {card.sub ?
{card.sub}
: null} -
-
- -
-
-
-
- ) - })} -
- ) -} diff --git a/src/modules/adaptive-practice/components/practice-stats-cards.tsx b/src/modules/adaptive-practice/components/practice-stats-cards.tsx deleted file mode 100644 index 9598475..0000000 --- a/src/modules/adaptive-practice/components/practice-stats-cards.tsx +++ /dev/null @@ -1,73 +0,0 @@ -"use client" - -import { useTranslations } from "next-intl" -import { Target, CheckCircle2, TrendingUp, Award } from "lucide-react" - -import { Card, CardContent } from "@/shared/components/ui/card" - -import type { PracticeStats } from "../types" - -interface PracticeStatsCardsProps { - stats: PracticeStats -} - -/** - * 专项练习统计卡片 - */ -export function PracticeStatsCards({ stats }: PracticeStatsCardsProps): React.ReactNode { - const t = useTranslations("practice") - - const accuracyPercent = Math.round(stats.overallAccuracy * 100) - - return ( -
- - -
-
-

{t("stats.totalSessions")}

-

{stats.totalSessions}

-
- -
-
-
- - - -
-
-

{t("stats.completed")}

-

{stats.completedSessions}

-
- -
-
-
- - - -
-
-

{t("stats.totalAnswered")}

-

{stats.totalQuestionsAnswered}

-
- -
-
-
- - - -
-
-

{t("stats.accuracy")}

-

{accuracyPercent}%

-
- -
-
-
-
- ) -} diff --git a/src/modules/announcements/components/announcement-list-skeleton.tsx b/src/modules/announcements/components/announcement-list-skeleton.tsx deleted file mode 100644 index a768c3a..0000000 --- a/src/modules/announcements/components/announcement-list-skeleton.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Card, CardContent, CardHeader } from "@/shared/components/ui/card" -import { Skeleton } from "@/shared/components/ui/skeleton" - -/** - * 公告列表骨架屏(P2-5 抽取)。 - * - * 此前用户端 `/announcements/loading.tsx` 与管理端 `/admin/announcements/loading.tsx` - * 几乎逐行重复,违反"重复超过 90% 必须抽取共享组件"规则。 - */ -export function AnnouncementListSkeleton({ - showCreateButton = false, -}: { - showCreateButton?: boolean -}) { - return ( -
-
-
- - -
- {showCreateButton ? : null} -
- -
- -
- -
- {Array.from({ length: 6 }).map((_, i) => ( - - - - - - - - - -
- - -
-
-
- ))} -
-
- ) -} diff --git a/src/modules/audit/components/audit-error-boundary.tsx b/src/modules/audit/components/audit-error-boundary.tsx deleted file mode 100644 index 5dd6fdd..0000000 --- a/src/modules/audit/components/audit-error-boundary.tsx +++ /dev/null @@ -1,23 +0,0 @@ -"use client" - -/** - * 审计模块独立数据区块错误边界。 - * - * 薄包装:委托给共享 SectionErrorBoundary,使用 common 命名空间。 - * 保留同名导出以兼容现有 import。 - */ - -import type { ReactNode } from "react" -import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" - -interface AuditErrorBoundaryProps { - children: ReactNode -} - -export function AuditErrorBoundary({ children }: AuditErrorBoundaryProps): ReactNode { - return ( - - {children} - - ) -} diff --git a/src/modules/audit/components/audit-log-filters.tsx b/src/modules/audit/components/audit-log-filters.tsx deleted file mode 100644 index 40766b5..0000000 --- a/src/modules/audit/components/audit-log-filters.tsx +++ /dev/null @@ -1,107 +0,0 @@ -"use client" - -import { useTranslations } from "next-intl" -import { useQueryState, parseAsString } from "nuqs" - -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/shared/components/ui/select" -import { Input } from "@/shared/components/ui/input" -import { FilterBar } from "@/shared/components/ui/filter-bar" - -interface AuditLogFiltersProps { - moduleOptions: string[] -} - -export function AuditLogFilters({ moduleOptions }: AuditLogFiltersProps) { - const t = useTranslations("audit") - const [module, setModule] = useQueryState("module", parseAsString.withOptions({ shallow: false })) - const [action, setAction] = useQueryState("action", parseAsString.withOptions({ shallow: false })) - const [status, setStatus] = useQueryState("status", parseAsString.withOptions({ shallow: false })) - const [userId, setUserId] = useQueryState("userId", parseAsString.withOptions({ shallow: false })) - const [startDate, setStartDate] = useQueryState( - "startDate", - parseAsString.withOptions({ shallow: false }), - ) - const [endDate, setEndDate] = useQueryState( - "endDate", - parseAsString.withOptions({ shallow: false }), - ) - - const hasFilters = Boolean(module || action || status || userId || startDate || endDate) - - return ( - { - setModule(null) - setAction(null) - setStatus(null) - setUserId(null) - setStartDate(null) - setEndDate(null) - }} - > - - - setAction(e.target.value || null)} - /> - - setUserId(e.target.value || null)} - /> - - - - setStartDate(e.target.value || null)} - /> - setEndDate(e.target.value || null)} - /> - - ) -} diff --git a/src/modules/audit/components/audit-log-table-skeleton.tsx b/src/modules/audit/components/audit-log-table-skeleton.tsx deleted file mode 100644 index bbcac42..0000000 --- a/src/modules/audit/components/audit-log-table-skeleton.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Skeleton } from "@/shared/components/ui/skeleton" - -/** - * 审计日志表格骨架屏。 - * 用于 Suspense fallback 和 loading.tsx。 - * aria-hidden 避免屏幕阅读器朗读占位元素。 - */ -export function AuditLogTableSkeleton() { - return ( - - ) -} diff --git a/src/modules/audit/components/audit-log-view.tsx b/src/modules/audit/components/audit-log-view.tsx index 24f947b..af2f4b2 100644 --- a/src/modules/audit/components/audit-log-view.tsx +++ b/src/modules/audit/components/audit-log-view.tsx @@ -1,10 +1,20 @@ "use client" import { Suspense } from "react" +import { useTranslations } from "next-intl" +import { useQueryState, parseAsString } from "nuqs" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/components/ui/select" +import { Input } from "@/shared/components/ui/input" +import { FilterBar } from "@/shared/components/ui/filter-bar" +import { SkeletonCard } from "@/shared/components/ui/skeleton" import { AuditLogTable } from "./audit-log-table" -import { AuditLogFilters } from "./audit-log-filters" -import { AuditLogTableSkeleton } from "./audit-log-table-skeleton" import { useLogPagination } from "../hooks/use-log-pagination" import type { AuditLog } from "../types" @@ -25,11 +35,94 @@ function AuditLogViewInner({ totalPages, moduleOptions, }: AuditLogViewProps) { + const t = useTranslations("audit") + const [module, setModule] = useQueryState("module", parseAsString.withOptions({ shallow: false })) + const [action, setAction] = useQueryState("action", parseAsString.withOptions({ shallow: false })) + const [status, setStatus] = useQueryState("status", parseAsString.withOptions({ shallow: false })) + const [userId, setUserId] = useQueryState("userId", parseAsString.withOptions({ shallow: false })) + const [startDate, setStartDate] = useQueryState( + "startDate", + parseAsString.withOptions({ shallow: false }), + ) + const [endDate, setEndDate] = useQueryState( + "endDate", + parseAsString.withOptions({ shallow: false }), + ) + + const hasFilters = Boolean(module || action || status || userId || startDate || endDate) const handlePageChange = useLogPagination() return (
- + { + setModule(null) + setAction(null) + setStatus(null) + setUserId(null) + setStartDate(null) + setEndDate(null) + }} + > + + + setAction(e.target.value || null)} + /> + + setUserId(e.target.value || null)} + /> + + + + setStartDate(e.target.value || null)} + /> + setEndDate(e.target.value || null)} + /> + + }> + }> ) diff --git a/src/modules/audit/components/audit-overview-view.tsx b/src/modules/audit/components/audit-overview-view.tsx index f7853dd..6688c2f 100644 --- a/src/modules/audit/components/audit-overview-view.tsx +++ b/src/modules/audit/components/audit-overview-view.tsx @@ -4,7 +4,7 @@ import { getTranslations } from "next-intl/server" import { ArrowRight, FileText, LogIn, Database } from "lucide-react" import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" -import { AuditErrorBoundary } from "@/modules/audit/components/audit-error-boundary" +import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" import { AuditOverviewStatsBar } from "@/modules/audit/components/audit-overview-stats-bar" import { AuditActivityTrendChart } from "@/modules/audit/components/audit-activity-trend-chart" import { DataChangeDistributionChart } from "@/modules/audit/components/data-change-distribution-chart" @@ -37,22 +37,22 @@ export async function AuditOverviewView({
{/* 统计卡片栏 */} - + - + {/* 趋势图 + 分布图 */}
- + - - + + - +
{/* 快捷入口 */} - + {t("quickLinks.title")} @@ -77,12 +77,12 @@ export async function AuditOverviewView({
- + {/* 数据保留策略配置 */} - + - +
) } diff --git a/src/modules/audit/components/data-change-log-filters.tsx b/src/modules/audit/components/data-change-log-filters.tsx deleted file mode 100644 index e95688c..0000000 --- a/src/modules/audit/components/data-change-log-filters.tsx +++ /dev/null @@ -1,149 +0,0 @@ -"use client" - -import { useTranslations } from "next-intl" -import { useQueryState, parseAsString } from "nuqs" -import { X } from "lucide-react" - -import { Badge } from "@/shared/components/ui/badge" -import { Button } from "@/shared/components/ui/button" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/shared/components/ui/select" -import { Input } from "@/shared/components/ui/input" -import { FilterBar } from "@/shared/components/ui/filter-bar" -import type { DataChangeStat } from "../types" - -interface DataChangeLogFiltersProps { - tableOptions: string[] - stats: DataChangeStat[] -} - -export function DataChangeLogFilters({ - tableOptions, - stats, -}: DataChangeLogFiltersProps) { - const t = useTranslations("audit") - const [tableName, setTableName] = useQueryState( - "tableName", - parseAsString.withOptions({ shallow: false }), - ) - const [action, setAction] = useQueryState( - "action", - parseAsString.withOptions({ shallow: false }), - ) - const [userId, setUserId] = useQueryState("userId", parseAsString.withOptions({ shallow: false })) - const [startDate, setStartDate] = useQueryState( - "startDate", - parseAsString.withOptions({ shallow: false }), - ) - const [endDate, setEndDate] = useQueryState( - "endDate", - parseAsString.withOptions({ shallow: false }), - ) - - const hasFilters = Boolean(tableName || action || userId || startDate || endDate) - - return ( -
- {stats.length > 0 && ( -
- {stats.slice(0, 8).map((s) => ( - - {s.tableName} - · - {s.count} - - ))} -
- )} - { - setTableName(null) - setAction(null) - setUserId(null) - setStartDate(null) - setEndDate(null) - }} - > - - - - - setUserId(e.target.value || null)} - /> - - setStartDate(e.target.value || null)} - /> - setEndDate(e.target.value || null)} - /> - - {hasFilters && ( - - )} - -
- ) -} diff --git a/src/modules/audit/components/data-change-log-view.tsx b/src/modules/audit/components/data-change-log-view.tsx index abf966b..36aa8d6 100644 --- a/src/modules/audit/components/data-change-log-view.tsx +++ b/src/modules/audit/components/data-change-log-view.tsx @@ -1,10 +1,21 @@ "use client" import { Suspense } from "react" +import { useTranslations } from "next-intl" +import { useQueryState, parseAsString } from "nuqs" +import { Badge } from "@/shared/components/ui/badge" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/components/ui/select" +import { Input } from "@/shared/components/ui/input" +import { FilterBar } from "@/shared/components/ui/filter-bar" +import { SkeletonCard } from "@/shared/components/ui/skeleton" import { DataChangeLogTable } from "./data-change-log-table" -import { DataChangeLogFilters } from "./data-change-log-filters" -import { AuditLogTableSkeleton } from "./audit-log-table-skeleton" import { useLogPagination } from "../hooks/use-log-pagination" import type { DataChangeLog, DataChangeStat } from "../types" @@ -27,11 +38,109 @@ function DataChangeLogViewInner({ tableOptions, stats, }: DataChangeLogViewProps) { + const t = useTranslations("audit") + const [tableName, setTableName] = useQueryState( + "tableName", + parseAsString.withOptions({ shallow: false }), + ) + const [action, setAction] = useQueryState( + "action", + parseAsString.withOptions({ shallow: false }), + ) + const [userId, setUserId] = useQueryState("userId", parseAsString.withOptions({ shallow: false })) + const [startDate, setStartDate] = useQueryState( + "startDate", + parseAsString.withOptions({ shallow: false }), + ) + const [endDate, setEndDate] = useQueryState( + "endDate", + parseAsString.withOptions({ shallow: false }), + ) + + const hasFilters = Boolean(tableName || action || userId || startDate || endDate) const handlePageChange = useLogPagination() return (
- + {stats.length > 0 && ( +
+ {stats.slice(0, 8).map((s) => ( + + {s.tableName} + · + {s.count} + + ))} +
+ )} + + { + setTableName(null) + setAction(null) + setUserId(null) + setStartDate(null) + setEndDate(null) + }} + > + + + + + setUserId(e.target.value || null)} + /> + + setStartDate(e.target.value || null)} + /> + setEndDate(e.target.value || null)} + /> + + }> + }> ) diff --git a/src/modules/audit/components/login-log-filters.tsx b/src/modules/audit/components/login-log-filters.tsx deleted file mode 100644 index 781e69c..0000000 --- a/src/modules/audit/components/login-log-filters.tsx +++ /dev/null @@ -1,91 +0,0 @@ -"use client" - -import { useTranslations } from "next-intl" -import { useQueryState, parseAsString } from "nuqs" - -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/shared/components/ui/select" -import { Input } from "@/shared/components/ui/input" -import { FilterBar } from "@/shared/components/ui/filter-bar" - -export function LoginLogFilters() { - const t = useTranslations("audit") - const [action, setAction] = useQueryState("action", parseAsString.withOptions({ shallow: false })) - const [status, setStatus] = useQueryState("status", parseAsString.withOptions({ shallow: false })) - const [userId, setUserId] = useQueryState("userId", parseAsString.withOptions({ shallow: false })) - const [startDate, setStartDate] = useQueryState( - "startDate", - parseAsString.withOptions({ shallow: false }), - ) - const [endDate, setEndDate] = useQueryState( - "endDate", - parseAsString.withOptions({ shallow: false }), - ) - - const hasFilters = Boolean(action || status || userId || startDate || endDate) - - return ( - { - setAction(null) - setStatus(null) - setUserId(null) - setStartDate(null) - setEndDate(null) - }} - > - - - setUserId(e.target.value || null)} - /> - - - - setStartDate(e.target.value || null)} - /> - setEndDate(e.target.value || null)} - /> - - ) -} diff --git a/src/modules/audit/components/login-log-view.tsx b/src/modules/audit/components/login-log-view.tsx index df6543e..f404af5 100644 --- a/src/modules/audit/components/login-log-view.tsx +++ b/src/modules/audit/components/login-log-view.tsx @@ -1,10 +1,20 @@ "use client" import { Suspense } from "react" +import { useTranslations } from "next-intl" +import { useQueryState, parseAsString } from "nuqs" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/components/ui/select" +import { Input } from "@/shared/components/ui/input" +import { FilterBar } from "@/shared/components/ui/filter-bar" +import { SkeletonCard } from "@/shared/components/ui/skeleton" import { LoginLogTable } from "./login-log-table" -import { LoginLogFilters } from "./login-log-filters" -import { AuditLogTableSkeleton } from "./audit-log-table-skeleton" import { useLogPagination } from "../hooks/use-log-pagination" import type { LoginLog } from "../types" @@ -23,11 +33,82 @@ function LoginLogViewInner({ total, totalPages, }: LoginLogViewProps) { + const t = useTranslations("audit") + const [action, setAction] = useQueryState("action", parseAsString.withOptions({ shallow: false })) + const [status, setStatus] = useQueryState("status", parseAsString.withOptions({ shallow: false })) + const [userId, setUserId] = useQueryState("userId", parseAsString.withOptions({ shallow: false })) + const [startDate, setStartDate] = useQueryState( + "startDate", + parseAsString.withOptions({ shallow: false }), + ) + const [endDate, setEndDate] = useQueryState( + "endDate", + parseAsString.withOptions({ shallow: false }), + ) + + const hasFilters = Boolean(action || status || userId || startDate || endDate) const handlePageChange = useLogPagination() return (
- + { + setAction(null) + setStatus(null) + setUserId(null) + setStartDate(null) + setEndDate(null) + }} + > + + + setUserId(e.target.value || null)} + /> + + + + setStartDate(e.target.value || null)} + /> + setEndDate(e.target.value || null)} + /> + + }> + }> ) diff --git a/src/modules/classes/components/class-error-boundary.tsx b/src/modules/classes/components/class-error-boundary.tsx deleted file mode 100644 index a5d2190..0000000 --- a/src/modules/classes/components/class-error-boundary.tsx +++ /dev/null @@ -1,57 +0,0 @@ -"use client" - -/** - * 班级模块 Error Boundary。 - * - * 薄包装:委托给共享 SectionErrorBoundary,通过自定义 fallback 实现 - * 重试时调用 router.refresh() 刷新服务端数据。 - * 保留同名导出以兼容现有 import。 - */ - -import type { ReactNode } from "react" -import { AlertCircle } from "lucide-react" -import { useRouter } from "next/navigation" -import { useTranslations } from "next-intl" -import { Button } from "@/shared/components/ui/button" -import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" - -interface ClassErrorBoundaryProps { - children: ReactNode - fallback?: ReactNode -} - -export function ClassErrorBoundary({ - children, - fallback, -}: ClassErrorBoundaryProps): ReactNode { - const t = useTranslations("classes") - const router = useRouter() - - const customFallback = (_error: Error, reset: () => void): ReactNode => { - const handleRetry = (): void => { - reset() - router.refresh() - } - return ( -
-
-
-

{t("errors.boundary.title")}

-

- {t("errors.boundary.description")} -

- -
- ) - } - - return ( - fallback : customFallback}> - {children} - - ) -} diff --git a/src/modules/classes/components/class-skeleton.tsx b/src/modules/classes/components/class-skeleton.tsx deleted file mode 100644 index f7de11f..0000000 --- a/src/modules/classes/components/class-skeleton.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import type { JSX } from "react" - -import { Card, CardContent, CardHeader } from "@/shared/components/ui/card" -import { Skeleton } from "@/shared/components/ui/skeleton" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/shared/components/ui/table" - -interface ClassListSkeletonProps { - rows?: number -} - -export function ClassListSkeleton({ rows = 5 }: ClassListSkeletonProps): JSX.Element { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - {Array.from({ length: rows }).map((_, index) => ( - - - - - - - - - - - - - - - - ))} - -
-
-
- ) -} - -export function ClassCardSkeleton(): JSX.Element { - return ( - - - - - - - - - - - ) -} - -export function ClassGridSkeleton({ count = 6 }: { count?: number }): JSX.Element { - return ( -
- {Array.from({ length: count }).map((_, index) => ( - - ))} -
- ) -} - -export function StudentListSkeleton({ rows = 8 }: { rows?: number }): JSX.Element { - return ( -
-
- -
-
- {Array.from({ length: rows }).map((_, index) => ( - - ))} -
-
- ) -} - -export function ScheduleGridSkeleton({ count = 6 }: { count?: number }): JSX.Element { - return ( -
- {Array.from({ length: count }).map((_, idx) => ( -
-
- - -
-
- - - -
-
- ))} -
- ) -} diff --git a/src/modules/messaging/components/message-list-section.tsx b/src/modules/messaging/components/message-list-section.tsx index 7d4c720..cc2ddeb 100644 --- a/src/modules/messaging/components/message-list-section.tsx +++ b/src/modules/messaging/components/message-list-section.tsx @@ -8,7 +8,7 @@ import { MessageList } from "@/modules/messaging/components/message-list" * * 独立获取数据,配合 Suspense + ErrorBoundary 实现流式渲染: * - 页面立即渲染,此区块异步加载 - * - 加载中显示 MessageListSkeleton + * - 加载中显示 SkeletonCard * - 加载失败由 SectionErrorBoundary 捕获,不影响其他区块 */ export async function MessageListSection({ diff --git a/src/modules/messaging/components/message-list-skeleton.tsx b/src/modules/messaging/components/message-list-skeleton.tsx deleted file mode 100644 index 9ead352..0000000 --- a/src/modules/messaging/components/message-list-skeleton.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { JSX } from "react" - -import { Card, CardContent, CardHeader } from "@/shared/components/ui/card" -import { Skeleton } from "@/shared/components/ui/skeleton" - -/** - * P2-9: 消息列表骨架屏。 - * - * 作为 Suspense fallback,在 MessageListSection 流式加载期间展示。 - */ -export function MessageListSkeleton(): JSX.Element { - return ( -
-
- - -
- - {Array.from({ length: 5 }).map((_, i) => ( - - ))} -
- ) -} diff --git a/src/modules/school/components/grade-insights-filters.tsx b/src/modules/school/components/grade-insights-filters.tsx deleted file mode 100644 index fec3acf..0000000 --- a/src/modules/school/components/grade-insights-filters.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import type { JSX } from "react" -import { getTranslations } from "next-intl/server" - -import { ChipNav } from "@/shared/components/ui/chip-nav" -import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" -import { Badge } from "@/shared/components/ui/badge" - -interface GradeInsightsFiltersProps { - grades: Array<{ id: string; name: string; schoolName: string }> - currentGradeId: string - buildHref: (gradeId: string) => string -} - -/** - * 年级洞察筛选器:使用 ChipNav 替代原生 form get, - * 点击 chip 即时通过 URL 参数切换,无整页刷新。 - */ -export async function GradeInsightsFilters({ - grades, - currentGradeId, - buildHref, -}: GradeInsightsFiltersProps): Promise { - const t = await getTranslations("school") - - const options = grades.map((g) => ({ - id: g.id, - name: `${g.schoolName} / ${g.name}`, - })) - - return ( - - - {t("grades.gradeInsights.title")} - - {grades.length} - - - - - - - ) -} diff --git a/src/modules/school/components/school-error-boundary.tsx b/src/modules/school/components/school-error-boundary.tsx deleted file mode 100644 index 38ad0eb..0000000 --- a/src/modules/school/components/school-error-boundary.tsx +++ /dev/null @@ -1,57 +0,0 @@ -"use client" - -/** - * 学校模块 Error Boundary。 - * - * 薄包装:委托给共享 SectionErrorBoundary,通过自定义 fallback 实现 - * 重试时调用 router.refresh() 刷新服务端数据。 - * 保留同名导出以兼容现有 import。 - */ - -import type { ReactNode } from "react" -import { AlertCircle } from "lucide-react" -import { useRouter } from "next/navigation" -import { useTranslations } from "next-intl" -import { Button } from "@/shared/components/ui/button" -import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" - -interface SchoolErrorBoundaryProps { - children: ReactNode - fallback?: ReactNode -} - -export function SchoolErrorBoundary({ - children, - fallback, -}: SchoolErrorBoundaryProps): ReactNode { - const t = useTranslations("school") - const router = useRouter() - - const customFallback = (_error: Error, reset: () => void): ReactNode => { - const handleRetry = (): void => { - reset() - router.refresh() - } - return ( -
-
-
-

{t("errors.boundary.title")}

-

- {t("errors.boundary.description")} -

- -
- ) - } - - return ( - fallback : customFallback}> - {children} - - ) -} diff --git a/src/modules/school/components/school-skeleton.tsx b/src/modules/school/components/school-skeleton.tsx deleted file mode 100644 index ff70356..0000000 --- a/src/modules/school/components/school-skeleton.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import type { JSX } from "react" - -import { Card, CardContent, CardHeader } from "@/shared/components/ui/card" -import { Skeleton } from "@/shared/components/ui/skeleton" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/shared/components/ui/table" - -interface SchoolListSkeletonProps { - rows?: number -} - -export function SchoolListSkeleton({ rows = 5 }: SchoolListSkeletonProps): JSX.Element { - return ( - - - - - - - - - - - - - - - - - - - - - - - {Array.from({ length: rows }).map((_, index) => ( - - - - - - - - - - - - - ))} - -
-
-
- ) -} - -export function SchoolCardSkeleton(): JSX.Element { - return ( - - - - - - - - - - - ) -} diff --git a/src/modules/settings/components/settings-section-error-boundary.tsx b/src/modules/settings/components/settings-section-error-boundary.tsx deleted file mode 100644 index 67690dc..0000000 --- a/src/modules/settings/components/settings-section-error-boundary.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client" - -/** - * 设置页分区 Error Boundary - * - * 薄包装:委托给共享 SectionErrorBoundary,使用 common 命名空间。 - * 保留同名导出以兼容现有 import。 - */ - -import type { ReactNode } from "react" -import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" - -interface SettingsSectionErrorBoundaryProps { - children: ReactNode -} - -export function SettingsSectionErrorBoundary({ - children, -}: SettingsSectionErrorBoundaryProps): ReactNode { - return ( - - {children} - - ) -} diff --git a/src/modules/settings/components/settings-view.tsx b/src/modules/settings/components/settings-view.tsx index 79d73ea..92fceb7 100644 --- a/src/modules/settings/components/settings-view.tsx +++ b/src/modules/settings/components/settings-view.tsx @@ -12,7 +12,7 @@ import { ProfileSettingsForm } from "@/modules/settings/components/profile-setti import { PasswordChangeForm } from "@/modules/settings/components/password-change-form" import { NotificationPreferencesForm } from "@/modules/settings/components/notification-preferences-form" import { SecurityCenterCard } from "@/modules/settings/components/security-center-card" -import { SettingsSectionErrorBoundary } from "@/modules/settings/components/settings-section-error-boundary" +import { SectionErrorBoundary } from "@/shared/components/section-error-boundary" import { Button } from "@/shared/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/shared/components/ui/card" import { Skeleton } from "@/shared/components/ui/skeleton" @@ -148,32 +148,32 @@ function SettingsViewInner({ - + }> {generalExtra} - + - + }> - + - + }> - + - + }> @@ -209,7 +209,7 @@ function SettingsViewInner({ - +
diff --git a/src/shared/i18n/messages/en/classes.json b/src/shared/i18n/messages/en/classes.json index 322adad..c56d506 100644 --- a/src/shared/i18n/messages/en/classes.json +++ b/src/shared/i18n/messages/en/classes.json @@ -405,7 +405,9 @@ "error": { "title": "Loading failed", "description": "An error occurred while loading data. Please retry.", - "retry": "Retry" + "retry": "Retry", + "boundaryTitle": "Class data failed to load", + "boundaryDescription": "An error occurred while loading this class data section. Please retry." }, "actions": { "classCreated": "Class created successfully", diff --git a/src/shared/i18n/messages/en/school.json b/src/shared/i18n/messages/en/school.json index b6f2f32..d12c5e4 100644 --- a/src/shared/i18n/messages/en/school.json +++ b/src/shared/i18n/messages/en/school.json @@ -403,6 +403,11 @@ "retry": "Retry" } }, + "error": { + "boundaryTitle": "Loading Failed", + "boundaryDescription": "An error occurred while loading data. Please retry.", + "retry": "Retry" + }, "orgTree": { "title": "Organization", "search": "Search school/grade/class...", diff --git a/src/shared/i18n/messages/en/settings.json b/src/shared/i18n/messages/en/settings.json index 65062cc..f324eee 100644 --- a/src/shared/i18n/messages/en/settings.json +++ b/src/shared/i18n/messages/en/settings.json @@ -315,6 +315,11 @@ "sectionLoadFailed": "This section failed to load", "sectionLoadFailedDesc": "Please try again later." }, + "error": { + "boundaryTitle": "Section Error", + "boundaryDescription": "An error occurred while processing the request. Please retry.", + "retry": "Retry" + }, "brand": { "title": "Brand Configuration", "description": "School brand information displayed on login/register pages.", diff --git a/src/shared/i18n/messages/zh-CN/classes.json b/src/shared/i18n/messages/zh-CN/classes.json index e1d24cc..68acc89 100644 --- a/src/shared/i18n/messages/zh-CN/classes.json +++ b/src/shared/i18n/messages/zh-CN/classes.json @@ -409,7 +409,9 @@ "error": { "title": "加载失败", "description": "数据加载时发生错误,请重试", - "retry": "重试" + "retry": "重试", + "boundaryTitle": "班级数据加载失败", + "boundaryDescription": "班级数据区块加载时发生错误,请重试。" }, "actions": { "classCreated": "班级创建成功", diff --git a/src/shared/i18n/messages/zh-CN/school.json b/src/shared/i18n/messages/zh-CN/school.json index 77f7b34..246a67a 100644 --- a/src/shared/i18n/messages/zh-CN/school.json +++ b/src/shared/i18n/messages/zh-CN/school.json @@ -403,6 +403,11 @@ "retry": "重试" } }, + "error": { + "boundaryTitle": "加载失败", + "boundaryDescription": "数据加载时发生错误,请重试", + "retry": "重试" + }, "orgTree": { "title": "组织架构", "search": "搜索学校/年级/班级...", diff --git a/src/shared/i18n/messages/zh-CN/settings.json b/src/shared/i18n/messages/zh-CN/settings.json index 0f10400..8a56fe7 100644 --- a/src/shared/i18n/messages/zh-CN/settings.json +++ b/src/shared/i18n/messages/zh-CN/settings.json @@ -315,6 +315,11 @@ "sectionLoadFailed": "该区块加载失败", "sectionLoadFailedDesc": "请稍后重试。" }, + "error": { + "boundaryTitle": "区块加载失败", + "boundaryDescription": "处理请求时发生错误,请重试。", + "retry": "重试" + }, "brand": { "title": "品牌配置", "description": "学校品牌信息,将显示在登录/注册页面。",