diff --git a/src/utils/listPageState.ts b/src/utils/listPageState.ts index a4957c9..1dd7cf4 100644 --- a/src/utils/listPageState.ts +++ b/src/utils/listPageState.ts @@ -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 +} + +function normalizeFilters(raw: unknown): Record { + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {} + const out: Record = {} + for (const [k, v] of Object.entries(raw as Record)) { + 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 | { page?: unknown; per_page?: unknown }, +): Record { + 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 { - const q: Record = {} + const q: Record = { ...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, b: Record): 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, + current: Record, +): boolean { + return queryEqual(next, current) +} + +/** Prefer URL query, then sessionStorage, then defaults. Filters: storage first, URL overlays. */ export function resolveListPageState( key: string, query: Record | { 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, + defaultPerPage = 15, +): Record { + 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, + ) } diff --git a/src/views/manage/ManageProjectDetailView.vue b/src/views/manage/ManageProjectDetailView.vue index e574ef7..ce6dea9 100644 --- a/src/views/manage/ManageProjectDetailView.vue +++ b/src/views/manage/ManageProjectDetailView.vue @@ -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 = {} - 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), }) } diff --git a/src/views/manage/ManageProjectsView.vue b/src/views/manage/ManageProjectsView.vue index df039c0..05ad28e 100644 --- a/src/views/manage/ManageProjectsView.vue +++ b/src/views/manage/ManageProjectsView.vue @@ -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 { + const f: Record = {} + 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 { - 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 }, diff --git a/src/views/reviewer/ReviewerApplicationDetailView.vue b/src/views/reviewer/ReviewerApplicationDetailView.vue index 138ef1b..820c3d8 100644 --- a/src/views/reviewer/ReviewerApplicationDetailView.vue +++ b/src/views/reviewer/ReviewerApplicationDetailView.vue @@ -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 = {} - 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(null) diff --git a/src/views/reviewer/ReviewerProjectsView.vue b/src/views/reviewer/ReviewerProjectsView.vue index 77cbdde..1441fd0 100644 --- a/src/views/reviewer/ReviewerProjectsView.vue +++ b/src/views/reviewer/ReviewerProjectsView.vue @@ -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 { + const f: Record = {} + 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 { - 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',