)
}
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 (
)
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 */}
Filter by Class
- setClassId("all")}
+ setClassId("all")}
className="text-xs flex items-center justify-between"
>
{t("filters.allClasses")}
@@ -117,9 +114,9 @@ export function StudentsFilters({ classes, defaultClassId }: { classes: TeacherC
{classes.map((c) => (
- setClassId(c.id)}
+ setClassId(c.id)}
className="text-xs flex items-center justify-between"
>
{c.name}
@@ -129,7 +126,6 @@ export function StudentsFilters({ classes, defaultClassId }: { classes: TeacherC
- {/* Status 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 (
-
-
-
-
-
-
-
-
-
-
-
- {Array.from({ length: 7 }).map((_, i) => (
-
- ))}
-
- {Array.from({ length: 8 }).map((_, row) => (
-
- {Array.from({ length: 7 }).map((_, col) => (
-
- ))}
-
- ))}
-
-
-
-
- )
-}
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({