- 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
130 lines
4.4 KiB
TypeScript
130 lines
4.4 KiB
TypeScript
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>
|
||
)
|
||
}
|