diff --git a/apps/web/src/oj/learn/components/CodeRunBlock.vue b/apps/web/src/oj/learn/components/CodeRunBlock.vue index e99dcaa..4f32afa 100644 --- a/apps/web/src/oj/learn/components/CodeRunBlock.vue +++ b/apps/web/src/oj/learn/components/CodeRunBlock.vue @@ -9,6 +9,7 @@ import { explainPythonCompileError } from "oj/problem/utils/pythonError" import { explainRuntimeError } from "oj/problem/utils/runtimeError" import { useAuthModalStore } from "shared/store/authModal" import { useUserStore } from "shared/store/user" +import { mainWrapExtensions, wrapMain } from "../wrapMain" const CodeEditor = defineAsyncComponent(() => import("shared/components/CodeEditor.vue")) @@ -18,11 +19,14 @@ const CodeEditor = defineAsyncComponent(() => import("shared/components/CodeEdit * 运行走题目页同一台判题机(POST /trial-runs,带 tutorialId),不算提交。 * 报错用题目页那套中文解释(pythonError / cError / runtimeError),原文收在下面。 * 代码里读输入(input() / scanf)才出输入框。 + * C 的片段(原文没有 main,只是讲一两行写法)跑不起来,不给「运行」按钮; + * 教程里标了 ```c main 的片段套上灰色的 main 再给(见 wrapMain.ts)。 */ const props = defineProps<{ code: string lang: "python" | "c" tutorialId: number + wrapMain?: boolean }>() const userStore = useUserStore() @@ -30,7 +34,9 @@ const authModal = useAuthModalStore() const message = useMessage() const { copy } = useClipboard({ legacy: true }) -const value = ref(props.code) +const original = computed(() => (props.wrapMain ? wrapMain(props.code) : props.code)) +const extensions = computed(() => (props.wrapMain ? mainWrapExtensions : [])) +const value = ref(original.value) const input = ref("") const running = ref(false) @@ -41,16 +47,15 @@ type Outcome = const outcome = ref(null) // 换课时组件实例可能被复用(v-for 的 key 是段落序号),跟着原文重置 -watch( - () => props.code, - (code) => { - value.value = code - input.value = "" - outcome.value = null - }, -) +watch(original, (code) => { + value.value = code + input.value = "" + outcome.value = null +}) -const edited = computed(() => value.value !== props.code) +const edited = computed(() => value.value !== original.value) +// 看原文不看改过的:学生把 main 删了,按钮还在,点了才知道错在哪 +const runnable = computed(() => props.lang === "python" || /\bmain\s*\(/.test(original.value)) const needsInput = computed(() => props.lang === "python" ? /\binput\s*\(/.test(value.value) @@ -173,7 +178,7 @@ async function run() { } function restore() { - value.value = props.code + value.value = original.value outcome.value = null } @@ -194,7 +199,14 @@ async function copyCode() { 还原 - @@ -203,10 +215,16 @@ async function copyCode() {
- +
@@ -315,6 +333,9 @@ async function copyCode() { background: transparent; border-right: 0; } +.editor :deep(.cm-main-wrap) { + opacity: 0.4; +} .editor :deep(.cm-scroller) { font-family: var(--learn-code-font); line-height: 22px; diff --git a/apps/web/src/oj/learn/components/LessonBody.vue b/apps/web/src/oj/learn/components/LessonBody.vue index 248c819..17625d4 100644 --- a/apps/web/src/oj/learn/components/LessonBody.vue +++ b/apps/web/src/oj/learn/components/LessonBody.vue @@ -39,6 +39,7 @@ function inline(text: string) { v-else-if="seg.type === 'code'" :code="seg.code" :lang="seg.lang" + :wrap-main="seg.wrapMain" :tutorial-id="tutorialId" /> ", "", "int main(void) {"] +const FOOT = [" return 0;", "}"] + +export function wrapMain(code: string) { + const body = code.split("\n").map((line) => (line ? ` ${line}` : line)) + return [...HEAD, ...body, ...FOOT].join("\n") +} + +/** 头尾两段的字符范围:头到片段第一行行首,尾从片段最后一行行尾起(换行符算在套的那边) */ +function lockedRanges(state: EditorState) { + const doc = state.doc + const headEnd = doc.line(HEAD.length + 1).from + const footStart = doc.line(doc.lines - FOOT.length + 1).from - 1 + return { headEnd, footStart } +} + +// 边界上插入是放行的(实测 changeFilter 只拦范围内部),所以片段首行行首、末行行尾照常能打字。 +// 只拦学生的编辑:「还原」、换课时 CodeMirror.vue 整篇替换文档,那一下不带 userEvent +const lock = EditorState.changeFilter.of((tr) => { + if (!tr.annotation(Transaction.userEvent)) return true + const { headEnd, footStart } = lockedRanges(tr.startState) + return [0, headEnd, footStart, tr.startState.doc.length] +}) + +const dim = Decoration.line({ class: "cm-main-wrap" }) + +const dimLines = EditorView.decorations.compute(["doc"], (state) => { + const builder = new RangeSetBuilder() + const doc = state.doc + for (let n = 1; n <= doc.lines; n++) { + if (n <= HEAD.length || n > doc.lines - FOOT.length) + builder.add(doc.line(n).from, doc.line(n).from, dim) + } + return builder.finish() +}) + +export const mainWrapExtensions: Extension[] = [lock, dimLines]