fix(题目): 修掉题目页重构审查查出的问题——提交途中换题会把结果算到新题上、运行例子把对的菱形判错、点评完「下一题」不跳等
题目页重构之后按「功能对照 / 状态 / 结果页 / 布局与后端」四路审查了一遍,查出的问题一起修: 提交与换题 - 提交、画流程图途中换题:A 题格式化好的代码会盖掉 B 的草稿,A 判对了把 B 标成已解决、 弹 B 的点评、题单里跳回题单。现在每个请求回来先核对题目,轮询和推送回来的也核对提交编号 - 画着流程图换到不能画的题,语言退到题目的第一门、还被记成偏好;现在回到学生习惯的那门 - 强制点评评完点「继续 / 下一题」,学生之前在看「题目」页签的话会停在原题 - 离开题目页之后判题、流程图的 WebSocket 一直连着,现在闲 15 分钟断开 运行例子 - 输出开头的空格被削掉,打印菱形这类题对的代码也显示「没对上」 - C 的编译警告拼进了「你的输出」(后台生成测试点也会写进 .out) - Python 的语法错误(中文标点)被说成「下标有没有越界、除数是不是 0」 - 比赛里仍然把空格画成「·」、把不同的地方标红 入口与协作 - 比赛里不给「课堂统计」、不绑协作:房间和统计都按题号找,比赛题号和公开题撞号 - 「看他交过什么」按整名匹配,不再混进学号相近的同学 - 协作中点上下文条、相似题、下一题开新标签,协作不断 - 题单入口给管理员的「⋯」里留「本题提交」 结果与题面 - 「去自己输入拿 51 这些数试试」取到了题面 HTML 里的颜色值;「题目原话」露出 " - 流程图 A 级的学生标题下还写着「还没交过」 - 「我的提交」上方补回「答案错误 × 3」这样的计数 - 流程图历次评分能点开看那一版的图;读分数失败不再说「还没交过流程图」;手机上能看流程图点评 - 「结果」页签有新结果没看时加粗上色,看过就恢复 - 空画布拖进第一个节点画布会跳一下;画布底下空着一条 - 比赛没开始写成「已结束」、倒计时超过一个月算错;分隔条拖到头工具栏被挤坏;题单里的题目页有备案页脚 - 流程图历次分数只读需要的几列(一个学生一道题 23 次提交:96 kB → 2 kB) - 比赛密码、比赛没开始等几条学生看得到的英文报错换成中文 设计文档照实际做法更新了第 5.5、5.6、6、8、9、10、11 节。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ee66cef367
commit
e7b95d211d
34 files changed
+579
-108
No files matched your search
@@ -23,7 +23,8 @@ function openHisSubmissions() {
|
||||
if (!room || !problem.value) return
|
||||
const target = router.resolve({
|
||||
name: "submissions",
|
||||
query: { problem: problem.value._id, username: room.peerName },
|
||||
// exactUsername:整名匹配,不然学号相近的同学(ks24a1 / ks24a10…)会混进来
|
||||
query: { problem: problem.value._id, username: room.peerName, exactUsername: "1" },
|
||||
})
|
||||
window.open(target.href, "_blank")
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useProblemStore } from "oj/store/problem"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { ContestStatus } from "utils/constants"
|
||||
import type { ProblemSet, ProblemSetProblem } from "utils/types"
|
||||
import { useProblemPageContext } from "../composables/problemPageContext"
|
||||
import ProblemChips, { type ChipItem } from "./ProblemChips.vue"
|
||||
@@ -213,6 +214,8 @@ const contestBar = computed(() => {
|
||||
title: contest.title,
|
||||
chips,
|
||||
remaining: contestStore.remaining,
|
||||
// 不在进行中的两种:管理员赛前看题是「还没开始」,不能一律写「已结束」
|
||||
notStarted: contestStore.contestStatus === ContestStatus.not_started,
|
||||
home: { name: "contest problems", params: { contestID: ctx.value.contestId } },
|
||||
rank: { name: "contest rank", params: { contestID: ctx.value.contestId } },
|
||||
}
|
||||
@@ -352,7 +355,7 @@ onSolvedHere(() => {
|
||||
<span class="meta">还剩</span>
|
||||
<span class="countdown">{{ contestBar.remaining }}</span>
|
||||
</template>
|
||||
<span v-else class="meta">已结束</span>
|
||||
<span v-else class="meta">{{ contestBar.notStarted ? "还没开始" : "已结束" }}</span>
|
||||
<router-link :to="contestBar.rank" class="contest-rank">排名</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -226,7 +226,7 @@ const changeLanguage = (v: LANGUAGE) => {
|
||||
</n-button>
|
||||
|
||||
<n-button
|
||||
v-if="statisticsInline && userStore.isTeacherOrAbove"
|
||||
v-if="ctx.classStats && statisticsInline && userStore.isTeacherOrAbove"
|
||||
:size="buttonSize"
|
||||
@click="statisticsOpen = true"
|
||||
>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { storeToRefs } from "pinia"
|
||||
import { useCodeStore } from "oj/store/code"
|
||||
import { isFlowchartPass, useFlowchartStore } from "oj/store/flowchart"
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import { sortFlowchartCriteria } from "utils/constants"
|
||||
import { decompressFromBase64, parseTime } from "utils/functions"
|
||||
|
||||
@@ -12,13 +13,29 @@ import { decompressFromBase64, parseTime } from "utils/functions"
|
||||
*
|
||||
* 原来评分详情是提交按钮旁一个 1000px 的弹框:盖住画布,学生没法一边看改进建议一边改图;
|
||||
* 翻历次评分要一页一页点分页器。现在摆在左栏,画布整列都在右边(设计文档第 6 节)。
|
||||
*
|
||||
* viewOnly:手机上只能看、不能画(设计文档 5.6),不给「照着它写代码」「载回画布」。
|
||||
*/
|
||||
const { viewOnly = false } = defineProps<{ viewOnly?: boolean }>()
|
||||
|
||||
const MyFlowchart = defineAsyncComponent(() => import("./MyFlowchart.vue"))
|
||||
|
||||
const flowchartStore = useFlowchartStore()
|
||||
const { phase, scores, hiddenCount, selectedId, details, detailLoading, editor } =
|
||||
const { phase, scores, hiddenCount, selectedId, details, detailLoading, scoresFailed, editor } =
|
||||
storeToRefs(flowchartStore)
|
||||
const problemStore = useProblemStore()
|
||||
const codeStore = useCodeStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 历次分数原来只由题面(ProblemContent)去读。手机上题面是另一个页签、不切过去就不挂载,
|
||||
// 直接打开 ?tab=result 的话「流程图点评」一条都没有。这里自己也读一次(同一道题只读一次)
|
||||
watch(
|
||||
() => [problemStore.problem?.id, userStore.isAuthed] as const,
|
||||
([id, authed]) => {
|
||||
if (id && authed) flowchartStore.ensureLoaded()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
const message = useMessage()
|
||||
const theme = useThemeVars()
|
||||
// 暗色主题的成功色是浅薄荷绿,上面的白字看不清
|
||||
@@ -33,6 +50,23 @@ const current = computed(() => {
|
||||
const detail = computed(() => (current.value ? details.value[current.value.id] : undefined))
|
||||
const viewingLatest = computed(() => selectedId.value === null)
|
||||
|
||||
// ==================== 这一版的图 ====================
|
||||
/**
|
||||
* 点历史分数条上的哪一次,就摆哪一次交上去的图(只读的 Mermaid 渲染,不动画布)。
|
||||
* 原来的评分弹框左边就是这张图;挪进结果页签时丢了,学生翻到「第 2 次 C 42」
|
||||
* 只看得到评语,想不起来那一版画成什么样。图从评语同一个详情接口来,随评语一起现拉。
|
||||
*/
|
||||
const versionIndex = computed(() =>
|
||||
current.value ? scores.value.findIndex((row) => row.id === current.value!.id) : -1,
|
||||
)
|
||||
const versionTitle = computed(() => {
|
||||
if (!current.value) return ""
|
||||
return `第 ${versionIndex.value + 1} 次画的图 · ${parseTime(current.value.createTime, "MM-DD HH:mm")}`
|
||||
})
|
||||
/** 收起来之后翻别的次也保持收着:左栏在机房屏上只有六百来像素高 */
|
||||
const versionOpen = ref(true)
|
||||
const versionZoom = ref(false)
|
||||
|
||||
// 评完的那次、点到的那次,评语都要现拉(历次分数里只有分数)
|
||||
watch(
|
||||
() => current.value?.id,
|
||||
@@ -162,10 +196,18 @@ function goCode() {
|
||||
<template>
|
||||
<!-- 设计稿「画流程图:画布占满右栏,AI 点评在左边」「流程图评分中」 -->
|
||||
<div class="flowchart-result">
|
||||
<div v-if="scoresFailed && !scores.length && phase !== 'evaluating'" class="failed load-failed">
|
||||
<span>以前交过的流程图评分没读出来,可能是网络断了一下</span>
|
||||
<n-button size="small" secondary @click="flowchartStore.ensureLoaded()">再读一次</n-button>
|
||||
</div>
|
||||
<n-empty
|
||||
v-if="!scores.length && phase !== 'evaluating'"
|
||||
v-else-if="!scores.length && phase !== 'evaluating'"
|
||||
class="empty"
|
||||
description="还没交过流程图。画好之后点「交给 AI 点评」,评分和建议会出现在这里"
|
||||
:description="
|
||||
viewOnly
|
||||
? '还没交过流程图。流程图要在电脑上画(手机上拖不动),交给 AI 点评之后,评分和建议在这里也看得到'
|
||||
: '还没交过流程图。画好之后点「交给 AI 点评」,评分和建议会出现在这里'
|
||||
"
|
||||
/>
|
||||
|
||||
<!-- 历次分数连成一条:一眼看出是不是越改越好;正在评的那次是虚线框 -->
|
||||
@@ -229,6 +271,35 @@ function goCode() {
|
||||
</n-flex>
|
||||
|
||||
<template v-if="detail">
|
||||
<div v-if="detail.mermaidCode" class="version">
|
||||
<div class="version-head">
|
||||
<span class="version-title">{{ versionTitle }}</span>
|
||||
<n-button text type="primary" size="small" @click="versionZoom = true">
|
||||
放大看
|
||||
</n-button>
|
||||
<n-button text size="small" @click="versionOpen = !versionOpen">
|
||||
{{ versionOpen ? "收起" : "展开" }}
|
||||
</n-button>
|
||||
</div>
|
||||
<div
|
||||
v-if="versionOpen"
|
||||
class="version-chart"
|
||||
role="button"
|
||||
:aria-label="`放大看${versionTitle}`"
|
||||
@click="versionZoom = true"
|
||||
>
|
||||
<MyFlowchart compact :code="detail.mermaidCode" />
|
||||
</div>
|
||||
<n-modal
|
||||
v-model:show="versionZoom"
|
||||
preset="card"
|
||||
:title="versionTitle"
|
||||
:style="{ maxWidth: '900px' }"
|
||||
>
|
||||
<MyFlowchart :code="detail.mermaidCode" />
|
||||
</n-modal>
|
||||
</div>
|
||||
|
||||
<div v-if="criteria.length" class="criteria">
|
||||
<div v-for="[name, item] in criteria" :key="name" class="criterion">
|
||||
<div class="criterion-head">
|
||||
@@ -254,7 +325,7 @@ function goCode() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<div v-if="!viewOnly" class="actions">
|
||||
<n-button
|
||||
v-if="isFlowchartPass(current.grade) && canCode"
|
||||
type="primary"
|
||||
@@ -369,6 +440,41 @@ function goCode() {
|
||||
border: 1px solid rgba(240, 160, 32, 0.35);
|
||||
}
|
||||
|
||||
.load-failed {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 12px;
|
||||
}
|
||||
|
||||
/* 这一版的图:和题面里「你画的流程图」同一个样子,点一下放大 */
|
||||
.version {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.version-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.version-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.version-chart {
|
||||
cursor: zoom-in;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(128, 128, 128, 0.06);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.version-chart :deep(.flowchart-container.compact) {
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.score-line {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
@@ -5,15 +5,17 @@ import { useMermaid } from "shared/composables/useMermaid"
|
||||
/**
|
||||
* 学生自己画到 A/S 的那张流程图。题面里是缩略的一小块(compact),点「放大看」弹出整张。
|
||||
* 原来是左栏单独一个「我的流程图」页签,评到 A/S 时还会把学生强行切过去。
|
||||
*
|
||||
* 结果页签里翻历次评分时也用它摆「这一版的图」,那时由 code 传进来,不读 store。
|
||||
*/
|
||||
const props = defineProps<{ compact?: boolean }>()
|
||||
const props = defineProps<{ compact?: boolean; code?: string }>()
|
||||
|
||||
const store = useMyFlowchartStore()
|
||||
const { renderError, renderFlowchart } = useMermaid()
|
||||
const mermaidContainer = useTemplateRef<HTMLElement>("mermaidContainer")
|
||||
|
||||
watch(
|
||||
() => store.mermaidCode,
|
||||
() => props.code ?? store.mermaidCode,
|
||||
async (code) => {
|
||||
if (!code) return
|
||||
await nextTick()
|
||||
|
||||
@@ -150,10 +150,14 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
|
||||
<n-tag v-if="myStatus === 'solved'" size="small" type="success" :bordered="false">
|
||||
已解决
|
||||
</n-tag>
|
||||
<n-tag v-else-if="myStatus === 'tried'" size="small" type="warning" :bordered="false">
|
||||
做过,还没对
|
||||
</n-tag>
|
||||
<n-tag v-else-if="myStatus === 'none'" size="small" :bordered="false">还没交过</n-tag>
|
||||
<!-- 流程图画到 A / S 就算做完了(决定 4):别在「流程图 A 级」旁边再写「还没交过」
|
||||
「做过,还没对」—— 那两个说的是代码 -->
|
||||
<template v-else-if="!flowchartGrade">
|
||||
<n-tag v-if="myStatus === 'tried'" size="small" type="warning" :bordered="false">
|
||||
做过,还没对
|
||||
</n-tag>
|
||||
<n-tag v-else-if="myStatus === 'none'" size="small" :bordered="false">还没交过</n-tag>
|
||||
</template>
|
||||
<n-tag v-if="flowchartGrade" size="small" type="success" :bordered="false">
|
||||
流程图 {{ flowchartGrade }} 级
|
||||
</n-tag>
|
||||
|
||||
@@ -12,7 +12,8 @@ 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 { useCollabHere, useTeacherCollab } from "../composables/teacherCollab"
|
||||
import { useProblemPageContext } from "../composables/problemPageContext"
|
||||
import CollabBar from "./CollabBar.vue"
|
||||
import { useFlowchartStore } from "oj/store/flowchart"
|
||||
|
||||
@@ -27,6 +28,7 @@ const codeStore = useCodeStore()
|
||||
const problemStore = useProblemStore()
|
||||
const collabStore = useCollabStore()
|
||||
const { problem } = storeToRefs(problemStore)
|
||||
const ctx = useProblemPageContext()
|
||||
|
||||
/**
|
||||
* 课堂求助的协作就开在这道题上。
|
||||
@@ -35,9 +37,7 @@ const { problem } = storeToRefs(problemStore)
|
||||
* 直接跳到题目页、就在页面这一个编辑器里协作 —— 顺带治好了「按一下 Esc 弹框就关、
|
||||
* 协作跟着结束」:页面上没有弹框可关,结束协作只有工具栏那个按钮和离开这一页两条路。
|
||||
*/
|
||||
const collabHere = computed(
|
||||
() => collabStore.room !== null && collabStore.room.problemId === problem.value?._id,
|
||||
)
|
||||
const collabHere = useCollabHere()
|
||||
|
||||
/** 协作中的教师:编辑器里是学生的代码,不是他自己的 */
|
||||
const teacherCollab = useTeacherCollab()
|
||||
@@ -151,6 +151,20 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* 画着流程图时忽然不让画了:工具栏的「写代码 / 画流程图」和语言下拉都跟着 canDraw 藏起来,
|
||||
* 编辑器要是还停在画布上,学生就回不去写代码了。拖窄到手机宽度时页面会换一套布局、编辑器
|
||||
* 重新挂载,loadCode 自己会把语言改掉;但同一道题重新拉题面(登录后补拉)时老师刚好关掉了
|
||||
* 流程图,编辑器不重挂,只能靠这里。和工具栏的「写代码」走同一条路,回到他习惯的那门语言
|
||||
*/
|
||||
watch(
|
||||
() => problemStore.canDraw,
|
||||
(canDraw) => {
|
||||
if (canDraw || codeStore.code.language !== "Flowchart") return
|
||||
problemStore.switchLanguage(problemStore.supportedLanguage(codeStore.preferredLanguage()))
|
||||
},
|
||||
)
|
||||
|
||||
// 流程图编辑器交给 store:结果页签(在左栏,不在编辑器底下)要拿当前画布比一比、
|
||||
// 把旧版本载回去。子组件 SubmitFlowchart 还是从 inject 拿
|
||||
const flowchartStore = useFlowchartStore()
|
||||
@@ -172,12 +186,17 @@ provide("flowchartEditorRef", flowchartEditorRef)
|
||||
学生自己切到流程图就是不写代码了,编辑器卸载、协作正常结束(SyncCodeEditor
|
||||
的 detach),这是原来就有的语义。
|
||||
-->
|
||||
<FlowchartEditor v-if="codeStore.code.language === 'Flowchart'" ref="flowchartEditorRef" />
|
||||
<FlowchartEditor
|
||||
v-if="codeStore.code.language === 'Flowchart'"
|
||||
ref="flowchartEditorRef"
|
||||
:height="editorHeight"
|
||||
/>
|
||||
<SyncCodeEditor
|
||||
v-else
|
||||
v-model:value="codeStore.code.value"
|
||||
:language="codeStore.code.language"
|
||||
:problem-id="problem!._id"
|
||||
:collab="ctx.collab"
|
||||
:height="editorHeight"
|
||||
:extra-extensions="editorExtensions"
|
||||
@update:model-value="changeCode"
|
||||
|
||||
@@ -109,6 +109,25 @@ async function listSubmissions() {
|
||||
listed.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 「我的提交统计」:这一页里各种结果各几次(「答案错误 × 3」「答案正确 × 1」),和原来一样
|
||||
* 只数当前这一页。被题单闸门锁住的那几次不数 —— 它们在列表里只露一行说明,这里也不该露出结果
|
||||
*/
|
||||
const statusDistribution = computed(() => {
|
||||
const counts = new Map<number, number>()
|
||||
for (const row of submissions.value) {
|
||||
if (row.showLink) counts.set(row.result, (counts.get(row.result) ?? 0) + 1)
|
||||
}
|
||||
return [...counts.entries()]
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map(([result, count]) => ({
|
||||
result,
|
||||
name: JUDGE_STATUS[result as keyof typeof JUDGE_STATUS]?.name ?? "未知",
|
||||
type: JUDGE_STATUS[result as keyof typeof JUDGE_STATUS]?.type ?? "info",
|
||||
count,
|
||||
}))
|
||||
})
|
||||
|
||||
/** 列表是新的在前;「第几次」从最早那次数起 */
|
||||
function attemptNo(index: number) {
|
||||
return total.value - (query.page - 1) * query.limit - index
|
||||
@@ -220,6 +239,19 @@ watch(query, () => {
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div v-if="statusDistribution.length" class="distribution">
|
||||
<n-tag
|
||||
v-for="item in statusDistribution"
|
||||
:key="item.result"
|
||||
:type="item.type"
|
||||
size="small"
|
||||
round
|
||||
:bordered="false"
|
||||
>
|
||||
{{ item.name }} × {{ item.count }}
|
||||
</n-tag>
|
||||
</div>
|
||||
|
||||
<n-empty
|
||||
v-if="listed && !submissions.length"
|
||||
class="empty"
|
||||
@@ -333,6 +365,13 @@ watch(query, () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.distribution {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.rank {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useProblemStore } from "oj/store/problem"
|
||||
import { useSubmissionStore } from "oj/store/submission"
|
||||
import { useCodeStore } from "oj/store/code"
|
||||
import { useTeacherCollab } from "../composables/teacherCollab"
|
||||
import { useProblemPageContext } from "../composables/problemPageContext"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { parseTime } from "utils/functions"
|
||||
import ResultHeader from "./ResultHeader.vue"
|
||||
@@ -30,6 +32,8 @@ const { submission, syntaxErrorInfo, resultSegment, formattedBeforeSubmit } =
|
||||
storeToRefs(submissionStore)
|
||||
const { problem } = storeToRefs(useProblemStore())
|
||||
const codeStore = useCodeStore()
|
||||
const ctx = useProblemPageContext()
|
||||
const { isDesktop } = useBreakpoints()
|
||||
|
||||
/** 协作中的老师自己还没交过:先摆学生最近一次交的 */
|
||||
const teacherCollab = useTeacherCollab()
|
||||
@@ -37,8 +41,25 @@ const teacherCollab = useTeacherCollab()
|
||||
const theme = useThemeVars()
|
||||
const { samplesRunAt } = submissionStore.trial
|
||||
|
||||
type Segment = "submit" | "samples" | "custom"
|
||||
const segments = computed<{ value: Segment; label: string; time: string }[]>(() => [
|
||||
/**
|
||||
* 手机上流程图题只能看点评、不能画(设计文档 5.6)。画不了,语言也就落不到 Flowchart 上,
|
||||
* 下面的 drawing 永远是假 —— 原来学生在电脑上画过、AI 评过的,手机上一条都看不到。
|
||||
* 这时多给一段「流程图点评」,只读。
|
||||
*/
|
||||
const flowchartReadable = computed(
|
||||
() => !isDesktop.value && !!problem.value?.allowFlowchart && ctx.value.entry !== "contest",
|
||||
)
|
||||
/** 在看「流程图点评」那一段。只在这一页记着:点运行例子、提交时照常切回对应的分段 */
|
||||
const viewingFlowchart = ref(false)
|
||||
watch(
|
||||
() => [submissionStore.resultSeq, problem.value?.id, flowchartReadable.value],
|
||||
() => {
|
||||
viewingFlowchart.value = false
|
||||
},
|
||||
)
|
||||
|
||||
type Segment = "submit" | "samples" | "custom" | "flowchart"
|
||||
const codeSegments = computed<{ value: Segment; label: string; time: string }[]>(() => [
|
||||
{
|
||||
value: "submit",
|
||||
// 语法没过就没交上去:那一段不叫「提交结果」
|
||||
@@ -64,32 +85,55 @@ const canTrial = computed(() => {
|
||||
const languages = problem.value?.languages ?? []
|
||||
return !languages.includes("SQL")
|
||||
})
|
||||
|
||||
const segments = computed(() => {
|
||||
const list = canTrial.value ? codeSegments.value : codeSegments.value.slice(0, 1)
|
||||
return flowchartReadable.value
|
||||
? [...list, { value: "flowchart" as const, label: "流程图点评", time: "" }]
|
||||
: list
|
||||
})
|
||||
/** 只有一段(SQL 题、又不是手机上的流程图题)就不摆分段了 */
|
||||
const showSegments = computed(() => segments.value.length > 1)
|
||||
|
||||
function isActive(segment: Segment) {
|
||||
if (viewingFlowchart.value) return segment === "flowchart"
|
||||
return resultSegment.value === segment
|
||||
}
|
||||
function pick(segment: Segment) {
|
||||
if (segment === "flowchart") {
|
||||
viewingFlowchart.value = true
|
||||
return
|
||||
}
|
||||
viewingFlowchart.value = false
|
||||
resultSegment.value = segment
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FlowchartResult v-if="drawing" />
|
||||
<div v-show="!drawing" class="result-pane">
|
||||
<!-- 设计稿:三个胶囊,选中的深色实心,带上那次结果的时间 -->
|
||||
<div v-if="canTrial" class="segments" role="tablist" aria-label="结果">
|
||||
<div v-if="showSegments" class="segments" role="tablist" aria-label="结果">
|
||||
<button
|
||||
v-for="segment in segments"
|
||||
:key="segment.value"
|
||||
type="button"
|
||||
role="tab"
|
||||
class="segment"
|
||||
:class="{ active: resultSegment === segment.value }"
|
||||
:aria-selected="resultSegment === segment.value"
|
||||
@click="resultSegment = segment.value"
|
||||
:class="{ active: isActive(segment.value) }"
|
||||
:aria-selected="isActive(segment.value)"
|
||||
@click="pick(segment.value)"
|
||||
>
|
||||
{{ segment.label }}<template v-if="segment.time"> · {{ segment.time }}</template>
|
||||
</button>
|
||||
<span v-if="resultSegment === 'custom'" class="segment-note"
|
||||
<span v-if="!viewingFlowchart && resultSegment === 'custom'" class="segment-note"
|
||||
>自己编数据试试,这里不判对错</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<FlowchartResult v-if="viewingFlowchart && flowchartReadable" view-only />
|
||||
<!-- 提交结果用 v-show:切去看运行例子时不卸载,挂着的错误说明在编辑器里标着红 -->
|
||||
<div v-show="resultSegment === 'submit' || !canTrial">
|
||||
<div v-show="!viewingFlowchart && (resultSegment === 'submit' || !canTrial)">
|
||||
<template v-if="syntaxErrorInfo">
|
||||
<ResultHeader kind="warning" title="代码有语法错误,还没有交上去" />
|
||||
<PythonErrorExplain :err-info="syntaxErrorInfo" />
|
||||
@@ -107,7 +151,7 @@ const canTrial = computed(() => {
|
||||
description="还没有提交过。写完代码按「提交」,结果会出现在这里"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="canTrial">
|
||||
<template v-if="canTrial && !viewingFlowchart">
|
||||
<SampleRunResult v-if="resultSegment === 'samples'" />
|
||||
<CustomRun v-else-if="resultSegment === 'custom'" />
|
||||
</template>
|
||||
|
||||
@@ -10,7 +10,12 @@ import { SubmissionStatus } from "utils/constants"
|
||||
/**
|
||||
* 「结果」页签的标题:带上这一次的状态。用图标而不是一个彩色小圆点 ——
|
||||
* 机房投影、色弱的学生只靠颜色分不出对错。
|
||||
*
|
||||
* 新结果出来、学生还没点开看过:「结果」两个字跟着状态上色、加粗。看过一次就算已读,
|
||||
* 图标留着、不再强调(设计文档 5.3)。流程图评完时学生在看题目不会被强制切过去,
|
||||
* 靠的就是这一下提醒
|
||||
*/
|
||||
const props = defineProps<{ active: boolean }>()
|
||||
|
||||
const theme = useThemeVars()
|
||||
const { submission, syntaxErrorInfo, judging, pending, submitting } =
|
||||
@@ -44,10 +49,29 @@ const status = computed(() => {
|
||||
return { icon: "ph:warning-bold", color: theme.value.warningColor }
|
||||
return { icon: "ph:x-bold", color: theme.value.errorColor }
|
||||
})
|
||||
|
||||
/**
|
||||
* 这一次结果的身份:哪条提交判成了什么、第几次流程图评分、哪一次语法没过。
|
||||
* 换了就是新结果,要重新提醒
|
||||
*/
|
||||
const resultKey = computed(() => {
|
||||
if (!status.value) return ""
|
||||
if (codeStore.code.language === "Flowchart")
|
||||
return `flowchart:${flowchart.evaluatedSeq.value}:${flowchart.phase.value}`
|
||||
if (syntaxErrorInfo.value) return `syntax:${syntaxErrorInfo.value}`
|
||||
return `submission:${submission.value?.id}:${submission.value?.result}`
|
||||
})
|
||||
const readKey = ref("")
|
||||
watchEffect(() => {
|
||||
if (props.active) readKey.value = resultKey.value
|
||||
})
|
||||
const unread = computed(
|
||||
() => !!status.value && !status.value.spin && !props.active && resultKey.value !== readKey.value,
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="label">
|
||||
<span class="label" :class="{ unread }" :style="unread ? { color: status!.color } : undefined">
|
||||
结果
|
||||
<Icon
|
||||
v-if="status"
|
||||
@@ -66,6 +90,10 @@ const status = computed(() => {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.unread {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@@ -76,8 +76,10 @@ function settleReview() {
|
||||
|
||||
function closeCommentPanel() {
|
||||
commentPanel.value = false
|
||||
// 「下一题」在结果页签里(LessonNext),评价完回到那里 —— 学生点评之前可能切去看了题目
|
||||
submissionStore.revealResult()
|
||||
// 「下一题」在结果页签里(LessonNext),评价完回到那里 —— 学生点评之前可能切去看了题目。
|
||||
// 接着要换题的就别切:切页签会 router.replace 改 `?tab=`,排在下面那次 push 前头,
|
||||
// 把 push 当成被新导航取消,人就停在原题
|
||||
if (!navigationAfterReview) submissionStore.revealResult()
|
||||
settleReview()
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { Submission } from "utils/types"
|
||||
import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
import { useSubmissionStore } from "oj/store/submission"
|
||||
import { markdownToText } from "oj/problem/utils/plainText"
|
||||
import PythonErrorExplain from "./PythonErrorExplain.vue"
|
||||
import RuntimeErrorExplain from "./RuntimeErrorExplain.vue"
|
||||
import WrongAnswerExplain from "./WrongAnswerExplain.vue"
|
||||
@@ -219,7 +220,9 @@ const customRunHint = computed(() => {
|
||||
(sample) => `${sample.input} ${sample.output}`.match(/-?\d+(?:\.\d+)?/g) ?? [],
|
||||
),
|
||||
)
|
||||
const numbers = [...new Set(problem.description.match(/-?\d+(?:\.\d+)?/g) ?? [])]
|
||||
// 先摊成纯文字:题面是 HTML,446 道题带着 style="color: rgb(51, 51, 51)",直接取数会建议「拿 51 试试」
|
||||
const description = markdownToText(problem.description, Infinity)
|
||||
const numbers = [...new Set(description.match(/-?\d+(?:\.\d+)?/g) ?? [])]
|
||||
.filter((value) => !inSamples.has(value))
|
||||
.slice(0, 2)
|
||||
return numbers.length
|
||||
|
||||
@@ -15,7 +15,7 @@ const message = computed(() => {
|
||||
switch (props.result) {
|
||||
case SubmissionStatus.compile_error:
|
||||
return props.language === "Python"
|
||||
? "代码没能跑起来,写法有错误。按「提交代码」会先检查一遍,告诉你错在哪一行。"
|
||||
? "代码有语法错误,程序一行都没跑。最常见的是用了中文标点(,:()“”)。按「提交」会先检查一遍,告诉你错在哪一行,中文标点能一键换成英文。"
|
||||
: "代码没能编译通过,写法有错误。看看是不是漏了分号、括号没配对、变量没声明。"
|
||||
case SubmissionStatus.runtime_error:
|
||||
return "程序运行到一半出错,停下来了。检查一下输入是怎么读的、下标有没有越界、除数是不是 0。"
|
||||
|
||||
@@ -52,8 +52,11 @@ const requirement = computed(() => {
|
||||
const TEXT_LIMIT = 2000
|
||||
|
||||
const onSample = computed(() => props.check.result === SubmissionStatus.wrong_answer)
|
||||
// plain(比赛)连差异类型都不算:把空格画成「·」、把不一样的那段标红,和提示句一样是格式线索
|
||||
const kind = computed(() =>
|
||||
onSample.value ? classifyDiff(props.check.expected ?? "", props.check.output ?? "") : null,
|
||||
onSample.value && !props.plain
|
||||
? classifyDiff(props.check.expected ?? "", props.check.output ?? "")
|
||||
: null,
|
||||
)
|
||||
const hint = computed(() => {
|
||||
if (props.check.passed) return ""
|
||||
@@ -95,7 +98,7 @@ function splitDiff(expected: string, output: string) {
|
||||
}
|
||||
|
||||
const diffParts = computed(() => {
|
||||
if (!onSample.value || diffLine.value < 0) return null
|
||||
if (!onSample.value || props.plain || diffLine.value < 0) return null
|
||||
const expected = lines(props.check.expected ?? "")[diffLine.value] ?? ""
|
||||
const output = lines(props.check.output ?? "")[diffLine.value] ?? ""
|
||||
return splitDiff(expected, output)
|
||||
|
||||
Reference in new issue
Block a user