fix(前台): 地址打错不再是一整页空白,题目、比赛、题单不存在时写明是哪样不存在、给一个回去的按钮

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-13 22:03:49 +08:00
1 parent f3956ad6a1
commit 06c679579c
6 files changed
+153 -37

No files matched your search

@@ -1,14 +0,0 @@
<template>
<n-flex align="center" :size="8" :wrap="false">
<n-tag type="error" size="small" v-if="top">置顶</n-tag>
<span>{{ title }}</span>
</n-flex>
</template>
<script setup lang="ts">
interface Props {
top: boolean
title: string
}
defineProps<Props>()
</script>
+28 -7
View File
@@ -3,6 +3,8 @@ import { useThemeVars } from "naive-ui"
import { useTone } from "oj/submission/composables/tone"
import { ContestStatus } from "utils/constants"
import { parseTime, secondsToDuration } from "utils/functions"
import NotFound from "shared/components/NotFound.vue"
import { errorCode } from "utils/api"
import { useContestStore } from "../store/contest"
import ContestBadge from "./components/ContestBadge.vue"
import ContestInfo from "./components/ContestInfo.vue"
@@ -34,18 +36,27 @@ async function check() {
}
}
/** 拉比赛失败:原来没接,链接不对就是一整页空白 */
const failure = ref<null | "missing" | "failed">(null)
async function load() {
failure.value = null
try {
await contestStore.init(props.contestID)
} catch (err) {
if (contestStore.contest) return
failure.value = errorCode(err) === "contest-not-found" ? "missing" : "failed"
}
}
watch(
() => contestStore.contestStatus,
(nv, ov) => {
if (nv === ContestStatus.underway && ov == ContestStatus.not_started) {
contestStore.init(props.contestID)
}
if (nv === ContestStatus.underway && ov == ContestStatus.not_started) load()
},
)
onMounted(() => {
contestStore.init(props.contestID)
})
onMounted(load)
onBeforeUnmount(contestStore.clear)
@@ -95,7 +106,17 @@ const to = (name: string) => ({ name, params: { contestID: props.contestID } })
</script>
<template>
<div v-if="contest" class="contest-page">
<NotFound
v-if="failure === 'missing'"
title="这场比赛不存在"
hint="可能是链接复制错了,或者这场比赛还没公开"
to="/contest"
to-label="去比赛列表"
/>
<NotFound v-else-if="failure === 'failed'" icon="ph:wifi-slash" title="比赛没拉下来">
<n-button type="primary" @click="load()">再试一次</n-button>
</NotFound>
<div v-else-if="contest" class="contest-page">
<header class="bar">
<ContestBadge :status="status" />
<h2 class="title">{{ contest.title }}</h2>
+39 -7
View File
@@ -11,6 +11,7 @@ import { useSubmissionStore } from "oj/store/submission"
import { useFlowchartStore } from "oj/store/flowchart"
import { useCollabStore } from "shared/store/collab"
import type { RouteLocationNormalized } from "vue-router"
import NotFound from "shared/components/NotFound.vue"
// 判完之后该发生的事(烟花、点评、回题单)。静态引入:它得在第一次判完之前就挂好
import SubmissionEffects from "./components/SubmissionEffects.vue"
import ResultTabLabel from "./components/ResultTabLabel.vue"
@@ -40,9 +41,10 @@ interface Props {
problemSetId?: string
}
const { problemID, contestID = "" } = defineProps<Props>()
const { problemID, contestID = "", problemSetId = "" } = defineProps<Props>()
const errMsg = ref("无数据")
/** 拉题面失败的原因:null 是还在拉(或拉到了),别的就整页换成 NotFound */
const failure = ref<null | "missing" | "not-started" | "failed">(null)
const route = useRoute()
const router = useRouter()
@@ -226,6 +228,7 @@ async function init() {
// 并行预取右侧编辑器 chunk(CodeMirror ~370K+),
// 避免等 getProblem 返回后才串行下载,编辑器才迟迟出现
loadProblemEditor()
failure.value = null
try {
const res = await getProblem(problemID, contestID)
if (mine !== initSeq) return
@@ -233,12 +236,25 @@ async function init() {
} catch (err) {
if (mine !== initSeq) return
problem.value = null
if (errorCode(err) === "contest-not-started") {
errMsg.value = "比赛还没有开始"
}
const code = errorCode(err)
failure.value =
code === "contest-not-started"
? "not-started"
: code === "problem-not-found"
? "missing"
: "failed"
}
}
onMounted(init)
/** 题不存在时回哪:比赛里回比赛,题单里回题单,否则回题目列表 */
const backTo = computed(() =>
contestID
? { to: `/contest/${contestID}`, toLabel: "回到比赛" }
: problemSetId
? { to: `/problemset/${problemSetId}`, toLabel: "回到题单" }
: { to: "/problem", toLabel: "去题目列表" },
)
watch(() => problemID, init)
// 题目详情里的 myStatus / myFailedCount 是按当前用户算的,而登录不重新挂载这个页面 ——
@@ -262,7 +278,7 @@ onBeforeUnmount(() => {
// 流程图那边同理,还在评的那次不再跟(照常评完落库)
useFlowchartStore().reset()
problem.value = null
errMsg.value = "无数据"
failure.value = null
myFlowchartStore.hide()
})
</script>
@@ -372,7 +388,23 @@ onBeforeUnmount(() => {
<MobileActionBar />
</div>
</template>
<n-empty v-else :description="errMsg"></n-empty>
<NotFound
v-else-if="failure === 'not-started'"
icon="ph:clock"
title="比赛还没有开始"
hint="开始以后才能看题"
:to="`/contest/${contestID}`"
to-label="回到比赛"
/>
<NotFound v-else-if="failure === 'failed'" icon="ph:wifi-slash" title="题目没拉下来">
<n-button type="primary" @click="init()">再试一次</n-button>
</NotFound>
<NotFound
v-else-if="failure === 'missing'"
title="这道题不存在"
hint="可能是题号打错了,或者这道题还没公开"
v-bind="backTo"
/>
</template>
<style scoped>
+7 -9
View File
@@ -9,6 +9,7 @@ import type { ProblemSet, ProblemSetProblem } from "utils/types"
import { useAuthModalStore } from "shared/store/authModal"
import { useUserStore } from "shared/store/user"
import { useFireworks } from "../problem/composables/useFireworks"
import NotFound from "shared/components/NotFound.vue"
import BadgeLadder from "./components/BadgeLadder.vue"
import ClassView from "./components/ClassView.vue"
@@ -133,16 +134,13 @@ const purple = { color: "#5b3fa8", background: "rgba(122, 95, 208, 0.12)" }
</script>
<template>
<n-result
<NotFound
v-if="missing"
status="404"
title="题单不存在"
description="可能还没公开,或者已经删掉了"
>
<template #footer>
<n-button @click="router.push({ name: 'problemsets' })">回到题单列表</n-button>
</template>
</n-result>
title="这个题单不存在"
hint="可能还没公开,或者已经删掉了"
to="/problemset"
to-label="回到题单列表"
/>
<div v-else-if="set" class="page oj-page">
<section class="band" :class="{ done: progress?.isCompleted }">
+8
View File
@@ -146,6 +146,14 @@ export const ojs: RouteRecordRaw = {
name: "problemset problem",
meta: { requiresAuth: true },
},
// 兜底:没有它,打错地址是一整页空白,连顶栏都没有。vue-router 按路径具体程度排序,
// 不按注册顺序,所以它吃不掉别的路由;/admin 下打错的也落到这里
{
path: ":pathMatch(.*)*",
name: "not found",
component: () => import("shared/components/NotFound.vue"),
props: { title: "这个页面不存在", hint: "可能是地址打错了,或者这个页面已经下线" },
},
],
}
@@ -0,0 +1,71 @@
<script setup lang="ts">
import { Icon } from "@iconify/vue"
import { useThemeVars } from "naive-ui"
import type { RouteLocationRaw } from "vue-router"
/**
* 「找不到」的整页提示:地址打错、题目 / 比赛 / 题单不存在或没公开、比赛还没开始。
* 原来各页各写各的 —— 打错地址是一整页空白、题号不对只有一个「无数据」,学生看不出出了什么事。
* 样子跟提交详情的「这条提交不存在」一致:图标、一句话、一句为什么、一个回去的按钮。
*/
const props = withDefaults(
defineProps<{
title: string
hint?: string
icon?: string
to?: RouteLocationRaw
toLabel?: string
}>(),
{ hint: "", icon: "ph:magnifying-glass", to: "/", toLabel: "回首页" },
)
const theme = useThemeVars()
const router = useRouter()
</script>
<template>
<div class="not-found">
<Icon :icon="props.icon" :width="40" class="icon" />
<span class="title">{{ props.title }}</span>
<span v-if="props.hint" class="hint">{{ props.hint }}</span>
<div class="actions">
<slot>
<n-button type="primary" @click="router.push(props.to)">{{ props.toLabel }}</n-button>
</slot>
</div>
</div>
</template>
<style scoped>
.not-found {
min-height: 360px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
padding: 48px 16px;
text-align: center;
}
.icon {
color: v-bind("theme.textColor3");
opacity: 0.7;
}
.title {
font-size: 18px;
font-weight: 600;
}
.hint {
font-size: var(--oj-fs-sec);
color: v-bind("theme.textColor3");
}
.actions {
display: flex;
gap: 10px;
margin-top: 8px;
}
</style>