- 提交列表:手机上点开一条,地址带上 ?id=,返回键就是关详情回到列表;刷新、转发链接 也还停在这一条;上一条 / 下一条、翻页、滚动位置都在;详情里点「只看他」先关详情再筛选 - 看板讲错误、排名班级详情、自学目录、题目页底部抽屉:手机上按返回只关它(useBackClose); 在自学目录里选一课,返回是回到上一课,不多一站 - 分页列表(提交、题目、比赛、后台几个列表)用浏览器后退翻回前一页时会多压一条历史, 前进的记录被冲掉、返回要多按一次 —— 电脑上也是,一起修了 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c149b0a194
commit
90ce1ab0f5
7 files changed
+200
-15
No files matched your search
@@ -14,6 +14,7 @@ import { onKeyStroke } from "@vueuse/core"
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import SubmissionView from "oj/submission/components/SubmissionView.vue"
|
||||
import UserName from "shared/components/UserName.vue"
|
||||
import { useBackClose } from "shared/composables/backClose"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import type { ClassBoardProblem } from "utils/types"
|
||||
import ProblemBrief from "./ProblemBrief.vue"
|
||||
@@ -38,6 +39,8 @@ const index = defineModel<number>("index", { required: true })
|
||||
|
||||
const theme = useThemeVars()
|
||||
const { isMobile } = useBreakpoints()
|
||||
// 手机上是整屏的:按返回键关掉它回到看板,不是退出看板
|
||||
useBackClose(show, isMobile)
|
||||
/**
|
||||
* 颜色走内联的 CSS 变量,不用 <style> 里的 v-bind():n-modal 把内容挂到 body 下面,
|
||||
* v-bind 的变量挂在组件根上,传不过去(弹层底色整个透明)
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Exercise } from "utils/types"
|
||||
import { getExercises, getLearnProgress, getTutorial, getTutorials } from "../api"
|
||||
import { parseLesson } from "./composables/useLessonParse"
|
||||
import { useLearnTrace } from "./composables/useLearnTrace"
|
||||
import { useBackClose } from "shared/composables/backClose"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import {
|
||||
@@ -52,6 +53,8 @@ const solvedIds = ref(new Set<number>())
|
||||
const loading = ref(true)
|
||||
const isEmpty = ref(false)
|
||||
const menuOpen = ref(false)
|
||||
// 手机上的目录是大半屏的底部抽屉:按返回键收起它,不是退出这一课
|
||||
useBackClose(menuOpen, () => !isDesktop.value)
|
||||
|
||||
const groups = computed(() => groupByProject(titles.value))
|
||||
const parsed = computed(() =>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
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,
|
||||
@@ -52,6 +53,8 @@ const show = computed({
|
||||
if (!value) open.value = null
|
||||
},
|
||||
})
|
||||
// 手机上是大半屏的底部抽屉(没有 to):按返回键收起它,不是退出题目
|
||||
useBackClose(show, () => !props.to)
|
||||
|
||||
// 内容和标题都按最后一次打开的来:关的时候有一段滑出去的动画,那时 open 已经是 null 了。
|
||||
// 关完 n-drawer 自己把内容卸掉
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getClassDetail } from "oj/api"
|
||||
import { classLabel } from "oj/submission/utils"
|
||||
import UserName from "shared/components/UserName.vue"
|
||||
import { useAIStream } from "shared/composables/aiStream"
|
||||
import { useBackClose } from "shared/composables/backClose"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import { parseTime } from "utils/functions"
|
||||
import { useRankPalette } from "../palette"
|
||||
@@ -24,6 +25,8 @@ const message = useMessage()
|
||||
const theme = useThemeVars()
|
||||
const palette = useRankPalette()
|
||||
const { isDesktop } = useBreakpoints()
|
||||
// 手机上占满一屏:按返回键关掉它回到排名
|
||||
useBackClose(show, () => !isDesktop.value)
|
||||
|
||||
const detail = ref<ClassDetail | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { DEFAULT_PERIOD } from "oj/statistics/period"
|
||||
import { useContestStore } from "oj/store/contest"
|
||||
import Pagination from "shared/components/Pagination.vue"
|
||||
import { useBackClose } from "shared/composables/backClose"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import { usePagination } from "shared/composables/pagination"
|
||||
import { useConfigStore } from "shared/store/config"
|
||||
@@ -71,6 +72,17 @@ const tone = useTone()
|
||||
const { isMobile, isDesktop } = useBreakpoints()
|
||||
|
||||
const inContest = computed(() => route.name === "contest submissions")
|
||||
|
||||
/**
|
||||
* 手机上点开的那一条,记在地址的 `?id=` 上:详情是从右边滑出的整屏,学生下意识按返回 ——
|
||||
* 返回键归路由管,退一步就是关掉详情、回到列表;刷新、转发链接也还停在这一条。
|
||||
* 列表在底下不卸载,滚到哪、上一条 / 下一条都还在
|
||||
*/
|
||||
const paneId = computed(() =>
|
||||
isMobile.value && typeof route.query.id === "string" ? route.query.id : "",
|
||||
)
|
||||
/** 从详情里点了「只看他」这类筛选:这一下要连 ?id= 一起换掉,见 leavePane() */
|
||||
let leavingPane = false
|
||||
const teacher = computed(() => userStore.isTeacherOrAbove)
|
||||
|
||||
// 一节课一个班要交一百来条,每页 10 条得翻十几页;30 条配上右栏的方向键正好
|
||||
@@ -85,7 +97,13 @@ const { query, clearQuery } = usePagination<SubmissionQuery>(
|
||||
// 原来写死成 "0",从别处带着 today=1 进来也不认
|
||||
today: useRouteQuery("today", "0").value,
|
||||
},
|
||||
{ defaultLimit: 30 },
|
||||
{
|
||||
defaultLimit: 30,
|
||||
// 手机上开着详情:翻页、换条件时地址里的 ?id= 带着走,也不多压历史(返回键是关详情)
|
||||
extraQuery: (): Record<string, string> =>
|
||||
paneId.value && !leavingPane ? { id: paneId.value } : {},
|
||||
replace: () => !!paneId.value && !leavingPane,
|
||||
},
|
||||
)
|
||||
|
||||
const flowMode = computed(() => !inContest.value && query.language === "Flowchart")
|
||||
@@ -294,6 +312,14 @@ function settleSelection() {
|
||||
}
|
||||
message.info("这一页没有可以查看的代码")
|
||||
}
|
||||
// 地址里带着 ?id= 的(手机上开着详情时刷新、别人发来的链接)就停在那一条
|
||||
const wanted = typeof route.query.id === "string" ? route.query.id : ""
|
||||
if (wanted && rows.value.some((row) => row.id === wanted)) {
|
||||
selectedId.value = wanted
|
||||
return
|
||||
}
|
||||
// 这一页上没有那一条:详情开不出来,把 ?id= 摘掉
|
||||
if (paneId.value) router.replace({ query: queryWithoutId() })
|
||||
if (rows.value.some((row) => row.id === selectedId.value)) return
|
||||
// 手机上没有右栏,不用预先选一条
|
||||
if (isMobile.value) {
|
||||
@@ -334,6 +360,7 @@ function move(step: 1 | -1) {
|
||||
}
|
||||
|
||||
const [todayPanel, toggleTodayPanel] = useToggle(false)
|
||||
useBackClose(todayPanel, isMobile)
|
||||
const statsPanel = ref(false)
|
||||
|
||||
// 只用 ↑ ↓(用户定的,← → 不要)
|
||||
@@ -360,13 +387,63 @@ onKeyStroke(["ArrowUp", "ArrowDown"], (e: KeyboardEvent) => {
|
||||
move(e.key === "ArrowUp" ? -1 : 1)
|
||||
})
|
||||
|
||||
// 手机上没有右栏:点一行从右边滑出整屏的详情
|
||||
const mobilePane = ref(false)
|
||||
// 手机上没有右栏:点一行从右边滑出整屏的详情,地址带上 ?id=(见 paneId)
|
||||
const mobilePane = computed({
|
||||
get: () => !!paneId.value,
|
||||
set: (open) => {
|
||||
if (!open) closePane()
|
||||
},
|
||||
})
|
||||
|
||||
function queryWithoutId() {
|
||||
const { id: _, ...rest } = route.query
|
||||
return rest
|
||||
}
|
||||
|
||||
function rowClicked(row: Row) {
|
||||
select(row.id)
|
||||
if (isMobile.value) mobilePane.value = true
|
||||
if (isMobile.value) router.push({ query: { ...route.query, id: row.id } })
|
||||
}
|
||||
|
||||
/**
|
||||
* 点 × 关:从列表点开的就退一步(和按返回键一样,不留一条多余的历史);
|
||||
* 直接带着 ?id= 进来的(刷新过、别人发的链接)后面没有列表可退,把地址换掉
|
||||
*/
|
||||
function closePane() {
|
||||
const back = history.state?.back
|
||||
if (typeof back === "string" && router.resolve(back).name === route.name) router.back()
|
||||
else router.replace({ query: queryWithoutId() })
|
||||
}
|
||||
|
||||
/**
|
||||
* 详情里点「只看他」「只看这道题」:先关详情(退回列表那一条),再改筛选。
|
||||
* 反过来的话筛选压的那条历史在详情后面,按返回又回到详情。
|
||||
* 等路由落地、再等分页把地址同步回 query 之后才改,不然改的值会被同步盖掉
|
||||
*/
|
||||
function leavePane(next: () => void) {
|
||||
if (!paneId.value) return next()
|
||||
leavingPane = true
|
||||
const off = router.afterEach(() => {
|
||||
off()
|
||||
nextTick(() => {
|
||||
leavingPane = false
|
||||
next()
|
||||
})
|
||||
})
|
||||
closePane()
|
||||
}
|
||||
|
||||
// 上一条 / 下一条换了人:地址里的 ?id= 跟着换(replace,返回键照样是关详情)
|
||||
watch(selectedId, (id) => {
|
||||
if (paneId.value && id && id !== paneId.value) {
|
||||
router.replace({ query: { ...route.query, id } })
|
||||
}
|
||||
})
|
||||
// 前进后退到另一条(或者刷新后列表回来之前地址就有了):选中跟着地址走
|
||||
watch(paneId, (id) => {
|
||||
if (id && rows.value.some((row) => row.id === id)) selectedId.value = id
|
||||
})
|
||||
|
||||
// ==================== 实时更新 ====================
|
||||
|
||||
/**
|
||||
@@ -1167,8 +1244,8 @@ function dayBreak(index: number) {
|
||||
:teacher="teacher"
|
||||
:position="null"
|
||||
narrow
|
||||
@filter-user="(name: string) => ((mobilePane = false), filterUser(name))"
|
||||
@filter-problem="(id: string) => ((mobilePane = false), filterProblem(id))"
|
||||
@filter-user="(name: string) => leavePane(() => filterUser(name))"
|
||||
@filter-problem="(id: string) => leavePane(() => filterProblem(id))"
|
||||
@open-problem="openProblem"
|
||||
@retry="retryFlowchart"
|
||||
/>
|
||||
@@ -1180,10 +1257,10 @@ function dayBreak(index: number) {
|
||||
:contest="inContest"
|
||||
:position="null"
|
||||
narrow
|
||||
@filter-user="(name: string) => ((mobilePane = false), filterUser(name))"
|
||||
@filter-class="(name: string) => ((mobilePane = false), filterClass(name))"
|
||||
@clear-class="() => ((mobilePane = false), (query.className = ''))"
|
||||
@filter-problem="(id: string) => ((mobilePane = false), filterProblem(id))"
|
||||
@filter-user="(name: string) => leavePane(() => filterUser(name))"
|
||||
@filter-class="(name: string) => leavePane(() => filterClass(name))"
|
||||
@clear-class="() => leavePane(() => (query.className = ''))"
|
||||
@filter-problem="(id: string) => leavePane(() => filterProblem(id))"
|
||||
@open-problem="openProblem"
|
||||
@rejudge="rejudge"
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user