feat(自学): 自学重做——首页课程地图、读一课按项目分目录、代码块就地运行、学完给题做
Deploy / deploy (push) Canceled after 0s

- 顶栏「自学」进新的自学首页 /learn:接着学(按服务端记录,不再串到上一个学生)+ 按项目分组的课程地图
- 读一课:左边目录按项目折叠,正文加宽,右边本课大纲带练一练标记;去掉了不能运行的示例代码栏
- 正文里的代码块能改能运行,报错用题目页那套中文解释;读输入的代码出输入框
- 课末:练一练做到几道 → 学完了 → 老师挑的几道题(必会标出来)→ 下一课
- 练一练统一成绿色提交、结果一行字;手机版目录从底部弹出
- 后台:教程列表按项目分组拖动排序,编辑页左写右看、练一练插到光标处、挑相关题;
  自学情况改成方块格 + 还没打开过的名单

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-12 09:40:34 +08:00
1 parent 11194543a3
commit 845e41c060
43 files changed
+4675 -1294

No files matched your search

+16 -18
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { continueStep, doneCount, lessonPath, splitTitle, type TutorialType } from "oj/learn/lesson"
import { Icon } from "@iconify/vue"
import {
getAnnouncement,
@@ -29,8 +30,6 @@ import type {
WeeklyRank,
} from "utils/types"
type TutorialType = "python" | "c"
interface Track {
type: TutorialType
label: string
@@ -38,8 +37,8 @@ interface Track {
step: number
title: string
total: number
/** 打开过的课数 */
read: number
/** 学完的课数(练一练全对) */
done: number
lastViewedAt: string | null
}
@@ -107,27 +106,26 @@ const weeklyText = computed(() => {
})
/**
* 接着学哪一课看的是**服务端留痕**里最近打开的那一课,不是导航栏用的
* localStorage:机房一台电脑轮着好几个班用,本地记的是上一个人学到哪。
* 接着学哪一课看的是**服务端留痕**(和自学首页同一套,oj/learn/lesson.ts),不是本机记的:
* 机房一台电脑轮着好几个班用,本地记的是上一个人学到哪。
*/
async function loadTrack(type: TutorialType, label: string): Promise<Track | null> {
const [titles, progress] = await Promise.all([getTutorials(type), getLearnProgress(type)])
const [titles, rows] = await Promise.all([getTutorials(type), getLearnProgress(type)])
if (!titles.length) return null
const seen = progress.filter((p) => p.lastViewedAt)
const latest = seen.reduce<(typeof seen)[number] | null>(
(acc, p) => (!acc || p.lastViewedAt! > acc.lastViewedAt! ? p : acc),
const progress = Object.fromEntries(rows.map((row) => [row.tutorialId, row]))
const step = continueStep(titles, progress) ?? 1
const lastViewedAt = rows.reduce<string | null>(
(acc, row) => (row.lastViewedAt && (!acc || row.lastViewedAt > acc) ? row.lastViewedAt : acc),
null,
)
const index = latest ? titles.findIndex((t) => t.id === latest.tutorialId) : -1
const step = index >= 0 ? index + 1 : 1
return {
type,
label,
step,
title: titles[step - 1]!.title,
title: splitTitle(titles[step - 1]!.title).kp,
total: titles.length,
read: seen.length,
lastViewedAt: latest?.lastViewedAt ?? null,
done: doneCount(titles, progress),
lastViewedAt,
}
}
@@ -271,7 +269,7 @@ watch(
)
function learnLink(track: Track) {
return `/learn/${track.type}/${track.step.toString().padStart(2, "0")}`
return lessonPath(track.type, track.step)
}
const { jump, jumping } = useProblemJump()
@@ -441,13 +439,13 @@ async function openAnnouncement(item: AnnouncementListItem) {
<div class="track-title">{{ track.title }}</div>
<n-progress
type="line"
:percentage="Math.round((track.read / track.total) * 100)"
:percentage="Math.round((track.done / track.total) * 100)"
status="success"
:show-indicator="false"
:height="6"
/>
<n-text depth="3" class="track-meta">
已打开 {{ track.read }} 课
学完 {{ track.done }} 课
<template v-if="track.lastViewedAt">
· 上次 {{ relativeTime(track.lastViewedAt) }}
</template>