fix(题目): 修掉代码审查查出的几处问题——流程图拿到的徽章不弹、点评弹窗读失败时关不掉等

- 流程图画到 A / S 拿到的题单徽章、成就原来不弹:流程图那条 WebSocket 只认评分完成 / 失败,
  成就消息被丢掉了,要等下次换路由拉 pending 才出来
- 强制点评弹窗读不出点评、或者一直交不上时,「下一题 / 继续」按钮也给出来:原来弹窗关不掉,
  只能按浏览器后退;拦下换题时就记上「继续」,按钮文字不会写错
- 「放回编辑器」走正常的换语言路子(画流程图时切回代码编辑器、读那门语言的草稿);
  这道题已经不收的语言(0020 摘掉的 Java / JS / Go)置灰,只能复制
- 举着手时刚交对、1.5 秒内点下一题:原来「确定要换题吗」要问两遍(点评完再问一次),
  现在同意过的那个地址不再问
- 课堂看板上评分中、评失败的流程图不再算「尝试过」,和学生的课堂条一个口径;
  新加的流程图查询和其他查询并行,不再多等一轮数据库
- 课堂条的数据只认最后发出的那次请求:进页面那次后到的话会把刚做完的那道盖回「没做」
- 「A / S 算做完」收进契约,前后端只留一份;协作中老师的判断统一用 useTeacherCollab;
  几处放错位置、说法过时的注释

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-28 19:35:07 +08:00
1 parent 10169f5792
commit 83b6b7a81a
16 files changed
+162 -99

No files matched your search

@@ -14,6 +14,7 @@ import { NFlex } from "naive-ui"
import SubmitCode from "./SubmitCode.vue"
import { useProblemPageContext } from "../composables/problemPageContext"
import { statisticsOpen, useEditorMenu } from "../composables/editorMenu"
import { useTeacherCollab } from "../composables/teacherCollab"
const SubmitFlowchart = defineAsyncComponent(() => import("./SubmitFlowchart.vue"))
@@ -73,14 +74,10 @@ const showHelpButton = computed(
)
/**
* 教师端:协作就开在这道题上。接单后直接跳到题目页协作(原来是 CollabModal 弹框),
* 所以状态和「结束协作」得摆在题目页的工具栏上 —— 也只有这一个按钮能结束,
* 不像弹框那样按一下 Esc 就把协作关掉了。
* 教师正在这道题上协作:编辑器里是学生的代码。协作状态和「结束协作」在工具栏上面那条
* 协作条里(CollabBar);工具栏这边禁掉语言选择、不给「写代码 / 画流程图」切换
*/
const collabHere = computed(
() => collabStore.room !== null && collabStore.room.problemId === problem.value?._id,
)
const showCollabBar = computed(() => collabHere.value && userStore.isTeacherOrAbove)
const showCollabBar = useTeacherCollab()
/**
* 状态全塞进按钮本身。原来旁边还挂一个 n-tag 说明排队情况,一行工具栏
@@ -56,7 +56,7 @@ const suggestions = computed(() =>
)
function gradeType(grade: string) {
if (grade === "S" || grade === "A") return "success"
if (isFlowchartPass(grade)) return "success"
if (grade === "B") return "warning"
return "error"
}
@@ -12,6 +12,7 @@ import { beginEditTrace, editTraceExtensions } from "oj/problem/utils/editTrace"
import { errorMarkExtensions } from "oj/problem/utils/errorMark"
import EditorToolbar from "./EditorToolbar.vue"
import { useDraftKey } from "../composables/draftKey"
import { useTeacherCollab } from "../composables/teacherCollab"
import CollabBar from "./CollabBar.vue"
import { useFlowchartStore } from "oj/store/flowchart"
@@ -39,7 +40,7 @@ const collabHere = computed(
)
/** 协作中的教师:编辑器里是学生的代码,不是他自己的 */
const teacherCollab = computed(() => collabHere.value && collabStore.isTeacher)
const teacherCollab = useTeacherCollab()
/** 教师接单前自己选的语言,协作结束后连同草稿一起还原 */
let teacherLanguageBefore: LANGUAGE | null = null
@@ -347,7 +347,14 @@ function sectorAriaLabel(index: number) {
</div>
<p v-if="submitFailed" class="error">没记上,再点一次</p>
<div v-if="state === 'results' && $slots.after" class="after">
<!--
强制弹窗关不掉,这个插槽(「下一题 / 继续」)是唯一的出口:读不出点评、交点评失败时也得给,
不能把学生关在弹窗里(设计文档第 7 节:接口出错时不能把学生卡住)
-->
<div
v-if="(state === 'results' || state === 'failed' || submitFailed) && $slots.after"
class="after"
>
<slot name="after" />
</div>
</section>
@@ -1,6 +1,7 @@
<script lang="ts" setup>
import { getRankOfProblem, getSubmission, getSubmissions } from "oj/api"
import { useCodeStore } from "oj/store/code"
import { useProblemStore } from "oj/store/problem"
import Pagination from "shared/components/Pagination.vue"
import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
import { useCollabStore } from "shared/store/collab"
@@ -22,6 +23,7 @@ import { useTeacherCollab } from "../composables/teacherCollab"
const userStore = useUserStore()
const codeStore = useCodeStore()
const problemStore = useProblemStore()
const collabStore = useCollabStore()
const ctx = useProblemPageContext()
const route = useRoute()
@@ -139,11 +141,22 @@ function loadedOf(id: string): Loaded | null {
return value && typeof value === "object" ? value : null
}
function putBack(id: string) {
/** 这道题现在还收不收那次提交的语言(0020 从题目里摘掉了 Java / JS / Go,老提交还在) */
function canPutBack(id: string) {
const loaded = loadedOf(id)
if (!loaded) return
return !!loaded && problemStore.codeLanguages.some((language) => language === loaded.language)
}
async function putBack(id: string) {
const loaded = loadedOf(id)
if (!loaded || !canPutBack(id)) return
// 语言不一样就先按正常的路子切过去(存语言偏好、读那门语言的草稿;画流程图时切回代码编辑器),
// 等编辑器切完再放代码 —— 直接改 codeStore 的语言会绕过这些,草稿存错地方
if (codeStore.code.language !== loaded.language) {
problemStore.switchLanguage(loaded.language)
await nextTick()
}
// 同一个编辑器里 dispatch 一次整段替换,CodeMirror 的历史记着它,Ctrl+Z 能撤回
codeStore.setLanguage(loaded.language)
codeStore.setCode(loaded.code)
message.success("已放回编辑器,按 Ctrl+Z 可以撤回")
}
@@ -233,6 +246,12 @@ watch(query, () => {
size="small"
type="primary"
secondary
:disabled="!canPutBack(row.id)"
:title="
canPutBack(row.id)
? undefined
: `这道题现在不收 ${LANGUAGE_SHOW_VALUE[loadedOf(row.id)!.language]} 了,只能复制`
"
@click="putBack(row.id)"
>
放回编辑器
@@ -43,6 +43,9 @@ const commentPanel = ref(false)
const reviewOwed = ref(false)
let reviewChecking = false
let navigationAfterReview: string | null = null
// navigationAfterReview 是普通变量(守卫里改,不需要触发渲染),弹窗按钮的文案(「继续」)
// 要一个响应式的影子:拦下的那一刻就记上,点评读不出来、交不上时按钮也得写对
const navigationAfterReviewShown = ref(false)
async function openReview() {
if (reviewChecking) return
@@ -67,6 +70,7 @@ function settleReview() {
reviewOwed.value = false
const target = navigationAfterReview
navigationAfterReview = null
navigationAfterReviewShown.value = false
if (target) router.push(target)
}
@@ -86,9 +90,6 @@ const reviewed = ref(false)
const continueButton = useTemplateRef<{ $el: HTMLElement }>("continueButton")
const lessonStore = useLessonStore()
// navigationAfterReview 是普通变量(守卫里改,不需要触发渲染),按钮文案要一个响应式的影子
const navigationAfterReviewShown = ref(false)
/** 题目名中位 7 字、最长 33 字,按钮里放不下那么长的 */
const shortTitle = (title: string) => (title.length > 14 ? `${title.slice(0, 13)}…` : title)
@@ -106,7 +107,6 @@ const continueAction = computed(() => {
async function onReviewed() {
reviewed.value = true
navigationAfterReviewShown.value = navigationAfterReview !== null
await nextTick()
continueButton.value?.$el.focus()
}
@@ -132,6 +132,7 @@ function holdForReview(to: RouteLocationNormalized, from: RouteLocationNormalize
if (!reviewOwed.value || commentPanel.value) return true
if (to.name === from.name && to.params.problemID === from.params.problemID) return true
navigationAfterReview = to.fullPath
navigationAfterReviewShown.value = true
cancelCommentPanel()
openReview()
return false
@@ -167,6 +168,7 @@ watch(
cancelCommentPanel()
reviewOwed.value = false
navigationAfterReview = null
navigationAfterReviewShown.value = false
cancelGoToProblemSet()
},
)
+21 -1
View File
@@ -140,9 +140,19 @@ watch(
*/
const collabStore = useCollabStore()
const dialog = useDialog()
/**
* 已经同意过要去的地方。通过之后 1.5 秒内点「下一题」:这里先问一次,接着被强制点评拦下
* (SubmissionEffects 的 holdForReview),评完再 push 同一个地址 —— 不记着的话会再问一遍,
* 第二次点「留在这道题」就成了同意换题之后又被留下
*/
let approvedTarget: string | null = null
function confirmLeavingHelp(to: RouteLocationNormalized, from: RouteLocationNormalized) {
if (collabStore.isTeacher || collabStore.helpStatus === "idle") return true
if (to.name === from.name && to.params.problemID === from.params.problemID) return true
if (approvedTarget === to.fullPath) {
approvedTarget = null
return true
}
const active = collabStore.helpStatus === "active"
return new Promise<boolean>((resolve) => {
dialog.warning({
@@ -152,7 +162,10 @@ function confirmLeavingHelp(to: RouteLocationNormalized, from: RouteLocationNorm
: "换题之后这次举手就撤掉了,要在新的题目里重新举手。确定要换吗?",
positiveText: "换题",
negativeText: "留在这道题",
onPositiveClick: () => resolve(true),
onPositiveClick: () => {
approvedTarget = to.fullPath
resolve(true)
},
onNegativeClick: () => resolve(false),
onClose: () => resolve(false),
onMaskClick: () => resolve(false),
@@ -161,6 +174,13 @@ function confirmLeavingHelp(to: RouteLocationNormalized, from: RouteLocationNorm
}
onBeforeRouteUpdate(confirmLeavingHelp)
onBeforeRouteLeave(confirmLeavingHelp)
// 到了就作废:没被点评拦下、一次就换过去的,不能留着让下次去同一道题时不再问
watch(
() => route.fullPath,
(path) => {
if (path === approvedTarget) approvedTarget = null
},
)
// 结果页签的标题带状态图标(ResultTabLabel),n-tab-pane 的 tab 接受渲染函数
const resultTab = () => h(ResultTabLabel)