add new type of self-learning
This commit is contained in:
@@ -8,6 +8,7 @@ import type {
|
||||
BlankProblem,
|
||||
Contest,
|
||||
Exercise,
|
||||
ExerciseType,
|
||||
Server,
|
||||
TestcaseUploadedReturns,
|
||||
Tutorial,
|
||||
@@ -272,7 +273,7 @@ export async function getAdminExercises(tutorialId: number) {
|
||||
|
||||
export async function createExercise(data: {
|
||||
tutorial_id: number
|
||||
type: "mcq" | "sort" | "fill"
|
||||
type: ExerciseType
|
||||
data: object
|
||||
order: number
|
||||
}) {
|
||||
@@ -282,7 +283,7 @@ export async function createExercise(data: {
|
||||
|
||||
export async function updateExercise(data: {
|
||||
id: number
|
||||
type: "mcq" | "sort" | "fill"
|
||||
type: ExerciseType
|
||||
data: object
|
||||
order: number
|
||||
}) {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Exercise,
|
||||
ExerciseType,
|
||||
ExerciseMcqData,
|
||||
ExerciseSortData,
|
||||
ExerciseFillData,
|
||||
ExerciseMatchData,
|
||||
ExercisePredictData,
|
||||
ExerciseDebugData,
|
||||
ExerciseGroupData,
|
||||
} from "utils/types"
|
||||
import {
|
||||
getAdminExercises,
|
||||
@@ -19,7 +24,7 @@ const dialog = useDialog()
|
||||
const exercises = ref<Exercise[]>([])
|
||||
const showForm = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
const formType = ref<"mcq" | "sort" | "fill">("mcq")
|
||||
const formType = ref<ExerciseType>("mcq")
|
||||
const formOrder = ref(0)
|
||||
|
||||
const mcqQuestion = ref("")
|
||||
@@ -32,16 +37,34 @@ const sortCode = ref("")
|
||||
const fillQuestion = ref("")
|
||||
const fillCode = ref("")
|
||||
|
||||
const matchQuestion = ref("")
|
||||
const matchLeft = ref("")
|
||||
const matchRight = ref("")
|
||||
|
||||
const predictQuestion = ref("")
|
||||
const predictCode = ref("")
|
||||
const predictAnswer = ref("")
|
||||
|
||||
const debugQuestion = ref("")
|
||||
const debugCode = ref("")
|
||||
const debugAnswer = ref<number[]>([])
|
||||
const debugExplanation = ref("")
|
||||
|
||||
const groupQuestion = ref("")
|
||||
const groupBuckets = ref("")
|
||||
const groupItems = ref("")
|
||||
|
||||
const debugLines = computed(() =>
|
||||
debugCode.value === "" ? [] : debugCode.value.split("\n"),
|
||||
)
|
||||
|
||||
async function load() {
|
||||
exercises.value = await getAdminExercises(props.tutorialId)
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = null
|
||||
formType.value = "mcq"
|
||||
formOrder.value = exercises.value.length
|
||||
function resetForms() {
|
||||
mcqQuestion.value = ""
|
||||
mcqOptions.value = ["", ""]
|
||||
mcqAnswer.value = []
|
||||
@@ -49,6 +72,26 @@ function openCreate() {
|
||||
sortCode.value = ""
|
||||
fillQuestion.value = ""
|
||||
fillCode.value = ""
|
||||
matchQuestion.value = ""
|
||||
matchLeft.value = ""
|
||||
matchRight.value = ""
|
||||
predictQuestion.value = ""
|
||||
predictCode.value = ""
|
||||
predictAnswer.value = ""
|
||||
debugQuestion.value = ""
|
||||
debugCode.value = ""
|
||||
debugAnswer.value = []
|
||||
debugExplanation.value = ""
|
||||
groupQuestion.value = ""
|
||||
groupBuckets.value = ""
|
||||
groupItems.value = ""
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = null
|
||||
formType.value = "mcq"
|
||||
formOrder.value = exercises.value.length
|
||||
resetForms()
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
@@ -56,6 +99,7 @@ function openEdit(ex: Exercise) {
|
||||
editingId.value = ex.id
|
||||
formType.value = ex.type
|
||||
formOrder.value = ex.order
|
||||
resetForms()
|
||||
if (ex.type === "mcq") {
|
||||
const d = ex.data as ExerciseMcqData
|
||||
mcqQuestion.value = d.question
|
||||
@@ -65,10 +109,34 @@ function openEdit(ex: Exercise) {
|
||||
const d = ex.data as ExerciseSortData
|
||||
sortQuestion.value = d.question
|
||||
sortCode.value = d.lines.join("\n")
|
||||
} else {
|
||||
} else if (ex.type === "fill") {
|
||||
const d = ex.data as ExerciseFillData
|
||||
fillQuestion.value = d.question
|
||||
fillCode.value = d.code
|
||||
} else if (ex.type === "match") {
|
||||
const d = ex.data as ExerciseMatchData
|
||||
matchQuestion.value = d.question
|
||||
matchLeft.value = d.left.join("\n")
|
||||
// 按答案顺序还原右列,重存时识别答案保持为顺序对应
|
||||
matchRight.value = d.answer.map((a) => d.right[a]).join("\n")
|
||||
} else if (ex.type === "predict") {
|
||||
const d = ex.data as ExercisePredictData
|
||||
predictQuestion.value = d.question
|
||||
predictCode.value = d.code
|
||||
predictAnswer.value = d.answer.join("\n===\n")
|
||||
} else if (ex.type === "debug") {
|
||||
const d = ex.data as ExerciseDebugData
|
||||
debugQuestion.value = d.question
|
||||
debugCode.value = d.lines.join("\n")
|
||||
debugAnswer.value = [...d.answer]
|
||||
debugExplanation.value = d.explanation ?? ""
|
||||
} else if (ex.type === "group") {
|
||||
const d = ex.data as ExerciseGroupData
|
||||
groupQuestion.value = d.question
|
||||
groupBuckets.value = d.buckets.join("\n")
|
||||
groupItems.value = d.items
|
||||
.map((it, i) => `${it} => ${d.buckets[d.answer[i]]}`)
|
||||
.join("\n")
|
||||
}
|
||||
showForm.value = true
|
||||
}
|
||||
@@ -79,26 +147,131 @@ function toggleAnswer(i: number) {
|
||||
else mcqAnswer.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (formType.value === "mcq" && mcqAnswer.value.length === 0) {
|
||||
message.error("请至少勾选一个正确答案")
|
||||
return
|
||||
}
|
||||
let data: Record<string, unknown>
|
||||
function toggleDebug(i: number) {
|
||||
const idx = debugAnswer.value.indexOf(i)
|
||||
if (idx === -1) debugAnswer.value.push(i)
|
||||
else debugAnswer.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
function splitLines(text: string): string[] {
|
||||
return text
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l !== "")
|
||||
}
|
||||
|
||||
function buildData(): Record<string, unknown> | null {
|
||||
if (formType.value === "mcq") {
|
||||
data = {
|
||||
if (mcqAnswer.value.length === 0) {
|
||||
message.error("请至少勾选一个正确答案")
|
||||
return null
|
||||
}
|
||||
return {
|
||||
question: mcqQuestion.value || "下面选项中正确是哪个?",
|
||||
options: mcqOptions.value,
|
||||
answer: mcqAnswer.value,
|
||||
}
|
||||
} else if (formType.value === "sort") {
|
||||
data = {
|
||||
}
|
||||
if (formType.value === "sort") {
|
||||
return {
|
||||
question: sortQuestion.value || "将下列代码行排列为正确顺序",
|
||||
lines: sortCode.value.split("\n").filter((l) => l.trim() !== ""),
|
||||
}
|
||||
} else {
|
||||
data = { question: fillQuestion.value, code: fillCode.value }
|
||||
}
|
||||
if (formType.value === "fill") {
|
||||
return { question: fillQuestion.value, code: fillCode.value }
|
||||
}
|
||||
if (formType.value === "match") {
|
||||
const left = splitLines(matchLeft.value)
|
||||
const right = splitLines(matchRight.value)
|
||||
if (left.length < 2 || left.length !== right.length) {
|
||||
message.error("左右两列需各至少 2 项且行数相等(按行一一对应)")
|
||||
return null
|
||||
}
|
||||
return {
|
||||
question: matchQuestion.value || "把左右两列正确连线",
|
||||
left,
|
||||
right,
|
||||
answer: left.map((_, i) => i),
|
||||
}
|
||||
}
|
||||
if (formType.value === "predict") {
|
||||
if (predictCode.value.trim() === "") {
|
||||
message.error("请填写代码")
|
||||
return null
|
||||
}
|
||||
const answer = predictAnswer.value
|
||||
.split(/\n===\n/)
|
||||
.map((a) => a.replace(/\s+$/, ""))
|
||||
.filter((a) => a.trim() !== "")
|
||||
if (answer.length === 0) {
|
||||
message.error("请填写至少一个正确输出")
|
||||
return null
|
||||
}
|
||||
return {
|
||||
question: predictQuestion.value || "这段代码会输出什么?",
|
||||
code: predictCode.value,
|
||||
answer,
|
||||
}
|
||||
}
|
||||
if (formType.value === "debug") {
|
||||
const lines = debugCode.value.split("\n")
|
||||
const answer = debugAnswer.value
|
||||
.filter((i) => i < lines.length)
|
||||
.sort((a, b) => a - b)
|
||||
if (lines.length === 0 || answer.length === 0) {
|
||||
message.error("请填写代码并勾选至少一行错误")
|
||||
return null
|
||||
}
|
||||
const data: Record<string, unknown> = {
|
||||
question: debugQuestion.value || "下面代码哪几行有错?",
|
||||
lines,
|
||||
answer,
|
||||
}
|
||||
if (debugExplanation.value.trim() !== "") {
|
||||
data.explanation = debugExplanation.value.trim()
|
||||
}
|
||||
return data
|
||||
}
|
||||
// group
|
||||
const buckets = splitLines(groupBuckets.value)
|
||||
if (buckets.length < 2) {
|
||||
message.error("请至少填写 2 个分组")
|
||||
return null
|
||||
}
|
||||
const items: string[] = []
|
||||
const answer: number[] = []
|
||||
for (const line of groupItems.value.split("\n")) {
|
||||
if (line.trim() === "") continue
|
||||
const parts = line.split("=>")
|
||||
if (parts.length !== 2) {
|
||||
message.error(`项目格式应为「项目 => 分组名」:${line}`)
|
||||
return null
|
||||
}
|
||||
const item = parts[0].trim()
|
||||
const bucket = buckets.indexOf(parts[1].trim())
|
||||
if (item === "" || bucket === -1) {
|
||||
message.error(`项目或分组名无效:${line}`)
|
||||
return null
|
||||
}
|
||||
items.push(item)
|
||||
answer.push(bucket)
|
||||
}
|
||||
if (items.length === 0) {
|
||||
message.error("请至少填写一个项目")
|
||||
return null
|
||||
}
|
||||
return {
|
||||
question: groupQuestion.value || "把下列项目归类到正确的分组",
|
||||
buckets,
|
||||
items,
|
||||
answer,
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const data = buildData()
|
||||
if (data === null) return
|
||||
|
||||
try {
|
||||
if (editingId.value) {
|
||||
@@ -143,16 +316,35 @@ function copyPlaceholder(id: number) {
|
||||
message.success(`已复制 [[exercise:${id}]]`)
|
||||
}
|
||||
|
||||
function typeName(type: string) {
|
||||
if (type === "mcq") return "选择题"
|
||||
if (type === "sort") return "代码排序"
|
||||
return "代码填空"
|
||||
const TYPE_NAMES: Record<ExerciseType, string> = {
|
||||
mcq: "选择题",
|
||||
sort: "代码排序",
|
||||
fill: "代码填空",
|
||||
match: "连线匹配",
|
||||
predict: "输出预测",
|
||||
debug: "代码找错",
|
||||
group: "归类分组",
|
||||
}
|
||||
|
||||
function typeTagType(type: string): "success" | "info" | "warning" {
|
||||
if (type === "mcq") return "success"
|
||||
if (type === "sort") return "info"
|
||||
return "warning"
|
||||
const TYPE_TAGS: Record<
|
||||
ExerciseType,
|
||||
"success" | "info" | "warning" | "error" | "primary" | "default"
|
||||
> = {
|
||||
mcq: "success",
|
||||
sort: "info",
|
||||
fill: "warning",
|
||||
match: "primary",
|
||||
predict: "error",
|
||||
debug: "info",
|
||||
group: "warning",
|
||||
}
|
||||
|
||||
function typeName(type: ExerciseType) {
|
||||
return TYPE_NAMES[type] ?? type
|
||||
}
|
||||
|
||||
function typeTagType(type: ExerciseType) {
|
||||
return TYPE_TAGS[type] ?? "default"
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -208,6 +400,10 @@ function typeTagType(type: string): "success" | "info" | "warning" {
|
||||
<n-radio value="mcq">选择题</n-radio>
|
||||
<n-radio value="sort">代码排序</n-radio>
|
||||
<n-radio value="fill">代码填空</n-radio>
|
||||
<n-radio value="match">连线匹配</n-radio>
|
||||
<n-radio value="predict">输出预测</n-radio>
|
||||
<n-radio value="debug">代码找错</n-radio>
|
||||
<n-radio value="group">归类分组</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
|
||||
@@ -251,7 +447,7 @@ function typeTagType(type: string): "success" | "info" | "warning" {
|
||||
@click="
|
||||
() => {
|
||||
mcqOptions.splice(i, 1)
|
||||
mcqAnswer.value = mcqAnswer.value
|
||||
mcqAnswer = mcqAnswer
|
||||
.filter((a) => a !== i)
|
||||
.map((a) => (a > i ? a - 1 : a))
|
||||
}
|
||||
@@ -287,7 +483,7 @@ function typeTagType(type: string): "success" | "info" | "warning" {
|
||||
</n-form-item>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<template v-else-if="formType === 'fill'">
|
||||
<n-form-item label="题目说明">
|
||||
<n-input
|
||||
v-model:value="fillQuestion"
|
||||
@@ -306,6 +502,143 @@ function typeTagType(type: string): "success" | "info" | "warning" {
|
||||
/>
|
||||
</n-form-item>
|
||||
</template>
|
||||
|
||||
<template v-else-if="formType === 'match'">
|
||||
<n-form-item label="题目说明">
|
||||
<n-input
|
||||
v-model:value="matchQuestion"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="例:把函数和它的功能连起来"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="左列(每行一项)">
|
||||
<n-input
|
||||
v-model:value="matchLeft"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="print len type"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="右列(与左列按行一一对应,保存后右列自动乱序)">
|
||||
<n-input
|
||||
v-model:value="matchRight"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="输出内容 返回长度 返回类型"
|
||||
/>
|
||||
</n-form-item>
|
||||
</template>
|
||||
|
||||
<template v-else-if="formType === 'predict'">
|
||||
<n-form-item label="题目说明">
|
||||
<n-input
|
||||
v-model:value="predictQuestion"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="例:这段代码会输出什么?"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="代码">
|
||||
<n-input
|
||||
v-model:value="predictCode"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
placeholder="print(1 + 2)"
|
||||
style="font-family: "Monaco""
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
label="正确输出(多个可接受答案之间用单独一行 === 分隔)"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="predictAnswer"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="3"
|
||||
style="font-family: "Monaco""
|
||||
/>
|
||||
</n-form-item>
|
||||
</template>
|
||||
|
||||
<template v-else-if="formType === 'debug'">
|
||||
<n-form-item label="题目说明">
|
||||
<n-input
|
||||
v-model:value="debugQuestion"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="例:下面代码哪几行有错?"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="代码(每行一项)">
|
||||
<n-input
|
||||
v-model:value="debugCode"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
placeholder="在此粘贴含错误的代码"
|
||||
style="font-family: "Monaco""
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="勾选错误行">
|
||||
<n-space vertical style="width: 100%">
|
||||
<n-empty
|
||||
v-if="debugLines.length === 0"
|
||||
description="先填写代码"
|
||||
size="small"
|
||||
/>
|
||||
<n-flex
|
||||
v-for="(line, i) in debugLines"
|
||||
:key="i"
|
||||
align="center"
|
||||
:size="8"
|
||||
>
|
||||
<n-checkbox
|
||||
:checked="debugAnswer.includes(i)"
|
||||
@update:checked="toggleDebug(i)"
|
||||
/>
|
||||
<n-text style="font-family: Monaco; white-space: pre">
|
||||
{{ i + 1 }}. {{ line }}
|
||||
</n-text>
|
||||
</n-flex>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
<n-form-item label="错误说明(可选,提交后展示)">
|
||||
<n-input
|
||||
v-model:value="debugExplanation"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="例:第 2 行少了冒号"
|
||||
/>
|
||||
</n-form-item>
|
||||
</template>
|
||||
|
||||
<template v-else-if="formType === 'group'">
|
||||
<n-form-item label="题目说明">
|
||||
<n-input
|
||||
v-model:value="groupQuestion"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="例:把下面的值归类到正确的类型"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="分组(每行一个分组名)">
|
||||
<n-input
|
||||
v-model:value="groupBuckets"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="int float str"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="项目(每行「项目 => 分组名」)">
|
||||
<n-input
|
||||
v-model:value="groupItems"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="3 => int 3.14 => float hello => str"
|
||||
style="font-family: "Monaco""
|
||||
/>
|
||||
</n-form-item>
|
||||
</template>
|
||||
</n-form>
|
||||
|
||||
<template #footer>
|
||||
|
||||
121
src/oj/learn/components/ExerciseDebug.vue
Normal file
121
src/oj/learn/components/ExerciseDebug.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { Exercise, ExerciseDebugData } from "utils/types"
|
||||
import { highlightLines } from "../composables/useCodeHighlight"
|
||||
import "./exercise-highlight.css"
|
||||
|
||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||
const data = computed(() => props.exercise.data as ExerciseDebugData)
|
||||
|
||||
const lineHtml = computed(() => highlightLines(data.value.lines, props.lang))
|
||||
const selected = ref<Set<number>>(new Set())
|
||||
const submitted = ref(false)
|
||||
|
||||
watch(() => props.exercise.id, reset, { immediate: true })
|
||||
|
||||
const allCorrect = computed(() => {
|
||||
const ans = new Set(data.value.answer)
|
||||
if (selected.value.size !== ans.size) return false
|
||||
for (const i of selected.value) if (!ans.has(i)) return false
|
||||
return true
|
||||
})
|
||||
const locked = computed(() => submitted.value && allCorrect.value)
|
||||
|
||||
function toggle(i: number) {
|
||||
if (locked.value) return
|
||||
submitted.value = false
|
||||
const s = new Set(selected.value)
|
||||
if (s.has(i)) s.delete(i)
|
||||
else s.add(i)
|
||||
selected.value = s
|
||||
}
|
||||
|
||||
function lineStatus(i: number): "correct" | "wrong" | "selected" | "default" {
|
||||
if (!submitted.value) return selected.value.has(i) ? "selected" : "default"
|
||||
const isAns = data.value.answer.includes(i)
|
||||
const isSel = selected.value.has(i)
|
||||
if (isAns) return isSel ? "correct" : "wrong" // 漏选也标红
|
||||
if (isSel) return "wrong"
|
||||
return "default"
|
||||
}
|
||||
|
||||
function lineStyle(i: number): Record<string, string> {
|
||||
const status = lineStatus(i)
|
||||
const color =
|
||||
status === "correct"
|
||||
? "#18a058"
|
||||
: status === "wrong"
|
||||
? "#d03050"
|
||||
: status === "selected"
|
||||
? "#2080f0"
|
||||
: "var(--n-border-color)"
|
||||
const plain = color === "var(--n-border-color)"
|
||||
return {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
padding: "6px 12px",
|
||||
borderRadius: "6px",
|
||||
border: `1.5px solid ${color}`,
|
||||
background: plain ? "transparent" : color + "14",
|
||||
cursor: locked.value ? "default" : "pointer",
|
||||
fontFamily: "Monaco",
|
||||
userSelect: "none",
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
submitted.value = true
|
||||
}
|
||||
|
||||
function reset() {
|
||||
selected.value = new Set()
|
||||
submitted.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card style="margin: 16px 0; border: 1.5px solid var(--n-border-color)">
|
||||
<template #header>
|
||||
<n-tag type="info" :bordered="false">练一练 · 代码找错</n-tag>
|
||||
</template>
|
||||
|
||||
<p style="font-weight: 500; font-size: 16px; margin-bottom: 8px">
|
||||
{{ data.question }}
|
||||
</p>
|
||||
<p style="color: var(--n-text-color-3); font-size: 13px; margin: 0 0 12px">
|
||||
点击你认为有错误的代码行(可多选)。
|
||||
</p>
|
||||
|
||||
<n-space vertical :size="6">
|
||||
<div
|
||||
v-for="(line, idx) in data.lines"
|
||||
:key="idx"
|
||||
:style="lineStyle(idx)"
|
||||
@click="toggle(idx)"
|
||||
>
|
||||
<span
|
||||
style="color: #bbb; width: 22px; text-align: right; flex-shrink: 0"
|
||||
>
|
||||
{{ idx + 1 }}
|
||||
</span>
|
||||
<span v-html="lineHtml[idx]" style="white-space: pre" />
|
||||
</div>
|
||||
</n-space>
|
||||
|
||||
<n-alert
|
||||
v-if="submitted"
|
||||
:type="allCorrect ? 'success' : 'error'"
|
||||
:title="allCorrect ? '找对了!' : '还没找全,红色行是错误所在'"
|
||||
style="margin-top: 12px"
|
||||
>
|
||||
<template v-if="submitted && data.explanation" #default>
|
||||
{{ data.explanation }}
|
||||
</template>
|
||||
</n-alert>
|
||||
|
||||
<n-space style="margin-top: 12px" :size="8">
|
||||
<n-button type="info" :disabled="locked" @click="submit">提交</n-button>
|
||||
<n-button @click="reset">重置</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</template>
|
||||
191
src/oj/learn/components/ExerciseGroup.vue
Normal file
191
src/oj/learn/components/ExerciseGroup.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<script setup lang="ts">
|
||||
import { Exercise, ExerciseGroupData } from "utils/types"
|
||||
|
||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||
const data = computed(() => props.exercise.data as ExerciseGroupData)
|
||||
|
||||
const order = ref<number[]>([]) // item 的稳定展示顺序(初始乱序)
|
||||
const placement = ref<number[]>([]) // placement[itemIdx] = 桶下标,-1 表示在池中
|
||||
const dragIdx = ref<number | null>(null)
|
||||
const submitted = ref(false)
|
||||
|
||||
function shuffle<T>(arr: T[]): T[] {
|
||||
const a = [...arr]
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1))
|
||||
;[a[i], a[j]] = [a[j], a[i]]
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
function init() {
|
||||
order.value = shuffle(data.value.items.map((_, i) => i))
|
||||
placement.value = Array(data.value.items.length).fill(-1)
|
||||
dragIdx.value = null
|
||||
submitted.value = false
|
||||
}
|
||||
|
||||
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 locked = computed(() => submitted.value && allCorrect.value)
|
||||
|
||||
function onDragStart(i: number) {
|
||||
if (locked.value) return
|
||||
dragIdx.value = i
|
||||
}
|
||||
|
||||
function dropTo(bucket: number) {
|
||||
if (locked.value || dragIdx.value === null) return
|
||||
placement.value[dragIdx.value] = bucket
|
||||
dragIdx.value = null
|
||||
submitted.value = false
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function itemStatus(i: number): "correct" | "wrong" | "default" {
|
||||
if (!submitted.value || placement.value[i] === -1) return "default"
|
||||
return placement.value[i] === data.value.answer[i] ? "correct" : "wrong"
|
||||
}
|
||||
|
||||
function chipStyle(i: number): Record<string, string> {
|
||||
const status = itemStatus(i)
|
||||
const color =
|
||||
status === "correct"
|
||||
? "#18a058"
|
||||
: status === "wrong"
|
||||
? "#d03050"
|
||||
: "var(--n-border-color)"
|
||||
const plain = color === "var(--n-border-color)"
|
||||
return {
|
||||
padding: "6px 12px",
|
||||
borderRadius: "6px",
|
||||
border: `1.5px solid ${color}`,
|
||||
background: plain ? "var(--n-color)" : color + "14",
|
||||
cursor: locked.value ? "default" : "grab",
|
||||
userSelect: "none",
|
||||
fontSize: "15px",
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
submitted.value = true
|
||||
}
|
||||
|
||||
function reset() {
|
||||
init()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card style="margin: 16px 0; border: 1.5px solid var(--n-border-color)">
|
||||
<template #header>
|
||||
<n-tag type="warning" :bordered="false">练一练 · 归类分组</n-tag>
|
||||
</template>
|
||||
|
||||
<p style="font-weight: 500; font-size: 16px; margin-bottom: 8px">
|
||||
{{ data.question }}
|
||||
</p>
|
||||
<p style="color: var(--n-text-color-3); font-size: 13px; margin: 0 0 12px">
|
||||
把下面的项目拖到对应的分组里,可在分组间拖动调整。
|
||||
</p>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '8px',
|
||||
minHeight: '48px',
|
||||
padding: '10px',
|
||||
border: '1.5px dashed var(--n-border-color)',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '14px',
|
||||
}"
|
||||
@dragover.prevent
|
||||
@drop="dropTo(-1)"
|
||||
>
|
||||
<span
|
||||
v-if="poolItems.length === 0"
|
||||
style="color: var(--n-text-color-3); font-size: 13px"
|
||||
>
|
||||
(已全部归类)
|
||||
</span>
|
||||
<div
|
||||
v-for="i in poolItems"
|
||||
:key="i"
|
||||
draggable="true"
|
||||
:style="chipStyle(i)"
|
||||
@dragstart="onDragStart(i)"
|
||||
>
|
||||
{{ data.items[i] }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${data.buckets.length}, 1fr)`,
|
||||
gap: '12px',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="(bucket, b) in data.buckets"
|
||||
:key="b"
|
||||
:style="{
|
||||
minHeight: '88px',
|
||||
padding: '10px',
|
||||
border: '1.5px solid var(--n-border-color)',
|
||||
borderRadius: '8px',
|
||||
}"
|
||||
@dragover.prevent
|
||||
@drop="dropTo(b)"
|
||||
>
|
||||
<p
|
||||
style="
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
"
|
||||
>
|
||||
{{ bucket }}
|
||||
</p>
|
||||
<n-space :size="8">
|
||||
<div
|
||||
v-for="i in itemsIn(b)"
|
||||
:key="i"
|
||||
draggable="true"
|
||||
:style="chipStyle(i)"
|
||||
@dragstart="onDragStart(i)"
|
||||
>
|
||||
{{ data.items[i] }}
|
||||
</div>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-alert
|
||||
v-if="submitted"
|
||||
:type="allCorrect ? 'success' : 'error'"
|
||||
:title="allCorrect ? '归类全部正确!' : '有归类错误,红色项需要调整'"
|
||||
style="margin-top: 12px"
|
||||
/>
|
||||
|
||||
<n-space style="margin-top: 12px" :size="8">
|
||||
<n-button type="warning" :disabled="!allPlaced || locked" @click="submit">
|
||||
提交
|
||||
</n-button>
|
||||
<n-button @click="reset">重置</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</template>
|
||||
192
src/oj/learn/components/ExerciseMatch.vue
Normal file
192
src/oj/learn/components/ExerciseMatch.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<script setup lang="ts">
|
||||
import { Exercise, ExerciseMatchData } from "utils/types"
|
||||
|
||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||
const data = computed(() => props.exercise.data as ExerciseMatchData)
|
||||
|
||||
const PALETTE = [
|
||||
"#2080f0",
|
||||
"#18a058",
|
||||
"#f0a020",
|
||||
"#d03050",
|
||||
"#8a2be2",
|
||||
"#0891b2",
|
||||
"#db2777",
|
||||
"#65a30d",
|
||||
]
|
||||
|
||||
const rightOrder = ref<number[]>([]) // 显示顺序里的 right 原始下标
|
||||
const pairs = ref<(number | null)[]>([]) // pairs[leftIdx] = 配对的 right 原始下标
|
||||
const selectedLeft = ref<number | null>(null)
|
||||
const submitted = ref(false)
|
||||
|
||||
function shuffle<T>(arr: T[]): T[] {
|
||||
const a = [...arr]
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1))
|
||||
;[a[i], a[j]] = [a[j], a[i]]
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
function init() {
|
||||
const n = data.value.right.length
|
||||
rightOrder.value = shuffle(Array.from({ length: n }, (_, i) => i))
|
||||
pairs.value = Array(data.value.left.length).fill(null)
|
||||
selectedLeft.value = null
|
||||
submitted.value = false
|
||||
}
|
||||
|
||||
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 locked = computed(() => submitted.value && allCorrect.value)
|
||||
|
||||
function leftOf(rightIdx: number): number {
|
||||
return pairs.value.findIndex((p) => p === rightIdx)
|
||||
}
|
||||
|
||||
function onLeftClick(i: number) {
|
||||
if (locked.value) return
|
||||
submitted.value = false
|
||||
if (pairs.value[i] !== null) {
|
||||
pairs.value[i] = null
|
||||
selectedLeft.value = i
|
||||
return
|
||||
}
|
||||
selectedLeft.value = selectedLeft.value === i ? null : i
|
||||
}
|
||||
|
||||
function onRightClick(rightIdx: number) {
|
||||
if (locked.value) return
|
||||
submitted.value = false
|
||||
if (selectedLeft.value === null) {
|
||||
const l = leftOf(rightIdx)
|
||||
if (l !== -1) pairs.value[l] = null
|
||||
return
|
||||
}
|
||||
const prev = leftOf(rightIdx)
|
||||
if (prev !== -1) pairs.value[prev] = null
|
||||
pairs.value[selectedLeft.value] = rightIdx
|
||||
selectedLeft.value = null
|
||||
}
|
||||
|
||||
function submit() {
|
||||
submitted.value = true
|
||||
}
|
||||
|
||||
function reset() {
|
||||
init()
|
||||
}
|
||||
|
||||
function leftColor(i: number): string {
|
||||
if (submitted.value) {
|
||||
if (pairs.value[i] === null) return "#d03050"
|
||||
return pairs.value[i] === data.value.answer[i] ? "#18a058" : "#d03050"
|
||||
}
|
||||
if (selectedLeft.value === i) return "#2080f0"
|
||||
if (pairs.value[i] !== null) return PALETTE[i % PALETTE.length]
|
||||
return "var(--n-border-color)"
|
||||
}
|
||||
|
||||
function rightColor(rightIdx: number): string {
|
||||
const l = leftOf(rightIdx)
|
||||
if (submitted.value) {
|
||||
if (l === -1) return "var(--n-border-color)"
|
||||
return pairs.value[l] === data.value.answer[l] ? "#18a058" : "#d03050"
|
||||
}
|
||||
if (l === -1) return "var(--n-border-color)"
|
||||
return PALETTE[l % PALETTE.length]
|
||||
}
|
||||
|
||||
function itemStyle(color: string, selected: boolean): Record<string, string> {
|
||||
const plain = color === "var(--n-border-color)"
|
||||
return {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
padding: "10px 12px",
|
||||
borderRadius: "6px",
|
||||
border: `${selected ? "2px" : "1.5px"} solid ${color}`,
|
||||
background: plain ? "transparent" : color + "14",
|
||||
cursor: locked.value ? "default" : "pointer",
|
||||
userSelect: "none",
|
||||
fontSize: "15px",
|
||||
}
|
||||
}
|
||||
|
||||
function dotStyle(color: string): Record<string, string> {
|
||||
return {
|
||||
width: "10px",
|
||||
height: "10px",
|
||||
borderRadius: "50%",
|
||||
background: color,
|
||||
flexShrink: "0",
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card style="margin: 16px 0; border: 1.5px solid var(--n-border-color)">
|
||||
<template #header>
|
||||
<n-tag type="primary" :bordered="false">练一练 · 连线匹配</n-tag>
|
||||
</template>
|
||||
|
||||
<p style="font-weight: 500; font-size: 16px; margin-bottom: 8px">
|
||||
{{ data.question }}
|
||||
</p>
|
||||
<p style="color: var(--n-text-color-3); font-size: 13px; margin: 0 0 12px">
|
||||
先点左边一项,再点右边一项即可连线;点击已连线的项可取消。
|
||||
</p>
|
||||
|
||||
<div style="display: flex; gap: 24px; align-items: flex-start">
|
||||
<n-space vertical :size="8" style="flex: 1">
|
||||
<div
|
||||
v-for="(item, i) in data.left"
|
||||
:key="'l' + i"
|
||||
:style="itemStyle(leftColor(i), selectedLeft === i)"
|
||||
@click="onLeftClick(i)"
|
||||
>
|
||||
<span
|
||||
v-if="pairs[i] !== null && !submitted"
|
||||
:style="dotStyle(PALETTE[i % PALETTE.length])"
|
||||
/>
|
||||
<span>{{ item }}</span>
|
||||
</div>
|
||||
</n-space>
|
||||
|
||||
<n-space vertical :size="8" style="flex: 1">
|
||||
<div
|
||||
v-for="rightIdx in rightOrder"
|
||||
:key="'r' + rightIdx"
|
||||
:style="itemStyle(rightColor(rightIdx), false)"
|
||||
@click="onRightClick(rightIdx)"
|
||||
>
|
||||
<span
|
||||
v-if="leftOf(rightIdx) !== -1 && !submitted"
|
||||
:style="dotStyle(PALETTE[leftOf(rightIdx) % PALETTE.length])"
|
||||
/>
|
||||
<span>{{ data.right[rightIdx] }}</span>
|
||||
</div>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<n-alert
|
||||
v-if="submitted"
|
||||
:type="allCorrect ? 'success' : 'error'"
|
||||
:title="allCorrect ? '全部匹配正确!' : '有匹配错误,红色项需要重新连线'"
|
||||
style="margin-top: 12px"
|
||||
/>
|
||||
|
||||
<n-space style="margin-top: 12px" :size="8">
|
||||
<n-button type="primary" :disabled="!allPaired || locked" @click="submit">
|
||||
提交
|
||||
</n-button>
|
||||
<n-button @click="reset">重置</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</template>
|
||||
92
src/oj/learn/components/ExercisePredict.vue
Normal file
92
src/oj/learn/components/ExercisePredict.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<script setup lang="ts">
|
||||
import { Exercise, ExercisePredictData } from "utils/types"
|
||||
import { highlight } from "../composables/useCodeHighlight"
|
||||
import "./exercise-highlight.css"
|
||||
|
||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||
const data = computed(() => props.exercise.data as ExercisePredictData)
|
||||
|
||||
const codeHtml = computed(() => highlight(data.value.code, props.lang))
|
||||
|
||||
const userInput = ref("")
|
||||
const submitted = ref(false)
|
||||
|
||||
watch(() => props.exercise.id, reset, { immediate: true })
|
||||
|
||||
function normalize(s: string): string {
|
||||
return s
|
||||
.replace(/\r\n/g, "\n")
|
||||
.split("\n")
|
||||
.map((l) => l.replace(/\s+$/, ""))
|
||||
.join("\n")
|
||||
.replace(/^\n+/, "")
|
||||
.replace(/\n+$/, "")
|
||||
}
|
||||
|
||||
const allCorrect = computed(() =>
|
||||
data.value.answer.some((a) => normalize(a) === normalize(userInput.value)),
|
||||
)
|
||||
|
||||
function submit() {
|
||||
submitted.value = true
|
||||
}
|
||||
|
||||
function reset() {
|
||||
userInput.value = ""
|
||||
submitted.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card style="margin: 16px 0; border: 1.5px solid var(--n-border-color)">
|
||||
<template #header>
|
||||
<n-tag type="error" :bordered="false">练一练 · 输出预测</n-tag>
|
||||
</template>
|
||||
|
||||
<p style="font-weight: 500; font-size: 16px; margin-bottom: 12px">
|
||||
{{ data.question }}
|
||||
</p>
|
||||
|
||||
<pre
|
||||
:style="{
|
||||
fontFamily: 'Monaco',
|
||||
fontSize: '16px',
|
||||
lineHeight: '1.6',
|
||||
background: 'var(--n-color)',
|
||||
border: '1px solid var(--n-border-color)',
|
||||
borderRadius: '6px',
|
||||
padding: '12px',
|
||||
overflowX: 'auto',
|
||||
margin: 0,
|
||||
}"
|
||||
><code v-html="codeHtml" /></pre>
|
||||
|
||||
<p style="font-weight: 500; margin: 14px 0 8px">这段代码会输出什么?</p>
|
||||
<n-input
|
||||
v-model:value="userInput"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:disabled="submitted && allCorrect"
|
||||
placeholder="在这里输入程序会打印的内容"
|
||||
style="font-family: Monaco"
|
||||
/>
|
||||
|
||||
<n-alert
|
||||
v-if="submitted"
|
||||
:type="allCorrect ? 'success' : 'error'"
|
||||
:title="allCorrect ? '输出正确!' : '输出不正确,再读读代码看看'"
|
||||
style="margin-top: 12px"
|
||||
/>
|
||||
|
||||
<n-space style="margin-top: 12px" :size="8">
|
||||
<n-button
|
||||
type="error"
|
||||
:disabled="submitted && allCorrect"
|
||||
@click="submit"
|
||||
>
|
||||
提交
|
||||
</n-button>
|
||||
<n-button @click="reset">重置</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</template>
|
||||
@@ -4,6 +4,12 @@ import { Exercise } from "utils/types"
|
||||
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 ExerciseDebug = defineAsyncComponent(() => import("./ExerciseDebug.vue"))
|
||||
const ExerciseGroup = defineAsyncComponent(() => import("./ExerciseGroup.vue"))
|
||||
|
||||
defineProps<{ exercise: Exercise; lang?: string }>()
|
||||
</script>
|
||||
@@ -20,4 +26,16 @@ defineProps<{ exercise: Exercise; lang?: string }>()
|
||||
:exercise="exercise"
|
||||
:lang="lang"
|
||||
/>
|
||||
<ExerciseMatch v-else-if="exercise.type === 'match'" :exercise="exercise" />
|
||||
<ExercisePredict
|
||||
v-else-if="exercise.type === 'predict'"
|
||||
:exercise="exercise"
|
||||
:lang="lang"
|
||||
/>
|
||||
<ExerciseDebug
|
||||
v-else-if="exercise.type === 'debug'"
|
||||
:exercise="exercise"
|
||||
:lang="lang"
|
||||
/>
|
||||
<ExerciseGroup v-else-if="exercise.type === 'group'" :exercise="exercise" />
|
||||
</template>
|
||||
|
||||
50
src/oj/learn/components/exercise-highlight.css
Normal file
50
src/oj/learn/components/exercise-highlight.css
Normal file
@@ -0,0 +1,50 @@
|
||||
/* 练一练代码高亮配色(明 / 暗),由涉及代码高亮的题型组件统一引入 */
|
||||
.hljs-keyword,
|
||||
.hljs-operator,
|
||||
.hljs-selector-tag {
|
||||
color: #d73a49;
|
||||
}
|
||||
.hljs-string,
|
||||
.hljs-regexp,
|
||||
.hljs-template-literal {
|
||||
color: #032f62;
|
||||
}
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #6a737d;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-number,
|
||||
.hljs-literal {
|
||||
color: #005cc5;
|
||||
}
|
||||
.hljs-built_in,
|
||||
.hljs-title.function_,
|
||||
.hljs-class .hljs-title {
|
||||
color: #6f42c1;
|
||||
}
|
||||
|
||||
.dark .hljs-keyword,
|
||||
.dark .hljs-operator,
|
||||
.dark .hljs-selector-tag {
|
||||
color: #c678dd;
|
||||
}
|
||||
.dark .hljs-string,
|
||||
.dark .hljs-regexp,
|
||||
.dark .hljs-template-literal {
|
||||
color: #98c379;
|
||||
}
|
||||
.dark .hljs-comment,
|
||||
.dark .hljs-quote {
|
||||
color: #7f848e;
|
||||
font-style: italic;
|
||||
}
|
||||
.dark .hljs-number,
|
||||
.dark .hljs-literal {
|
||||
color: #e5c07b;
|
||||
}
|
||||
.dark .hljs-built_in,
|
||||
.dark .hljs-title.function_,
|
||||
.dark .hljs-class .hljs-title {
|
||||
color: #61afef;
|
||||
}
|
||||
41
src/oj/learn/composables/useCodeHighlight.ts
Normal file
41
src/oj/learn/composables/useCodeHighlight.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import hljs from "highlight.js/lib/core"
|
||||
import python from "highlight.js/lib/languages/python"
|
||||
import c from "highlight.js/lib/languages/c"
|
||||
|
||||
hljs.registerLanguage("python", python)
|
||||
hljs.registerLanguage("c", c)
|
||||
|
||||
export function escapeHtml(text: string): string {
|
||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
||||
}
|
||||
|
||||
function normalizeLang(lang?: string): "python" | "c" | null {
|
||||
return lang === "python" ? "python" : lang === "c" ? "c" : null
|
||||
}
|
||||
|
||||
// 把整段代码高亮为 HTML;不支持的语言或异常时回退为转义文本
|
||||
export function highlight(code: string, lang?: string): string {
|
||||
const language = normalizeLang(lang)
|
||||
if (language) {
|
||||
try {
|
||||
return hljs.highlight(code, { language }).value
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
return escapeHtml(code)
|
||||
}
|
||||
|
||||
// 按行高亮:整体高亮后再按行切分,保证跨行 token 着色正确,返回逐行 HTML 数组
|
||||
export function highlightLines(lines: string[], lang?: string): string[] {
|
||||
const language = normalizeLang(lang)
|
||||
if (language) {
|
||||
try {
|
||||
const html = hljs.highlight(lines.join("\n"), { language }).value
|
||||
return html.split("\n")
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
return lines.map((line) => escapeHtml(line))
|
||||
}
|
||||
@@ -627,10 +627,53 @@ export interface ExerciseFillData {
|
||||
code: string
|
||||
}
|
||||
|
||||
export interface ExerciseMatchData {
|
||||
question: string
|
||||
left: string[]
|
||||
right: string[]
|
||||
answer: number[]
|
||||
}
|
||||
|
||||
export interface ExercisePredictData {
|
||||
question: string
|
||||
code: string
|
||||
answer: string[]
|
||||
}
|
||||
|
||||
export interface ExerciseDebugData {
|
||||
question: string
|
||||
lines: string[]
|
||||
answer: number[]
|
||||
explanation?: string
|
||||
}
|
||||
|
||||
export interface ExerciseGroupData {
|
||||
question: string
|
||||
buckets: string[]
|
||||
items: string[]
|
||||
answer: number[]
|
||||
}
|
||||
|
||||
export type ExerciseType =
|
||||
| "mcq"
|
||||
| "sort"
|
||||
| "fill"
|
||||
| "match"
|
||||
| "predict"
|
||||
| "debug"
|
||||
| "group"
|
||||
|
||||
export interface Exercise {
|
||||
id: number
|
||||
type: "mcq" | "sort" | "fill"
|
||||
data: ExerciseMcqData | ExerciseSortData | ExerciseFillData
|
||||
type: ExerciseType
|
||||
data:
|
||||
| ExerciseMcqData
|
||||
| ExerciseSortData
|
||||
| ExerciseFillData
|
||||
| ExerciseMatchData
|
||||
| ExercisePredictData
|
||||
| ExerciseDebugData
|
||||
| ExerciseGroupData
|
||||
order: number
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user