style(格式化): 改成单引号、一行最长 160,全仓重新格式化
Deploy / deploy (push) Canceled after 0s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 13:52:24 +08:00
1 parent 530e1a3d5d
commit 195069450f
480 files changed
+15507 -24650

No files matched your search

@@ -7,18 +7,18 @@ export function useChallengeTone() {
return computed(() =>
isDark.value
? {
solid: "#b49cf5",
onSolid: "#17112b",
text: "#c9b8ff",
soft: "rgba(180, 156, 245, 0.14)",
border: "rgba(180, 156, 245, 0.35)",
solid: '#b49cf5',
onSolid: '#17112b',
text: '#c9b8ff',
soft: 'rgba(180, 156, 245, 0.14)',
border: 'rgba(180, 156, 245, 0.35)',
}
: {
solid: "#5b34c4",
onSolid: "#ffffff",
text: "#2c1a66",
soft: "#f4f0fe",
border: "#ddd3f6",
solid: '#5b34c4',
onSolid: '#ffffff',
text: '#2c1a66',
soft: '#f4f0fe',
border: '#ddd3f6',
},
)
}
@@ -1,5 +1,5 @@
import { useCodeStore } from "oj/store/code"
import { useProblemStore } from "oj/store/problem"
import { useCodeStore } from 'oj/store/code'
import { useProblemStore } from 'oj/store/problem'
/**
* 这道题、这门语言的本地代码草稿存在哪个键下。编辑器(存、读草稿)和「重置代码」
@@ -16,7 +16,7 @@ export function useDraftKey() {
const problemStore = useProblemStore()
return computed(() => {
const id = problemStore.problem?._id
if (!id) return ""
if (!id) return ''
const language = codeStore.code.language
const problemSetId = route.params.problemSetId
if (problemSetId) return `problem_${id}_problemset_${problemSetId}_lang_${language}`
@@ -1,23 +1,23 @@
import { storeToRefs } from "pinia"
import type { Ref } from "vue"
import { useCodeStore } from "oj/store/code"
import { useProblemStore } from "oj/store/problem"
import { useBreakpoints } from "shared/composables/breakpoints"
import { useUserStore } from "shared/store/user"
import { LANGUAGE_FORMAT_VALUE, SOURCES } from "utils/constants"
import { compressToBase64, copyToClipboard } from "utils/functions"
import storage from "utils/storage"
import { useDraftKey } from "./draftKey"
import { useTeacherCollab } from "./teacherCollab"
import { useFlowchartStore } from "oj/store/flowchart"
import { useProblemPageContext } from "./problemPageContext"
import { storeToRefs } from 'pinia'
import type { Ref } from 'vue'
import { useCodeStore } from 'oj/store/code'
import { useProblemStore } from 'oj/store/problem'
import { useBreakpoints } from 'shared/composables/breakpoints'
import { useUserStore } from 'shared/store/user'
import { LANGUAGE_FORMAT_VALUE, SOURCES } from 'utils/constants'
import { compressToBase64, copyToClipboard } from 'utils/functions'
import storage from 'utils/storage'
import { useDraftKey } from './draftKey'
import { useTeacherCollab } from './teacherCollab'
import { useFlowchartStore } from 'oj/store/flowchart'
import { useProblemPageContext } from './problemPageContext'
/**
* 「课堂统计」:新标签打开数据统计页,带上这道题(原来是题目页上的弹框)。
* 工具栏按钮和手机页签行的「⋯」都走这一个。新标签是因为老师多半正开着这道题在看
*/
export function openStatistics(problemDisplayId: string) {
window.open(`/statistics?problem=${encodeURIComponent(problemDisplayId)}`, "_blank")
window.open(`/statistics?problem=${encodeURIComponent(problemDisplayId)}`, '_blank')
}
/**
@@ -44,42 +44,41 @@ export function useEditorMenu(statisticsInline: Ref<boolean>) {
const items: DropdownOption[] = []
// 「本题提交」挪到了「我的提交」抽屉的底部;题单入口没有抽屉,给管理员角色留在这里
if (ctx.value.allSubmissionsInMenu && userStore.isAdminRole) {
items.push({ label: "本题提交", key: "allSubmissions" })
items.push({ label: '本题提交', key: 'allSubmissions' })
}
if (ctx.value.classStats && !statisticsInline.value && userStore.isTeacherOrAbove) {
items.push({ label: "课堂统计", key: "statistics" })
items.push({ label: '课堂统计', key: 'statistics' })
}
if (codeStore.code.language === "Flowchart") {
items.push({ label: "清空画布", key: "clearFlowchart" })
if (codeStore.code.language === 'Flowchart') {
items.push({ label: '清空画布', key: 'clearFlowchart' })
} else {
if (codeStore.code.language !== "SQL") items.push({ label: "去自测猫", key: "testcat" })
items.push({ label: "复制代码", key: "copy" })
if (codeStore.code.language !== 'SQL') items.push({ label: '去自测猫', key: 'testcat' })
items.push({ label: '复制代码', key: 'copy' })
// 协作中的教师不给「重置代码」:那会儿编辑器里是**学生的**代码,而 v-model
// 一写回去就顺着 Yjs 同步过去,等于一键清空学生的作业,他还没法撤回
if (!teacherCollab.value) items.push({ label: "重置代码", key: "reset" })
if (!teacherCollab.value) items.push({ label: '重置代码', key: 'reset' })
}
if (isDesktop.value && problem.value && userStore.canEditProblem(problem.value))
items.push({ label: "编辑题目", key: "edit" })
if (isDesktop.value && problem.value && userStore.canEditProblem(problem.value)) items.push({ label: '编辑题目', key: 'edit' })
return items
})
async function copy() {
const success = await copyToClipboard(codeStore.code.value)
message[success ? "success" : "error"](`代码复制${success ? "成功" : "失败"}`)
message[success ? 'success' : 'error'](`代码复制${success ? '成功' : '失败'}`)
}
// 重置会把编辑器里的代码换成模板、连本地草稿一起删掉,点错一下写了半节课的代码就没了,先问一句
function reset() {
dialog.warning({
title: "重置代码",
content: "编辑器里的代码会换回题目给的模板,存着的草稿也会删掉。确定吗?",
positiveText: "重置",
negativeText: "再想想",
title: '重置代码',
content: '编辑器里的代码会换回题目给的模板,存着的草稿也会删掉。确定吗?',
positiveText: '重置',
negativeText: '再想想',
onPositiveClick: () => {
const language = codeStore.code.language
codeStore.setCode(problem.value!.template[language] || SOURCES[language])
storage.remove(draftKey.value)
message.success("已换回模板,按 Ctrl+Z 可以撤回")
message.success('已换回模板,按 Ctrl+Z 可以撤回')
},
})
}
@@ -90,13 +89,13 @@ export function useEditorMenu(statisticsInline: Ref<boolean>) {
const editor = flowchartStore.editor
if (!editor) return
dialog.warning({
title: "清空画布",
content: "画布上的图会全部清掉。确定吗?",
positiveText: "清空",
negativeText: "再想想",
title: '清空画布',
content: '画布上的图会全部清掉。确定吗?',
positiveText: '清空',
negativeText: '再想想',
onPositiveClick: () => {
editor.clear()
message.success("已清空,按 Ctrl+Z 可以撤回")
message.success('已清空,按 Ctrl+Z 可以撤回')
},
})
}
@@ -106,52 +105,50 @@ export function useEditorMenu(statisticsInline: Ref<boolean>) {
lang: LANGUAGE_FORMAT_VALUE[codeStore.code.language],
code: codeStore.code.value,
// 没有例子的题原来在这里抛 TypeError,点了没反应
input: problem.value?.samples[0]?.input ?? "",
input: problem.value?.samples[0]?.input ?? '',
}
const base64 = compressToBase64(JSON.stringify(data))
window.open(`${import.meta.env.PUBLIC_CODE_URL}?share=${encodeURIComponent(base64)}`, "_blank")
window.open(`${import.meta.env.PUBLIC_CODE_URL}?share=${encodeURIComponent(base64)}`, '_blank')
}
function goEdit() {
const p = problem.value!
const url = p.contestId
? `/admin/contest/${p.contestId}/problem/edit/${p.id}`
: `/admin/problem/edit/${p.id}`
window.open(router.resolve(url).href, "_blank")
const url = p.contestId ? `/admin/contest/${p.contestId}/problem/edit/${p.id}` : `/admin/problem/edit/${p.id}`
window.open(router.resolve(url).href, '_blank')
}
function goAllSubmissions() {
const href = router.resolve({
name: "submissions",
name: 'submissions',
query: { problem: problem.value!._id },
}).href
// 协作中走新标签,跳走这一页协作就断了(设计文档第 9 节)
if (teacherCollab.value) window.open(href, "_blank")
if (teacherCollab.value) window.open(href, '_blank')
else router.push(href)
}
/** 菜单项被点了;不是这张菜单里的 key 返回 false,调用方自己处理(页签行的抽屉) */
function select(key: string) {
switch (key) {
case "statistics":
case 'statistics':
openStatistics(problem.value!._id)
return true
case "testcat":
case 'testcat':
goTestCat()
return true
case "copy":
case 'copy':
copy()
return true
case "reset":
case 'reset':
reset()
return true
case "clearFlowchart":
case 'clearFlowchart':
clearFlowchart()
return true
case "edit":
case 'edit':
goEdit()
return true
case "allSubmissions":
case 'allSubmissions':
goAllSubmissions()
return true
}
@@ -1,4 +1,4 @@
import type { RouteLocationNormalizedLoaded } from "vue-router"
import type { RouteLocationNormalizedLoaded } from 'vue-router'
/**
* 题目页的三种入口:题库(含课堂)、题单、比赛。同一个 detail.vue 挂在三条路由上。
@@ -7,14 +7,14 @@ import type { RouteLocationNormalizedLoaded } from "vue-router"
* 在题单里是「禁用」、在比赛里是「隐藏」,加一处功能就得把每个组件翻一遍
* (见 docs/specs/2026-09-28-problem-page-redesign-design.md 第 8 节)。
*/
export type ProblemEntry = "problem" | "problemset" | "contest"
export type ProblemEntry = 'problem' | 'problemset' | 'contest'
export type ProblemDrawer = "info" | "reaction" | "submission"
export type ProblemDrawer = 'info' | 'reaction' | 'submission'
export const DRAWER_TITLE: Record<ProblemDrawer, string> = {
info: "统计",
reaction: "点评",
submission: "我的提交",
info: '统计',
reaction: '点评',
submission: '我的提交',
}
interface EntryRules {
@@ -58,7 +58,7 @@ interface EntryRules {
const RULES: Record<ProblemEntry, EntryRules> = {
problem: {
drawers: ["info", "reaction", "submission"],
drawers: ['info', 'reaction', 'submission'],
statusTag: true,
similar: true,
lessonNext: true,
@@ -87,7 +87,7 @@ const RULES: Record<ProblemEntry, EntryRules> = {
allSubmissionsInMenu: true,
},
contest: {
drawers: ["info", "submission"],
drawers: ['info', 'submission'],
statusTag: true,
// 相似题接口按 displayId 在公开题库里找,比赛题的编号默认是 1/2/3,
// 撞上同号公开题时反而会在比赛中把题库列给学生
@@ -104,9 +104,9 @@ const RULES: Record<ProblemEntry, EntryRules> = {
}
export function problemEntryOf(route: RouteLocationNormalizedLoaded): ProblemEntry {
if (route.name === "contest problem") return "contest"
if (route.name === "problemset problem") return "problemset"
return "problem"
if (route.name === 'contest problem') return 'contest'
if (route.name === 'problemset problem') return 'problemset'
return 'problem'
}
/**
@@ -118,8 +118,8 @@ export function useProblemPageContext() {
const entry = problemEntryOf(route)
return {
entry,
contestId: entry === "contest" ? String(route.params.contestID ?? "") : "",
problemSetId: entry === "problemset" ? String(route.params.problemSetId ?? "") : "",
contestId: entry === 'contest' ? String(route.params.contestID ?? '') : '',
problemSetId: entry === 'problemset' ? String(route.params.problemSetId ?? '') : '',
...RULES[entry],
}
})
@@ -1,7 +1,7 @@
import { useProblemStore } from "oj/store/problem"
import { useCollabStore } from "shared/store/collab"
import { useUserStore } from "shared/store/user"
import { useProblemPageContext } from "./problemPageContext"
import { useProblemStore } from 'oj/store/problem'
import { useCollabStore } from 'shared/store/collab'
import { useUserStore } from 'shared/store/user'
import { useProblemPageContext } from './problemPageContext'
/**
* 老师正在这道题上协作:编辑器里是**学生的**代码,「我的提交」看的是学生的,
@@ -21,10 +21,5 @@ export function useCollabHere() {
const collabStore = useCollabStore()
const problemStore = useProblemStore()
const ctx = useProblemPageContext()
return computed(
() =>
ctx.value.collab &&
collabStore.room !== null &&
collabStore.room.problemId === problemStore.problem?._id,
)
return computed(() => ctx.value.collab && collabStore.room !== null && collabStore.room.problemId === problemStore.problem?._id)
}
@@ -1,5 +1,5 @@
import confetti from "canvas-confetti"
import { getCurrentScope, onScopeDispose } from "vue"
import confetti from 'canvas-confetti'
import { getCurrentScope, onScopeDispose } from 'vue'
// 表情图标取自 @iconify 的 streamline-emojis 图标集,与项目内 <Icon icon="streamline-emojis:..."> 用法保持一致
const EMOJI_SVGS = [
@@ -16,16 +16,16 @@ let emojiShapesPromise: Promise<confetti.Shape[]> | null = null
function svgToBitmapShape(svg: string): Promise<confetti.Shape> {
return new Promise((resolve, reject) => {
const blob = new Blob([svg], { type: "image/svg+xml" })
const blob = new Blob([svg], { type: 'image/svg+xml' })
const url = URL.createObjectURL(blob)
const img = new Image()
img.onload = async () => {
const renderSize = 64
const baseUnit = 10
const canvas = document.createElement("canvas")
const canvas = document.createElement('canvas')
canvas.width = renderSize
canvas.height = renderSize
const ctx = canvas.getContext("2d")!
const ctx = canvas.getContext('2d')!
ctx.drawImage(img, 0, 0, renderSize, renderSize)
URL.revokeObjectURL(url)
@@ -33,7 +33,7 @@ function svgToBitmapShape(svg: string): Promise<confetti.Shape> {
const bitmap = await createImageBitmap(canvas)
const scale = baseUnit / renderSize
resolve({
type: "bitmap",
type: 'bitmap',
bitmap,
matrix: [scale, 0, 0, scale, (-renderSize * scale) / 2, (-renderSize * scale) / 2],
} as unknown as confetti.Shape)
@@ -54,16 +54,16 @@ function loadEmojiShapes(): Promise<confetti.Shape[]> {
}
const COLORS = {
green: "#18a058",
mint: "#2cc27a",
gold: "#f0a020",
lightGold: "#ffd36b",
orange: "#f06a1a",
pink: "#e8457f",
lightPink: "#ff7aa8",
blue: "#2080f0",
purple: "#7d50e8",
silver: "#8f99aa",
green: '#18a058',
mint: '#2cc27a',
gold: '#f0a020',
lightGold: '#ffd36b',
orange: '#f06a1a',
pink: '#e8457f',
lightPink: '#ff7aa8',
blue: '#2080f0',
purple: '#7d50e8',
silver: '#8f99aa',
}
const PALETTES = [
[COLORS.green, COLORS.gold, COLORS.blue, COLORS.pink],
@@ -92,9 +92,9 @@ function loadPathShapes() {
if (!pathShapes) {
const matrix = [0.5, 0, 0, 0.5, -5, -5] as unknown as DOMMatrix
pathShapes = {
check: confetti.shapeFromPath({ path: "M1 11 L4 8 L8 12 L16 3 L19 6 L8 17 Z", matrix }),
check: confetti.shapeFromPath({ path: 'M1 11 L4 8 L8 12 L16 3 L19 6 L8 17 Z', matrix }),
heart: confetti.shapeFromPath({
path: "M10 18 C2 12 0 7 3 4 C6 1 9 3 10 5 C11 3 14 1 17 4 C20 7 18 12 10 18 Z",
path: 'M10 18 C2 12 0 7 3 4 C6 1 9 3 10 5 C11 3 14 1 17 4 C20 7 18 12 10 18 Z',
matrix,
}),
}
@@ -185,7 +185,7 @@ export function useFireworks() {
ticks: 26,
scalar: 0.65,
flat: true,
shapes: ["circle"],
shapes: ['circle'],
colors: [COLORS.gold, COLORS.lightGold],
origin: { x: x0 + (x - x0) * eased, y: 1.02 - (1.02 - y) * eased },
})
@@ -201,7 +201,7 @@ export function useFireworks() {
decay: 0.92,
gravity: 0.55,
flat: true,
shapes: ["circle"],
shapes: ['circle'],
colors,
origin: { x, y },
}
@@ -236,12 +236,7 @@ export function useFireworks() {
for (let i = 0; i < 7; i++) {
runLater(() => {
const left = i % 2 === 0
shell(
left ? 0.08 + Math.random() * 0.3 : 0.62 + Math.random() * 0.3,
0.14 + Math.random() * 0.26,
pick(PALETTES),
0.9 + Math.random() * 0.3,
)
shell(left ? 0.08 + Math.random() * 0.3 : 0.62 + Math.random() * 0.3, 0.14 + Math.random() * 0.26, pick(PALETTES), 0.9 + Math.random() * 0.3)
}, i * 240)
}
},
@@ -258,7 +253,7 @@ export function useFireworks() {
decay: 0.97,
ticks: 120,
scalar: 1.1,
shapes: ["star"],
shapes: ['star'],
colors: [COLORS.gold, COLORS.gold, COLORS.pink, COLORS.blue, COLORS.silver],
origin: { x: Math.random(), y: -0.05 },
})
@@ -269,13 +264,7 @@ export function useFireworks() {
() => {
for (let i = 0; i < 5; i++) {
runLater(() => {
shell(
0.14 + 0.18 * i,
0.38 - 0.2 * Math.sin((Math.PI * i) / 4),
[RAINBOW[i], COLORS.silver],
1,
0.5,
)
shell(0.14 + 0.18 * i, 0.38 - 0.2 * Math.sin((Math.PI * i) / 4), [RAINBOW[i], COLORS.silver], 1, 0.5)
}, i * 170)
}
},
@@ -294,7 +283,7 @@ export function useFireworks() {
ticks: 95,
scalar: 0.8,
flat: true,
shapes: ["circle"],
shapes: ['circle'],
colors: [color],
}
fire({ ...base, angle: 62, origin: { x: 0, y: 1 } })
@@ -318,7 +307,7 @@ export function useFireworks() {
ticks: 30,
scalar: 0.6,
flat: true,
shapes: ["circle"],
shapes: ['circle'],
colors: [COLORS.silver, COLORS.gold],
origin: {
x: 0.5 + (Math.random() - 0.5) * 0.3,
@@ -349,7 +338,7 @@ export function useFireworks() {
ticks: 40,
scalar: 0.8,
flat: true,
shapes: ["circle"],
shapes: ['circle'],
colors: [arm ? COLORS.blue : COLORS.pink],
origin: { x: 0.5 + Math.sin(phase) * 0.05 * (1 - progress * 0.6), y },
})
@@ -365,7 +354,7 @@ export function useFireworks() {
const list = [
{ x: 0.22, y: 0.36, shape: shapes.check, colors: [COLORS.green, COLORS.mint] },
{ x: 0.5, y: 0.3, shape: shapes.heart, colors: [COLORS.pink, COLORS.lightPink] },
{ x: 0.78, y: 0.36, shape: "star" as const, colors: [COLORS.gold, COLORS.orange] },
{ x: 0.78, y: 0.36, shape: 'star' as const, colors: [COLORS.gold, COLORS.orange] },
]
list.forEach((item, i) => {
runLater(() => {
@@ -406,7 +395,7 @@ export function useFireworks() {
ticks: 50,
scalar: 0.9,
flat: true,
shapes: ["circle"],
shapes: ['circle'],
colors: [colors[arm]],
origin: { x: 0.5, y: 0.42 },
})
@@ -430,7 +419,7 @@ export function useFireworks() {
ticks: 110,
scalar: 0.8,
flat: true,
shapes: ["circle"],
shapes: ['circle'],
colors: [COLORS.gold, COLORS.gold, COLORS.silver, COLORS.orange],
origin: { x: 0.06 + Math.random() * 0.88, y: 0.06 },
})
@@ -470,10 +459,7 @@ export function useFireworks() {
*/
function celebrate(): Promise<void> {
pick(fireworkTypes)()
return Promise.race([
finished(),
new Promise<void>((resolve) => setTimeout(resolve, MAX_WAIT_MS)),
])
return Promise.race([finished(), new Promise<void>((resolve) => setTimeout(resolve, MAX_WAIT_MS))])
}
return {
@@ -1,6 +1,6 @@
import type { AiHintDone } from "@oj2/contract"
import { submitHintFeedback } from "oj/api"
import { useAIStream } from "shared/composables/aiStream"
import type { AiHintDone } from '@oj2/contract'
import { submitHintFeedback } from 'oj/api'
import { useAIStream } from 'shared/composables/aiStream'
/**
* 一次提交的 AI 提示。
@@ -14,12 +14,12 @@ import { useAIStream } from "shared/composables/aiStream"
* 逐字显示改在这边模拟。
*/
export function useSubmissionHint() {
const hintTarget = ref("")
const hintContent = ref("")
const hintTarget = ref('')
const hintContent = ref('')
const hintStream = useAIStream()
// 整条流跑完才算结束:提示是一次推全文,没有「第一个字到了」的中间态
const hintLoading = hintStream.running
const hintError = ref("")
const hintError = ref('')
// 这条提示在 ai_hint 里的 id,生成完由 done 事件带回来;后端落库失败时没有,就不出评价按钮
const hintId = ref<number | null>(null)
// 这条提示是哪一级(-1 = 编译错误那一档,不在阶梯上),以及还能不能再往上要一级。
@@ -55,9 +55,9 @@ export function useSubmissionHint() {
function resetHint() {
stopTyping()
hintTarget.value = ""
hintContent.value = ""
hintError.value = ""
hintTarget.value = ''
hintContent.value = ''
hintError.value = ''
hintId.value = null
hintLevel.value = null
hintCanEscalate.value = false
@@ -76,7 +76,7 @@ export function useSubmissionHint() {
resetHint()
try {
await hintStream.run<AiHintDone>(
"ai/hint",
'ai/hint',
{ submissionId, more },
{
onDelta(content) {
@@ -1,10 +1,10 @@
import { ref, computed } from "vue"
import { getSubmission } from "oj/api"
import { SubmissionStatus } from "utils/constants"
import type { PendingAchievement, Submission } from "utils/types"
import { useAchievementStore } from "shared/store/achievement"
import { useSubmissionWebSocket, type SubmissionUpdate } from "shared/composables/websocket"
import { useFallbackPoll } from "shared/composables/fallbackPoll"
import { ref, computed } from 'vue'
import { getSubmission } from 'oj/api'
import { SubmissionStatus } from 'utils/constants'
import type { PendingAchievement, Submission } from 'utils/types'
import { useAchievementStore } from 'shared/store/achievement'
import { useSubmissionWebSocket, type SubmissionUpdate } from 'shared/composables/websocket'
import { useFallbackPoll } from 'shared/composables/fallbackPoll'
/**
* 判题监控 Composable
@@ -12,7 +12,7 @@ import { useFallbackPoll } from "shared/composables/fallbackPoll"
*/
export function useSubmissionMonitor() {
// ==================== 状态 ====================
const submissionId = ref("")
const submissionId = ref('')
const submission = ref<Submission>()
// ==================== WebSocket 处理 ====================
@@ -23,18 +23,18 @@ export function useSubmissionMonitor() {
type: string
achievements?: PendingAchievement[]
}
if (frame.type === "achievement_unlocked") {
if (frame.type === 'achievement_unlocked') {
useAchievementStore().enqueue(frame.achievements ?? [])
return
}
if (frame.type !== "submission_update") {
if (frame.type !== 'submission_update') {
return
}
console.log("[SubmissionMonitor] 收到WebSocket更新:", data)
console.log('[SubmissionMonitor] 收到WebSocket更新:', data)
if (data.submissionId !== submissionId.value) {
console.log("[SubmissionMonitor] 提交ID不匹配,忽略")
console.log('[SubmissionMonitor] 提交ID不匹配,忽略')
return
}
// 订阅后服务端立刻回的那一帧(还在排队也回)就算:WebSocket 是通的,轮询退到慢档
@@ -44,11 +44,11 @@ export function useSubmissionMonitor() {
submission.value = {} as Submission
}
submission.value.result = data.result as Submission["result"]
submission.value.result = data.result as Submission['result']
// 判题完成或出错,获取完整详情
if (data.status === "finished" || data.status === "error") {
console.log(`[SubmissionMonitor] 判题${data.status === "finished" ? "完成" : "出错"}`)
if (data.status === 'finished' || data.status === 'error') {
console.log(`[SubmissionMonitor] 判题${data.status === 'finished' ? '完成' : '出错'}`)
// 停止轮询(WebSocket已成功)
fallback.stop()
@@ -66,8 +66,7 @@ export function useSubmissionMonitor() {
}
// 初始化 WebSocket
const { connect, subscribe, unsubscribe, scheduleDisconnect, cancelScheduledDisconnect, status } =
useSubmissionWebSocket(handleSubmissionUpdate)
const { connect, subscribe, unsubscribe, scheduleDisconnect, cancelScheduledDisconnect, status } = useSubmissionWebSocket(handleSubmissionUpdate)
// ==================== 轮询兜底 ====================
const fallback = useFallbackPoll({
@@ -119,7 +118,7 @@ export function useSubmissionMonitor() {
// 连接由 store 持有,离开题目页组件卸了它也还在。和判完之后一样,空闲 15 分钟断开;
// 不在这里立刻断,是因为刚交的那条判完之后还可能推成就过来
scheduleDisconnect(15 * 60 * 1000)
submissionId.value = ""
submissionId.value = ''
submission.value = undefined
}
@@ -1,8 +1,8 @@
import type { StatisticInfo, TrialRunResponse } from "@oj2/contract"
import { errorMessage } from "utils/api"
import { SubmissionStatus } from "utils/constants"
import { isRunnableLanguage, trialRun } from "utils/judge"
import type { Code } from "utils/types"
import type { StatisticInfo, TrialRunResponse } from '@oj2/contract'
import { errorMessage } from 'utils/api'
import { SubmissionStatus } from 'utils/constants'
import { isRunnableLanguage, trialRun } from 'utils/judge'
import type { Code } from 'utils/types'
/**
* 试跑:「运行例子」和「自己输入」。走本站判题机(utils/judge.ts → POST /trial-runs),
@@ -26,27 +26,26 @@ interface TrialOutcome {
/** 没跑成、或者要换一种说法时给学生看的中文,有它就不用 TrialErrorNote 的默认说明 */
note?: string
/** 运行时错误的诊断(第几行、什么异常),和提交的同一套,交给 RuntimeErrorExplain */
runtimeError?: NonNullable<StatisticInfo["runtime_error"]>
runtimeError?: NonNullable<StatisticInfo['runtime_error']>
}
const TOO_MUCH_OUTPUT =
"程序输出太多了,一直停不下来:多半是循环没有结束的条件(条件一直成立、循环变量忘了改),在不停地打印。"
const TOO_MUCH_OUTPUT = '程序输出太多了,一直停不下来:多半是循环没有结束的条件(条件一直成立、循环变量忘了改),在不停地打印。'
/**
* 把一次试跑的回包摊成每组一条。编译没过就每组都是编译失败、带同一份报错;
* 输出太多的时候判题机回包被掐断了,分不出是哪一组,每组都挂上同一句说明。
*/
function outcomes(response: TrialRunResponse, count: number): TrialOutcome[] {
if (response.status === "compile-error") {
if (response.status === 'compile-error') {
return Array.from({ length: count }, () => ({
result: SubmissionStatus.compile_error,
output: response.message,
}))
}
if (response.status === "too-much-output") {
if (response.status === 'too-much-output') {
return Array.from({ length: count }, () => ({
result: SubmissionStatus.runtime_error,
output: "",
output: '',
note: TOO_MUCH_OUTPUT,
}))
}
@@ -54,8 +53,7 @@ function outcomes(response: TrialRunResponse, count: number): TrialOutcome[] {
// 开头的空格是输出的一部分(打印菱形、三角形),不能动
return response.cases.map((item) => {
const result = item.result as SubmissionStatus
const finished =
result === SubmissionStatus.accepted || result === SubmissionStatus.wrong_answer
const finished = result === SubmissionStatus.accepted || result === SubmissionStatus.wrong_answer
return {
result,
output: finished ? item.output.trimEnd() : item.output,
@@ -64,15 +62,11 @@ function outcomes(response: TrialRunResponse, count: number): TrialOutcome[] {
})
}
async function runCases(
code: Code,
target: TrialTarget,
cases: { input: string; output?: string }[],
): Promise<TrialOutcome[]> {
async function runCases(code: Code, target: TrialTarget, cases: { input: string; output?: string }[]): Promise<TrialOutcome[]> {
if (!isRunnableLanguage(code.language)) {
return cases.map(() => ({
result: SubmissionStatus.system_error,
output: "",
output: '',
note: `${code.language} 不能试运行`,
}))
}
@@ -87,8 +81,8 @@ async function runCases(
return outcomes(response, cases.length)
} catch (error) {
// 后端的文案是给学生看的(「上一次还没跑完」「试运行的人太多了」)
const note = errorMessage(error, "试跑的服务暂时连不上,稍后再试一次。")
return cases.map(() => ({ result: SubmissionStatus.system_error, output: "", note }))
const note = errorMessage(error, '试跑的服务暂时连不上,稍后再试一次。')
return cases.map(() => ({ result: SubmissionStatus.system_error, output: '', note }))
}
}
@@ -100,7 +94,7 @@ export interface SampleRun {
output: string
result: SubmissionStatus
note?: string
runtimeError?: TrialOutcome["runtimeError"]
runtimeError?: TrialOutcome['runtimeError']
}
export function useTrialRun() {
@@ -111,13 +105,13 @@ export function useTrialRun() {
/** 跑例子时用的那份代码:错因说明要看代码(比如有没有 input("提示文字")),编辑器里可能已经改了 */
const samplesCode = ref<Code | null>(null)
const customInput = ref("")
const customOutput = ref("")
const customInput = ref('')
const customOutput = ref('')
const customResult = ref<SubmissionStatus | null>(null)
const customNote = ref("")
const customRuntimeError = ref<TrialOutcome["runtimeError"] | null>(null)
const customNote = ref('')
const customRuntimeError = ref<TrialOutcome['runtimeError'] | null>(null)
/** 跑「自己输入」时用的那份代码:出错那一行要从它里面摆出来,编辑器里可能已经改了 */
const customCode = ref("")
const customCode = ref('')
const customRunning = ref(false)
/**
@@ -127,11 +121,7 @@ export function useTrialRun() {
let samplesGeneration = 0
let customGeneration = 0
async function runSamples(
code: Code,
target: TrialTarget,
samples: { input: string; output: string }[],
) {
async function runSamples(code: Code, target: TrialTarget, samples: { input: string; output: string }[]) {
const mine = ++samplesGeneration
samplesRunning.value = true
const runs = await runCases(code, target, samples)
@@ -155,7 +145,7 @@ export function useTrialRun() {
if (mine !== customGeneration) return
customOutput.value = run!.output
customResult.value = run!.result
customNote.value = run!.note ?? ""
customNote.value = run!.note ?? ''
customRuntimeError.value = run!.runtimeError ?? null
customCode.value = code.value
customRunning.value = false
@@ -169,12 +159,12 @@ export function useTrialRun() {
samplesRunning.value = false
samplesRunAt.value = null
samplesCode.value = null
customInput.value = ""
customOutput.value = ""
customInput.value = ''
customOutput.value = ''
customResult.value = null
customNote.value = ""
customNote.value = ''
customRuntimeError.value = null
customCode.value = ""
customCode.value = ''
customRunning.value = false
}