100
src/oj/problem/components/SubmissionResult.vue
Normal file
100
src/oj/problem/components/SubmissionResult.vue
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { JUDGE_STATUS, SubmissionStatus } from "utils/constants"
|
||||||
|
import { submissionMemoryFormat, submissionTimeFormat } from "utils/functions"
|
||||||
|
import type { Submission } from "utils/types"
|
||||||
|
import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
submission?: Submission
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 错误信息格式化
|
||||||
|
const msg = computed(() => {
|
||||||
|
if (!props.submission) return ""
|
||||||
|
|
||||||
|
let msg = ""
|
||||||
|
const result = props.submission.result
|
||||||
|
|
||||||
|
// 编译错误或运行时错误时给出提示
|
||||||
|
if (
|
||||||
|
result === SubmissionStatus.compile_error ||
|
||||||
|
result === SubmissionStatus.runtime_error
|
||||||
|
) {
|
||||||
|
msg += "请仔细检查,看看代码的格式是不是写错了!\n\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.submission.statistic_info?.err_info) {
|
||||||
|
msg += props.submission.statistic_info.err_info
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试用例表格数据(只在部分通过时显示)
|
||||||
|
const infoTable = computed(() => {
|
||||||
|
if (!props.submission?.info?.data?.length) return []
|
||||||
|
|
||||||
|
const result = props.submission.result
|
||||||
|
// AC、编译错误、运行时错误不显示测试用例表格
|
||||||
|
if (
|
||||||
|
result === SubmissionStatus.accepted ||
|
||||||
|
result === SubmissionStatus.compile_error ||
|
||||||
|
result === SubmissionStatus.runtime_error
|
||||||
|
) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = props.submission.info.data
|
||||||
|
// 只有存在失败的测试用例时才显示
|
||||||
|
return data.some((item) => item.result === 0) ? data : []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 测试用例表格列配置
|
||||||
|
const columns: DataTableColumn<Submission["info"]["data"][number]>[] = [
|
||||||
|
{ title: "测试用例", key: "test_case" },
|
||||||
|
{
|
||||||
|
title: "测试状态",
|
||||||
|
key: "result",
|
||||||
|
render: (row) => h(SubmissionResultTag, { result: row.result }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "占用内存",
|
||||||
|
key: "memory",
|
||||||
|
render: (row) => submissionMemoryFormat(row.memory),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "执行耗时",
|
||||||
|
key: "real_time",
|
||||||
|
render: (row) => submissionTimeFormat(row.real_time),
|
||||||
|
},
|
||||||
|
{ title: "信号", key: "signal" },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="submission">
|
||||||
|
<n-alert
|
||||||
|
:type="JUDGE_STATUS[submission.result]['type']"
|
||||||
|
:title="JUDGE_STATUS[submission.result]['name']"
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
<n-flex vertical v-if="msg || infoTable.length">
|
||||||
|
<n-card v-if="msg" embedded class="msg">{{ msg }}</n-card>
|
||||||
|
<n-data-table
|
||||||
|
v-if="infoTable.length"
|
||||||
|
striped
|
||||||
|
:data="infoTable"
|
||||||
|
:columns="columns"
|
||||||
|
/>
|
||||||
|
</n-flex>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.msg {
|
||||||
|
white-space: pre;
|
||||||
|
word-break: break-all;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -1,213 +1,88 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from "@iconify/vue"
|
import { Icon } from "@iconify/vue"
|
||||||
import confetti from "canvas-confetti"
|
import confetti from "canvas-confetti"
|
||||||
import { getComment, getSubmission, submitCode } from "oj/api"
|
import { getComment, submitCode } from "oj/api"
|
||||||
import { code } from "oj/composables/code"
|
import { code } from "oj/composables/code"
|
||||||
import { problem } from "oj/composables/problem"
|
import { problem } from "oj/composables/problem"
|
||||||
import { JUDGE_STATUS, SubmissionStatus } from "utils/constants"
|
import { useSubmissionMonitor } from "oj/problem/composables/useSubmissionMonitor"
|
||||||
import { submissionMemoryFormat, submissionTimeFormat } from "utils/functions"
|
import { SubmissionStatus } from "utils/constants"
|
||||||
import { Submission, SubmitCodePayload } from "utils/types"
|
import type { SubmitCodePayload } from "utils/types"
|
||||||
import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
|
import SubmissionResult from "./SubmissionResult.vue"
|
||||||
import { isDesktop } from "shared/composables/breakpoints"
|
import { isDesktop } from "shared/composables/breakpoints"
|
||||||
import {
|
|
||||||
useSubmissionWebSocket,
|
|
||||||
type SubmissionUpdate,
|
|
||||||
} from "shared/composables/websocket"
|
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
|
|
||||||
|
// ==================== 异步组件 ====================
|
||||||
const ProblemComment = defineAsyncComponent(
|
const ProblemComment = defineAsyncComponent(
|
||||||
() => import("./ProblemComment.vue"),
|
() => import("./ProblemComment.vue")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ==================== 基础状态 ====================
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const contestID = <string>route.params.contestID ?? ""
|
const contestID = <string>route.params.contestID ?? ""
|
||||||
|
|
||||||
const submissionId = ref("")
|
|
||||||
const submission = ref<Submission>()
|
|
||||||
const [submitted] = useToggle()
|
|
||||||
const [commentPanel] = useToggle()
|
const [commentPanel] = useToggle()
|
||||||
|
|
||||||
const { start: submitPending, isPending } = useTimeout(5000, {
|
// ==================== 判题监控 ====================
|
||||||
|
const {
|
||||||
|
submission,
|
||||||
|
submissionId,
|
||||||
|
judging,
|
||||||
|
pending,
|
||||||
|
submitting,
|
||||||
|
isProcessing,
|
||||||
|
startMonitoring,
|
||||||
|
} = useSubmissionMonitor()
|
||||||
|
|
||||||
|
// ==================== 提交冷却 ====================
|
||||||
|
const { start: startCooldown, isPending: isCooldown } = useTimeout(5000, {
|
||||||
controls: true,
|
controls: true,
|
||||||
immediate: false,
|
immediate: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { start: showCommentPanel } = useTimeoutFn(
|
// ==================== AC后显示评论框 ====================
|
||||||
|
const { start: showCommentPanelDelayed } = useTimeoutFn(
|
||||||
async () => {
|
async () => {
|
||||||
const res = await getComment(problem.value!.id)
|
const res = await getComment(problem.value!.id)
|
||||||
if (res.data) return
|
if (!res.data) {
|
||||||
commentPanel.value = true
|
commentPanel.value = true
|
||||||
},
|
|
||||||
2000,
|
|
||||||
{ immediate: false },
|
|
||||||
)
|
|
||||||
|
|
||||||
const { start: fetchSubmission, stop: stopFetchSubmission } = useTimeoutFn(
|
|
||||||
async () => {
|
|
||||||
const res = await getSubmission(submissionId.value)
|
|
||||||
submission.value = res.data
|
|
||||||
const result = submission.value.result
|
|
||||||
if (
|
|
||||||
result === SubmissionStatus.judging ||
|
|
||||||
result === SubmissionStatus.pending
|
|
||||||
) {
|
|
||||||
fetchSubmission()
|
|
||||||
} else {
|
|
||||||
submitted.value = false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
2000,
|
1500,
|
||||||
{ immediate: false },
|
{ immediate: false }
|
||||||
)
|
|
||||||
|
|
||||||
// WebSocket 消息处理器
|
|
||||||
const handleSubmissionUpdate = (data: SubmissionUpdate) => {
|
|
||||||
console.log("[Submit] 收到提交更新:", data)
|
|
||||||
|
|
||||||
if (data.submission_id !== submissionId.value) {
|
|
||||||
console.log(`[Submit] 提交ID不匹配: 期望=${submissionId.value}, 实际=${data.submission_id}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!submission.value) {
|
|
||||||
submission.value = {} as Submission
|
|
||||||
}
|
|
||||||
|
|
||||||
submission.value.result = data.result as Submission["result"]
|
|
||||||
|
|
||||||
// 判题完成,获取完整提交详情
|
|
||||||
if (data.status === "finished") {
|
|
||||||
console.log("[Submit] 判题完成,获取详细信息")
|
|
||||||
getSubmission(submissionId.value).then((res) => {
|
|
||||||
submission.value = res.data
|
|
||||||
submitted.value = false
|
|
||||||
|
|
||||||
// 判题完成后,15 分钟无新提交则断开 WebSocket 连接
|
|
||||||
// 考虑到学生换题间隔约 10 分钟,15 分钟可覆盖大部分场景
|
|
||||||
scheduleDisconnect(15 * 60 * 1000)
|
|
||||||
})
|
|
||||||
} else if (data.status === "error") {
|
|
||||||
console.log("[Submit] 判题出错")
|
|
||||||
submitted.value = false
|
|
||||||
// 判题出错后,15 分钟无新提交则断开 WebSocket 连接
|
|
||||||
scheduleDisconnect(15 * 60 * 1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化 WebSocket(按需连接模式)
|
|
||||||
const {
|
|
||||||
connect,
|
|
||||||
subscribe,
|
|
||||||
scheduleDisconnect,
|
|
||||||
cancelScheduledDisconnect,
|
|
||||||
status: wsStatus,
|
|
||||||
} = useSubmissionWebSocket(handleSubmissionUpdate)
|
|
||||||
|
|
||||||
// 组件卸载时停止轮询
|
|
||||||
onUnmounted(() => {
|
|
||||||
stopFetchSubmission()
|
|
||||||
})
|
|
||||||
|
|
||||||
const judging = computed(
|
|
||||||
() => submission.value?.result === SubmissionStatus.judging,
|
|
||||||
)
|
|
||||||
|
|
||||||
const pending = computed(
|
|
||||||
() => submission.value?.result === SubmissionStatus.pending,
|
|
||||||
)
|
|
||||||
|
|
||||||
const submitting = computed(
|
|
||||||
() => submission.value?.result === SubmissionStatus.submitting,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ==================== 计算属性 ====================
|
||||||
|
// 按钮禁用逻辑
|
||||||
const submitDisabled = computed(() => {
|
const submitDisabled = computed(() => {
|
||||||
return (
|
return (
|
||||||
!userStore.isAuthed ||
|
!userStore.isAuthed ||
|
||||||
code.value.trim() === "" ||
|
code.value.trim() === "" ||
|
||||||
judging.value ||
|
isProcessing.value ||
|
||||||
pending.value ||
|
isCooldown.value
|
||||||
submitting.value ||
|
|
||||||
submitted.value ||
|
|
||||||
isPending.value
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 按钮文案
|
||||||
const submitLabel = computed(() => {
|
const submitLabel = computed(() => {
|
||||||
if (!userStore.isAuthed) {
|
if (!userStore.isAuthed) return "请先登录"
|
||||||
return "请先登录"
|
if (submitting.value) return "正在提交"
|
||||||
}
|
if (judging.value || pending.value) return "正在评分"
|
||||||
if (submitting.value) {
|
if (isCooldown.value) return "正在冷却"
|
||||||
return "正在提交"
|
|
||||||
}
|
|
||||||
if (judging.value || pending.value) {
|
|
||||||
return "正在评分"
|
|
||||||
}
|
|
||||||
if (isPending.value) {
|
|
||||||
return "正在冷却"
|
|
||||||
}
|
|
||||||
return "提交代码"
|
return "提交代码"
|
||||||
})
|
})
|
||||||
|
|
||||||
const msg = computed(() => {
|
// 按钮图标
|
||||||
if (!submission.value) return ""
|
const submitIcon = computed(() => {
|
||||||
|
if (isProcessing.value) return "eos-icons:loading"
|
||||||
let msg = ""
|
if (isCooldown.value) return "ph:lightbulb-fill"
|
||||||
const result = submission.value.result
|
return "ph:play-fill"
|
||||||
|
|
||||||
if (
|
|
||||||
result === SubmissionStatus.compile_error ||
|
|
||||||
result === SubmissionStatus.runtime_error
|
|
||||||
) {
|
|
||||||
msg += "请仔细检查,看看代码的格式是不是写错了!\n\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (submission.value.statistic_info?.err_info) {
|
|
||||||
msg += submission.value.statistic_info.err_info
|
|
||||||
}
|
|
||||||
|
|
||||||
return msg
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const infoTable = computed(() => {
|
// ==================== 提交函数 ====================
|
||||||
if (!submission.value?.info?.data?.length) return []
|
|
||||||
|
|
||||||
const result = submission.value.result
|
|
||||||
if (
|
|
||||||
result === SubmissionStatus.accepted ||
|
|
||||||
result === SubmissionStatus.compile_error ||
|
|
||||||
result === SubmissionStatus.runtime_error
|
|
||||||
) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = submission.value.info.data
|
|
||||||
return data.some((item) => item.result === 0) ? data : []
|
|
||||||
})
|
|
||||||
|
|
||||||
const columns: DataTableColumn<Submission["info"]["data"][number]>[] = [
|
|
||||||
{ title: "测试用例", key: "test_case" },
|
|
||||||
{
|
|
||||||
title: "测试状态",
|
|
||||||
key: "result",
|
|
||||||
render: (row) => h(SubmissionResultTag, { result: row.result }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "占用内存",
|
|
||||||
key: "memory",
|
|
||||||
render: (row) => submissionMemoryFormat(row.memory),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "执行耗时",
|
|
||||||
key: "real_time",
|
|
||||||
render: (row) => submissionTimeFormat(row.real_time),
|
|
||||||
},
|
|
||||||
{ title: "信号", key: "signal" },
|
|
||||||
]
|
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
if (!userStore.isAuthed) return
|
if (!userStore.isAuthed) return
|
||||||
|
|
||||||
|
// 1. 构建提交数据
|
||||||
const data: SubmitCodePayload = {
|
const data: SubmitCodePayload = {
|
||||||
problem_id: problem.value!.id,
|
problem_id: problem.value!.id,
|
||||||
language: code.language,
|
language: code.language,
|
||||||
@@ -217,63 +92,25 @@ async function submit() {
|
|||||||
data.contest_id = parseInt(contestID)
|
data.contest_id = parseInt(contestID)
|
||||||
}
|
}
|
||||||
|
|
||||||
submission.value = { result: 9 } as Submission
|
// 2. 提交代码到后端
|
||||||
const res = await submitCode(data)
|
const res = await submitCode(data)
|
||||||
submissionId.value = res.data.submission_id
|
console.log(`[Submit] 代码已提交: ID=${res.data.submission_id}`)
|
||||||
console.log(`[Submit] 代码已提交: ID=${submissionId.value}`)
|
|
||||||
submitPending()
|
|
||||||
submitted.value = true
|
|
||||||
|
|
||||||
// 取消之前安排的断开倒计时(如果有新提交)
|
// 3. 启动冷却 + 监控
|
||||||
cancelScheduledDisconnect()
|
startCooldown()
|
||||||
|
startMonitoring(res.data.submission_id)
|
||||||
// 按需连接 WebSocket
|
|
||||||
if (wsStatus.value !== "connected") {
|
|
||||||
console.log(`[Submit] WebSocket 未连接,正在连接... 当前状态: ${wsStatus.value}`)
|
|
||||||
connect()
|
|
||||||
} else {
|
|
||||||
console.log("[Submit] WebSocket 已连接,直接订阅")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优先使用 WebSocket 实时更新
|
|
||||||
if (wsStatus.value === "connected" || wsStatus.value === "connecting") {
|
|
||||||
// 等待连接完成后订阅
|
|
||||||
const checkConnection = setInterval(() => {
|
|
||||||
if (wsStatus.value === "connected") {
|
|
||||||
clearInterval(checkConnection)
|
|
||||||
console.log(`[Submit] 订阅提交更新: ID=${submissionId.value}`)
|
|
||||||
subscribe(submissionId.value)
|
|
||||||
}
|
|
||||||
}, 100)
|
|
||||||
|
|
||||||
// 5 秒后如果还在判题中,降级到轮询作为保险
|
|
||||||
// 考虑到判题一般 1-3 秒完成,5 秒足以覆盖绝大部分情况
|
|
||||||
setTimeout(() => {
|
|
||||||
clearInterval(checkConnection)
|
|
||||||
if (
|
|
||||||
submission.value &&
|
|
||||||
(submission.value.result === SubmissionStatus.judging ||
|
|
||||||
submission.value.result === SubmissionStatus.pending ||
|
|
||||||
submission.value.result === 9)
|
|
||||||
) {
|
|
||||||
console.log("WebSocket 未及时响应,降级到轮询模式")
|
|
||||||
fetchSubmission()
|
|
||||||
}
|
|
||||||
}, 5000)
|
|
||||||
} else {
|
|
||||||
// WebSocket 连接失败,直接使用轮询
|
|
||||||
fetchSubmission()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== AC庆祝效果 ====================
|
||||||
watch(
|
watch(
|
||||||
() => submission.value?.result,
|
() => submission.value?.result,
|
||||||
(result) => {
|
(result) => {
|
||||||
if (result !== SubmissionStatus.accepted) return
|
if (result !== SubmissionStatus.accepted) return
|
||||||
|
|
||||||
// 刷新题目状态
|
// 1. 刷新题目状态
|
||||||
problem.value!.my_status = 0
|
problem.value!.my_status = 0
|
||||||
// 放烟花
|
|
||||||
|
// 2. 放烟花
|
||||||
confetti({
|
confetti({
|
||||||
particleCount: 300,
|
particleCount: 300,
|
||||||
startVelocity: 30,
|
startVelocity: 30,
|
||||||
@@ -281,13 +118,17 @@ watch(
|
|||||||
spread: 350,
|
spread: 350,
|
||||||
origin: { x: 0.5, y: 0.4 },
|
origin: { x: 0.5, y: 0.4 },
|
||||||
})
|
})
|
||||||
// 题目在第一次完成之后,弹出点评框
|
|
||||||
if (!contestID) showCommentPanel()
|
// 3. 显示评价框(非比赛模式)
|
||||||
},
|
if (!contestID) {
|
||||||
|
showCommentPanelDelayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- 提交按钮 + 结果弹窗 -->
|
||||||
<n-popover
|
<n-popover
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
@@ -304,34 +145,18 @@ watch(
|
|||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon>
|
<n-icon>
|
||||||
<Icon
|
<Icon :icon="submitIcon" />
|
||||||
v-if="judging || pending || submitting"
|
|
||||||
icon="eos-icons:loading"
|
|
||||||
></Icon>
|
|
||||||
<Icon v-else-if="isPending" icon="ph:lightbulb-fill"></Icon>
|
|
||||||
<Icon v-else icon="ph:play-fill"></Icon>
|
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
{{ submitLabel }}
|
{{ submitLabel }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
<template #header>
|
|
||||||
<n-alert
|
<!-- 结果展示 -->
|
||||||
v-if="submission"
|
<SubmissionResult :submission="submission" />
|
||||||
:type="JUDGE_STATUS[submission.result]['type']"
|
|
||||||
:title="JUDGE_STATUS[submission.result]['name']"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<n-flex vertical v-if="msg || infoTable.length">
|
|
||||||
<n-card v-if="msg" embedded class="msg">{{ msg }}</n-card>
|
|
||||||
<n-data-table
|
|
||||||
v-if="infoTable.length"
|
|
||||||
striped
|
|
||||||
:data="infoTable"
|
|
||||||
:columns="columns"
|
|
||||||
/>
|
|
||||||
</n-flex>
|
|
||||||
</n-popover>
|
</n-popover>
|
||||||
|
|
||||||
|
<!-- 评价弹窗 -->
|
||||||
<n-modal
|
<n-modal
|
||||||
preset="card"
|
preset="card"
|
||||||
title="恭喜你成功提交,请对该题进行评价(一星差评,五星好评)"
|
title="恭喜你成功提交,请对该题进行评价(一星差评,五星好评)"
|
||||||
@@ -342,10 +167,3 @@ watch(
|
|||||||
<ProblemComment :showStatistics="false" />
|
<ProblemComment :showStatistics="false" />
|
||||||
</n-modal>
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
|
||||||
.msg {
|
|
||||||
white-space: pre;
|
|
||||||
word-break: break-all;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
172
src/oj/problem/composables/useSubmissionMonitor.ts
Normal file
172
src/oj/problem/composables/useSubmissionMonitor.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { ref } from "vue"
|
||||||
|
import { getSubmission } from "oj/api"
|
||||||
|
import { SubmissionStatus } from "utils/constants"
|
||||||
|
import type { Submission } from "utils/types"
|
||||||
|
import {
|
||||||
|
useSubmissionWebSocket,
|
||||||
|
type SubmissionUpdate,
|
||||||
|
} from "shared/composables/websocket"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判题监控 Composable
|
||||||
|
* 负责通过 WebSocket + 轮询双保险机制监控判题结果
|
||||||
|
*/
|
||||||
|
export function useSubmissionMonitor() {
|
||||||
|
// ==================== 状态 ====================
|
||||||
|
const submissionId = ref("")
|
||||||
|
const submission = ref<Submission>()
|
||||||
|
|
||||||
|
// ==================== 轮询机制 ====================
|
||||||
|
const { pause: pausePolling, resume: resumePolling } = useIntervalFn(
|
||||||
|
async () => {
|
||||||
|
if (!submissionId.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getSubmission(submissionId.value)
|
||||||
|
submission.value = res.data
|
||||||
|
|
||||||
|
const result = res.data.result
|
||||||
|
// 判题完成,停止轮询
|
||||||
|
if (
|
||||||
|
result !== SubmissionStatus.judging &&
|
||||||
|
result !== SubmissionStatus.pending
|
||||||
|
) {
|
||||||
|
pausePolling()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[SubmissionMonitor] 轮询失败:", error)
|
||||||
|
pausePolling()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
2000,
|
||||||
|
{ immediate: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==================== WebSocket 处理 ====================
|
||||||
|
const handleSubmissionUpdate = (data: SubmissionUpdate) => {
|
||||||
|
console.log("[SubmissionMonitor] 收到WebSocket更新:", data)
|
||||||
|
|
||||||
|
if (data.submission_id !== submissionId.value) {
|
||||||
|
console.log("[SubmissionMonitor] 提交ID不匹配,忽略")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!submission.value) {
|
||||||
|
submission.value = {} as Submission
|
||||||
|
}
|
||||||
|
|
||||||
|
submission.value.result = data.result as Submission["result"]
|
||||||
|
|
||||||
|
// 判题完成或出错,获取完整详情
|
||||||
|
if (data.status === "finished" || data.status === "error") {
|
||||||
|
console.log(
|
||||||
|
`[SubmissionMonitor] 判题${data.status === "finished" ? "完成" : "出错"}`
|
||||||
|
)
|
||||||
|
|
||||||
|
// 停止轮询(WebSocket已成功)
|
||||||
|
pausePolling()
|
||||||
|
|
||||||
|
getSubmission(submissionId.value).then((res) => {
|
||||||
|
submission.value = res.data
|
||||||
|
// 15分钟无新提交则断开WebSocket(节省资源)
|
||||||
|
scheduleDisconnect(15 * 60 * 1000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 WebSocket
|
||||||
|
const {
|
||||||
|
connect,
|
||||||
|
subscribe,
|
||||||
|
scheduleDisconnect,
|
||||||
|
cancelScheduledDisconnect,
|
||||||
|
status: wsStatus,
|
||||||
|
} = useSubmissionWebSocket(handleSubmissionUpdate)
|
||||||
|
|
||||||
|
// ==================== 轮询保底启动 ====================
|
||||||
|
const { start: startPollingFallback } = useTimeoutFn(
|
||||||
|
() => {
|
||||||
|
if (
|
||||||
|
submission.value &&
|
||||||
|
(submission.value.result === SubmissionStatus.judging ||
|
||||||
|
submission.value.result === SubmissionStatus.pending ||
|
||||||
|
submission.value.result === 9) // 9 = submitting
|
||||||
|
) {
|
||||||
|
console.log("[SubmissionMonitor] WebSocket未及时响应,启动轮询保底")
|
||||||
|
resumePolling()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
{ immediate: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==================== 启动监控 ====================
|
||||||
|
const startMonitoring = (id: string) => {
|
||||||
|
submissionId.value = id
|
||||||
|
submission.value = { result: 9 } as Submission // 9 = submitting
|
||||||
|
|
||||||
|
// 取消之前的断开计划
|
||||||
|
cancelScheduledDisconnect()
|
||||||
|
|
||||||
|
// 如果WebSocket未连接,先连接
|
||||||
|
if (wsStatus.value !== "connected") {
|
||||||
|
console.log("[SubmissionMonitor] 启动WebSocket连接...")
|
||||||
|
connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待WebSocket连接并订阅
|
||||||
|
const unwatch = watch(
|
||||||
|
wsStatus,
|
||||||
|
(status) => {
|
||||||
|
if (status === "connected") {
|
||||||
|
console.log("[SubmissionMonitor] WebSocket已连接,订阅提交:", id)
|
||||||
|
subscribe(id)
|
||||||
|
unwatch() // 订阅成功后停止监听
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 5秒后启动轮询保底(防止WebSocket失败)
|
||||||
|
startPollingFallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 计算属性 ====================
|
||||||
|
const judging = computed(
|
||||||
|
() => submission.value?.result === SubmissionStatus.judging
|
||||||
|
)
|
||||||
|
|
||||||
|
const pending = computed(
|
||||||
|
() => submission.value?.result === SubmissionStatus.pending
|
||||||
|
)
|
||||||
|
|
||||||
|
const submitting = computed(
|
||||||
|
() => submission.value?.result === SubmissionStatus.submitting
|
||||||
|
)
|
||||||
|
|
||||||
|
const isProcessing = computed(() => {
|
||||||
|
return judging.value || pending.value || submitting.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== 清理 ====================
|
||||||
|
onUnmounted(() => {
|
||||||
|
pausePolling()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
submissionId,
|
||||||
|
submission,
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
judging,
|
||||||
|
pending,
|
||||||
|
submitting,
|
||||||
|
isProcessing,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
startMonitoring,
|
||||||
|
pausePolling,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user