diff --git a/src/modules/adaptive-practice/components/class-knowledge-point-weakness-chart.tsx b/src/modules/adaptive-practice/components/class-knowledge-point-weakness-chart.tsx
index 1f36cb1..e692482 100644
--- a/src/modules/adaptive-practice/components/class-knowledge-point-weakness-chart.tsx
+++ b/src/modules/adaptive-practice/components/class-knowledge-point-weakness-chart.tsx
@@ -1,5 +1,6 @@
"use client"
+import { useCallback } from "react"
import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Cell } from "recharts"
import { useTranslations } from "next-intl"
import { BarChart3 } from "lucide-react"
@@ -16,6 +17,26 @@ import { cn } from "@/shared/lib/utils"
import type { ClassKnowledgePointWeakness } from "@/modules/adaptive-practice/data-access-analytics"
+/** recharts 内联对象常量:避免每次渲染创建新对象触发 re-render */
+const CHART_MARGIN = { left: 8, right: 8, top: 8, bottom: 8 }
+const GRID_PROPS = { vertical: false, strokeDasharray: "4 4", strokeOpacity: 0.4 }
+const X_AXIS_BASE_PROPS = {
+ dataKey: "name",
+ tickLine: false,
+ axisLine: false,
+ tickMargin: 8,
+}
+const Y_AXIS_BASE_PROPS = { tickLine: false, axisLine: false, width: 40 }
+const BAR_RADIUS: [number, number, number, number] = [4, 4, 0, 0]
+
+function truncateTick(value: string): string {
+ return value.length > 8 ? `${value.slice(0, 8)}...` : value
+}
+
+function formatPercentTick(value: number): string {
+ return `${value}%`
+}
+
interface ClassKnowledgePointWeaknessChartProps {
data: ClassKnowledgePointWeakness[]
className?: string
@@ -41,6 +62,36 @@ export function ClassKnowledgePointWeaknessChart({
}: ClassKnowledgePointWeaknessChartProps) {
const t = useTranslations("practice")
+ const renderTooltip = useCallback(
+ (payload: unknown) => {
+ // 从 unknown 转换:recharts tooltip payload 类型未知,需类型断言
+ const p = payload as {
+ name: string
+ errorRate: number
+ totalAnswers: number
+ wrongAnswers: number
+ }
+ return (
+
+
{p.name}
+
+ {t("teacher.knowledgePointWeakness.totalAnswers")}:
+ {p.totalAnswers}
+
+
+ {t("teacher.knowledgePointWeakness.wrongAnswers")}:
+ {p.wrongAnswers}
+
+
+ {t("teacher.knowledgePointWeakness.errorRate")}:
+ {p.errorRate}%
+
+
+ )
+ },
+ [t]
+ )
+
if (data.length === 0) {
return (
@@ -82,56 +133,25 @@ export function ClassKnowledgePointWeaknessChart({
-
-
+
+
- value.length > 8 ? `${value.slice(0, 8)}...` : value
- }
+ {...X_AXIS_BASE_PROPS}
+ tickFormatter={truncateTick}
/>
`${value}%`}
+ {...Y_AXIS_BASE_PROPS}
+ tickFormatter={formatPercentTick}
/>
{
- const p = payload as unknown as {
- name: string
- errorRate: number
- totalAnswers: number
- wrongAnswers: number
- }
- return (
-
-
{p.name}
-
- {t("teacher.knowledgePointWeakness.totalAnswers")}:
- {p.totalAnswers}
-
-
- {t("teacher.knowledgePointWeakness.wrongAnswers")}:
- {p.wrongAnswers}
-
-
- {t("teacher.knowledgePointWeakness.errorRate")}:
- {p.errorRate}%
-
-
- )
- }}
+ formatter={renderTooltip}
/>
}
/>
-
+
{chartData.map((_, idx) => (
|
))}
diff --git a/src/modules/adaptive-practice/components/practice-type-breakdown-chart.tsx b/src/modules/adaptive-practice/components/practice-type-breakdown-chart.tsx
index 9f091a7..d50f90f 100644
--- a/src/modules/adaptive-practice/components/practice-type-breakdown-chart.tsx
+++ b/src/modules/adaptive-practice/components/practice-type-breakdown-chart.tsx
@@ -1,5 +1,6 @@
"use client"
+import { useCallback } from "react"
import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Cell } from "recharts"
import { useTranslations } from "next-intl"
@@ -14,6 +15,18 @@ import { cn } from "@/shared/lib/utils"
import type { PracticeTypeBreakdown } from "@/modules/adaptive-practice/data-access-analytics"
+/** recharts 内联对象常量:避免每次渲染创建新对象触发 re-render */
+const CHART_MARGIN = { left: 8, right: 8, top: 8, bottom: 8 }
+const GRID_PROPS = { vertical: false, strokeDasharray: "4 4", strokeOpacity: 0.4 }
+const X_AXIS_PROPS = {
+ dataKey: "name",
+ tickLine: false,
+ axisLine: false,
+ tickMargin: 8,
+}
+const Y_AXIS_PROPS = { tickLine: false, axisLine: false, width: 36 }
+const BAR_RADIUS: [number, number, number, number] = [4, 4, 0, 0]
+
interface PracticeTypeBreakdownChartProps {
data: PracticeTypeBreakdown[]
className?: string
@@ -39,6 +52,41 @@ export function PracticeTypeBreakdownChart({
}: PracticeTypeBreakdownChartProps) {
const t = useTranslations("practice")
+ const renderTooltip = useCallback(
+ (payload: unknown) => {
+ // 从 unknown 转换:recharts tooltip payload 类型未知,需类型断言
+ const p = payload as {
+ name: string
+ sessionCount: number
+ totalQuestions: number
+ correctCount: number
+ accuracy: number
+ }
+ return (
+
+
{p.name}
+
+ {t("teacher.classComparison.totalSessions")}:
+ {p.sessionCount}
+
+
+ {t("teacher.classComparison.totalAnswered")}:
+ {p.totalQuestions}
+
+
+ {t("teacher.knowledgePointWeakness.wrongAnswers")}:
+ {p.totalQuestions - p.correctCount}
+
+
+ {t("teacher.classComparison.averageAccuracy")}:
+ {p.accuracy}%
+
+
+ )
+ },
+ [t]
+ )
+
if (data.length === 0) return null
const chartData = data.map((d) => ({
@@ -64,53 +112,19 @@ export function PracticeTypeBreakdownChart({
-
-
-
-
+
+
+
+
{
- const p = payload as unknown as {
- name: string
- sessionCount: number
- totalQuestions: number
- correctCount: number
- accuracy: number
- }
- return (
-
-
{p.name}
-
- {t("teacher.classComparison.totalSessions")}:
- {p.sessionCount}
-
-
- {t("teacher.classComparison.totalAnswered")}:
- {p.totalQuestions}
-
-
- {t("teacher.knowledgePointWeakness.wrongAnswers")}:
- {p.totalQuestions - p.correctCount}
-
-
- {t("teacher.classComparison.averageAccuracy")}:
- {p.accuracy}%
-
-
- )
- }}
+ formatter={renderTooltip}
/>
}
/>
-
+
{chartData.map((_, idx) => (
|
))}
diff --git a/src/modules/adaptive-practice/data-access-analytics.ts b/src/modules/adaptive-practice/data-access-analytics.ts
index 05e6ad0..837040e 100644
--- a/src/modules/adaptive-practice/data-access-analytics.ts
+++ b/src/modules/adaptive-practice/data-access-analytics.ts
@@ -1,6 +1,6 @@
import "server-only"
-import { cache } from "react"
+import { cacheFn } from "@/shared/lib/cache"
import { and, count, desc, eq, inArray, sql } from "drizzle-orm"
import { db } from "@/shared/db"
@@ -64,7 +64,7 @@ export interface PracticeTypeBreakdown {
*
* @param classId 班级 ID
*/
-export const getClassPracticeStats = cache(async (
+export const getClassPracticeStatsRaw = async (
classId: string,
): Promise => {
const studentIds = await getActiveStudentIdsByClassId(classId)
@@ -99,6 +99,12 @@ export const getClassPracticeStats = cache(async (
averageAccuracy: totalQuestionsAnswered > 0 ? totalCorrect / totalQuestionsAnswered : 0,
activeStudents,
}
+}
+
+export const getClassPracticeStats = cacheFn(getClassPracticeStatsRaw, {
+ tags: ["adaptive-practice"],
+ ttl: 300,
+ keyParts: ["adaptive-practice", "getClassPracticeStats"],
})
/**
@@ -106,7 +112,7 @@ export const getClassPracticeStats = cache(async (
*
* @param classId 班级 ID
*/
-export const getClassStudentPracticeSummaries = cache(async (
+export const getClassStudentPracticeSummariesRaw = async (
classId: string,
): Promise => {
const studentIds = await getActiveStudentIdsByClassId(classId)
@@ -138,6 +144,12 @@ export const getClassStudentPracticeSummaries = cache(async (
: 0,
lastPracticeAt: r.lastPracticeAt,
}))
+}
+
+export const getClassStudentPracticeSummaries = cacheFn(getClassStudentPracticeSummariesRaw, {
+ tags: ["adaptive-practice"],
+ ttl: 300,
+ keyParts: ["adaptive-practice", "getClassStudentPracticeSummaries"],
})
/**
@@ -148,7 +160,7 @@ export const getClassStudentPracticeSummaries = cache(async (
*
* @param gradeId 年级 ID
*/
-export const getGradePracticeStats = cache(async (
+export const getGradePracticeStatsRaw = async (
gradeId: string,
): Promise<{
totalSessions: number
@@ -186,6 +198,12 @@ export const getGradePracticeStats = cache(async (
averageAccuracy: totalQuestionsAnswered > 0 ? totalCorrect / totalQuestionsAnswered : 0,
activeStudents: Number(row.activeStudents),
}
+}
+
+export const getGradePracticeStats = cacheFn(getGradePracticeStatsRaw, {
+ tags: ["adaptive-practice"],
+ ttl: 300,
+ keyParts: ["adaptive-practice", "getGradePracticeStats"],
})
/**
@@ -193,7 +211,7 @@ export const getGradePracticeStats = cache(async (
*
* @param studentIds 学生 ID 列表
*/
-export const getPracticeTypeBreakdown = cache(async (
+export const getPracticeTypeBreakdownRaw = async (
studentIds: string[],
): Promise => {
if (studentIds.length === 0) return []
@@ -218,6 +236,12 @@ export const getPracticeTypeBreakdown = cache(async (
? Number(r.correctCount) / Number(r.totalQuestions)
: 0,
}))
+}
+
+export const getPracticeTypeBreakdown = cacheFn(getPracticeTypeBreakdownRaw, {
+ tags: ["adaptive-practice"],
+ ttl: 300,
+ keyParts: ["adaptive-practice", "getPracticeTypeBreakdown"],
})
/**
@@ -228,7 +252,7 @@ export const getPracticeTypeBreakdown = cache(async (
*
* @param classId 班级 ID
*/
-export const getStudentsWithoutPractice = cache(async (
+export const getStudentsWithoutPracticeRaw = async (
classId: string,
): Promise => {
const studentIds = await getActiveStudentIdsByClassId(classId)
@@ -245,6 +269,12 @@ export const getStudentsWithoutPractice = cache(async (
// 返回没有练习记录的学生
return studentIds.filter((id) => !activeSet.has(id))
+}
+
+export const getStudentsWithoutPractice = cacheFn(getStudentsWithoutPracticeRaw, {
+ tags: ["adaptive-practice"],
+ ttl: 300,
+ keyParts: ["adaptive-practice", "getStudentsWithoutPractice"],
})
// ---------------------------------------------------------------------------
@@ -278,7 +308,7 @@ export interface TeacherClassPracticeOverview {
*
* @param classIds 教师/年级主任可访问的班级 ID 列表
*/
-export const getTeacherClassPracticeOverviews = cache(async (
+export const getTeacherClassPracticeOverviewsRaw = async (
classIds: string[],
): Promise => {
if (classIds.length === 0) return []
@@ -351,6 +381,12 @@ export const getTeacherClassPracticeOverviews = cache(async (
participationRate: s.totalStudents > 0 ? activeStudents / s.totalStudents : 0,
}
})
+}
+
+export const getTeacherClassPracticeOverviews = cacheFn(getTeacherClassPracticeOverviewsRaw, {
+ tags: ["adaptive-practice"],
+ ttl: 300,
+ keyParts: ["adaptive-practice", "getTeacherClassPracticeOverviews"],
})
// ---------------------------------------------------------------------------
@@ -378,7 +414,7 @@ export interface ClassKnowledgePointWeakness {
* @param classId 班级 ID
* @param limit 返回条数(默认 10)
*/
-export const getClassKnowledgePointWeakness = cache(async (
+export const getClassKnowledgePointWeaknessRaw = async (
classId: string,
limit = 10,
): Promise => {
@@ -418,6 +454,12 @@ export const getClassKnowledgePointWeakness = cache(async (
errorRate: totalAnswers > 0 ? wrongAnswers / totalAnswers : 0,
}
})
+}
+
+export const getClassKnowledgePointWeakness = cacheFn(getClassKnowledgePointWeaknessRaw, {
+ tags: ["adaptive-practice"],
+ ttl: 300,
+ keyParts: ["adaptive-practice", "getClassKnowledgePointWeakness"],
})
// ---------------------------------------------------------------------------
@@ -447,7 +489,7 @@ export interface GradeClassPracticeComparison {
*
* @param gradeId 年级 ID
*/
-export const getGradeClassPracticeComparison = cache(async (
+export const getGradeClassPracticeComparisonRaw = async (
gradeId: string,
): Promise => {
const classList = await getClassesByGradeId(gradeId)
@@ -521,6 +563,12 @@ export const getGradeClassPracticeComparison = cache(async (
// 按参与率降序排列
return results.sort((a, b) => b.participationRate - a.participationRate)
+}
+
+export const getGradeClassPracticeComparison = cacheFn(getGradeClassPracticeComparisonRaw, {
+ tags: ["adaptive-practice"],
+ ttl: 300,
+ keyParts: ["adaptive-practice", "getGradeClassPracticeComparison"],
})
// ---------------------------------------------------------------------------
@@ -554,7 +602,7 @@ export interface ClassLearningProfile {
* @param classId 班级 ID
* @param weakKpLimit 知识点薄弱度返回条数(默认 5)
*/
-export const getClassLearningProfile = cache(async (
+export const getClassLearningProfileRaw = async (
classId: string,
weakKpLimit = 5,
): Promise => {
@@ -584,6 +632,12 @@ export const getClassLearningProfile = cache(async (
inactiveStudentIds: inactiveIds,
weakKnowledgePoints: weakKps,
}
+}
+
+export const getClassLearningProfile = cacheFn(getClassLearningProfileRaw, {
+ tags: ["adaptive-practice"],
+ ttl: 300,
+ keyParts: ["adaptive-practice", "getClassLearningProfile"],
})
/**
@@ -594,7 +648,7 @@ export const getClassLearningProfile = cache(async (
*
* @param studentIds 学生 ID 列表
*/
-export const getStudentNameMap = cache(async (
+export const getStudentNameMapRaw = async (
studentIds: string[],
): Promise