refactor(目录): 目录名对上界面上的名字,自学的「一课」不再也叫 lesson
Deploy / deploy (push) Canceled after 0s

- 后台课堂任务 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 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-11-02 22:59:43 +08:00
1 parent 29c31640b5
commit 48590eee60
37 files changed
+1238 -1236

No files matched your search

+1 -1
View File
@@ -1195,7 +1195,7 @@ export const problemsetBadge = pgTable(
* 冲突目标就是 (user_id, tutorial_id),再挂个 id 序列没有任何用处。
*
* **只记「读到哪一课 + 停留多久」。** 练一练的作答另有一张表(下面的 `exerciseAttempt`),
* 一个学生一道练习一行;「学完」由两张表合起来判(契约的 `isLessonDone`)。
* 一个学生一道练习一行;「学完」由两张表合起来判(契约的 `isTutorialDone`)。
*
* 外键这里**用了库级 CASCADE**,和 Django 建的那些 NO ACTION 外键不同:删教程、删用户
* 都不必再记得回来手工清一遍子表(后台删教程的事务里就没清它,靠的就是这里)。
+11 -11
View File
@@ -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<number, LearnStudentDetail['lessons'][number]['exercises']>()
const byTutorial = new Map<number, LearnStudentDetail['tutorials'][number]['exercises']>()
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 }) => {