From 40bd331a1ca4d21d74d38dcfc4fbc8dbe4832810 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 4 Aug 2026 21:08:06 -0600 Subject: [PATCH] =?UTF-8?q?feat(learn):=20=E5=AF=BC=E8=88=AA=E6=A0=8F?= =?UTF-8?q?=E8=87=AA=E5=AD=A6=E4=BA=8C=E7=BA=A7=E8=8F=9C=E5=8D=95=EF=BC=8C?= =?UTF-8?q?=E6=8C=89=E8=AF=AD=E8=A8=80=E8=AE=B0=E4=BD=8F=E5=AD=A6=E4=B9=A0?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 --- src/oj/learn/index.vue | 3 +++ src/shared/components/Header.vue | 24 +++++++++++++++++++++++- src/shared/composables/learnProgress.ts | 15 +++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/shared/composables/learnProgress.ts diff --git a/src/oj/learn/index.vue b/src/oj/learn/index.vue index 42078d6..6dc880f 100644 --- a/src/oj/learn/index.vue +++ b/src/oj/learn/index.vue @@ -144,6 +144,7 @@ import type { Tutorial, Exercise, LANGUAGE } from "utils/types" import { getTutorial, getTutorials, getExercises } from "../api" import { parseExercises } from "./composables/useExerciseParse" import { useBreakpoints } from "shared/composables/breakpoints" +import { useLearnProgress } from "shared/composables/learnProgress" const ExerciseWidget = defineAsyncComponent( () => import("./components/ExerciseWidget.vue"), @@ -156,6 +157,7 @@ const isDark = useDark() const route = useRoute() const router = useRouter() const { isDesktop } = useBreakpoints() +const { learnStep } = useLearnProgress() const step = computed(() => { const value = route.params.step as string | undefined @@ -214,6 +216,7 @@ async function init() { ]) if (res2.status === "fulfilled") tutorial.value = res2.value.data exercises.value = exs.status === "fulfilled" ? exs.value : [] + learnStep.value[type.value] = step.value } watch( diff --git a/src/shared/components/Header.vue b/src/shared/components/Header.vue index 44ed653..5db6aa4 100644 --- a/src/shared/components/Header.vue +++ b/src/shared/components/Header.vue @@ -2,6 +2,7 @@ import { Icon } from "@iconify/vue" import { RouterLink } from "vue-router" import { useBreakpoints } from "shared/composables/breakpoints" +import { useLearnProgress } from "shared/composables/learnProgress" import { useAuthModalStore } from "shared/store/authModal" import { useScreenModeStore } from "shared/store/screenMode" import { logout } from "../api" @@ -17,6 +18,7 @@ const route = useRoute() const router = useRouter() const { isMobile, isDesktop } = useBreakpoints() +const { learnStep } = useLearnProgress() const isDark = useDark() @@ -125,11 +127,31 @@ function renderIcon(icon: string) { return () => h(Icon, { icon, width: 20 }) } +function learnLink(type: "python" | "c") { + return `/learn/${type}/${learnStep.value[type].toString().padStart(2, "0")}` +} + const menus = computed(() => [ { - label: () => h(RouterLink, { to: "/learn/01" }, { default: () => "自学" }), + label: "自学", key: "learn", icon: renderIcon("fluent-emoji:books"), + children: [ + { + label: () => + h( + RouterLink, + { to: learnLink("python") }, + { default: () => "Python" }, + ), + key: "learn-python", + }, + { + label: () => + h(RouterLink, { to: learnLink("c") }, { default: () => "C语言" }), + key: "learn-c", + }, + ], }, { label: () => h(RouterLink, { to: "/" }, { default: () => "题目" }), diff --git a/src/shared/composables/learnProgress.ts b/src/shared/composables/learnProgress.ts new file mode 100644 index 0000000..8a638e6 --- /dev/null +++ b/src/shared/composables/learnProgress.ts @@ -0,0 +1,15 @@ +import { useStorage } from "@vueuse/core" +import { STORAGE_KEY } from "utils/constants" + +export type TutorialType = "python" | "c" + +// 模块级单例:学习页写入、导航栏读取,必须共用同一个响应式引用, +// 否则导航栏的链接不会随学习进度更新 +const learnStep = useStorage>( + STORAGE_KEY.LEARN_CURRENT_STEP, + { python: 1, c: 1 }, +) + +export function useLearnProgress() { + return { learnStep } +}