master
lion 1 month ago
parent f931ad90cf
commit e4ce4ca173

@ -1,13 +1,33 @@
/** Persist list pagination across detail → back navigation. */
/** Persist list pagination and filters across detail → back navigation. */
export function parsePositiveInt(v: unknown, fallback: number): number {
const n = typeof v === 'string' ? Number(v) : typeof v === 'number' ? v : NaN
return Number.isInteger(n) && n > 0 ? n : fallback
}
const QUERY_RESERVED = new Set(['page', 'per_page'])
export type ListPageState = {
page: number
perPage: number
filters: Record<string, string>
}
function normalizeFilters(raw: unknown): Record<string, string> {
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}
const out: Record<string, string> = {}
for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
if (QUERY_RESERVED.has(k) || v == null) continue
const s = String(v).trim()
if (s !== '') out[k] = s
}
return out
}
export function filtersFromQuery(
query: Record<string, unknown> | { page?: unknown; per_page?: unknown },
): Record<string, string> {
return normalizeFilters(query)
}
export function listPageStateKey(scope: string, slug: string): string {
@ -17,7 +37,14 @@ export function listPageStateKey(scope: string, slug: string): string {
export function saveListPageState(key: string, state: ListPageState): void {
if (!key || typeof sessionStorage === 'undefined') return
try {
sessionStorage.setItem(key, JSON.stringify({ page: state.page, perPage: state.perPage }))
sessionStorage.setItem(
key,
JSON.stringify({
page: state.page,
perPage: state.perPage,
filters: normalizeFilters(state.filters),
}),
)
} catch {
/* ignore quota / private mode */
}
@ -28,10 +55,11 @@ export function loadListPageState(key: string): ListPageState | null {
try {
const raw = sessionStorage.getItem(key)
if (!raw) return null
const o = JSON.parse(raw) as { page?: unknown; perPage?: unknown }
const o = JSON.parse(raw) as { page?: unknown; perPage?: unknown; filters?: unknown }
return {
page: parsePositiveInt(o.page, 1),
perPage: parsePositiveInt(o.perPage, 15),
filters: normalizeFilters(o.filters),
}
} catch {
return null
@ -42,24 +70,61 @@ export function listPageStateToQuery(
state: ListPageState,
defaultPerPage = 15,
): Record<string, string> {
const q: Record<string, string> = {}
const q: Record<string, string> = { ...normalizeFilters(state.filters) }
if (state.page > 1) q.page = String(state.page)
if (state.perPage !== defaultPerPage) q.per_page = String(state.perPage)
return q
}
/** Prefer URL query, then sessionStorage, then defaults. */
function queryEqual(a: Record<string, string>, b: Record<string, unknown>): boolean {
const keys = new Set([...Object.keys(a), ...Object.keys(b)])
for (const k of keys) {
const av = a[k] ?? ''
const bv = typeof b[k] === 'string' ? b[k] : b[k] == null ? '' : String(b[k])
if (av !== bv) return false
}
return true
}
export function listQueryUnchanged(
next: Record<string, string>,
current: Record<string, unknown>,
): boolean {
return queryEqual(next, current)
}
/** Prefer URL query, then sessionStorage, then defaults. Filters: storage first, URL overlays. */
export function resolveListPageState(
key: string,
query: Record<string, unknown> | { page?: unknown; per_page?: unknown },
defaults: ListPageState = { page: 1, perPage: 15 },
defaults: ListPageState = { page: 1, perPage: 15, filters: {} },
): ListPageState {
const stored = loadListPageState(key)
const qPage = parsePositiveInt(query.page, 0)
const qPer = parsePositiveInt(query.per_page, 0)
if (qPage > 0) {
return { page: qPage, perPage: qPer > 0 ? qPer : defaults.perPage }
const qFilters = filtersFromQuery(query)
return {
page: qPage > 0 ? qPage : stored?.page ?? defaults.page,
perPage: qPer > 0 ? qPer : stored?.perPage ?? defaults.perPage,
filters: { ...defaults.filters, ...stored?.filters, ...qFilters },
}
const stored = loadListPageState(key)
if (stored) return stored
return { ...defaults }
}
/** Detail → list:优先当前详情 URL,缺的筛选从 sessionStorage 补。 */
export function listReturnQueryFromDetail(
storageKey: string,
query: Record<string, unknown>,
defaultPerPage = 15,
): Record<string, string> {
const qPage = parsePositiveInt(query.page, 0)
const qPer = parsePositiveInt(query.per_page, 0)
const stored = loadListPageState(storageKey)
return listPageStateToQuery(
{
page: qPage > 0 ? qPage : stored?.page ?? 1,
perPage: qPer > 0 ? qPer : stored?.perPage ?? defaultPerPage,
filters: { ...stored?.filters, ...filtersFromQuery(query) },
},
defaultPerPage,
)
}

@ -8,9 +8,7 @@ import type { AdminApplicationDetail, AdminApplicationFileRow } from '../../api/
import { isDownloadBusy } from '../../utils/downloadFeedback'
import {
listPageStateKey,
listPageStateToQuery,
loadListPageState,
parsePositiveInt,
listReturnQueryFromDetail,
} from '../../utils/listPageState'
const route = useRoute()
@ -76,22 +74,10 @@ async function load() {
function goBack() {
const slug = competitionSlug.value
const fromRoutePage = parsePositiveInt(route.query.page, 0)
const fromRoutePer = parsePositiveInt(route.query.per_page, 0)
let query: Record<string, string> = {}
if (fromRoutePage > 0) {
query = listPageStateToQuery({
page: fromRoutePage,
perPage: fromRoutePer > 0 ? fromRoutePer : 15,
})
} else {
const stored = loadListPageState(listPageStateKey('manage-projects', slug))
if (stored) query = listPageStateToQuery(stored)
}
void router.push({
name: 'manage-projects',
params: { slug },
query,
query: listReturnQueryFromDetail(listPageStateKey('manage-projects', slug), route.query),
})
}

@ -23,6 +23,7 @@ import type {
import {
listPageStateKey,
listPageStateToQuery,
listQueryUnchanged,
resolveListPageState,
saveListPageState,
} from '../../utils/listPageState'
@ -41,14 +42,37 @@ function listStateStorageKey(): string {
return listPageStateKey('manage-projects', competitionSlug.value)
}
function currentListFilters(): Record<string, string> {
const f: Record<string, string> = {}
if (filters.keyword.trim()) f.keyword = filters.keyword.trim()
if (filters.track) f.track = filters.track
if (filters.entry_group) f.entry_group = filters.entry_group
if (filters.review_result) f.review_result = filters.review_result
if (filters.review_eligible) f.review_eligible = filters.review_eligible
if (filters.signup_channel_id !== '') f.signup_channel_id = String(filters.signup_channel_id)
if (filters.public_source_channel_id !== '') {
f.public_source_channel_id = String(filters.public_source_channel_id)
}
if (sortDir.value !== 'desc') f.sort_dir = sortDir.value
return f
}
function listReturnQuery(): Record<string, string> {
return listPageStateToQuery({ page: pager.page, perPage: pager.perPage })
return listPageStateToQuery({
page: pager.page,
perPage: pager.perPage,
filters: currentListFilters(),
})
}
function persistListState(): void {
const key = listStateStorageKey()
if (!key.endsWith('.')) {
saveListPageState(key, { page: pager.page, perPage: pager.perPage })
saveListPageState(key, {
page: pager.page,
perPage: pager.perPage,
filters: currentListFilters(),
})
}
}
@ -56,13 +80,32 @@ function applyListState(): void {
const next = resolveListPageState(listStateStorageKey(), route.query)
pager.page = next.page
pager.perPage = next.perPage
filters.keyword = next.filters.keyword ?? ''
filters.track = next.filters.track ?? ''
if (next.filters.entry_group) filters.entry_group = next.filters.entry_group
filters.review_result =
next.filters.review_result === 'passed'
|| next.filters.review_result === 'rejected'
|| next.filters.review_result === 'pending'
? next.filters.review_result
: ''
if (next.filters.review_eligible === '1' || next.filters.review_eligible === '0') {
filters.review_eligible = next.filters.review_eligible
}
filters.signup_channel_id = next.filters.signup_channel_id
? Number(next.filters.signup_channel_id)
: ''
filters.public_source_channel_id = next.filters.public_source_channel_id
? Number(next.filters.public_source_channel_id)
: ''
if (next.filters.sort_dir === 'asc' || next.filters.sort_dir === 'desc') {
sortDir.value = next.filters.sort_dir
}
}
function syncListQueryToRoute(): void {
const next = listReturnQuery()
const curPage = typeof route.query.page === 'string' ? route.query.page : undefined
const curPer = typeof route.query.per_page === 'string' ? route.query.per_page : undefined
if (curPage === next.page && curPer === next.per_page) return
if (listQueryUnchanged(next, route.query)) return
void router.replace({
name: 'manage-projects',
params: { slug: competitionSlug.value },

@ -13,9 +13,7 @@ import { Modal } from 'bootstrap'
import ApplicationSignupDetailFields from '../../components/portal/ApplicationSignupDetailFields.vue'
import {
listPageStateKey,
listPageStateToQuery,
loadListPageState,
parsePositiveInt,
listReturnQueryFromDetail,
} from '../../utils/listPageState'
interface FileItem {
@ -315,19 +313,11 @@ function goBack() {
void router.push('/c')
return
}
const fromRoutePage = parsePositiveInt(route.query.page, 0)
const fromRoutePer = parsePositiveInt(route.query.per_page, 0)
let query: Record<string, string> = {}
if (fromRoutePage > 0) {
query = listPageStateToQuery({
page: fromRoutePage,
perPage: fromRoutePer > 0 ? fromRoutePer : 15,
})
} else {
const stored = loadListPageState(listPageStateKey('reviewer-projects', s))
if (stored) query = listPageStateToQuery(stored)
}
void router.push({ name: 'reviewer-projects', params: { slug: s }, query })
void router.push({
name: 'reviewer-projects',
params: { slug: s },
query: listReturnQueryFromDetail(listPageStateKey('reviewer-projects', s), route.query),
})
}
const downloadingFileId = ref<number | null>(null)

@ -10,6 +10,7 @@ import PortalPagination from '../../components/portal/PortalPagination.vue'
import {
listPageStateKey,
listPageStateToQuery,
listQueryUnchanged,
resolveListPageState,
saveListPageState,
} from '../../utils/listPageState'
@ -62,14 +63,30 @@ function listStateStorageKey(): string {
return listPageStateKey('reviewer-projects', slug.value)
}
function currentListFilters(): Record<string, string> {
const f: Record<string, string> = {}
if (filters.entry_group) f.entry_group = filters.entry_group
if (filters.keyword.trim()) f.keyword = filters.keyword.trim()
if (filters.review_status) f.review_status = filters.review_status
return f
}
function listReturnQuery(): Record<string, string> {
return listPageStateToQuery({ page: page.value, perPage: perPage.value })
return listPageStateToQuery({
page: page.value,
perPage: perPage.value,
filters: currentListFilters(),
})
}
function persistListState(): void {
const key = listStateStorageKey()
if (!key.endsWith('.')) {
saveListPageState(key, { page: page.value, perPage: perPage.value })
saveListPageState(key, {
page: page.value,
perPage: perPage.value,
filters: currentListFilters(),
})
}
}
@ -77,13 +94,17 @@ function applyListState(): void {
const next = resolveListPageState(listStateStorageKey(), route.query)
page.value = next.page
perPage.value = next.perPage
if (next.filters.entry_group) filters.entry_group = next.filters.entry_group
filters.keyword = next.filters.keyword ?? ''
filters.review_status =
next.filters.review_status === 'pending' || next.filters.review_status === 'reviewed'
? next.filters.review_status
: ''
}
function syncListQueryToRoute(): void {
const next = listReturnQuery()
const curPage = typeof route.query.page === 'string' ? route.query.page : undefined
const curPer = typeof route.query.per_page === 'string' ? route.query.per_page : undefined
if (curPage === next.page && curPer === next.per_page) return
if (listQueryUnchanged(next, route.query)) return
if (!slug.value) return
void router.replace({
name: 'reviewer-projects',

Loading…
Cancel
Save