From 27210eac1b7c8a11ac8f8f59cf35a419fde8eeee Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 13 Oct 2026 19:20:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=87=AA=E5=AD=A6):=20C=20=E6=95=99?= =?UTF-8?q?=E7=A8=8B=E9=87=8C=E6=B2=A1=E5=86=99=20main=20=E7=9A=84?= =?UTF-8?q?=E7=89=87=E6=AE=B5=E4=B9=9F=E8=83=BD=E8=BF=90=E8=A1=8C=E2=80=94?= =?UTF-8?q?=E2=80=94=E4=B8=8A=E4=B8=8B=E5=A5=97=E4=B8=80=E5=B1=82=E7=81=B0?= =?UTF-8?q?=E8=89=B2=E7=9A=84=20main=EF=BC=8C=E7=81=B0=E7=9A=84=E5=87=A0?= =?UTF-8?q?=E8=A1=8C=E6=94=B9=E4=B8=8D=E4=BA=86=EF=BC=9B=E5=85=B6=E4=BD=99?= =?UTF-8?q?=E6=B2=A1=20main=20=E7=9A=84=E7=89=87=E6=AE=B5=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E7=BB=99=E4=B8=80=E6=8C=89=E5=B0=B1=E6=8A=A5=E9=94=99?= =?UTF-8?q?=E7=9A=84=E3=80=8C=E8=BF=90=E8=A1=8C=E3=80=8D=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 教程正文里写 ```c main 的片段才套。套的行就在编辑器里,报错行号和看到的一致,复制出去是完整程序。 Co-Authored-By: Claude Opus 5.5 --- .../src/oj/learn/components/CodeRunBlock.vue | 49 +++++++++++++------ .../src/oj/learn/components/LessonBody.vue | 1 + .../oj/learn/composables/useLessonParse.ts | 13 +++-- apps/web/src/oj/learn/wrapMain.ts | 46 +++++++++++++++++ 4 files changed, 92 insertions(+), 17 deletions(-) create mode 100644 apps/web/src/oj/learn/wrapMain.ts 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]