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:
@@ -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()
|
||||
|
||||
|
||||
@@ -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("")
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 显示 fallback,CSR 期间显示真实内容,
|
||||
* 组件树结构不同导致 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 {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
// 按班级分组
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 显示 fallback,CSR 期间显示真实内容,
|
||||
* 组件树结构不同导致 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 {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user