From 48590eee60beba75088ddac306b637772dce3aa4 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Mon, 2 Nov 2026 22:59:43 +0800 Subject: [PATCH] =?UTF-8?q?refactor(=E7=9B=AE=E5=BD=95):=20=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E5=90=8D=E5=AF=B9=E4=B8=8A=E7=95=8C=E9=9D=A2=E4=B8=8A?= =?UTF-8?q?=E7=9A=84=E5=90=8D=E5=AD=97=EF=BC=8C=E8=87=AA=E5=AD=A6=E7=9A=84?= =?UTF-8?q?=E3=80=8C=E4=B8=80=E8=AF=BE=E3=80=8D=E4=B8=8D=E5=86=8D=E4=B9=9F?= =?UTF-8?q?=E5=8F=AB=20lesson?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后台课堂任务 admin/lesson → admin/task(/admin/task),和前台 oj/task 对上 - 后台首页 admin/setting/home.vue → admin/home/,设置 → admin/config/ - 卡点分析、年度趋势从 admin/problem 搬到 admin/analytics(/admin/analytics/stuck|trend),和菜单的「学情分析」、后端 analytics.ts 对上 - 首页 dashboard.vue / TeacherHome.vue → student.vue / teacher.vue;自学首页 home.vue → index.vue,看一课 index.vue → tutorial.vue - lesson 只表示课堂上的一节:自学那边改叫 tutorial(oj/learn/lesson.ts → utils.ts、isLessonDone → isTutorialDone、Lesson*.vue → Tutorial*.vue、自学情况的 lessons 字段 → tutorials) - 旧地址不留跳转 Co-Authored-By: Claude Opus 5.5 --- apps/api/src/db/schema.ts | 2 +- apps/api/src/routes/admin/learn.ts | 22 +- .../components/StuckDetail.vue | 2 +- .../components/stuckMath.ts | 2 +- .../Stuck.vue => analytics/stuck.vue} | 0 .../Trend.vue => analytics/trend.vue} | 0 .../{setting/config.vue => config/index.vue} | 0 .../{setting/home.vue => home/index.vue} | 0 apps/web/src/admin/learn/StudentDrawer.vue | 88 +- apps/web/src/admin/learn/index.vue | 88 +- .../problem/{ => components}/SqlDetail.vue | 10 +- apps/web/src/admin/problem/editor.vue | 2 +- .../components/LessonEditModal.vue | 0 apps/web/src/admin/{lesson => task}/list.vue | 0 apps/web/src/admin/tutorial/detail.vue | 12 +- apps/web/src/admin/tutorial/list.vue | 12 +- apps/web/src/oj/home/index.vue | 10 +- .../oj/home/{dashboard.vue => student.vue} | 6 +- .../oj/home/{TeacherHome.vue => teacher.vue} | 0 .../src/oj/learn/components/ProjectTile.vue | 2 +- .../src/oj/learn/components/ProjectTree.vue | 38 +- apps/web/src/oj/learn/components/SegBar.vue | 12 +- .../{LessonBody.vue => TutorialBody.vue} | 6 +- .../{LessonDot.vue => TutorialDot.vue} | 4 +- .../{LessonEnd.vue => TutorialEnd.vue} | 4 +- ...{LessonOutline.vue => TutorialOutline.vue} | 2 +- .../{LessonRow.vue => TutorialRow.vue} | 8 +- ...{useLessonParse.ts => useTutorialParse.ts} | 6 +- apps/web/src/oj/learn/home.vue | 516 ---------- apps/web/src/oj/learn/index.vue | 944 ++++++++---------- apps/web/src/oj/learn/tutorial.vue | 608 +++++++++++ apps/web/src/oj/learn/{lesson.ts => utils.ts} | 24 +- apps/web/src/oj/task/index.vue | 2 +- apps/web/src/routes.ts | 26 +- apps/web/src/shared/layout/admin.vue | 8 +- packages/contract/src/admin.ts | 6 +- packages/contract/src/content.ts | 2 +- 37 files changed, 1238 insertions(+), 1236 deletions(-) rename apps/web/src/admin/{problem => analytics}/components/StuckDetail.vue (99%) rename apps/web/src/admin/{problem => analytics}/components/stuckMath.ts (86%) rename apps/web/src/admin/{problem/Stuck.vue => analytics/stuck.vue} (100%) rename apps/web/src/admin/{problem/Trend.vue => analytics/trend.vue} (100%) rename apps/web/src/admin/{setting/config.vue => config/index.vue} (100%) rename apps/web/src/admin/{setting/home.vue => home/index.vue} (100%) rename apps/web/src/admin/problem/{ => components}/SqlDetail.vue (99%) rename apps/web/src/admin/{lesson => task}/components/LessonEditModal.vue (100%) rename apps/web/src/admin/{lesson => task}/list.vue (100%) rename apps/web/src/oj/home/{dashboard.vue => student.vue} (99%) rename apps/web/src/oj/home/{TeacherHome.vue => teacher.vue} (100%) rename apps/web/src/oj/learn/components/{LessonBody.vue => TutorialBody.vue} (94%) rename apps/web/src/oj/learn/components/{LessonDot.vue => TutorialDot.vue} (89%) rename apps/web/src/oj/learn/components/{LessonEnd.vue => TutorialEnd.vue} (98%) rename apps/web/src/oj/learn/components/{LessonOutline.vue => TutorialOutline.vue} (97%) rename apps/web/src/oj/learn/components/{LessonRow.vue => TutorialRow.vue} (88%) rename apps/web/src/oj/learn/composables/{useLessonParse.ts => useTutorialParse.ts} (97%) delete mode 100644 apps/web/src/oj/learn/home.vue create mode 100644 apps/web/src/oj/learn/tutorial.vue rename apps/web/src/oj/learn/{lesson.ts => utils.ts} (82%) diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index d930519..919aa8f 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -1195,7 +1195,7 @@ export const problemsetBadge = pgTable( * 冲突目标就是 (user_id, tutorial_id),再挂个 id 序列没有任何用处。 * * **只记「读到哪一课 + 停留多久」。** 练一练的作答另有一张表(下面的 `exerciseAttempt`), - * 一个学生一道练习一行;「学完」由两张表合起来判(契约的 `isLessonDone`)。 + * 一个学生一道练习一行;「学完」由两张表合起来判(契约的 `isTutorialDone`)。 * * 外键这里**用了库级 CASCADE**,和 Django 建的那些 NO ACTION 外键不同:删教程、删用户 * 都不必再记得回来手工清一遍子表(后台删教程的事务里就没清它,靠的就是这里)。 diff --git a/apps/api/src/routes/admin/learn.ts b/apps/api/src/routes/admin/learn.ts index 2ca2c3d..3fd4b81 100644 --- a/apps/api/src/routes/admin/learn.ts +++ b/apps/api/src/routes/admin/learn.ts @@ -1,7 +1,7 @@ import { FLOWCHART_PASS_GRADES, STUDENT_ROLES, - isLessonDone, + isTutorialDone, type LearnBoard, type LearnBoardStudent, type LearnExerciseAttemptList, @@ -198,7 +198,7 @@ adminLearnRoutes.get('/learn-analytics/board', requireTeacher, async (c) => { students: students.map((student) => { const opened: number[] = [] const done: number[] = [] - const lessons: LearnBoardStudent['lessons'] = [] + const attempts: LearnBoardStudent['tutorials'] = [] let exerciseSolved = 0 let totalSeconds = 0 let lastViewedAt: string | null = null @@ -210,26 +210,26 @@ adminLearnRoutes.get('/learn-analytics/board', requireTeacher, async (c) => { exerciseSolved += solvedHere if (exercises) { const { tried, solved, wrongAttempts } = exercises - lessons.push({ tutorialId: tutorial.id, tried, solved, wrongAttempts }) + attempts.push({ tutorialId: tutorial.id, tried, solved, wrongAttempts }) } if (row) { opened.push(tutorial.id) totalSeconds += row.totalSeconds if (!lastViewedAt || row.lastViewedAt > lastViewedAt) lastViewedAt = row.lastViewedAt } - const lesson = { + const here = { viewCount: row?.viewCount ?? 0, exerciseTotal: tutorial.exerciseTotal, exerciseSolved: solvedHere, } - if (isLessonDone(lesson)) done.push(tutorial.id) + if (isTutorialDone(here)) done.push(tutorial.id) } return { ...student, opened, done, exerciseSolved, - lessons, + tutorials: attempts, problems: problemsOf.get(student.userId) ?? [], totalSeconds, lastViewedAt, @@ -394,27 +394,27 @@ adminLearnRoutes.get('/learn-analytics/students/:id', requireTeacher, async (c) ).map((row) => [row.problemId, row]), ) - const byLesson = new Map() + const byTutorial = new Map() for (const { tutorialId, ...row } of exercises) { - const list = byLesson.get(tutorialId) ?? [] + const list = byTutorial.get(tutorialId) ?? [] list.push({ ...row, attempts: row.attempts ?? 0, solved: row.solved ?? false, }) - byLesson.set(tutorialId, list) + byTutorial.set(tutorialId, list) } return success(c, { ...student, - lessons: tutorials + tutorials: tutorials .map((row) => ({ tutorialId: row.tutorialId, title: row.title, viewCount: row.viewCount ?? 0, totalSeconds: row.totalSeconds ?? 0, lastViewedAt: row.lastViewedAt, - exercises: byLesson.get(row.tutorialId) ?? [], + exercises: byTutorial.get(row.tutorialId) ?? [], problems: related .filter((problem) => problem.tutorialId === row.tutorialId) .map(({ id: problemId, displayId, title }) => { diff --git a/apps/web/src/admin/problem/components/StuckDetail.vue b/apps/web/src/admin/analytics/components/StuckDetail.vue similarity index 99% rename from apps/web/src/admin/problem/components/StuckDetail.vue rename to apps/web/src/admin/analytics/components/StuckDetail.vue index 0941c07..c71ff90 100644 --- a/apps/web/src/admin/problem/components/StuckDetail.vue +++ b/apps/web/src/admin/analytics/components/StuckDetail.vue @@ -8,7 +8,7 @@ import { avgTries, rate } from './stuckMath' /** * 卡点分析右栏:一道题的详情(做过几人、一次就对、错在哪、各班、交了几次才对、历年、 - * 还没做对的人)。从 Stuck.vue 拆出来,取数和三栏的联动还在那边。 + * 还没做对的人)。从 stuck.vue 拆出来,取数和三栏的联动还在那边。 */ const props = defineProps<{ detail: StuckProblemDetail diff --git a/apps/web/src/admin/problem/components/stuckMath.ts b/apps/web/src/admin/analytics/components/stuckMath.ts similarity index 86% rename from apps/web/src/admin/problem/components/stuckMath.ts rename to apps/web/src/admin/analytics/components/stuckMath.ts index 1091be9..e77d56a 100644 --- a/apps/web/src/admin/problem/components/stuckMath.ts +++ b/apps/web/src/admin/analytics/components/stuckMath.ts @@ -1,4 +1,4 @@ -/** 卡点分析(Stuck.vue)和它的单题详情(StuckDetail.vue)共用的两个算法 */ +/** 卡点分析(stuck.vue)和它的单题详情(StuckDetail.vue)共用的两个算法 */ /** 百分比,取整;分母是 0 时给 0 */ export function rate(part: number, whole: number) { diff --git a/apps/web/src/admin/problem/Stuck.vue b/apps/web/src/admin/analytics/stuck.vue similarity index 100% rename from apps/web/src/admin/problem/Stuck.vue rename to apps/web/src/admin/analytics/stuck.vue diff --git a/apps/web/src/admin/problem/Trend.vue b/apps/web/src/admin/analytics/trend.vue similarity index 100% rename from apps/web/src/admin/problem/Trend.vue rename to apps/web/src/admin/analytics/trend.vue diff --git a/apps/web/src/admin/setting/config.vue b/apps/web/src/admin/config/index.vue similarity index 100% rename from apps/web/src/admin/setting/config.vue rename to apps/web/src/admin/config/index.vue diff --git a/apps/web/src/admin/setting/home.vue b/apps/web/src/admin/home/index.vue similarity index 100% rename from apps/web/src/admin/setting/home.vue rename to apps/web/src/admin/home/index.vue diff --git a/apps/web/src/admin/learn/StudentDrawer.vue b/apps/web/src/admin/learn/StudentDrawer.vue index 65fd9e3..53b6ba8 100644 --- a/apps/web/src/admin/learn/StudentDrawer.vue +++ b/apps/web/src/admin/learn/StudentDrawer.vue @@ -7,7 +7,7 @@ import type { LearnStudentDetail } from 'utils/types' import UserName from 'shared/components/UserName.vue' import { useBackClose } from 'shared/composables/backClose' import { useBreakpoints } from 'shared/composables/breakpoints' -import { splitTitle, type TutorialType } from 'oj/learn/lesson' +import { splitTitle, type TutorialType } from 'oj/learn/utils' import { EXERCISE_KIND } from './kinds' /** @@ -30,31 +30,31 @@ const theme = useThemeVars() const { isDesktop } = useBreakpoints() useBackClose(show, () => !isDesktop.value) -type Lesson = LearnStudentDetail['lessons'][number] -type Exercise = Lesson['exercises'][number] -type Problem = Lesson['problems'][number] +type Tutorial = LearnStudentDetail['tutorials'][number] +type Exercise = Tutorial['exercises'][number] +type Problem = Tutorial['problems'][number] const detail = ref(null) const loading = ref(false) const expanded = ref(new Set()) const summary = computed(() => { - const lessons = detail.value?.lessons ?? [] - const exercises = lessons.flatMap((lesson) => lesson.exercises) + const tutorials = detail.value?.tutorials ?? [] + const exercises = tutorials.flatMap((tutorial) => tutorial.exercises) // 一道题挂在两课只算一次 - const problems = new Map(lessons.flatMap((lesson) => lesson.problems).map((p) => [p.id, p])) + const problems = new Map(tutorials.flatMap((tutorial) => tutorial.problems).map((p) => [p.id, p])) return { problemSolved: [...problems.values()].filter((p) => p.solved).length, problemTotal: problems.size, - opened: lessons.filter((lesson) => lesson.viewCount > 0).length, - seconds: lessons.reduce((n, lesson) => n + lesson.totalSeconds, 0), + opened: tutorials.filter((tutorial) => tutorial.viewCount > 0).length, + seconds: tutorials.reduce((n, tutorial) => n + tutorial.totalSeconds, 0), solved: exercises.filter((ex) => ex.solved).length, total: exercises.length, } }) -function solvedOf(lesson: Lesson) { - return lesson.exercises.filter((ex) => ex.solved).length +function solvedOf(tutorial: Tutorial) { + return tutorial.exercises.filter((ex) => ex.solved).length } function stateOf(ex: Exercise) { @@ -92,13 +92,13 @@ watch( const data = await getLearnStudent(userId, { type }) detail.value = data expanded.value = new Set( - data.lessons + data.tutorials .filter( - (lesson) => - (lesson.exercises.some((ex) => ex.attempts > 0) && lesson.exercises.some((ex) => !ex.solved)) || - lesson.problems.some((p) => p.submissions > 0 && !p.solved), + (tutorial) => + (tutorial.exercises.some((ex) => ex.attempts > 0) && tutorial.exercises.some((ex) => !ex.solved)) || + tutorial.problems.some((p) => p.submissions > 0 && !p.solved), ) - .map((lesson) => lesson.tutorialId), + .map((tutorial) => tutorial.tutorialId), ) } catch { detail.value = null @@ -133,40 +133,42 @@ watch( '--bad': theme.errorColor, }" > -
还没打开过
-
-