Files
OJ2/apps/web/src/oj/problem/components/ProblemDrawer.vue
T
xuyueandClaude Opus 5.5 90ce1ab0f5
Deploy / deploy (push) Canceled after 0s
fix(手机): 点开提交、班级详情、自学目录这类整屏的东西,按返回直接退出了整页,现在是先关掉它
- 提交列表:手机上点开一条,地址带上 ?id=,返回键就是关详情回到列表;刷新、转发链接
  也还停在这一条;上一条 / 下一条、翻页、滚动位置都在;详情里点「只看他」先关详情再筛选
- 看板讲错误、排名班级详情、自学目录、题目页底部抽屉:手机上按返回只关它(useBackClose);
  在自学目录里选一课,返回是回到上一课,不多一站
- 分页列表(提交、题目、比赛、后台几个列表)用浏览器后退翻回前一页时会多压一条历史,
  前进的记录被冲掉、返回要多按一次 —— 电脑上也是,一起修了

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:55:30 +08:00

220 lines
6.8 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 { useThemeVars } from "naive-ui"
import { storeToRefs } from "pinia"
import { useProblemStore } from "oj/store/problem"
import { useBackClose } from "shared/composables/backClose"
import { useUserStore } from "shared/store/user"
import {
DRAWER_TITLE,
useProblemPageContext,
type ProblemDrawer,
} from "../composables/problemPageContext"
import { useTeacherCollab } from "../composables/teacherCollab"
/**
* 统计 / 点评 / 我的提交。原来是左栏的三个页签,课上几乎没人点,却和「题目」挤在一排;
* 现在收进抽屉:盖住整个左栏(连上下文条、页签行),编辑器不动;抽屉顶上有自己的页签,
* 三样之间直接切(设计稿「我的提交 / 统计 / 点评:抽屉盖住左栏」)。
*
* 抽屉里的内容关上就卸载:再打开时「我的提交」要重新拉,刚交的那次才看得见。
*/
const ProblemInfo = defineAsyncComponent(() => import("./ProblemInfo.vue"))
const ProblemReaction = defineAsyncComponent(() => import("./ProblemReaction.vue"))
const ProblemSubmission = defineAsyncComponent(() => import("./ProblemSubmission.vue"))
const open = defineModel<ProblemDrawer | null>({ required: true })
const props = defineProps<{
/** 桌面:挂进左栏(它得是 position: relative);手机:不传,从底下拉起来盖住整屏 */
to?: HTMLElement | null
}>()
/** 抽屉顶上的页签,顺序照设计稿:最常看的「我的提交」在前 */
const tabs = computed(() =>
(["submission", "info", "reaction"] as const).filter((key) => ctx.value.drawers.includes(key)),
)
const theme = useThemeVars()
// trap-focus 关着(编辑器还要能点),焦点不在抽屉里时 Naive 自己的 Esc 不生效,这里补一个
useEventListener(document, "keydown", (event: KeyboardEvent) => {
if (event.key === "Escape" && open.value) open.value = null
})
const ctx = useProblemPageContext()
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const { problem } = storeToRefs(useProblemStore())
const show = computed({
get: () => open.value !== null,
set: (value) => {
if (!value) open.value = null
},
})
// 手机上是大半屏的底部抽屉(没有 to):按返回键收起它,不是退出题目
useBackClose(show, () => !props.to)
// 内容和标题都按最后一次打开的来:关的时候有一段滑出去的动画,那时 open 已经是 null 了。
// 关完 n-drawer 自己把内容卸掉
const lastOpened = ref<ProblemDrawer>("info")
watch(open, (key) => {
if (key) lastOpened.value = key
})
// 换题就关:抽屉里是上一道题的统计和提交
watch(
() => problem.value?._id,
() => {
open.value = null
},
)
// 协作中的老师看的是学生的提交(ProblemSubmission 里按学生用户名查),页签写「他的提交」
const teacherCollab = useTeacherCollab()
/**
* 「看这道题所有人的提交」,原来是工具栏上的「本题提交」。可见条件照旧:
* 比赛里总给(看的是这场比赛的提交),题库里要管理员或者开着提交列表。
*/
const showAllSubmissions = computed(
() => ctx.value.entry === "contest" || userStore.isAdminRole || userStore.showSubmissions,
)
function goAllSubmissions() {
const target = {
name: ctx.value.entry === "contest" ? "contest submissions" : "submissions",
params: ctx.value.entry === "contest" ? { contestID: route.params.contestID } : {},
query: { problem: problem.value!._id },
}
// 协作中走新标签:教师端「页面即协作现场」,跳走这一页协作就结束了
// (求助会退回排队,但老师还得再接一次)。而「看看这学生都交了什么」恰好是
// 协作时最常点的一个
if (teacherCollab.value) {
window.open(router.resolve(target).href, "_blank")
return
}
router.push(target)
}
</script>
<template>
<n-drawer
v-model:show="show"
:to="props.to ?? undefined"
:placement="props.to ? 'left' : 'bottom'"
:width="props.to ? '100%' : undefined"
:height="props.to ? undefined : '85%'"
:trap-focus="false"
:block-scroll="!props.to"
>
<n-drawer-content
:native-scrollbar="false"
:header-style="{ padding: 0 }"
:body-content-style="{ padding: '16px 20px' }"
>
<template #header>
<div class="drawer-head">
<div class="drawer-tabs" role="tablist">
<button
v-for="key in tabs"
:key="key"
type="button"
role="tab"
class="drawer-tab"
:class="{ active: lastOpened === key }"
:aria-selected="lastOpened === key"
@click="open = key"
>
{{ key === "submission" && teacherCollab ? "他的提交" : DRAWER_TITLE[key] }}
</button>
</div>
<button type="button" class="drawer-close" aria-label="关闭" @click="open = null">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
>
<path d="M6 6l12 12M18 6L6 18" />
</svg>
</button>
</div>
</template>
<ProblemInfo v-if="lastOpened === 'info'" @open-submissions="open = 'submission'" />
<ProblemReaction v-else-if="lastOpened === 'reaction'" />
<template v-else>
<ProblemSubmission />
<div v-if="showAllSubmissions" class="all-submissions">
<n-button text type="primary" @click="goAllSubmissions">看这道题所有人的提交 ›</n-button>
</div>
</template>
</n-drawer-content>
</n-drawer>
</template>
<style scoped>
.drawer-head {
height: 48px;
box-sizing: border-box;
padding: 0 12px 0 20px;
display: flex;
align-items: center;
gap: 4px;
border-bottom: 1px solid v-bind("theme.dividerColor");
}
.drawer-tabs {
display: flex;
height: 100%;
}
.drawer-tab {
height: 48px;
padding: 0 12px;
border: 0;
border-bottom: 2px solid transparent;
background: transparent;
font: inherit;
font-size: 14px;
font-weight: normal;
color: v-bind("theme.textColor3");
cursor: pointer;
}
.drawer-tab.active {
border-bottom-color: v-bind("theme.primaryColor");
color: v-bind("theme.textColor1");
font-weight: 600;
}
.drawer-close {
margin-left: auto;
width: 36px;
height: 36px;
border: 0;
border-radius: 4px;
background: transparent;
color: v-bind("theme.textColor2");
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.drawer-close:hover {
background-color: v-bind("theme.hoverColor");
}
.all-submissions {
margin-top: 12px;
display: flex;
justify-content: flex-end;
font-size: 13px;
}
</style>