- 顶栏「自学」进新的自学首页 /learn:接着学(按服务端记录,不再串到上一个学生)+ 按项目分组的课程地图 - 读一课:左边目录按项目折叠,正文加宽,右边本课大纲带练一练标记;去掉了不能运行的示例代码栏 - 正文里的代码块能改能运行,报错用题目页那套中文解释;读输入的代码出输入框 - 课末:练一练做到几道 → 学完了 → 老师挑的几道题(必会标出来)→ 下一课 - 练一练统一成绿色提交、结果一行字;手机版目录从底部弹出 - 后台:教程列表按项目分组拖动排序,编辑页左写右看、练一练插到光标处、挑相关题; 自学情况改成方块格 + 还没打开过的名单 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
11194543a3
commit
845e41c060
43 files changed
+4675
-1294
No files matched your search
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { Exercise } from "utils/types"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import type { Exercise, ExerciseMcqData } from "utils/types"
|
||||
import { reportExerciseAttempt } from "oj/api"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
|
||||
@@ -11,10 +12,33 @@ const ExercisePredict = defineAsyncComponent(() => import("./ExercisePredict.vue
|
||||
const ExerciseDebug = defineAsyncComponent(() => import("./ExerciseDebug.vue"))
|
||||
const ExerciseGroup = defineAsyncComponent(() => import("./ExerciseGroup.vue"))
|
||||
|
||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||
const props = defineProps<{
|
||||
exercise: Exercise
|
||||
lang?: string
|
||||
/** 这一课的第几道、一共几道 */
|
||||
index: number
|
||||
total: number
|
||||
/** 做对过(以前做对的,或者刚才做对的) */
|
||||
solved: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{ solved: [exerciseId: number] }>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const KIND: Record<string, string> = {
|
||||
sort: "代码排序",
|
||||
fill: "代码填空",
|
||||
match: "连线匹配",
|
||||
predict: "输出预测",
|
||||
debug: "代码找错",
|
||||
group: "归类分组",
|
||||
}
|
||||
|
||||
const kind = computed(() => {
|
||||
if (props.exercise.type !== "mcq") return KIND[props.exercise.type] ?? "练习"
|
||||
return (props.exercise.data as ExerciseMcqData).answer.length === 1 ? "单选题" : "多选题"
|
||||
})
|
||||
|
||||
/**
|
||||
* 七种题型各自判完对错后都往上抛 attempt,留痕只在这里做一次 ——
|
||||
* 每个题型组件里各写一遍上报,早晚会漏掉一两个。
|
||||
@@ -36,6 +60,8 @@ watch(
|
||||
)
|
||||
|
||||
function onAttempt(payload: { correct: boolean; answer?: string }) {
|
||||
// 做对了就告诉页面(目录、大纲、课末的进度跟着变),没登录也算 —— 只是不留痕
|
||||
if (payload.correct) emit("solved", props.exercise.id)
|
||||
if (!userStore.isAuthed || solved) return
|
||||
const answer = payload.answer ?? ""
|
||||
if (answer === lastAnswer) return
|
||||
@@ -46,31 +72,95 @@ function onAttempt(payload: { correct: boolean; answer?: string }) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ExerciseMcq v-if="exercise.type === 'mcq'" :exercise="exercise" @attempt="onAttempt" />
|
||||
<ExerciseSort
|
||||
v-else-if="exercise.type === 'sort'"
|
||||
:exercise="exercise"
|
||||
:lang="lang"
|
||||
@attempt="onAttempt"
|
||||
/>
|
||||
<ExerciseFill
|
||||
v-else-if="exercise.type === 'fill'"
|
||||
:exercise="exercise"
|
||||
:lang="lang"
|
||||
@attempt="onAttempt"
|
||||
/>
|
||||
<ExerciseMatch v-else-if="exercise.type === 'match'" :exercise="exercise" @attempt="onAttempt" />
|
||||
<ExercisePredict
|
||||
v-else-if="exercise.type === 'predict'"
|
||||
:exercise="exercise"
|
||||
:lang="lang"
|
||||
@attempt="onAttempt"
|
||||
/>
|
||||
<ExerciseDebug
|
||||
v-else-if="exercise.type === 'debug'"
|
||||
:exercise="exercise"
|
||||
:lang="lang"
|
||||
@attempt="onAttempt"
|
||||
/>
|
||||
<ExerciseGroup v-else-if="exercise.type === 'group'" :exercise="exercise" @attempt="onAttempt" />
|
||||
<section class="exercise" :id="`exercise-${exercise.id}`">
|
||||
<header class="head">
|
||||
<span class="tag">练一练</span>
|
||||
<span class="kind">{{ kind }}</span>
|
||||
<span class="grow" />
|
||||
<span v-if="solved" class="done"><Icon icon="ph:check-bold" :width="13" />做对过</span>
|
||||
<span class="num">第 {{ index }} 道 · 共 {{ total }} 道</span>
|
||||
</header>
|
||||
<ExerciseMcq v-if="exercise.type === 'mcq'" :exercise="exercise" @attempt="onAttempt" />
|
||||
<ExerciseSort
|
||||
v-else-if="exercise.type === 'sort'"
|
||||
:exercise="exercise"
|
||||
:lang="lang"
|
||||
@attempt="onAttempt"
|
||||
/>
|
||||
<ExerciseFill
|
||||
v-else-if="exercise.type === 'fill'"
|
||||
:exercise="exercise"
|
||||
:lang="lang"
|
||||
@attempt="onAttempt"
|
||||
/>
|
||||
<ExerciseMatch
|
||||
v-else-if="exercise.type === 'match'"
|
||||
:exercise="exercise"
|
||||
@attempt="onAttempt"
|
||||
/>
|
||||
<ExercisePredict
|
||||
v-else-if="exercise.type === 'predict'"
|
||||
:exercise="exercise"
|
||||
:lang="lang"
|
||||
@attempt="onAttempt"
|
||||
/>
|
||||
<ExerciseDebug
|
||||
v-else-if="exercise.type === 'debug'"
|
||||
:exercise="exercise"
|
||||
:lang="lang"
|
||||
@attempt="onAttempt"
|
||||
/>
|
||||
<ExerciseGroup
|
||||
v-else-if="exercise.type === 'group'"
|
||||
:exercise="exercise"
|
||||
@attempt="onAttempt"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.exercise {
|
||||
margin: 0 0 18px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid var(--learn-line);
|
||||
border-radius: 8px;
|
||||
scroll-margin-top: 16px;
|
||||
}
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.tag {
|
||||
height: 22px;
|
||||
padding: 0 8px;
|
||||
border-radius: 3px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background: var(--learn-green-bg);
|
||||
color: var(--learn-green-text);
|
||||
}
|
||||
.kind {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.grow {
|
||||
flex: 1;
|
||||
}
|
||||
.done {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
color: #18a058;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.num {
|
||||
font-size: 12px;
|
||||
opacity: 0.55;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user