From 30f71c5db270ede6a436fd0f114b789dca715019 Mon Sep 17 00:00:00 2001
From: yuetsh <517252939@qq.com>
Date: Thu, 23 Apr 2026 13:48:36 -0600
Subject: [PATCH] add fills
---
src/admin/api.ts | 4 +-
.../tutorial/components/ExerciseManager.vue | 139 ++++++++++++---
src/admin/tutorial/detail.vue | 1 -
src/oj/api.ts | 4 +-
src/oj/learn/components/ExerciseFill.vue | 160 ++++++++++++++++++
src/oj/learn/components/ExerciseMcq.vue | 19 ++-
src/oj/learn/components/ExerciseSort.vue | 74 ++++++--
src/oj/learn/components/ExerciseWidget.vue | 12 +-
src/oj/learn/composables/useExerciseParse.ts | 10 +-
src/oj/learn/index.vue | 46 ++++-
src/shared/components/Header.vue | 37 ++--
src/shared/components/MermaidEditor.vue | 3 +-
src/utils/types.ts | 9 +-
13 files changed, 435 insertions(+), 83 deletions(-)
create mode 100644 src/oj/learn/components/ExerciseFill.vue
diff --git a/src/admin/api.ts b/src/admin/api.ts
index 2e68b2c..e0fb00c 100644
--- a/src/admin/api.ts
+++ b/src/admin/api.ts
@@ -263,7 +263,9 @@ export function setTutorialVisibility(id: number, is_public: boolean) {
}
export async function getAdminExercises(tutorialId: number) {
- const res = await http.get("admin/exercise", { params: { tutorial_id: tutorialId } })
+ const res = await http.get("admin/exercise", {
+ params: { tutorial_id: tutorialId },
+ })
return res.data as Exercise[]
}
diff --git a/src/admin/tutorial/components/ExerciseManager.vue b/src/admin/tutorial/components/ExerciseManager.vue
index c538ca3..9237a67 100644
--- a/src/admin/tutorial/components/ExerciseManager.vue
+++ b/src/admin/tutorial/components/ExerciseManager.vue
@@ -1,5 +1,10 @@
@@ -110,7 +150,9 @@ function typeName(type: string) {
共 {{ exercises.length }} 道练习题
- + 添加练习题
+ + 添加练习题
@@ -119,11 +161,7 @@ function typeName(type: string) {
-
+
{{ typeName(ex.type) }}
@@ -133,12 +171,16 @@ function typeName(type: string) {
- 复制占位符
+ 复制占位符
将 [[exercise:{{ ex.id }}]] 粘贴到 Markdown 内容中
编辑
- 删除
+ 删除
@@ -155,11 +197,16 @@ function typeName(type: string) {
选择题
代码排序
+ 代码填空
-
+
@@ -168,7 +215,12 @@ function typeName(type: string) {
-
+
{ mcqOptions.splice(i, 1); if (mcqAnswer >= mcqOptions.length) mcqAnswer = mcqOptions.length - 1 }"
+ @click="
+ () => {
+ mcqOptions.splice(i, 1)
+ if (mcqAnswer >= mcqOptions.length)
+ mcqAnswer = mcqOptions.length - 1
+ }
+ "
>
✕
- + 添加选项
+ + 添加选项
-
+
-
+
+
+
+
+
+
+
+
+
+
diff --git a/src/admin/tutorial/detail.vue b/src/admin/tutorial/detail.vue
index 3941a8d..7780832 100644
--- a/src/admin/tutorial/detail.vue
+++ b/src/admin/tutorial/detail.vue
@@ -64,7 +64,6 @@ async function submit() {
await updateTutorial(tutorial)
message.success("修改已保存")
}
- router.push({ name: "admin tutorial list" })
} catch (err: any) {
message.error(err.data)
}
diff --git a/src/oj/api.ts b/src/oj/api.ts
index ee0aab2..8a77a28 100644
--- a/src/oj/api.ts
+++ b/src/oj/api.ts
@@ -423,6 +423,8 @@ export function getProblemSetUserProgress(
}
export async function getExercises(tutorialId: number): Promise {
- const res = await http.get("exercises", { params: { tutorial_id: tutorialId } })
+ const res = await http.get("exercises", {
+ params: { tutorial_id: tutorialId },
+ })
return res.data
}
diff --git a/src/oj/learn/components/ExerciseFill.vue b/src/oj/learn/components/ExerciseFill.vue
new file mode 100644
index 0000000..52c2b16
--- /dev/null
+++ b/src/oj/learn/components/ExerciseFill.vue
@@ -0,0 +1,160 @@
+
+
+
+
+
+ 练一练 · 代码填空
+
+
+ {{ data.question }}
+
+
+
+
+
+
+
+ 提交
+
+ 重置
+
+
+
diff --git a/src/oj/learn/components/ExerciseMcq.vue b/src/oj/learn/components/ExerciseMcq.vue
index 9b49b13..07220a4 100644
--- a/src/oj/learn/components/ExerciseMcq.vue
+++ b/src/oj/learn/components/ExerciseMcq.vue
@@ -39,10 +39,15 @@ function optionType(idx: number): "default" | "primary" | "success" {
-
+
- 练一练 · 选择题
+ 练一练 · 选择题
@@ -56,11 +61,17 @@ function optionType(idx: number): "default" | "primary" | "success" {
:secondary="optionType(idx) !== 'default'"
:tertiary="optionType(idx) === 'default'"
:strong="idx === selected"
- :style="{ justifyContent: 'flex-start', width: '100%', textAlign: 'left' }"
+ :style="{
+ justifyContent: 'flex-start',
+ width: '100%',
+ textAlign: 'left',
+ }"
@click="select(idx)"
>
- {{ String.fromCharCode(65 + idx) }}
+ {{
+ String.fromCharCode(65 + idx)
+ }}
{{ opt }}
diff --git a/src/oj/learn/components/ExerciseSort.vue b/src/oj/learn/components/ExerciseSort.vue
index 32a1b1c..91776d6 100644
--- a/src/oj/learn/components/ExerciseSort.vue
+++ b/src/oj/learn/components/ExerciseSort.vue
@@ -27,7 +27,9 @@ function shuffle(arr: LineItem[]): LineItem[] {
}
function init() {
- lines.value = shuffle(data.value.lines.map((text, idx) => ({ originalIdx: idx, text })))
+ lines.value = shuffle(
+ data.value.lines.map((text, idx) => ({ originalIdx: idx, text })),
+ )
submitted.value = false
}
@@ -55,7 +57,9 @@ 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
@@ -72,11 +76,14 @@ function escapeHtml(text: string): string {
const lineHtmlMap = computed>(() => {
const rawLines = data.value.lines
const map: Record = {}
- const lang = props.lang === "python" ? "python" : props.lang === "c" ? "c" : null
+ const lang =
+ props.lang === "python" ? "python" : props.lang === "c" ? "c" : null
if (lang) {
try {
- const result = hljs.highlight(rawLines.join("\n"), { language: lang }).value
+ const result = hljs.highlight(rawLines.join("\n"), {
+ language: lang,
+ }).value
result.split("\n").forEach((html, i) => {
map[i] = html
})
@@ -94,9 +101,14 @@ const lineHtmlMap = computed>(() => {
-
+
- 练一练 · 代码排序
+ 练一练 · 代码排序
{{ data.question }}
@@ -127,7 +139,6 @@ const lineHtmlMap = computed>(() => {
: 'transparent',
cursor: 'grab',
fontFamily: 'monospace',
- fontSize: '13px',
userSelect: 'none',
}"
@dragstart="onDragStart(idx)"
@@ -147,7 +158,12 @@ const lineHtmlMap = computed>(() => {
/>
-
+
提交
重置
@@ -158,29 +174,51 @@ const lineHtmlMap = computed>(() => {
diff --git a/src/oj/learn/components/ExerciseWidget.vue b/src/oj/learn/components/ExerciseWidget.vue
index b678b2d..b7fbfc8 100644
--- a/src/oj/learn/components/ExerciseWidget.vue
+++ b/src/oj/learn/components/ExerciseWidget.vue
@@ -3,11 +3,21 @@ import { Exercise } from "utils/types"
const ExerciseMcq = defineAsyncComponent(() => import("./ExerciseMcq.vue"))
const ExerciseSort = defineAsyncComponent(() => import("./ExerciseSort.vue"))
+const ExerciseFill = defineAsyncComponent(() => import("./ExerciseFill.vue"))
defineProps<{ exercise: Exercise; lang?: string }>()
-
+
+
diff --git a/src/oj/learn/composables/useExerciseParse.ts b/src/oj/learn/composables/useExerciseParse.ts
index 5751235..e033f6d 100644
--- a/src/oj/learn/composables/useExerciseParse.ts
+++ b/src/oj/learn/composables/useExerciseParse.ts
@@ -4,7 +4,10 @@ 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
@@ -13,7 +16,10 @@ export function parseExercises(content: string, exercises: Exercise[]): Segment[
while ((match = regex.exec(content)) !== null) {
if (match.index > lastIndex) {
- segments.push({ type: "md", content: content.slice(lastIndex, match.index) })
+ segments.push({
+ type: "md",
+ content: content.slice(lastIndex, match.index),
+ })
}
const id = parseInt(match[1])
const exercise = exerciseMap.get(id)
diff --git a/src/oj/learn/index.vue b/src/oj/learn/index.vue
index 858270f..2966a7e 100644
--- a/src/oj/learn/index.vue
+++ b/src/oj/learn/index.vue
@@ -34,7 +34,11 @@
:theme="isDark ? 'dark' : 'light'"
:model-value="seg.content"
/>
-
+
@@ -74,7 +78,11 @@
:theme="isDark ? 'dark' : 'light'"
:model-value="seg.content"
/>
-
+
@@ -86,11 +94,21 @@
-
+
← 上一课
{{ step }} / {{ titles.length }}
-
+
下一课 →
@@ -106,8 +124,12 @@ import { getTutorial, getTutorials, getExercises } from "../api"
import { parseExercises } from "./composables/useExerciseParse"
import { useBreakpoints } from "shared/composables/breakpoints"
-const ExerciseWidget = defineAsyncComponent(() => import("./components/ExerciseWidget.vue"))
-const CodeEditor = defineAsyncComponent(() => import("shared/components/CodeEditor.vue"))
+const ExerciseWidget = defineAsyncComponent(
+ () => import("./components/ExerciseWidget.vue"),
+)
+const CodeEditor = defineAsyncComponent(
+ () => import("shared/components/CodeEditor.vue"),
+)
const isDark = useDark()
const route = useRoute()
@@ -119,7 +141,12 @@ const step = computed(() => {
return parseInt(route.params.step[0])
})
-const tutorial = ref>({ id: 0, title: "", content: "", code: "" })
+const tutorial = ref>({
+ id: 0,
+ title: "",
+ content: "",
+ code: "",
+})
const titles = ref<{ id: number; title: string }[]>([])
const exercises = ref([])
const activeTab = ref("content")
@@ -147,7 +174,10 @@ async function init() {
titles.value = res1.data
if (titles.value.length === 0) 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.data
exercises.value = exs.status === "fulfilled" ? exs.value : []
}
diff --git a/src/shared/components/Header.vue b/src/shared/components/Header.vue
index b9519cd..8d08f73 100644
--- a/src/shared/components/Header.vue
+++ b/src/shared/components/Header.vue
@@ -30,23 +30,26 @@ function toggleDark(event: MouseEvent) {
isDark.value = !isDark.value
return
}
- document.startViewTransition(() => {
- isDark.value = !isDark.value
- }).ready.then(() => {
- document.documentElement.animate(
- {
- clipPath: [
- `circle(0px at ${x}px ${y}px)`,
- `circle(${radius}px at ${x}px ${y}px)`,
- ],
- },
- {
- duration: 400,
- easing: "ease-in-out",
- pseudoElement: "::view-transition-new(root)",
- },
- )
- }).catch(() => {})
+ document
+ .startViewTransition(() => {
+ isDark.value = !isDark.value
+ })
+ .ready.then(() => {
+ document.documentElement.animate(
+ {
+ clipPath: [
+ `circle(0px at ${x}px ${y}px)`,
+ `circle(${radius}px at ${x}px ${y}px)`,
+ ],
+ },
+ {
+ duration: 400,
+ easing: "ease-in-out",
+ pseudoElement: "::view-transition-new(root)",
+ },
+ )
+ })
+ .catch(() => {})
}
// 从 store 中获取屏幕模式状态
diff --git a/src/shared/components/MermaidEditor.vue b/src/shared/components/MermaidEditor.vue
index 6c8b65d..5656dbc 100644
--- a/src/shared/components/MermaidEditor.vue
+++ b/src/shared/components/MermaidEditor.vue
@@ -73,7 +73,8 @@ const renderMermaid = async () => {
renderSuccess.value = false
const errorDiv = document.createElement("div")
- errorDiv.style.cssText = "color: #ff4d4f; padding: 20px; text-align: center; border: 1px dashed #ff4d4f; border-radius: 4px;"
+ errorDiv.style.cssText =
+ "color: #ff4d4f; padding: 20px; text-align: center; border: 1px dashed #ff4d4f; border-radius: 4px;"
const titleP = document.createElement("p")
titleP.textContent = "Mermaid语法错误"
const detailP = document.createElement("p")
diff --git a/src/utils/types.ts b/src/utils/types.ts
index bb33383..949af2d 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -590,10 +590,15 @@ export interface ExerciseSortData {
lines: string[]
}
+export interface ExerciseFillData {
+ question: string
+ code: string
+}
+
export interface Exercise {
id: number
- type: 'mcq' | 'sort'
- data: ExerciseMcqData | ExerciseSortData
+ type: "mcq" | "sort" | "fill"
+ data: ExerciseMcqData | ExerciseSortData | ExerciseFillData
order: number
}