Files
OJ2/apps/web/src/oj/problem/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

302 lines
9.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 { errorCode } from "utils/api"
import { getProblem } from "oj/api"
import { useBreakpoints } from "shared/composables/breakpoints"
import { storeToRefs } from "pinia"
import { useProblemStore } from "oj/store/problem"
import { useScreenModeStore } from "shared/store/screenMode"
import { useMyFlowchartStore } from "shared/store/myFlowchart"
import { useUserStore } from "shared/store/user"
// 抽成具名 loader,便于进页面时与接口并行预取编辑器 chunk
const loadProblemEditor = () => import("./components/ProblemEditor.vue")
const loadContestEditor = () => import("./components/BasicEditor.vue")
const ProblemEditor = defineAsyncComponent(loadProblemEditor)
const BasicEditor = defineAsyncComponent(loadContestEditor)
const EditorForTest = defineAsyncComponent(
() => import("./components/EditorForTest.vue"),
)
const ProblemContent = defineAsyncComponent(
() => import("./components/ProblemContent.vue"),
)
const ProblemInfo = defineAsyncComponent(
() => import("./components/ProblemInfo.vue"),
)
const ProblemSubmission = defineAsyncComponent(
() => import("./components/ProblemSubmission.vue"),
)
const ProblemReaction = defineAsyncComponent(
() => import("./components/ProblemReaction.vue"),
)
const ProblemFlowchart = defineAsyncComponent(
() => import("./components/ProblemFlowchart.vue"),
)
const MyFlowchartTab = defineAsyncComponent(
() => import("./components/MyFlowchartTab.vue"),
)
interface Props {
problemID: string
contestID?: string
problemSetId?: string
}
const { problemID, contestID = "", problemSetId = "" } = defineProps<Props>()
const errMsg = ref("无数据")
const route = useRoute()
const router = useRouter()
const problemStore = useProblemStore()
const screenModeStore = useScreenModeStore()
const myFlowchartStore = useMyFlowchartStore()
const { problem } = storeToRefs(problemStore)
const { shouldShowProblem } = storeToRefs(screenModeStore)
const { isMobile, isDesktop } = useBreakpoints()
// tab 选项和面板必须用同一个条件。后端在 allowFlowchart 为真时会把 mermaidCode
// 置成 null(不能把标准答案下发给正要自己画图的学生),只看 showFlowchart 的话,
// 两个开关同时打开就会做出一个「选项存在、面板不存在」的 tab —— URL 里带
// ?tab=flowchart 会选中一个渲染不出任何东西的页签。
const canShowFlowchart = computed(
() => !!problem.value?.showFlowchart && !!problem.value?.mermaidCode,
)
const tabOptions = computed(() => {
const options: string[] = ["content"]
if (canShowFlowchart.value) {
options.push("flowchart")
}
if (isMobile.value) {
options.push("editor")
}
options.push("info")
if (!contestID) {
options.push("comment")
}
if (myFlowchartStore.showing) {
options.push("my-flowchart")
}
options.push("submission")
return options
})
const currentTab = ref("content")
const isPublicProblemRoute = computed(() => route.name === "problem")
watch(
[() => route.query.tab, () => tabOptions.value],
([rawTab]) => {
const tabs = tabOptions.value
const fallback = tabs[0] ?? "content"
currentTab.value = tabs.includes(rawTab as string)
? (rawTab as string)
: fallback
},
{ immediate: true },
)
watch(currentTab, (tab) => {
if (!tabOptions.value.includes(tab) || route.query.tab === tab) return
router.replace({
query: { ...route.query, tab },
})
})
watch(
() => myFlowchartStore.showing,
(showing) => {
if (showing) currentTab.value = "my-flowchart"
},
)
async function init() {
screenModeStore.resetScreenMode()
// 并行预取右侧编辑器 chunk(CodeMirror ~370K+),
// 避免等 getProblem 返回后才串行下载,编辑器才迟迟出现
;(isPublicProblemRoute.value ? loadProblemEditor : loadContestEditor)()
try {
const res = await getProblem(problemID, contestID)
problem.value = res
} catch (err) {
problem.value = null
if (errorCode(err) === "contest-not-started") {
errMsg.value = "比赛还没有开始"
}
}
}
onMounted(init)
watch(() => problemID, init)
// 题目详情里的 myStatus / myFailedCount 是按当前用户算的,而登录不重新挂载这个页面 ——
// 会话过期后直接在题目页登录的(机房里最常见的那条路)不补拉一次,AI 提示的解锁进度
// 就还是匿名时的 0,等于白改。只换 problem,不走 init:那里还会重置分栏模式。
watch(
() => useUserStore().isAuthed,
async (authed) => {
if (!authed || !problem.value) return
try {
problem.value = await getProblem(problemID, contestID)
} catch {
// 拉不到就留着现在这份题面,不要把页面清空
}
},
)
onBeforeUnmount(() => {
problem.value = null
errMsg.value = "无数据"
screenModeStore.resetScreenMode()
myFlowchartStore.hide()
})
watch(isMobile, (value) => {
if (value) screenModeStore.resetScreenMode()
})
// SQL 题不支持"自测"模式(外部代码运行器无法执行 SQL),切到该屏时自动跳到下一模式
watch(
() => screenModeStore.isCodeOnlyMode,
(codeOnly) => {
if (codeOnly && problem.value?.languages.includes("SQL")) {
screenModeStore.switchScreenMode()
}
},
)
</script>
<template>
<template v-if="problem">
<n-split
v-if="isDesktop && screenModeStore.isBothMode"
direction="horizontal"
:default-size="0.43"
:min="0.2"
:max="0.8"
style="height: calc(100vh - 92px)"
>
<template #1>
<n-scrollbar style="height: 100%">
<n-tabs v-model:value="currentTab" type="segment">
<n-tab-pane name="content" tab="题目描述">
<ProblemContent />
</n-tab-pane>
<n-tab-pane v-if="canShowFlowchart" name="flowchart" tab="流程图表">
<ProblemFlowchart />
</n-tab-pane>
<n-tab-pane name="info" tab="题目统计" :disabled="!!problemSetId">
<ProblemInfo />
</n-tab-pane>
<n-tab-pane
v-if="!contestID"
name="comment"
tab="题目点评"
:disabled="!!problemSetId"
>
<ProblemReaction />
</n-tab-pane>
<n-tab-pane
v-if="myFlowchartStore.showing"
name="my-flowchart"
tab="我的流程图"
>
<MyFlowchartTab />
</n-tab-pane>
<n-tab-pane
name="submission"
tab="我的提交"
:disabled="!!problemSetId"
>
<ProblemSubmission />
</n-tab-pane>
</n-tabs>
</n-scrollbar>
</template>
<template #2>
<component :is="isPublicProblemRoute ? ProblemEditor : BasicEditor" />
</template>
</n-split>
<!-- Desktop: code only mode -->
<template v-else-if="isDesktop && screenModeStore.isCodeOnlyMode">
<EditorForTest />
</template>
<!-- Desktop: problem only mode -->
<template v-else-if="isDesktop && shouldShowProblem">
<n-scrollbar style="max-height: calc(100vh - 92px)">
<n-tabs v-model:value="currentTab" type="segment">
<n-tab-pane name="content" tab="题目描述">
<ProblemContent />
</n-tab-pane>
<n-tab-pane v-if="canShowFlowchart" name="flowchart" tab="流程图表">
<ProblemFlowchart />
</n-tab-pane>
<n-tab-pane name="info" tab="题目统计" :disabled="!!problemSetId">
<ProblemInfo />
</n-tab-pane>
<n-tab-pane
v-if="!contestID"
name="comment"
tab="题目点评"
:disabled="!!problemSetId"
>
<ProblemReaction />
</n-tab-pane>
<n-tab-pane
v-if="myFlowchartStore.showing"
name="my-flowchart"
tab="我的流程图"
>
<MyFlowchartTab />
</n-tab-pane>
<n-tab-pane
name="submission"
tab="我的提交"
:disabled="!!problemSetId"
>
<ProblemSubmission />
</n-tab-pane>
</n-tabs>
</n-scrollbar>
</template>
<!-- Mobile -->
<n-tabs v-else v-model:value="currentTab" type="segment">
<n-tab-pane name="content" tab="描述">
<ProblemContent />
</n-tab-pane>
<n-tab-pane v-if="canShowFlowchart" name="flowchart" tab="流程">
<ProblemFlowchart />
</n-tab-pane>
<n-tab-pane name="editor" tab="代码">
<component :is="isPublicProblemRoute ? ProblemEditor : BasicEditor" />
</n-tab-pane>
<n-tab-pane name="info" tab="统计" :disabled="!!problemSetId">
<ProblemInfo />
</n-tab-pane>
<n-tab-pane
v-if="!contestID"
name="comment"
tab="点评"
:disabled="!!problemSetId"
>
<ProblemReaction />
</n-tab-pane>
<n-tab-pane
v-if="myFlowchartStore.showing"
name="my-flowchart"
tab="我的流程图"
>
<MyFlowchartTab />
</n-tab-pane>
<n-tab-pane name="submission" tab="提交" :disabled="!!problemSetId">
<ProblemSubmission />
</n-tab-pane>
</n-tabs>
</template>
<n-empty v-else :description="errMsg"></n-empty>
</template>