fix(learn): 修复填空题代码高亮丢失,去重高亮与洗牌逻辑
- ExerciseFill.vue 之前未导入 exercise-highlight.css,若某课只含 fill 类型练习(不加载 ExerciseSort),高亮代码会渲染成无色纯文本 - ExerciseFill/ExerciseSort 改为复用 useCodeHighlight 的 highlight()/highlightLines(),删除各自重复的 hljs 注册与 escapeHtml 实现 - 新增 useShuffle.ts,供 ExerciseSort/ExerciseMatch/ExerciseGroup 共用洗牌逻辑,替代三处几乎逐字相同的实现 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import hljs from "highlight.js/lib/core"
|
||||
import python from "highlight.js/lib/languages/python"
|
||||
import c from "highlight.js/lib/languages/c"
|
||||
import type { Exercise, ExerciseFillData } from "utils/types"
|
||||
|
||||
hljs.registerLanguage("python", python)
|
||||
hljs.registerLanguage("c", c)
|
||||
import { highlight } from "../composables/useCodeHighlight"
|
||||
import "./exercise-highlight.css"
|
||||
|
||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||
const data = computed(() => props.exercise.data as ExerciseFillData)
|
||||
@@ -14,10 +10,6 @@ type CodeSeg = { type: "code"; html: string }
|
||||
type BlankSeg = { type: "blank"; answers: string[]; index: number }
|
||||
type Segment = CodeSeg | BlankSeg
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
||||
}
|
||||
|
||||
const segments = computed<Segment[]>(() => {
|
||||
const blanks: string[][] = []
|
||||
const markedCode = data.value.code.replace(/\{\{([^}]+)\}\}/g, (_, inner) => {
|
||||
@@ -25,18 +17,7 @@ const segments = computed<Segment[]>(() => {
|
||||
return `____${blanks.length - 1}____`
|
||||
})
|
||||
|
||||
const lang =
|
||||
props.lang === "python" ? "python" : props.lang === "c" ? "c" : null
|
||||
let highlighted: string
|
||||
if (lang) {
|
||||
try {
|
||||
highlighted = hljs.highlight(markedCode, { language: lang }).value
|
||||
} catch {
|
||||
highlighted = escapeHtml(markedCode)
|
||||
}
|
||||
} else {
|
||||
highlighted = escapeHtml(markedCode)
|
||||
}
|
||||
const highlighted = highlight(markedCode, props.lang)
|
||||
|
||||
const parts = highlighted.split(/____(\d+)____/)
|
||||
const result: Segment[] = []
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { Exercise, ExerciseGroupData } from "utils/types"
|
||||
import { shuffle } from "../composables/useShuffle"
|
||||
|
||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||
const data = computed(() => props.exercise.data as ExerciseGroupData)
|
||||
@@ -9,15 +10,6 @@ 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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { Exercise, ExerciseMatchData } from "utils/types"
|
||||
import { shuffle } from "../composables/useShuffle"
|
||||
|
||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||
const data = computed(() => props.exercise.data as ExerciseMatchData)
|
||||
@@ -20,15 +21,6 @@ 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))
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import hljs from "highlight.js/lib/core"
|
||||
import python from "highlight.js/lib/languages/python"
|
||||
import c from "highlight.js/lib/languages/c"
|
||||
import type { Exercise, ExerciseSortData } from "utils/types"
|
||||
|
||||
hljs.registerLanguage("python", python)
|
||||
hljs.registerLanguage("c", c)
|
||||
import { shuffle } from "../composables/useShuffle"
|
||||
import { highlightLines } from "../composables/useCodeHighlight"
|
||||
import "./exercise-highlight.css"
|
||||
|
||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||
const data = computed(() => props.exercise.data as ExerciseSortData)
|
||||
@@ -15,21 +12,16 @@ type LineItem = { originalIdx: number; text: string }
|
||||
const lines = ref<LineItem[]>([])
|
||||
const submitted = ref(false)
|
||||
|
||||
function shuffle(arr: LineItem[]): LineItem[] {
|
||||
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]]
|
||||
}
|
||||
const isCorrect = a.every((item, i) => item.originalIdx === i)
|
||||
if (isCorrect && a.length > 1) [a[0], a[1]] = [a[1], a[0]]
|
||||
return a
|
||||
}
|
||||
|
||||
function init() {
|
||||
lines.value = shuffle(
|
||||
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) {
|
||||
;[shuffled[0], shuffled[1]] = [shuffled[1], shuffled[0]]
|
||||
}
|
||||
lines.value = shuffled
|
||||
submitted.value = false
|
||||
}
|
||||
|
||||
@@ -69,35 +61,9 @@ function reset() {
|
||||
init()
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
||||
}
|
||||
|
||||
const lineHtmlMap = computed<Record<number, string>>(() => {
|
||||
const rawLines = data.value.lines
|
||||
const map: Record<number, string> = {}
|
||||
const lang =
|
||||
props.lang === "python" ? "python" : props.lang === "c" ? "c" : null
|
||||
|
||||
if (lang) {
|
||||
try {
|
||||
const result = hljs.highlight(rawLines.join("\n"), {
|
||||
language: lang,
|
||||
}).value
|
||||
result.split("\n").forEach((html, i) => {
|
||||
map[i] = html
|
||||
})
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
|
||||
rawLines.forEach((text, i) => {
|
||||
if (map[i] === undefined) map[i] = escapeHtml(text)
|
||||
})
|
||||
|
||||
return map
|
||||
})
|
||||
const lineHtml = computed<string[]>(() =>
|
||||
highlightLines(data.value.lines, props.lang),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -143,7 +109,7 @@ const lineHtmlMap = computed<Record<number, string>>(() => {
|
||||
@drop="onDrop(idx)"
|
||||
>
|
||||
<span style="color: #bbb; cursor: grab">⠿</span>
|
||||
<span v-html="lineHtmlMap[line.originalIdx]" style="white-space: pre" />
|
||||
<span v-html="lineHtml[line.originalIdx]" style="white-space: pre" />
|
||||
</div>
|
||||
</n-space>
|
||||
|
||||
@@ -162,55 +128,3 @@ const lineHtmlMap = computed<Record<number, string>>(() => {
|
||||
</n-space>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
|
||||
9
src/oj/learn/composables/useShuffle.ts
Normal file
9
src/oj/learn/composables/useShuffle.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// Fisher–Yates 洗牌,返回新数组,不修改原数组
|
||||
export 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
|
||||
}
|
||||
Reference in New Issue
Block a user