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">
|
<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"
|
import type { Exercise, ExerciseFillData } from "utils/types"
|
||||||
|
import { highlight } from "../composables/useCodeHighlight"
|
||||||
hljs.registerLanguage("python", python)
|
import "./exercise-highlight.css"
|
||||||
hljs.registerLanguage("c", c)
|
|
||||||
|
|
||||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||||
const data = computed(() => props.exercise.data as ExerciseFillData)
|
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 BlankSeg = { type: "blank"; answers: string[]; index: number }
|
||||||
type Segment = CodeSeg | BlankSeg
|
type Segment = CodeSeg | BlankSeg
|
||||||
|
|
||||||
function escapeHtml(s: string): string {
|
|
||||||
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
||||||
}
|
|
||||||
|
|
||||||
const segments = computed<Segment[]>(() => {
|
const segments = computed<Segment[]>(() => {
|
||||||
const blanks: string[][] = []
|
const blanks: string[][] = []
|
||||||
const markedCode = data.value.code.replace(/\{\{([^}]+)\}\}/g, (_, inner) => {
|
const markedCode = data.value.code.replace(/\{\{([^}]+)\}\}/g, (_, inner) => {
|
||||||
@@ -25,18 +17,7 @@ const segments = computed<Segment[]>(() => {
|
|||||||
return `____${blanks.length - 1}____`
|
return `____${blanks.length - 1}____`
|
||||||
})
|
})
|
||||||
|
|
||||||
const lang =
|
const highlighted = highlight(markedCode, props.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 parts = highlighted.split(/____(\d+)____/)
|
const parts = highlighted.split(/____(\d+)____/)
|
||||||
const result: Segment[] = []
|
const result: Segment[] = []
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Exercise, ExerciseGroupData } from "utils/types"
|
import type { Exercise, ExerciseGroupData } from "utils/types"
|
||||||
|
import { shuffle } from "../composables/useShuffle"
|
||||||
|
|
||||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||||
const data = computed(() => props.exercise.data as ExerciseGroupData)
|
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 dragIdx = ref<number | null>(null)
|
||||||
const submitted = ref(false)
|
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() {
|
function init() {
|
||||||
order.value = shuffle(data.value.items.map((_, i) => i))
|
order.value = shuffle(data.value.items.map((_, i) => i))
|
||||||
placement.value = Array(data.value.items.length).fill(-1)
|
placement.value = Array(data.value.items.length).fill(-1)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Exercise, ExerciseMatchData } from "utils/types"
|
import type { Exercise, ExerciseMatchData } from "utils/types"
|
||||||
|
import { shuffle } from "../composables/useShuffle"
|
||||||
|
|
||||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||||
const data = computed(() => props.exercise.data as ExerciseMatchData)
|
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 selectedLeft = ref<number | null>(null)
|
||||||
const submitted = ref(false)
|
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() {
|
function init() {
|
||||||
const n = data.value.right.length
|
const n = data.value.right.length
|
||||||
rightOrder.value = shuffle(Array.from({ length: n }, (_, i) => i))
|
rightOrder.value = shuffle(Array.from({ length: n }, (_, i) => i))
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<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"
|
import type { Exercise, ExerciseSortData } from "utils/types"
|
||||||
|
import { shuffle } from "../composables/useShuffle"
|
||||||
hljs.registerLanguage("python", python)
|
import { highlightLines } from "../composables/useCodeHighlight"
|
||||||
hljs.registerLanguage("c", c)
|
import "./exercise-highlight.css"
|
||||||
|
|
||||||
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
const props = defineProps<{ exercise: Exercise; lang?: string }>()
|
||||||
const data = computed(() => props.exercise.data as ExerciseSortData)
|
const data = computed(() => props.exercise.data as ExerciseSortData)
|
||||||
@@ -15,21 +12,16 @@ type LineItem = { originalIdx: number; text: string }
|
|||||||
const lines = ref<LineItem[]>([])
|
const lines = ref<LineItem[]>([])
|
||||||
const submitted = ref(false)
|
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() {
|
function init() {
|
||||||
lines.value = shuffle(
|
const shuffled = shuffle(
|
||||||
data.value.lines.map((text, idx) => ({ originalIdx: idx, text })),
|
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
|
submitted.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,35 +61,9 @@ function reset() {
|
|||||||
init()
|
init()
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(text: string): string {
|
const lineHtml = computed<string[]>(() =>
|
||||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
highlightLines(data.value.lines, props.lang),
|
||||||
}
|
)
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -143,7 +109,7 @@ const lineHtmlMap = computed<Record<number, string>>(() => {
|
|||||||
@drop="onDrop(idx)"
|
@drop="onDrop(idx)"
|
||||||
>
|
>
|
||||||
<span style="color: #bbb; cursor: grab">⠿</span>
|
<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>
|
</div>
|
||||||
</n-space>
|
</n-space>
|
||||||
|
|
||||||
@@ -162,55 +128,3 @@ const lineHtmlMap = computed<Record<number, string>>(() => {
|
|||||||
</n-space>
|
</n-space>
|
||||||
</n-card>
|
</n-card>
|
||||||
</template>
|
</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