diff --git a/apps/web/src/oj/problem/components/EditorToolbar.vue b/apps/web/src/oj/problem/components/EditorToolbar.vue index f477e18..7802337 100644 --- a/apps/web/src/oj/problem/components/EditorToolbar.vue +++ b/apps/web/src/oj/problem/components/EditorToolbar.vue @@ -214,12 +214,22 @@ const copy = async () => { message[success ? "success" : "error"](`代码复制${success ? "成功" : "失败"}`) } +// 重置会把编辑器里的代码换成模板、连本地草稿一起删掉,点错一下写了半节课的代码就没了,先问一句 +const dialog = useDialog() const reset = () => { - codeStore.setCode( - problem.value!.template[codeStore.code.language] || SOURCES[codeStore.code.language], - ) - storage.remove(storageKey) - message.success("代码重置成功") + dialog.warning({ + title: "重置代码", + content: "编辑器里的代码会换回题目给的模板,存着的草稿也会删掉。确定吗?", + positiveText: "重置", + negativeText: "再想想", + onPositiveClick: () => { + codeStore.setCode( + problem.value!.template[codeStore.code.language] || SOURCES[codeStore.code.language], + ) + storage.remove(storageKey) + message.success("已换回模板,按 Ctrl+Z 可以撤回") + }, + }) } const changeLanguage = (v: LANGUAGE) => { diff --git a/apps/web/src/oj/problem/components/ProblemContent.vue b/apps/web/src/oj/problem/components/ProblemContent.vue index a2f3545..0931730 100644 --- a/apps/web/src/oj/problem/components/ProblemContent.vue +++ b/apps/web/src/oj/problem/components/ProblemContent.vue @@ -5,13 +5,12 @@ import { useProblemStore } from "oj/store/problem" import { DIFFICULTY } from "utils/constants" import { getTagColor } from "utils/functions" import { useSubmissionStore } from "oj/store/submission" -import type { ProblemRow } from "utils/types" import Copy from "shared/components/Copy.vue" import { useDark } from "@vueuse/core" import { MdPreview } from "md-editor-v3" import "md-editor-v3/lib/preview.css" -import { getSimilarProblems } from "oj/api" import SQLDataTable from "./SQLDataTable.vue" +import SimilarProblems from "./SimilarProblems.vue" import { useFlowchartStore } from "oj/store/flowchart" import { useMyFlowchartStore } from "shared/store/myFlowchart" import { useUserStore } from "shared/store/user" @@ -37,42 +36,9 @@ const sqlChangedTables = computed(() => { return exp && "changed_tables" in exp ? exp.changed_tables : [] }) -const router = useRouter() - -// 相似题目推荐 -const similarProblems = ref([]) -const similarLoaded = ref(false) - -async function loadSimilarProblems() { - if (similarLoaded.value || !problem.value) return - // 比赛、题单里不推荐(理由见 problemPageContext 那张表) - if (!ctx.value.similar) return - try { - similarProblems.value = await getSimilarProblems(problem.value._id) - } catch { - similarProblems.value = [] - } - similarLoaded.value = true -} - -// 切换题目时重置相似推荐状态 -watch( - () => problem.value?._id, - () => { - similarProblems.value = [] - similarLoaded.value = false - }, -) - -// AC 或失败次数 >= 3 时加载推荐 -watch( - () => [problem.value?._id, problem.value?.myStatus, problemStore.failCount], - ([, status, failCount]) => { - if (status === 0 || (failCount as number) >= 3) { - loadSimilarProblems() - } - }, - { immediate: true }, +// 相似题推荐:做对了、或者错了 3 次以上才给(比赛、题单里不给,见 problemPageContext) +const showSimilar = computed( + () => ctx.value.similar && (problem.value?.myStatus === 0 || problemStore.failCount >= 3), ) /** @@ -352,49 +318,7 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem /> - -
- -

- - 相似题目推荐 -

- - - - - {{ sp._id }} - - {{ sp.title }} - - - - - {{ sp.difficulty }} - - - - -
+ diff --git a/apps/web/src/oj/problem/components/ResultPane.vue b/apps/web/src/oj/problem/components/ResultPane.vue index 7c5ccf5..b2e895e 100644 --- a/apps/web/src/oj/problem/components/ResultPane.vue +++ b/apps/web/src/oj/problem/components/ResultPane.vue @@ -23,7 +23,8 @@ const FlowchartResult = defineAsyncComponent(() => import("./FlowchartResult.vue const PeerLatest = defineAsyncComponent(() => import("./PeerLatest.vue")) const submissionStore = useSubmissionStore() -const { submission, syntaxErrorInfo, resultSegment } = storeToRefs(submissionStore) +const { submission, syntaxErrorInfo, resultSegment, formattedBeforeSubmit } = + storeToRefs(submissionStore) const { problem } = storeToRefs(useProblemStore()) const codeStore = useCodeStore() @@ -55,7 +56,12 @@ const canTrial = computed(() => { - + { margin-bottom: 14px; } +.formatted { + margin: 0 0 10px; + font-size: 13px; + opacity: 0.7; +} + .empty { margin-top: 48px; } diff --git a/apps/web/src/oj/problem/components/SimilarProblems.vue b/apps/web/src/oj/problem/components/SimilarProblems.vue new file mode 100644 index 0000000..a6b9e0b --- /dev/null +++ b/apps/web/src/oj/problem/components/SimilarProblems.vue @@ -0,0 +1,97 @@ + + + + + + + diff --git a/apps/web/src/oj/problem/components/SubmissionResult.vue b/apps/web/src/oj/problem/components/SubmissionResult.vue index 6e3c448..adf2fd2 100644 --- a/apps/web/src/oj/problem/components/SubmissionResult.vue +++ b/apps/web/src/oj/problem/components/SubmissionResult.vue @@ -19,6 +19,8 @@ import PythonErrorExplain from "./PythonErrorExplain.vue" import RuntimeErrorExplain from "./RuntimeErrorExplain.vue" import WrongAnswerExplain from "./WrongAnswerExplain.vue" import LessonNext from "./LessonNext.vue" +import SimilarProblems from "./SimilarProblems.vue" +import { useLessonStore } from "oj/store/lesson" import { useProblemPageContext } from "../composables/problemPageContext" import { MdPreview } from "md-editor-v3" import "md-editor-v3/lib/preview.css" @@ -98,6 +100,20 @@ const showLessonNext = computed( !props.peer && props.submission?.result === SubmissionStatus.accepted && ctx.value.lessonNext, ) +/** + * 通过了、但这道题不在这节课里(没有「下一题」可给):给几道相似题接着练。 + * 原来相似题只在题面最底下,交完不会有人再滚回去看 + */ +const lessonStore = useLessonStore() +const showSimilar = computed( + () => + !props.peer && + props.submission?.result === SubmissionStatus.accepted && + ctx.value.similar && + !!problemStore.problem && + !lessonStore.includes(problemStore.problem._id), +) + const msg = computed(() => { if (!props.submission) return "" // 走 PythonErrorExplain / RuntimeErrorExplain 那两张中文卡片 @@ -216,6 +232,7 @@ const columns: DataTableColumn[] = [ v-if="showLessonNext && problemStore.problem" :problem-display-id="problemStore.problem._id" /> + }), ) +/** + * 这道题刚交对了:「提交」降成描边按钮,让结果页签里的「下一题」是唯一的主按钮 + * (设计文档 5.4)。改了代码想再交一次照样能交 + */ +const justAccepted = computed( + () => submissionStore.submission?.result === SubmissionStatus.accepted, +) + function submit() { if (buttonState.value.disabled) return submissionStore.submit({ @@ -46,7 +55,7 @@ function submit() { diff --git a/apps/web/src/oj/problem/utils/wrongAnswer.ts b/apps/web/src/oj/problem/utils/wrongAnswer.ts index 8c2bbed..eca877a 100644 --- a/apps/web/src/oj/problem/utils/wrongAnswer.ts +++ b/apps/web/src/oj/problem/utils/wrongAnswer.ts @@ -147,10 +147,40 @@ export function explainDiff( case "values": return "输出的格式对了,但算出来的数不对。拿这个例子的输入,自己一步一步算一遍,看看程序在哪一步和你想的不一样。" case "other": - return "对照一下你的输出和正确输出,找找从哪里开始不一样。" + return otherHint(check) } } +/** 放进一句话里的值:太长的截断,免得一句提示撑满一屏 */ +function quote(text: string, max = 24) { + const flat = text.trim() + return `「${flat.length > max ? `${flat.slice(0, max)}…` : flat}」` +} + +/** + * 「其他」:格式和内容都不一样(占样例错误的 44%)。原来只说「对照一下,找找从哪里开始 + * 不一样」,对 D / E 这种整个值不同的情况没用 —— 学生看得出不一样,看不出为什么。 + * 改成把具体的值说出来,再告诉他拿这个例子的输入去走一遍程序(设计文档第 6 节)。 + */ +function otherHint(check: SampleCheck) { + const expected = (check.expected ?? "").replace(/\s+$/, "") + const output = (check.output ?? "").replace(/\s+$/, "") + const no = (check.index ?? 0) + 1 + const input = (check.input ?? "").trim() + const walk = + input && !input.includes("\n") && input.length <= 24 + ? `拿例子 ${no} 的输入${quote(input)},一行一行走一遍你的程序,看它走进了哪个分支、算出了什么。` + : `拿例子 ${no} 的输入,一行一行走一遍你的程序,看它走进了哪个分支、算出了什么。` + if (!expected.includes("\n") && !output.includes("\n")) { + return `你的程序输出了${quote(output)},正确的是${quote(expected)}。${walk}` + } + const line = firstDifferentLine(expected, output) + const want = expected.split("\n")[line] ?? "" + const got = output.split("\n")[line] + const gotText = got === undefined ? "什么都没有" : quote(got) + return `从第 ${line + 1} 行开始不一样:正确的是${quote(want)},你输出的是${gotText}。${walk}` +} + /** 第一处不一样的行号,0 起;完全一样(只差结尾空白)时为 -1 */ export function firstDifferentLine(expected: string, output: string) { const e = expected.replace(/\s+$/, "").split("\n") diff --git a/apps/web/src/oj/store/submission.ts b/apps/web/src/oj/store/submission.ts index 0a36e18..ee8009e 100644 --- a/apps/web/src/oj/store/submission.ts +++ b/apps/web/src/oj/store/submission.ts @@ -71,6 +71,12 @@ export const useSubmissionStore = defineStore("submission", () => { */ const syntaxErrorInfo = ref("") + /** + * 这次提交之前自动排版改了代码。排版会直接改掉编辑器里的代码(缩进、空格、换行), + * 学生交完发现代码「变了样」却不知道为什么 —— 结果页签里提一句 + */ + const formattedBeforeSubmit = ref(false) + const { start: startCooldown, isPending: isCooldown } = useTimeout(5000, { controls: true, immediate: false, @@ -84,6 +90,7 @@ export const useSubmissionStore = defineStore("submission", () => { const problem = problemStore.problem if (!problem) return syntaxErrorInfo.value = "" + formattedBeforeSubmit.value = false // 0. 提交前自动格式化(Python 用 ruff,C/C++ 用 clang-format,SQL 用 sqlparse)。 // Python 在格式化之前先由服务端的 CPython 查一遍语法,有错就不提交 @@ -95,7 +102,10 @@ export const useSubmissionStore = defineStore("submission", () => { code: codeStore.code.value, language: formatLang, }) - codeStore.setCode(res.code) + if (res.code !== codeStore.code.value) { + codeStore.setCode(res.code) + formattedBeforeSubmit.value = true + } } catch (e) { if (errorCode(e) === "syntax-error") { // 仅 Python 会出现:message 是 CPython 的报错原文,交给 PythonErrorExplain 翻译 @@ -151,6 +161,7 @@ export const useSubmissionStore = defineStore("submission", () => { */ function reset() { syntaxErrorInfo.value = "" + formattedBeforeSubmit.value = false resultSegment.value = "submit" monitor.reset() hint.clearHint() @@ -183,6 +194,7 @@ export const useSubmissionStore = defineStore("submission", () => { isFormatting, isSubmittingRequest, syntaxErrorInfo, + formattedBeforeSubmit, isCooldown, submit, reset, diff --git a/apps/web/src/shared/components/AchievementToast.vue b/apps/web/src/shared/components/AchievementToast.vue index 1d83dc0..ab64c64 100644 --- a/apps/web/src/shared/components/AchievementToast.vue +++ b/apps/web/src/shared/components/AchievementToast.vue @@ -59,8 +59,9 @@ onUnmounted(() => {