feat(error-book): update components and analytics data-access

- Update all error-book components (filters, cards, charts, dialogs)

- Update data-access-analytics.ts

- Update student-error-book-list-client.tsx
This commit is contained in:
SpecialX
2026-07-04 10:22:36 +08:00
parent 6ea8ba763b
commit eee0145274
18 changed files with 39 additions and 25 deletions

View File

@@ -64,7 +64,7 @@ export function StudentErrorBookListClient({
studentId,
}: StudentErrorBookListClientProps): JSX.Element {
const router = useRouter()
const t = useTranslations("error-book")
const t = useTranslations("errorBook")
const tPractice = useTranslations("practice")
const [, startTransition] = useTransition()

View File

@@ -43,7 +43,7 @@ export function AddErrorBookDialog({
questionOptions: questionOptionsProp,
onLoadQuestions,
}: AddErrorBookDialogProps) {
const t = useTranslations("error-book")
const t = useTranslations("errorBook")
const [open, setOpen] = useState(false)
const [isPending, startTransition] = useTransition()
const [questionId, setQuestionId] = useState("")

View File

@@ -29,7 +29,7 @@ export function AnalyticsStatsCards({
knowledgePointCount,
className,
}: AnalyticsStatsCardsProps) {
const t = useTranslations("error-book")
const t = useTranslations("errorBook")
const avg = (totalStudents > 0 ? totalErrorItems / totalStudents : 0).toFixed(1)
const cards = [

View File

@@ -47,7 +47,7 @@ function isChapterChartPayload(v: unknown): v is ChapterChartPayload {
* 每个柱子可展开显示该章节下错得最多的知识点
*/
export function ChapterWeaknessChart({ data, className }: ChapterWeaknessChartProps) {
const t = useTranslations("error-book")
const t = useTranslations("errorBook")
if (data.length === 0) return null

View File

@@ -55,7 +55,7 @@ const CHART_COLORS = [
* 颜色按班级区分tooltip 显示学生数/人均/掌握率
*/
export function ClassErrorBarChart({ data, className }: ClassErrorBarChartProps) {
const t = useTranslations("error-book")
const t = useTranslations("errorBook")
if (data.length === 0) return null

View File

@@ -1,6 +1,6 @@
"use client"
import { useRouter, useSearchParams } from "next/navigation"
import { useRouter } from "next/navigation"
import { useTranslations } from "next-intl"
import { cn } from "@/shared/lib/utils"
import { Badge } from "@/shared/components/ui/badge"
@@ -18,6 +18,10 @@ interface ClassFilterProps {
/**
* 班级筛选器(教师视图)
* 显示教师所教的所有班级,点击切换查看单个班级
*
* v3: 移除 useSearchParams(),改用 window.location.search 在点击时读取当前 URL 参数。
* useSearchParams() 在 SSR 期间会导致 Suspense 显示 fallbackCSR 期间显示真实内容,
* 组件树结构不同导致 useId() 生成的 id 不匹配,触发 hydration mismatch。
*/
export function ClassFilter({
classes,
@@ -25,11 +29,13 @@ export function ClassFilter({
paramName = "classId",
}: ClassFilterProps) {
const router = useRouter()
const searchParams = useSearchParams()
const t = useTranslations("error-book")
const t = useTranslations("errorBook")
const handleSelect = (classId: string) => {
const params = new URLSearchParams(searchParams.toString())
// 在 client 端点击时读取当前 URL 参数,避免 useSearchParams() 的 SSR/CSR 不一致
const params = new URLSearchParams(
typeof window !== "undefined" ? window.location.search : ""
)
if (classId === "all") {
params.delete(paramName)
} else {

View File

@@ -60,7 +60,7 @@ export function ErrorBookDetailDialog({
const [isPending, startTransition] = useTransition()
const [note, setNote] = useState(item.note ?? "")
const [errorTags, setErrorTags] = useState<string[]>(item.errorTags ?? [])
const t = useTranslations("error-book")
const t = useTranslations("errorBook")
function handleSaveNote() {
startTransition(async () => {

View File

@@ -13,7 +13,7 @@ import {
import { FilterBar, FilterSearchInput } from "@/shared/components/ui/filter-bar"
export function ErrorBookFilters() {
const t = useTranslations("error-book")
const t = useTranslations("errorBook")
const [search, setSearch] = useQueryState("q", parseAsString.withDefault(""))
const [status, setStatus] = useQueryState("status", parseAsString.withDefault("all"))
const [sourceType, setSourceType] = useQueryState("source", parseAsString.withDefault("all"))

View File

@@ -24,7 +24,7 @@ interface ErrorBookItemCardProps {
}
export function ErrorBookItemCard({ item, children }: ErrorBookItemCardProps) {
const t = useTranslations("error-book")
const t = useTranslations("errorBook")
const preview = item.question
? extractQuestionPreview(item.question.content, t("itemCard.questionContent"))
: t("itemCard.questionDeleted")

View File

@@ -23,7 +23,7 @@ export function ErrorBookList({
aiAnalysisSlot,
onStartVariantPractice,
}: ErrorBookListProps): JSX.Element {
const t = useTranslations("error-book")
const t = useTranslations("errorBook")
if (items.length === 0) {
return (

View File

@@ -12,7 +12,7 @@ interface ErrorBookStatsCardsProps {
}
export function ErrorBookStatsCards({ stats, isLoading }: ErrorBookStatsCardsProps) {
const t = useTranslations("error-book")
const t = useTranslations("errorBook")
const masteredPercent = stats.totalCount > 0
? Math.round(stats.masteredRate * 100)
: 0

View File

@@ -34,7 +34,7 @@ export function GroupedStudentErrorTable({
studentNames,
basePath,
}: GroupedStudentErrorTableProps) {
const t = useTranslations("error-book")
const t = useTranslations("errorBook")
const [expandedClasses, setExpandedClasses] = useState<Set<string>>(new Set())
// 按班级分组

View File

@@ -50,7 +50,7 @@ export function KnowledgePointWeaknessChart({
data,
className,
}: KnowledgePointWeaknessChartProps) {
const t = useTranslations("error-book")
const t = useTranslations("errorBook")
if (data.length === 0) return null

View File

@@ -23,7 +23,7 @@ type ReviewOption = {
}
export function ReviewButtons({ itemId, onReviewed }: ReviewButtonsProps) {
const t = useTranslations("error-book")
const t = useTranslations("errorBook")
const [isPending, startTransition] = useTransition()
const [selected, setSelected] = useState<ErrorBookReviewResultValue | null>(null)

View File

@@ -54,7 +54,7 @@ export function SubjectDistributionChart({
data,
className,
}: SubjectDistributionChartProps) {
const t = useTranslations("error-book")
const t = useTranslations("errorBook")
if (data.length === 0) return null

View File

@@ -1,6 +1,6 @@
"use client"
import { useRouter, useSearchParams } from "next/navigation"
import { useRouter } from "next/navigation"
import { useTranslations } from "next-intl"
import { cn } from "@/shared/lib/utils"
import { Badge } from "@/shared/components/ui/badge"
@@ -18,6 +18,10 @@ interface SubjectTabsProps {
/**
* 学科切换 Tab教师/管理员视图)
* 显示每个学科的错题数概览,点击切换
*
* v3: 移除 useSearchParams(),改用 window.location.search 在点击时读取当前 URL 参数。
* useSearchParams() 在 SSR 期间会导致 Suspense 显示 fallbackCSR 期间显示真实内容,
* 组件树结构不同导致 useId() 生成的 id 不匹配,触发 hydration mismatch。
*/
export function SubjectTabs({
subjects,
@@ -25,11 +29,13 @@ export function SubjectTabs({
paramName = "subject",
}: SubjectTabsProps) {
const router = useRouter()
const searchParams = useSearchParams()
const t = useTranslations("error-book")
const t = useTranslations("errorBook")
const handleSelect = (subjectId: string | null) => {
const params = new URLSearchParams(searchParams.toString())
// 在 client 端点击时读取当前 URL 参数,避免 useSearchParams() 的 SSR/CSR 不一致
const params = new URLSearchParams(
typeof window !== "undefined" ? window.location.search : ""
)
if (subjectId === null) {
params.delete(paramName)
} else {

View File

@@ -34,7 +34,7 @@ function isKnownQuestionType(v: string): v is KnownQuestionType {
}
export function TopWrongQuestions({ questions }: TopWrongQuestionsProps) {
const t = useTranslations("error-book")
const t = useTranslations("errorBook")
if (questions.length === 0) {
return (

View File

@@ -109,11 +109,13 @@ export async function getTopWrongQuestionsByStudentIds(
const whereClause = buildStudentErrorWhereClause(studentIds, subjectId)
// 使用子查询聚合,避免 GROUP BY JSON 列的问题
// 注意:子查询中的 raw SQL 字段count()、sql``)必须声明 alias
// 否则外层引用时会报 "doesn't have an alias declared" 错误
const aggregatedSubquery = db
.select({
questionId: errorBookItems.questionId,
errorCount: count(),
masteredCount: sql<number>`sum(case when ${errorBookItems.status} = 'mastered' then 1 else 0 end)`,
errorCount: count().as("errorCount"),
masteredCount: sql<number>`sum(case when ${errorBookItems.status} = 'mastered' then 1 else 0 end)`.as("masteredCount"),
})
.from(errorBookItems)
.where(whereClause)