Files
OJ2/apps/web/src/oj/submission/detail.vue
T
xuyueandClaude Opus 5.5 5ae830a7f9 refactor: 一批名不副实的命名改掉(只动一端,不碰契约)
后端:
- getUserByToken → authenticateToken:它会删会话、续 TTL、记在线,不是纯读
- todayStart → dayStart(instant):本来就接受任意时刻,不只是「今天」
- objectValue → asRecord,删掉 4 份一模一样的私有拷贝和 `as toObject` 别名
- 报错文案 "does not exists" / "Already be a public problem" 改通顺

前端:
- utils/types 里 Contest / User / Problem 三个别名删掉:Contest 原来指后台
  比赛、契约里同名的却是学生侧。现在 Contest = 学生侧、AdminContest = 后台,
  后台用户表单叫 AdminUserForm,题目详情直接用 ProblemDetail
- SubmissionInfo → ContestSubmissionInfo,免得和 submission.info 撞名
- utoa / atou → compressToBase64 / decompressFromBase64(实际是 zlib + base64),
  encode / decode → base64EncodeUtf8 / base64DecodeUtf8
- filterResult → toProblemRow、toProblemListItem → toAdminProblemRow,
  行字段 submission / rate → submissionCount / acRate
- 组件:Modal → AddFromLibraryModal、Form → EditorToolbar、
  ContestEditor → BasicEditor(题单也在用它)、rank 的 Index → RankMedal、
  Name → UserNameCell、Password → PasswordCell、ContestType → ContestAccessTag

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:08:21 +08:00

183 lines
5.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { getSubmission } from "oj/api"
import type { JudgeCaseResult } from "@oj2/contract"
import {
JUDGE_STATUS,
LANGUAGE_FORMAT_VALUE,
LANGUAGE_SHOW_VALUE,
} from "utils/constants"
import {
parseTime,
submissionCaseResults,
submissionMemoryFormat,
submissionResultTitle,
submissionTimeFormat,
compressToBase64,
} from "utils/functions"
import type { Submission } from "utils/types"
import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
import { useBreakpoints } from "shared/composables/breakpoints"
import { useCodeStore } from "oj/store/code"
import storage from "utils/storage"
const props = defineProps<{
submissionID: string
problemID?: string
submission?: Submission
hideList?: boolean
}>()
// 在弹框中使用时,父组件监听此事件关闭弹框,否则弹框会挡住已更新的编辑器
const emit = defineEmits<{ copied: [] }>()
const route = useRoute()
const router = useRouter()
const codeStore = useCodeStore()
const { isMobile, isDesktop } = useBreakpoints()
const submission = ref<Submission>()
const loading = ref(false)
/**
* 测试点明细。`info` 在契约里是「完整形状或空对象」的联合(非管理员拿到的是空对象),
* `data` 本身也可能为 null —— 两种情况都由这个访问器归成空数组,模板里不再直接取。
*/
const caseResults = computed(() =>
submissionCaseResults(submission.value?.info),
)
async function init() {
submission.value = props.submission
if (submission.value) return
loading.value = true
const res = await getSubmission(props.submissionID)
submission.value = res
loading.value = false
}
const columns: DataTableColumn<JudgeCaseResult>[] = [
{ 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),
},
]
function copyToCat() {
const lang = LANGUAGE_FORMAT_VALUE[submission.value!.language]
const data = {
lang,
code: submission.value!.code,
input: "",
}
const base64 = compressToBase64(JSON.stringify(data))
const url = `${import.meta.env.PUBLIC_CODE_URL}?share=${encodeURIComponent(base64)}`
window.open(url, "_blank")
}
function copyToProblem() {
const { code, language, contestId } = submission.value!
// 编辑器的 storageKey 用 display id(problem._id),不是 submission.problemId
// (内部数字 id)。**不能只靠 props.problemID** —— 独立的 /submission/:id 路由
// 只喂 submissionID,那个 prop 是 undefined,原来会一路带进 router.push 抛
// `Missing required param "problemID"`。响应里的 problemDisplayId 就是干这个的。
const problemID = props.problemID ?? submission.value!.problemDisplayId
const contestIDForKey = contestId || null
const storageKey = `problem_${problemID}_contest_${contestIDForKey}_lang_${language}`
storage.set(storageKey, code)
// 设置语言 + 代码:localStorage 覆盖全新挂载的编辑器,
// setCode 覆盖已挂载(同页 modal)的编辑器
codeStore.setLanguage(language)
codeStore.setCode(code)
const problemSetId = (route.params.problemSetId as string) ?? ""
if (contestId) {
router.push({
name: "contest problem",
params: { contestID: String(contestId), problemID },
})
} else if (problemSetId) {
router.push({
name: "problemset problem",
params: { problemSetId, problemID },
})
} else {
router.push({
name: "problem",
params: { problemID },
})
}
emit("copied")
}
onMounted(init)
</script>
<template>
<n-flex vertical v-if="submission" :size="24">
<n-flex :vertical="isMobile" justify="space-between">
<n-alert
style="flex: 1"
:type="JUDGE_STATUS[submission.result]['type']"
:title="submissionResultTitle(submission)"
>
<n-flex>
<span>提交时间:{{ parseTime(submission.createTime) }}</span>
<span>编程语言:{{ LANGUAGE_SHOW_VALUE[submission.language] }}</span>
<span>用户:{{ submission.username }}</span>
</n-flex>
</n-alert>
<n-flex :vertical="isDesktop" justify="center">
<n-button
v-if="submission.language !== 'SQL'"
secondary
@click="copyToCat"
>
复制到自测猫
</n-button>
<n-button secondary @click="copyToProblem">复制回到题目</n-button>
</n-flex>
</n-flex>
<n-card embedded>
<n-code
class="code"
:language="LANGUAGE_FORMAT_VALUE[submission.language]"
:code="submission.code"
show-line-numbers
/>
</n-card>
<n-data-table
v-if="!hideList && caseResults.length"
:columns="columns"
:data="caseResults"
/>
</n-flex>
<n-spin v-else :show="loading" class="loading-container"> </n-spin>
</template>
<style scoped>
.code {
font-size: 20px;
overflow: auto;
}
.loading-container {
min-height: 200px;
display: flex;
justify-content: center;
align-items: center;
}
</style>