1 parent
23844ae301
commit
d25b5dbe6d
268 files changed
+3684
-8861
No files matched your search
@@ -102,9 +102,7 @@ function reset() {
|
||||
:style="lineStyle(idx)"
|
||||
@click="toggle(idx)"
|
||||
>
|
||||
<span
|
||||
style="color: #bbb; width: 22px; text-align: right; flex-shrink: 0"
|
||||
>
|
||||
<span style="color: #bbb; width: 22px; text-align: right; flex-shrink: 0">
|
||||
{{ idx + 1 }}
|
||||
</span>
|
||||
<span v-html="lineHtml[idx]" style="white-space: pre" />
|
||||
|
||||
@@ -36,9 +36,7 @@ const segments = computed<Segment[]>(() => {
|
||||
return result
|
||||
})
|
||||
|
||||
const blankCount = computed(
|
||||
() => segments.value.filter((s) => s.type === "blank").length,
|
||||
)
|
||||
const blankCount = computed(() => segments.value.filter((s) => s.type === "blank").length)
|
||||
const userInputs = ref<string[]>([])
|
||||
const wrongBlanks = ref<Set<number>>(new Set())
|
||||
const allCorrect = ref(false)
|
||||
@@ -134,9 +132,7 @@ function inputWidth(idx: number): string {
|
||||
/>
|
||||
|
||||
<n-space style="margin-top: 12px" :size="8">
|
||||
<n-button type="warning" :disabled="allCorrect" @click="submit">
|
||||
提交
|
||||
</n-button>
|
||||
<n-button type="warning" :disabled="allCorrect" @click="submit"> 提交 </n-button>
|
||||
<n-button @click="reset">重置</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
@@ -25,9 +25,7 @@ onMounted(init)
|
||||
watch(() => props.exercise.id, init)
|
||||
|
||||
const allPlaced = computed(() => placement.value.every((p) => p !== -1))
|
||||
const allCorrect = computed(() =>
|
||||
placement.value.every((p, i) => p === data.value.answer[i]),
|
||||
)
|
||||
const allCorrect = computed(() => placement.value.every((p, i) => p === data.value.answer[i]))
|
||||
const locked = computed(() => submitted.value && allCorrect.value)
|
||||
|
||||
function onDragStart(i: number) {
|
||||
@@ -42,9 +40,7 @@ function dropTo(bucket: number) {
|
||||
submitted.value = false
|
||||
}
|
||||
|
||||
const poolItems = computed(() =>
|
||||
order.value.filter((i) => placement.value[i] === -1),
|
||||
)
|
||||
const poolItems = computed(() => order.value.filter((i) => placement.value[i] === -1))
|
||||
function itemsIn(bucket: number): number[] {
|
||||
return order.value.filter((i) => placement.value[i] === bucket)
|
||||
}
|
||||
@@ -57,11 +53,7 @@ function itemStatus(i: number): "correct" | "wrong" | "default" {
|
||||
function chipStyle(i: number): Record<string, string> {
|
||||
const status = itemStatus(i)
|
||||
const color =
|
||||
status === "correct"
|
||||
? "#18a058"
|
||||
: status === "wrong"
|
||||
? "#d03050"
|
||||
: "var(--n-border-color)"
|
||||
status === "correct" ? "#18a058" : status === "wrong" ? "#d03050" : "var(--n-border-color)"
|
||||
const plain = color === "var(--n-border-color)"
|
||||
return {
|
||||
padding: "6px 12px",
|
||||
@@ -114,10 +106,7 @@ function reset() {
|
||||
@dragover.prevent
|
||||
@drop="dropTo(-1)"
|
||||
>
|
||||
<span
|
||||
v-if="poolItems.length === 0"
|
||||
style="color: var(--n-text-color-3); font-size: 13px"
|
||||
>
|
||||
<span v-if="poolItems.length === 0" style="color: var(--n-text-color-3); font-size: 13px">
|
||||
(已全部归类)
|
||||
</span>
|
||||
<div
|
||||
@@ -150,14 +139,7 @@ function reset() {
|
||||
@dragover.prevent
|
||||
@drop="dropTo(b)"
|
||||
>
|
||||
<p
|
||||
style="
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
"
|
||||
>
|
||||
<p style="font-weight: 600; margin: 0 0 8px; text-align: center; font-size: 14px">
|
||||
{{ bucket }}
|
||||
</p>
|
||||
<n-space :size="8">
|
||||
@@ -182,9 +164,7 @@ function reset() {
|
||||
/>
|
||||
|
||||
<n-space style="margin-top: 12px" :size="8">
|
||||
<n-button type="warning" :disabled="!allPlaced || locked" @click="submit">
|
||||
提交
|
||||
</n-button>
|
||||
<n-button type="warning" :disabled="!allPlaced || locked" @click="submit"> 提交 </n-button>
|
||||
<n-button @click="reset">重置</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
@@ -37,9 +37,7 @@ onMounted(init)
|
||||
watch(() => props.exercise.id, init)
|
||||
|
||||
const allPaired = computed(() => pairs.value.every((p) => p !== null))
|
||||
const allCorrect = computed(() =>
|
||||
pairs.value.every((p, i) => p === data.value.answer[i]),
|
||||
)
|
||||
const allCorrect = computed(() => pairs.value.every((p, i) => p === data.value.answer[i]))
|
||||
const locked = computed(() => submitted.value && allCorrect.value)
|
||||
|
||||
function leftOf(rightIdx: number): number {
|
||||
@@ -183,9 +181,7 @@ function dotStyle(color: string): Record<string, string> {
|
||||
/>
|
||||
|
||||
<n-space style="margin-top: 12px" :size="8">
|
||||
<n-button type="primary" :disabled="!allPaired || locked" @click="submit">
|
||||
提交
|
||||
</n-button>
|
||||
<n-button type="primary" :disabled="!allPaired || locked" @click="submit"> 提交 </n-button>
|
||||
<n-button @click="reset">重置</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
@@ -33,8 +33,7 @@ function submit() {
|
||||
if (selected.value.size === 0 || correct.value) return
|
||||
const answer = new Set(data.value.answer)
|
||||
const sel = selected.value
|
||||
const isEqual =
|
||||
sel.size === answer.size && [...sel].every((v) => answer.has(v))
|
||||
const isEqual = sel.size === answer.size && [...sel].every((v) => answer.has(v))
|
||||
emit("attempt", { correct: isEqual, answer: describe(sel) })
|
||||
if (isEqual) {
|
||||
correct.value = true
|
||||
@@ -105,9 +104,7 @@ function optionType(idx: number): "default" | "primary" | "success" {
|
||||
@click="select(idx)"
|
||||
>
|
||||
<template #icon>
|
||||
<span style="font-weight: 700">{{
|
||||
String.fromCharCode(65 + idx)
|
||||
}}</span>
|
||||
<span style="font-weight: 700">{{ String.fromCharCode(65 + idx) }}</span>
|
||||
</template>
|
||||
{{ opt }}
|
||||
</n-button>
|
||||
@@ -116,18 +113,12 @@ function optionType(idx: number): "default" | "primary" | "success" {
|
||||
<n-alert
|
||||
v-if="correct || wrong || partial"
|
||||
:type="correct ? 'success' : partial ? 'warning' : 'error'"
|
||||
:title="
|
||||
correct ? '正确!' : partial ? '部分正确,请重试' : '选择有误,请重试'
|
||||
"
|
||||
:title="correct ? '正确!' : partial ? '部分正确,请重试' : '选择有误,请重试'"
|
||||
style="margin-top: 12px"
|
||||
/>
|
||||
|
||||
<n-space style="margin-top: 12px" :size="8">
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="selected.size === 0 || correct"
|
||||
@click="submit"
|
||||
>
|
||||
<n-button type="primary" :disabled="selected.size === 0 || correct" @click="submit">
|
||||
提交
|
||||
</n-button>
|
||||
<n-button @click="reset">重置</n-button>
|
||||
|
||||
@@ -96,13 +96,7 @@ function reset() {
|
||||
/>
|
||||
|
||||
<n-space style="margin-top: 12px" :size="8">
|
||||
<n-button
|
||||
type="error"
|
||||
:disabled="submitted && allCorrect"
|
||||
@click="submit"
|
||||
>
|
||||
提交
|
||||
</n-button>
|
||||
<n-button type="error" :disabled="submitted && allCorrect" @click="submit"> 提交 </n-button>
|
||||
<n-button @click="reset">重置</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
@@ -17,9 +17,7 @@ const lines = ref<LineItem[]>([])
|
||||
const submitted = ref(false)
|
||||
|
||||
function init() {
|
||||
const shuffled = shuffle(
|
||||
data.value.lines.map((text, idx) => ({ originalIdx: idx, text })),
|
||||
)
|
||||
const shuffled = shuffle(data.value.lines.map((text, idx) => ({ originalIdx: idx, text })))
|
||||
// 打乱后若恰好与原顺序一致,交换前两项,避免一进入就是已解出状态
|
||||
const isCorrect = shuffled.every((item, i) => item.originalIdx === i)
|
||||
if (isCorrect && shuffled.length > 1) {
|
||||
@@ -53,9 +51,7 @@ function lineStatus(idx: number): "correct" | "wrong" | "default" {
|
||||
return lines.value[idx].originalIdx === idx ? "correct" : "wrong"
|
||||
}
|
||||
|
||||
const allCorrect = computed(() =>
|
||||
lines.value.every((item, i) => item.originalIdx === i),
|
||||
)
|
||||
const allCorrect = computed(() => lines.value.every((item, i) => item.originalIdx === i))
|
||||
|
||||
function submit() {
|
||||
submitted.value = true
|
||||
@@ -70,9 +66,7 @@ function reset() {
|
||||
init()
|
||||
}
|
||||
|
||||
const lineHtml = computed<string[]>(() =>
|
||||
highlightLines(data.value.lines, props.lang),
|
||||
)
|
||||
const lineHtml = computed<string[]>(() => highlightLines(data.value.lines, props.lang))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -130,9 +124,7 @@ const lineHtml = computed<string[]>(() =>
|
||||
/>
|
||||
|
||||
<n-space style="margin-top: 12px" :size="8">
|
||||
<n-button type="info" :disabled="submitted && allCorrect" @click="submit">
|
||||
提交
|
||||
</n-button>
|
||||
<n-button type="info" :disabled="submitted && allCorrect" @click="submit"> 提交 </n-button>
|
||||
<n-button @click="reset">重置</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
@@ -7,9 +7,7 @@ const ExerciseMcq = defineAsyncComponent(() => import("./ExerciseMcq.vue"))
|
||||
const ExerciseSort = defineAsyncComponent(() => import("./ExerciseSort.vue"))
|
||||
const ExerciseFill = defineAsyncComponent(() => import("./ExerciseFill.vue"))
|
||||
const ExerciseMatch = defineAsyncComponent(() => import("./ExerciseMatch.vue"))
|
||||
const ExercisePredict = defineAsyncComponent(
|
||||
() => import("./ExercisePredict.vue"),
|
||||
)
|
||||
const ExercisePredict = defineAsyncComponent(() => import("./ExercisePredict.vue"))
|
||||
const ExerciseDebug = defineAsyncComponent(() => import("./ExerciseDebug.vue"))
|
||||
const ExerciseGroup = defineAsyncComponent(() => import("./ExerciseGroup.vue"))
|
||||
|
||||
@@ -48,11 +46,7 @@ function onAttempt(payload: { correct: boolean; answer?: string }) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ExerciseMcq
|
||||
v-if="exercise.type === 'mcq'"
|
||||
:exercise="exercise"
|
||||
@attempt="onAttempt"
|
||||
/>
|
||||
<ExerciseMcq v-if="exercise.type === 'mcq'" :exercise="exercise" @attempt="onAttempt" />
|
||||
<ExerciseSort
|
||||
v-else-if="exercise.type === 'sort'"
|
||||
:exercise="exercise"
|
||||
@@ -65,11 +59,7 @@ function onAttempt(payload: { correct: boolean; answer?: string }) {
|
||||
:lang="lang"
|
||||
@attempt="onAttempt"
|
||||
/>
|
||||
<ExerciseMatch
|
||||
v-else-if="exercise.type === 'match'"
|
||||
:exercise="exercise"
|
||||
@attempt="onAttempt"
|
||||
/>
|
||||
<ExerciseMatch v-else-if="exercise.type === 'match'" :exercise="exercise" @attempt="onAttempt" />
|
||||
<ExercisePredict
|
||||
v-else-if="exercise.type === 'predict'"
|
||||
:exercise="exercise"
|
||||
@@ -82,9 +72,5 @@ function onAttempt(payload: { correct: boolean; answer?: string }) {
|
||||
:lang="lang"
|
||||
@attempt="onAttempt"
|
||||
/>
|
||||
<ExerciseGroup
|
||||
v-else-if="exercise.type === 'group'"
|
||||
:exercise="exercise"
|
||||
@attempt="onAttempt"
|
||||
/>
|
||||
<ExerciseGroup v-else-if="exercise.type === 'group'" :exercise="exercise" @attempt="onAttempt" />
|
||||
</template>
|
||||
@@ -10,18 +10,14 @@ const props = defineProps<{
|
||||
|
||||
const stats = computed(() => {
|
||||
const rows = props.titles.map((t) => props.progress[t.id])
|
||||
const read = rows.filter(
|
||||
(p) => p && p.totalSeconds >= TUTORIAL_READ_SECONDS,
|
||||
).length
|
||||
const read = rows.filter((p) => p && p.totalSeconds >= TUTORIAL_READ_SECONDS).length
|
||||
const solved = rows.reduce((n, p) => n + (p?.exerciseSolved ?? 0), 0)
|
||||
const total = rows.reduce((n, p) => n + (p?.exerciseTotal ?? 0), 0)
|
||||
return { read, solved, total }
|
||||
})
|
||||
|
||||
const percent = computed(() =>
|
||||
props.titles.length
|
||||
? Math.round((stats.value.read / props.titles.length) * 100)
|
||||
: 0,
|
||||
props.titles.length ? Math.round((stats.value.read / props.titles.length) * 100) : 0,
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -36,9 +32,7 @@ const percent = computed(() =>
|
||||
/>
|
||||
<n-text depth="3" class="numbers">
|
||||
已读 {{ stats.read }}/{{ titles.length }} 课
|
||||
<template v-if="stats.total">
|
||||
· 练一练 {{ stats.solved }}/{{ stats.total }}
|
||||
</template>
|
||||
<template v-if="stats.total"> · 练一练 {{ stats.solved }}/{{ stats.total }} </template>
|
||||
</n-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -6,9 +6,7 @@ import type { Segment } from "../composables/useExerciseParse"
|
||||
defineProps<{ segments: Segment[]; lang?: string }>()
|
||||
|
||||
const isDark = useDark()
|
||||
const ExerciseWidget = defineAsyncComponent(
|
||||
() => import("./ExerciseWidget.vue"),
|
||||
)
|
||||
const ExerciseWidget = defineAsyncComponent(() => import("./ExerciseWidget.vue"))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -55,9 +55,7 @@ function hint(id: number) {
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
<n-text v-if="!traced" depth="3" class="login-tip">
|
||||
登录后可以记录学习进度
|
||||
</n-text>
|
||||
<n-text v-if="!traced" depth="3" class="login-tip"> 登录后可以记录学习进度 </n-text>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -8,9 +8,7 @@ const emit = defineEmits<{ go: [lesson: number] }>()
|
||||
|
||||
<template>
|
||||
<nav class="pager" :style="{ background: theme.bodyColor }">
|
||||
<n-button secondary :disabled="step <= 1" @click="emit('go', step - 1)">
|
||||
← 上一课
|
||||
</n-button>
|
||||
<n-button secondary :disabled="step <= 1" @click="emit('go', step - 1)"> ← 上一课 </n-button>
|
||||
<n-text depth="3">{{ step }} / {{ total }}</n-text>
|
||||
<n-button
|
||||
type="primary"
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import type { Exercise } from "utils/types"
|
||||
|
||||
export type Segment =
|
||||
| { type: "md"; content: string }
|
||||
| { type: "exercise"; exercise: Exercise }
|
||||
export type Segment = { type: "md"; content: string } | { type: "exercise"; exercise: Exercise }
|
||||
|
||||
export function parseExercises(
|
||||
content: string,
|
||||
exercises: Exercise[],
|
||||
): Segment[] {
|
||||
export function parseExercises(content: string, exercises: Exercise[]): Segment[] {
|
||||
const exerciseMap = new Map(exercises.map((e) => [e.id, e]))
|
||||
const segments: Segment[] = []
|
||||
const regex = /\[\[exercise:(\d+)\]\]/g
|
||||
|
||||
@@ -2,17 +2,9 @@
|
||||
<div class="learn-container">
|
||||
<template v-if="tutorial.id">
|
||||
<!-- 桌面端:目录 | 正文(居中限宽) | 可收起的示例代码 -->
|
||||
<div
|
||||
v-if="isDesktop"
|
||||
class="learn-layout"
|
||||
:class="{ 'with-code': codeOpen }"
|
||||
>
|
||||
<div v-if="isDesktop" class="learn-layout" :class="{ 'with-code': codeOpen }">
|
||||
<aside class="rail">
|
||||
<LearnSummary
|
||||
:titles="titles"
|
||||
:progress="progress"
|
||||
:traced="traced"
|
||||
/>
|
||||
<LearnSummary :titles="titles" :progress="progress" :traced="traced" />
|
||||
<LessonList
|
||||
:titles="titles"
|
||||
:step="step"
|
||||
@@ -28,12 +20,7 @@
|
||||
<n-text depth="3">第 {{ step }} / {{ titles.length }} 课</n-text>
|
||||
<n-flex align="center" justify="space-between" :wrap="false">
|
||||
<span />
|
||||
<n-button
|
||||
v-if="tutorial.code"
|
||||
size="small"
|
||||
secondary
|
||||
@click="codeOpen = !codeOpen"
|
||||
>
|
||||
<n-button v-if="tutorial.code" size="small" secondary @click="codeOpen = !codeOpen">
|
||||
{{ codeOpen ? "收起示例代码" : "展开示例代码" }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
@@ -44,11 +31,7 @@
|
||||
</main>
|
||||
|
||||
<aside v-if="tutorial.code && codeOpen" class="code-panel">
|
||||
<CodeEditor
|
||||
:language="editorLanguage"
|
||||
v-model="tutorial.code"
|
||||
height="100%"
|
||||
/>
|
||||
<CodeEditor :language="editorLanguage" v-model="tutorial.code" height="100%" />
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -76,27 +59,13 @@
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<n-empty
|
||||
v-if="isEmpty"
|
||||
description="该教程还没有公开"
|
||||
style="margin-top: 80px"
|
||||
/>
|
||||
<n-empty v-if="isEmpty" description="该教程还没有公开" style="margin-top: 80px" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
Tutorial,
|
||||
Exercise,
|
||||
LANGUAGE,
|
||||
TutorialProgress,
|
||||
} from "utils/types"
|
||||
import {
|
||||
getTutorial,
|
||||
getTutorials,
|
||||
getExercises,
|
||||
getLearnProgress,
|
||||
} from "../api"
|
||||
import type { Tutorial, Exercise, LANGUAGE, TutorialProgress } from "utils/types"
|
||||
import { getTutorial, getTutorials, getExercises, getLearnProgress } from "../api"
|
||||
import { parseExercises } from "./composables/useExerciseParse"
|
||||
import { useLearnTrace } from "./composables/useLearnTrace"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
@@ -106,9 +75,7 @@ import LessonList from "./components/LessonList.vue"
|
||||
import LearnSummary from "./components/LearnSummary.vue"
|
||||
import LessonBody from "./components/LessonBody.vue"
|
||||
import PagerBar from "./components/PagerBar.vue"
|
||||
const CodeEditor = defineAsyncComponent(
|
||||
() => import("shared/components/CodeEditor.vue"),
|
||||
)
|
||||
const CodeEditor = defineAsyncComponent(() => import("shared/components/CodeEditor.vue"))
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -125,9 +92,7 @@ const step = computed(() => {
|
||||
return parseInt(value)
|
||||
})
|
||||
|
||||
const type = computed<"python" | "c">(() =>
|
||||
route.params.type === "c" ? "c" : "python",
|
||||
)
|
||||
const type = computed<"python" | "c">(() => (route.params.type === "c" ? "c" : "python"))
|
||||
|
||||
const tutorial = ref<Partial<Tutorial>>({
|
||||
id: 0,
|
||||
@@ -136,9 +101,7 @@ const tutorial = ref<Partial<Tutorial>>({
|
||||
code: "",
|
||||
})
|
||||
|
||||
const editorLanguage = computed<LANGUAGE>(() =>
|
||||
tutorial.value.type === "c" ? "C" : "Python",
|
||||
)
|
||||
const editorLanguage = computed<LANGUAGE>(() => (tutorial.value.type === "c" ? "C" : "Python"))
|
||||
const titles = ref<{ id: number; title: string }[]>([])
|
||||
const progress = ref<Record<number, TutorialProgress>>({})
|
||||
const exercises = ref<Exercise[]>([])
|
||||
@@ -147,9 +110,7 @@ const activeTab = ref("content")
|
||||
const codeOpen = useStorage("oj2:learn-code-open", true)
|
||||
const isEmpty = ref(false)
|
||||
|
||||
const segments = computed(() =>
|
||||
parseExercises(tutorial.value.content ?? "", exercises.value),
|
||||
)
|
||||
const segments = computed(() => parseExercises(tutorial.value.content ?? "", exercises.value))
|
||||
|
||||
// 留痕的计时器。tutorial.id 变了才算换课 —— 用 step 会在内容还没加载好时就上报
|
||||
useLearnTrace(
|
||||
@@ -159,9 +120,7 @@ useLearnTrace(
|
||||
|
||||
function goToLesson(lessonNumber: number) {
|
||||
activeTab.value = "content"
|
||||
router.push(
|
||||
`/learn/${type.value}/${lessonNumber.toString().padStart(2, "0")}`,
|
||||
)
|
||||
router.push(`/learn/${type.value}/${lessonNumber.toString().padStart(2, "0")}`)
|
||||
}
|
||||
/**
|
||||
* 拉自己的自学留痕,给目录打勾。失败就当没有 —— 目录少几个勾不影响上课,
|
||||
@@ -174,9 +133,7 @@ async function loadProgress() {
|
||||
}
|
||||
try {
|
||||
const rows = await getLearnProgress(type.value)
|
||||
progress.value = Object.fromEntries(
|
||||
rows.map((row) => [row.tutorialId, row]),
|
||||
)
|
||||
progress.value = Object.fromEntries(rows.map((row) => [row.tutorialId, row]))
|
||||
} catch {
|
||||
progress.value = {}
|
||||
}
|
||||
@@ -188,10 +145,7 @@ async function init() {
|
||||
isEmpty.value = titles.value.length === 0
|
||||
if (isEmpty.value) return
|
||||
const id = titles.value[step.value - 1].id
|
||||
const [res2, exs] = await Promise.allSettled([
|
||||
getTutorial(id),
|
||||
getExercises(id),
|
||||
])
|
||||
const [res2, exs] = await Promise.allSettled([getTutorial(id), getExercises(id)])
|
||||
if (res2.status === "fulfilled") tutorial.value = res2.value
|
||||
exercises.value = exs.status === "fulfilled" ? exs.value : []
|
||||
learnStep.value[type.value] = step.value
|
||||
|
||||
Reference in new issue
Block a user