- 提交列表:手机上点开一条,地址带上 ?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
@@ -0,0 +1,65 @@
|
||||
import type { MaybeRefOrGetter, Ref } from "vue"
|
||||
|
||||
/**
|
||||
* 手机上整屏(或大半屏)的弹框、抽屉:按返回键先关它,而不是退出整页。
|
||||
*
|
||||
* 学生在手机上看到一整屏的东西,下意识就按返回 —— 原来这一下直接退到了前一页(提交列表点开
|
||||
* 一条、再按返回,回到的是首页)。所以打开时往历史里压一条同地址的记录:
|
||||
* - 按返回:浏览器弹掉这一条,这里收到 popstate,把弹框关掉,页面原地不动;
|
||||
* - 点 × / 点遮罩关掉:自己 history.back() 把那条弹掉,不然要多按一次返回才退得出去。
|
||||
*
|
||||
* 压的是 history.state 的原样拷贝(vue-router 要的 position 等字段都在)。弹掉它时
|
||||
* **不能让 vue-router 看见**:它会当成一次同地址的跳转,route 对象换新,页面上盯着
|
||||
* route.query 的同步逻辑(usePagination)跟着跑一遍,又压一条历史,返回键就要多按一次。
|
||||
* 抢在它前面拦不住 —— window 上的 popstate 按注册顺序派发(捕获监听也不优先,实测过),
|
||||
* 路由的监听最早注册。所以记录压着的这段时间把 router.listening 整个关掉,弹掉以后再开:
|
||||
* 关着的只是「响应浏览器前进后退」这一项,弹框里点链接 router.push 照常走。
|
||||
*
|
||||
* 弹框开着时点了站内链接,路由守卫先把它拦下,等那条记录弹完再跳(back() 是异步的,
|
||||
* 先跳的话会把刚跳过去的那一条弹掉)。
|
||||
*/
|
||||
export function useBackClose(show: Ref<boolean>, enabled: MaybeRefOrGetter<boolean> = true) {
|
||||
const router = useRouter()
|
||||
/** 压了记录、还没弹掉(这段时间路由不听前进后退) */
|
||||
let pushed = false
|
||||
let afterPop: (() => void) | null = null
|
||||
|
||||
watch(show, (open) => {
|
||||
if (open && !pushed && toValue(enabled)) {
|
||||
history.pushState(history.state, "")
|
||||
pushed = true
|
||||
router.listening = false
|
||||
} else if (!open && pushed) {
|
||||
// 点 × 关的:自己弹掉那条,路由照旧关着,等下面收到 popstate 再开
|
||||
history.back()
|
||||
}
|
||||
})
|
||||
|
||||
useEventListener(window, "popstate", () => {
|
||||
if (!pushed) return
|
||||
pushed = false
|
||||
// 按了返回键的:只管关(watch 里看到 pushed 已经是 false,不会再 back)
|
||||
show.value = false
|
||||
router.listening = true
|
||||
const next = afterPop
|
||||
afterPop = null
|
||||
next?.()
|
||||
})
|
||||
|
||||
// 弹框里点了链接要跳走(自学目录里选一课):先关弹框、把压的那条退掉,再跳。
|
||||
// 直接跳的话新页面压在那条后面,回来时要多按一次返回,而且那一下路由不听(见上)
|
||||
const removeGuard = router.beforeEach((to) => {
|
||||
if (!pushed) return
|
||||
afterPop = () => void router.push(to.fullPath)
|
||||
show.value = false
|
||||
return false
|
||||
})
|
||||
|
||||
// 弹框开着组件就没了:路由一定要开回来,不然整站的返回键都失灵
|
||||
onBeforeUnmount(() => {
|
||||
removeGuard()
|
||||
if (!pushed) return
|
||||
pushed = false
|
||||
router.listening = true
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { reactive, watch } from "vue"
|
||||
import { nextTick, reactive, watch } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { filterEmptyValue } from "utils/functions"
|
||||
|
||||
@@ -15,6 +15,13 @@ export interface UsePaginationOptions {
|
||||
defaultPage?: number
|
||||
/** 当其他查询条件变化时是否重置页码 */
|
||||
resetPageOnChange?: boolean
|
||||
/**
|
||||
* 不归分页管、但同步地址时要带上的键。不给的话地址整个按 query 重写,别的键会被丢掉
|
||||
* (提交列表手机上点开一条是 `?id=`,翻一页详情就关了)
|
||||
*/
|
||||
extraQuery?: () => Record<string, string>
|
||||
/** 返回 true 时同步地址一律 replace,不压历史(提交列表手机上开着详情时翻页) */
|
||||
replace?: () => boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,7 +34,13 @@ export function usePagination<T extends Record<string, any>>(
|
||||
initialQuery: Omit<T, "page" | "limit"> = {} as Omit<T, "page" | "limit">,
|
||||
options: UsePaginationOptions = {},
|
||||
) {
|
||||
const { defaultLimit = 10, defaultPage = 1, resetPageOnChange = true } = options
|
||||
const {
|
||||
defaultLimit = 10,
|
||||
defaultPage = 1,
|
||||
resetPageOnChange = true,
|
||||
extraQuery,
|
||||
replace,
|
||||
} = options
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -41,8 +54,18 @@ export function usePagination<T extends Record<string, any>>(
|
||||
// 键是运行时按 initialQuery 枚举出来的,静态类型写不出来;写入统一走这一个口子
|
||||
const writable = query as Record<string, unknown>
|
||||
|
||||
/**
|
||||
* 正在从地址同步回来:这一拍里 query 的变化本来就是地址带来的,别再写回地址。
|
||||
* 写回去的话(浏览器后退到第 1 页,地址没写默认的 today=0,比一下「不一样」)会压一条
|
||||
* 新历史,把前进的那几条冲掉,返回键还得多按一次。
|
||||
* 页面自己盯着 route.query 补默认值的(提交列表)也在同一拍里,一起算
|
||||
*/
|
||||
let syncing = false
|
||||
|
||||
// 同步 URL 查询参数到本地状态
|
||||
function syncFromRoute() {
|
||||
syncing = true
|
||||
nextTick(() => (syncing = false))
|
||||
writable.page = parseInt(<string>route.query.page) || defaultPage
|
||||
writable.limit = parseInt(<string>route.query.limit) || defaultLimit
|
||||
|
||||
@@ -71,7 +94,7 @@ export function usePagination<T extends Record<string, any>>(
|
||||
|
||||
// 更新 URL
|
||||
function updateRoute() {
|
||||
const newQuery = filterEmptyValue(query)
|
||||
const newQuery = { ...filterEmptyValue(query), ...extraQuery?.() }
|
||||
// 和当前地址只差默认的 page / limit(后退到 /problem,同步完又补上 ?page=1&limit=30):
|
||||
// 是同一个页面,用 replace,别多压一条历史、让人得多按一次后退
|
||||
const normalized = (q: Record<string, unknown>) =>
|
||||
@@ -85,7 +108,8 @@ export function usePagination<T extends Record<string, any>>(
|
||||
.map(([key, value]) => [key, String(value)])
|
||||
.sort(),
|
||||
)
|
||||
const navigate = normalized(route.query) === normalized(newQuery) ? router.replace : router.push
|
||||
const navigate =
|
||||
replace?.() || normalized(route.query) === normalized(newQuery) ? router.replace : router.push
|
||||
navigate({
|
||||
path: route.path,
|
||||
query: newQuery,
|
||||
@@ -115,12 +139,18 @@ export function usePagination<T extends Record<string, any>>(
|
||||
}
|
||||
|
||||
// 监听页码变化,同步到 URL
|
||||
watch(() => query.page, updateRoute)
|
||||
watch(
|
||||
() => query.page,
|
||||
() => {
|
||||
if (!syncing) updateRoute()
|
||||
},
|
||||
)
|
||||
|
||||
// 监听每页条数变化,重置页码并同步到 URL
|
||||
watch(
|
||||
() => query.limit,
|
||||
() => {
|
||||
if (syncing) return
|
||||
if (resetPageOnChange) {
|
||||
resetPage()
|
||||
}
|
||||
@@ -134,6 +164,7 @@ export function usePagination<T extends Record<string, any>>(
|
||||
watch(
|
||||
() => otherQueryKeys.map((key) => query[key]),
|
||||
() => {
|
||||
if (syncing) return
|
||||
resetPage()
|
||||
updateRoute()
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user