feat(题目): 判题结果从提交按钮下的浮层挪到左栏「结果」页签——改代码时结果就摆在旁边

原来结果是提交按钮下面的 n-popover:点一下外面就收起、最高 600px、盖住编辑器,
学生没法一边看「正确输出 / 你的输出」一边改代码,还得靠「上次结果」按钮找回来。

- 左栏新增「结果」页签(手机上排在「代码」后面),交上去或者语法检查没过就自动切过去;
  页签标题带状态图标(✕ / ✓ / ! / 判题中转圈),不只靠颜色
- 页签用 show:lazy:切走不卸载,编辑器里的错误标记、AI 提示都留着
- 「题目描述」改叫「题目」;去掉浮层和「上次结果」按钮

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-28 14:21:57 +08:00
1 parent 5cb7cfa924
commit f4db7dc81c
6 files changed
+160 -67

No files matched your search

@@ -0,0 +1,43 @@
<script setup lang="ts">
import { storeToRefs } from "pinia"
import { useSubmissionStore } from "oj/store/submission"
/**
* 左栏「结果」页签的内容:提交前语法检查没过的说明、这一次提交的判题结果,或者空状态。
*
* 原来是提交按钮下面的一个浮层(n-popover),点一下外面就收起、最高 600px、盖住编辑器,
* 学生没法一边看「正确输出 / 你的输出」一边改代码。放进左栏页签之后编辑器整列都在,
* 改代码时结果就摆在旁边(见 docs/specs/2026-09-28-problem-page-redesign-design.md)。
*/
// 第一次切到「结果」时才加载:它带着 DataTable 和 Markdown 渲染,只看题不提交的人不用付
const SubmissionResult = defineAsyncComponent(() => import("./SubmissionResult.vue"))
const PythonErrorExplain = defineAsyncComponent(() => import("./PythonErrorExplain.vue"))
const { submission, syntaxErrorInfo } = storeToRefs(useSubmissionStore())
</script>
<template>
<div class="result-pane">
<n-flex v-if="syntaxErrorInfo" vertical>
<n-alert type="warning" title="代码有语法错误,还没有提交" />
<PythonErrorExplain :err-info="syntaxErrorInfo" />
</n-flex>
<SubmissionResult v-else-if="submission" :submission="submission" />
<n-empty
v-else
class="empty"
description="还没有提交过。写完代码按「提交代码」,结果会出现在这里"
/>
</div>
</template>
<style scoped>
.result-pane {
padding: 4px 0 16px;
}
.empty {
margin-top: 48px;
}
</style>
@@ -0,0 +1,60 @@
<script setup lang="ts">
import { Icon } from "@iconify/vue"
import { useThemeVars } from "naive-ui"
import { storeToRefs } from "pinia"
import { useSubmissionStore } from "oj/store/submission"
import { SubmissionStatus } from "utils/constants"
/**
* 「结果」页签的标题:带上这一次的状态。用图标而不是一个彩色小圆点 ——
* 机房投影、色弱的学生只靠颜色分不出对错。
*/
const theme = useThemeVars()
const { submission, syntaxErrorInfo, judging, pending, submitting } =
storeToRefs(useSubmissionStore())
const status = computed(() => {
if (syntaxErrorInfo.value) return { icon: "ph:warning-bold", color: theme.value.warningColor }
if (!submission.value) return null
if (judging.value || pending.value || submitting.value)
return { icon: "ph:circle-notch-bold", color: theme.value.infoColor, spin: true }
const result = submission.value.result
if (result === SubmissionStatus.accepted)
return { icon: "ph:check-bold", color: theme.value.successColor }
if (result === SubmissionStatus.ast_check_failed)
return { icon: "ph:warning-bold", color: theme.value.warningColor }
return { icon: "ph:x-bold", color: theme.value.errorColor }
})
</script>
<template>
<span class="label">
结果
<Icon
v-if="status"
:icon="status.icon"
:color="status.color"
:class="{ spin: status.spin }"
width="14"
/>
</span>
</template>
<style scoped>
.label {
display: inline-flex;
align-items: center;
gap: 4px;
}
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
@@ -73,9 +73,8 @@ function settleReview() {
function closeCommentPanel() {
commentPanel.value = false
// 点评价弹窗时,结果面板会当成「点了外面」收起来 —— 而「下一题」就在面板里
// (LessonNext),评价完得把它重新打开
submissionStore.showResult = true
// 「下一题」在结果页签里(LessonNext),评价完回到那里 —— 学生点评之前可能切去看了题目
submissionStore.revealResult()
settleReview()
}
@@ -1,6 +1,5 @@
<script setup lang="ts">
import { Icon } from "@iconify/vue"
import { storeToRefs } from "pinia"
import { useCodeStore } from "oj/store/code"
import { useSubmissionStore } from "oj/store/submission"
import { getSubmitButtonState } from "./submitButtonState"
@@ -8,20 +7,14 @@ import { useBreakpoints } from "shared/composables/breakpoints"
import { useUserStore } from "shared/store/user"
/**
* 提交按钮 + 结果面板。只管显示和把点击交给 store:提交的状态在 submission store,
* 判完之后该发生的事(烟花、点评、回题单)在页面这一层的 SubmissionEffects ——
* 提交按钮。只把点击交给 store:提交的状态在 submission store,结果显示在左栏的
* 「结果」页签(ResultPane),判完之后该发生的事在页面这一层的 SubmissionEffects ——
* 这个组件在编辑器的工具栏里,编辑器卸载时它也跟着卸载,所以什么都不能只存在这里。
*/
// 结果面板第一次弹出(也就是第一次提交)时才加载:它带着 DataTable,而判题要等
// 好几秒,这点下载时间藏得住。进页面就加载的话,只看题不提交的人也要付这笔
const SubmissionResult = defineAsyncComponent(() => import("./SubmissionResult.vue"))
const PythonErrorExplain = defineAsyncComponent(() => import("./PythonErrorExplain.vue"))
const userStore = useUserStore()
const codeStore = useCodeStore()
const submissionStore = useSubmissionStore()
const { submission, showResult, syntaxErrorInfo } = storeToRefs(submissionStore)
const route = useRoute()
const { isDesktop } = useBreakpoints()
@@ -47,51 +40,17 @@ function submit() {
</script>
<template>
<!-- 提交按钮 + 结果弹窗。
display-directive 默认是 "if":面板一收起来整个 SubmissionResult 就被卸载。
AI 提示的内容现在在 store 里,卸了也不丢,但重新挂载要重渲染 Markdown、
重新拉一次「这节课的下一题」,留着更省事。 -->
<n-popover
trigger="manual"
display-directive="show"
placement="bottom-end"
scrollable
:show-arrow="false"
style="max-height: 600px"
:show="showResult"
@clickoutside="showResult = false"
>
<template #trigger>
<n-button
:size="isDesktop ? 'medium' : 'small'"
type="primary"
:disabled="buttonState.disabled"
@click="submit"
>
<template #icon>
<n-icon>
<Icon :icon="buttonState.icon" />
</n-icon>
</template>
{{ buttonState.label }}
</n-button>
</template>
<n-flex v-if="syntaxErrorInfo" vertical style="max-width: 560px">
<n-alert type="warning" title="代码有语法错误,还没有提交" />
<PythonErrorExplain :err-info="syntaxErrorInfo" />
</n-flex>
<SubmissionResult v-show="!syntaxErrorInfo" :submission="submission" />
</n-popover>
<!-- 结果面板点一下别处就收起来,而 showResult 只在提交时被置 true ——
原来唯一的重开方式是「再提交一次」,AI 提示读到一半去看眼题面就回不来了。
只在这次会话提交过之后才出现,没提交时工具栏保持原样。 -->
<n-button
v-if="(submission || syntaxErrorInfo) && !showResult"
:size="isDesktop ? 'medium' : 'small'"
@click="showResult = true"
type="primary"
:disabled="buttonState.disabled"
@click="submit"
>
上次结果
<template #icon>
<n-icon>
<Icon :icon="buttonState.icon" />
</n-icon>
</template>
{{ buttonState.label }}
</n-button>
</template>