Compare commits

...

4 Commits

Author SHA1 Message Date
0ca1a142a4 fix: use mcqAnswer.value in template delete handler
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled
2026-04-24 02:06:38 -06:00
5c9972315c feat: update exercise manager to support multi-answer checkboxes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 02:04:07 -06:00
9afb57a9ed feat: rewrite ExerciseMcq for multi-select with partial feedback 2026-04-24 02:01:35 -06:00
21a3ff322b feat: update ExerciseMcqData.answer type to number[] 2026-04-24 02:00:38 -06:00
3 changed files with 51 additions and 26 deletions

View File

@@ -24,7 +24,7 @@ const formOrder = ref(0)
const mcqQuestion = ref("") const mcqQuestion = ref("")
const mcqOptions = ref(["", ""]) const mcqOptions = ref(["", ""])
const mcqAnswer = ref(0) const mcqAnswer = ref<number[]>([])
const sortQuestion = ref("") const sortQuestion = ref("")
const sortCode = ref("") const sortCode = ref("")
@@ -44,7 +44,7 @@ function openCreate() {
formOrder.value = exercises.value.length formOrder.value = exercises.value.length
mcqQuestion.value = "" mcqQuestion.value = ""
mcqOptions.value = ["", ""] mcqOptions.value = ["", ""]
mcqAnswer.value = 0 mcqAnswer.value = []
sortQuestion.value = "" sortQuestion.value = ""
sortCode.value = "" sortCode.value = ""
fillQuestion.value = "" fillQuestion.value = ""
@@ -60,7 +60,7 @@ function openEdit(ex: Exercise) {
const d = ex.data as ExerciseMcqData const d = ex.data as ExerciseMcqData
mcqQuestion.value = d.question mcqQuestion.value = d.question
mcqOptions.value = [...d.options] mcqOptions.value = [...d.options]
mcqAnswer.value = d.answer mcqAnswer.value = [...d.answer]
} else if (ex.type === "sort") { } else if (ex.type === "sort") {
const d = ex.data as ExerciseSortData const d = ex.data as ExerciseSortData
sortQuestion.value = d.question sortQuestion.value = d.question
@@ -73,6 +73,12 @@ function openEdit(ex: Exercise) {
showForm.value = true showForm.value = true
} }
function toggleAnswer(i: number) {
const idx = mcqAnswer.value.indexOf(i)
if (idx === -1) mcqAnswer.value.push(i)
else mcqAnswer.value.splice(idx, 1)
}
async function save() { async function save() {
let data: Record<string, unknown> let data: Record<string, unknown>
if (formType.value === "mcq") { if (formType.value === "mcq") {
@@ -218,7 +224,7 @@ function typeTagType(type: string): "success" | "info" | "warning" {
placeholder="下面选项中正确是哪个?" placeholder="下面选项中正确是哪个?"
/> />
</n-form-item> </n-form-item>
<n-form-item label="选项(正确答案前选择单选按钮"> <n-form-item label="选项(勾选所有正确答案)">
<n-space vertical style="width: 100%"> <n-space vertical style="width: 100%">
<n-flex <n-flex
v-for="(opt, i) in mcqOptions" v-for="(opt, i) in mcqOptions"
@@ -226,10 +232,9 @@ function typeTagType(type: string): "success" | "info" | "warning" {
align="center" align="center"
:size="8" :size="8"
> >
<n-radio <n-checkbox
:value="i" :checked="mcqAnswer.includes(i)"
:checked="mcqAnswer === i" @update:checked="toggleAnswer(i)"
@update:checked="$event && (mcqAnswer = i)"
/> />
<n-input <n-input
v-model:value="mcqOptions[i]" v-model:value="mcqOptions[i]"
@@ -242,8 +247,9 @@ function typeTagType(type: string): "success" | "info" | "warning" {
@click=" @click="
() => { () => {
mcqOptions.splice(i, 1) mcqOptions.splice(i, 1)
if (mcqAnswer >= mcqOptions.length) mcqAnswer.value = mcqAnswer.value
mcqAnswer = mcqOptions.length - 1 .filter((a) => a !== i)
.map((a) => (a > i ? a - 1 : a))
} }
" "
> >

View File

@@ -4,36 +4,53 @@ import { Exercise, ExerciseMcqData } from "utils/types"
const props = defineProps<{ exercise: Exercise }>() const props = defineProps<{ exercise: Exercise }>()
const data = computed(() => props.exercise.data as ExerciseMcqData) const data = computed(() => props.exercise.data as ExerciseMcqData)
const selected = ref<number | null>(null) const selected = ref<Set<number>>(new Set())
const correct = ref(false) const correct = ref(false)
const wrong = ref(false) const wrong = ref(false)
const partial = ref(false)
function select(idx: number) { function select(idx: number) {
if (correct.value) return if (correct.value) return
selected.value = idx const s = new Set(selected.value)
if (s.has(idx)) s.delete(idx)
else s.add(idx)
selected.value = s
wrong.value = false wrong.value = false
partial.value = false
} }
function submit() { function submit() {
if (selected.value === null || correct.value) return if (selected.value.size === 0 || correct.value) return
if (selected.value === data.value.answer) { const answer = new Set(data.value.answer)
const sel = selected.value
const isEqual =
sel.size === answer.size && [...sel].every((v) => answer.has(v))
if (isEqual) {
correct.value = true correct.value = true
wrong.value = false wrong.value = false
partial.value = false
} else { } else {
wrong.value = true const hasIntersection = [...sel].some((v) => answer.has(v))
selected.value = null if (hasIntersection) {
partial.value = true
wrong.value = false
} else {
wrong.value = true
partial.value = false
}
} }
} }
function reset() { function reset() {
selected.value = null selected.value = new Set()
correct.value = false correct.value = false
wrong.value = false wrong.value = false
partial.value = false
} }
function optionType(idx: number): "default" | "primary" | "success" { function optionType(idx: number): "default" | "primary" | "success" {
if (correct.value && idx === data.value.answer) return "success" if (correct.value && data.value.answer.includes(idx)) return "success"
if (idx === selected.value) return "primary" if (selected.value.has(idx)) return "primary"
return "default" return "default"
} }
</script> </script>
@@ -46,7 +63,7 @@ function optionType(idx: number): "default" | "primary" | "success" {
<template #header> <template #header>
<n-space align="center" :size="8"> <n-space align="center" :size="8">
<n-tag type="success" size="small" :bordered="false" <n-tag type="success" size="small" :bordered="false"
>练一练 · </n-tag >练一练 · 选题</n-tag
> >
</n-space> </n-space>
</template> </template>
@@ -60,7 +77,7 @@ function optionType(idx: number): "default" | "primary" | "success" {
:type="optionType(idx)" :type="optionType(idx)"
:secondary="optionType(idx) !== 'default'" :secondary="optionType(idx) !== 'default'"
:tertiary="optionType(idx) === 'default'" :tertiary="optionType(idx) === 'default'"
:strong="idx === selected" :strong="selected.has(idx)"
:style="{ :style="{
justifyContent: 'flex-start', justifyContent: 'flex-start',
width: '100%', width: '100%',
@@ -78,9 +95,11 @@ function optionType(idx: number): "default" | "primary" | "success" {
</n-space> </n-space>
<n-alert <n-alert
v-if="correct || wrong" v-if="correct || wrong || partial"
:type="correct ? 'success' : 'error'" :type="correct ? 'success' : partial ? 'warning' : 'error'"
:title="correct ? '正确!' : '选择有误,请重试'" :title="
correct ? '正确!' : partial ? '部分正确,请重试' : '选择有误,请重试'
"
style="margin-top: 12px" style="margin-top: 12px"
/> />
@@ -88,7 +107,7 @@ function optionType(idx: number): "default" | "primary" | "success" {
<n-button <n-button
type="primary" type="primary"
size="small" size="small"
:disabled="selected === null || correct" :disabled="selected.size === 0 || correct"
@click="submit" @click="submit"
> >
提交 提交

View File

@@ -582,7 +582,7 @@ export interface Tutorial {
export interface ExerciseMcqData { export interface ExerciseMcqData {
question: string question: string
options: string[] options: string[]
answer: number answer: number[]
} }
export interface ExerciseSortData { export interface ExerciseSortData {