后端: - 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>
183 lines
5.2 KiB
Vue
183 lines
5.2 KiB
Vue
<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>
|