Files
NextEdu/src/app/(dashboard)/student/error-book/page.tsx
SpecialX 37d2688a28 feat(app): add lesson-plans, practice, and grade dashboard routes
- Add admin/lesson-plans, parent/lesson-plans, student/lesson-plans routes

- Add student/practice and teacher/practice routes for adaptive practice

- Add management/grade/dashboard and management/grade/practice routes

- Add teacher/lesson-plans error and loading boundaries

- Update existing admin, parent, student, teacher pages with new features

- Update globals.css and proxy middleware
2026-06-24 12:03:47 +08:00

130 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { JSX } from "react"
import { Suspense } from "react"
import { getTranslations } from "next-intl/server"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { getParam, type SearchParams } from "@/shared/lib/search-params"
import { Skeleton } from "@/shared/components/ui/skeleton"
import { getErrorBookItems, getErrorBookStats } from "@/modules/error-book/data-access"
import { ErrorBookStatsCards } from "@/modules/error-book/components/error-book-stats-cards"
import { ErrorBookFilters } from "@/modules/error-book/components/error-book-filters"
import { ErrorBookList } from "@/modules/error-book/components/error-book-list"
import { AddErrorBookDialog } from "@/modules/error-book/components/add-error-book-dialog"
import type { ErrorBookStatusValue, ErrorBookSourceTypeValue } from "@/modules/error-book/types"
import {
AiClientProvider,
type AiClientService,
} from "@/modules/ai/context/ai-client-provider"
import {
aiChatAction,
suggestSimilarQuestionsAction,
suggestGradingAction,
generateLessonContentAction,
generateQuestionVariantAction,
analyzeWeaknessAction,
} from "@/modules/ai/actions"
export const dynamic = "force-dynamic"
const VALID_STATUS = new Set(["new", "learning", "mastered", "archived"])
const VALID_SOURCE = new Set(["exam", "homework", "manual"])
function parseStatus(v?: string): ErrorBookStatusValue | undefined {
return v && VALID_STATUS.has(v) ? (v as ErrorBookStatusValue) : undefined
}
function parseSource(v?: string): ErrorBookSourceTypeValue | undefined {
return v && VALID_SOURCE.has(v) ? (v as ErrorBookSourceTypeValue) : undefined
}
/**
* 构建 AI 客户端服务Server Action 引用集合)
*
* 通过 React Context 注入,客户端组件不直接 import actions
* 遵循依赖注入模式,便于测试时替换为 mock。
*/
function createAiClientService(): AiClientService {
return {
chat: aiChatAction,
suggestSimilarQuestions: suggestSimilarQuestionsAction,
suggestGrading: suggestGradingAction,
generateLessonContent: generateLessonContentAction,
generateQuestionVariant: generateQuestionVariantAction,
analyzeWeakness: analyzeWeaknessAction,
}
}
async function ErrorBookResults({ searchParams }: { searchParams: Promise<SearchParams> }): Promise<JSX.Element> {
const params = await searchParams
const ctx = await requirePermission(Permissions.ERROR_BOOK_READ)
const q = getParam(params, "q")
const status = parseStatus(getParam(params, "status"))
const sourceType = parseSource(getParam(params, "source"))
const dueOnly = getParam(params, "due") === "due"
const { data: items } = await getErrorBookItems({
studentId: ctx.userId,
q: q || undefined,
status,
sourceType,
dueOnly,
pageSize: 50,
})
return <ErrorBookList items={items} studentId={ctx.userId} errorItems={items} />
}
function ErrorBookResultsFallback() {
return (
<div className="grid gap-3 md:grid-cols-2">
{Array.from({ length: 4 }).map((_, idx) => (
<Skeleton key={idx} className="h-[180px] w-full rounded-md" />
))}
</div>
)
}
export default async function StudentErrorBookPage({
searchParams,
}: {
searchParams: Promise<SearchParams>
}): Promise<JSX.Element> {
const ctx = await requirePermission(Permissions.ERROR_BOOK_READ)
const t = await getTranslations("student")
const stats = await getErrorBookStats(ctx.userId)
const aiClientService = createAiClientService()
return (
<AiClientProvider service={aiClientService}>
<div className="flex h-full flex-col space-y-8 p-8">
<div className="flex flex-col justify-between space-y-4 md:flex-row md:items-center md:space-y-0">
<div>
<h1 className="text-2xl font-bold tracking-tight">{t("errorBook.title")}</h1>
<p className="text-muted-foreground">
{t("errorBook.description")}
</p>
</div>
<div className="flex items-center gap-2">
<AddErrorBookDialog />
</div>
</div>
<ErrorBookStatsCards stats={stats} />
<div className="space-y-4">
<Suspense fallback={<div className="h-10 w-full animate-pulse rounded-md bg-muted" />}>
<ErrorBookFilters />
</Suspense>
<Suspense fallback={<ErrorBookResultsFallback />}>
<ErrorBookResults searchParams={searchParams} />
</Suspense>
</div>
</div>
</AiClientProvider>
)
}