From 8ae2785aaa915d3ab1a4fbd802f8c2ebd6399bd0 Mon Sep 17 00:00:00 2001 From: lion <120344285@qq.com> Date: Wed, 29 Jul 2026 09:59:15 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=94=E5=9B=9E=E5=88=97=E8=A1=A8=E4=BF=9D?= =?UTF-8?q?=E7=95=99=E5=8E=9F=E5=88=86=E9=A1=B5=E9=A1=B5=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/listPageState.ts | 65 +++++++++++++++++++ src/views/manage/ManageProjectDetailView.vue | 24 +++++-- src/views/manage/ManageProjectsView.vue | 47 +++++++++----- .../ReviewerApplicationDetailView.vue | 21 ++++-- src/views/reviewer/ReviewerProjectsView.vue | 41 ++++++++---- 5 files changed, 161 insertions(+), 37 deletions(-) create mode 100644 src/utils/listPageState.ts diff --git a/src/utils/listPageState.ts b/src/utils/listPageState.ts new file mode 100644 index 0000000..a4957c9 --- /dev/null +++ b/src/utils/listPageState.ts @@ -0,0 +1,65 @@ +/** Persist list pagination 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 +} + +export type ListPageState = { + page: number + perPage: number +} + +export function listPageStateKey(scope: string, slug: string): string { + return `cxxfds.list.${scope}.${slug}` +} + +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 })) + } catch { + /* ignore quota / private mode */ + } +} + +export function loadListPageState(key: string): ListPageState | null { + if (!key || typeof sessionStorage === 'undefined') return null + try { + const raw = sessionStorage.getItem(key) + if (!raw) return null + const o = JSON.parse(raw) as { page?: unknown; perPage?: unknown } + return { + page: parsePositiveInt(o.page, 1), + perPage: parsePositiveInt(o.perPage, 15), + } + } catch { + return null + } +} + +export function listPageStateToQuery( + state: ListPageState, + defaultPerPage = 15, +): Record { + const q: Record = {} + 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. */ +export function resolveListPageState( + key: string, + query: Record | { page?: unknown; per_page?: unknown }, + defaults: ListPageState = { page: 1, perPage: 15 }, +): ListPageState { + 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 stored = loadListPageState(key) + if (stored) return stored + return { ...defaults } +} diff --git a/src/views/manage/ManageProjectDetailView.vue b/src/views/manage/ManageProjectDetailView.vue index df3b57e..4ca34f6 100644 --- a/src/views/manage/ManageProjectDetailView.vue +++ b/src/views/manage/ManageProjectDetailView.vue @@ -5,6 +5,12 @@ import { ElMessage } from 'element-plus' import ApplicationSignupDetailFields from '../../components/portal/ApplicationSignupDetailFields.vue' import { downloadAdminApplicationFile, getAdminApplication } from '../../api/admin/applications' import type { AdminApplicationDetail, AdminApplicationFileRow } from '../../api/admin/types' +import { + listPageStateKey, + listPageStateToQuery, + loadListPageState, + parsePositiveInt, +} from '../../utils/listPageState' const route = useRoute() const router = useRouter() @@ -97,14 +103,22 @@ async function load() { } function goBack() { - const query: Record = {} - if (typeof route.query.page === 'string' && route.query.page) query.page = route.query.page - if (typeof route.query.per_page === 'string' && route.query.per_page) { - query.per_page = route.query.per_page + 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: competitionSlug.value }, + params: { slug }, query, }) } diff --git a/src/views/manage/ManageProjectsView.vue b/src/views/manage/ManageProjectsView.vue index 0dc0380..85779e5 100644 --- a/src/views/manage/ManageProjectsView.vue +++ b/src/views/manage/ManageProjectsView.vue @@ -20,6 +20,12 @@ import type { PublicSourceChannelRow, SignupChannelRow, } from '../../api/admin/types' +import { + listPageStateKey, + listPageStateToQuery, + resolveListPageState, + saveListPageState, +} from '../../utils/listPageState' const route = useRoute() const router = useRouter() @@ -30,31 +36,37 @@ const loading = ref(false) const rows = ref([]) const pager = reactive({ page: 1, perPage: 15, total: 0 }) -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 +function listStateStorageKey(): string { + return listPageStateKey('manage-projects', competitionSlug.value) } function listReturnQuery(): Record { - const q: Record = {} - if (pager.page > 1) q.page = String(pager.page) - if (pager.perPage !== 15) q.per_page = String(pager.perPage) - return q + return listPageStateToQuery({ page: pager.page, perPage: pager.perPage }) } -function applyListQueryFromRoute(): void { - pager.page = parsePositiveInt(route.query.page, 1) - pager.perPage = parsePositiveInt(route.query.per_page, 15) +function persistListState(): void { + const key = listStateStorageKey() + if (!key.endsWith('.')) { + saveListPageState(key, { page: pager.page, perPage: pager.perPage }) + } +} + +function applyListState(): void { + const next = resolveListPageState(listStateStorageKey(), route.query) + pager.page = next.page + pager.perPage = next.perPage } 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 - const nextPage = next.page - const nextPer = next.per_page - if (curPage === nextPage && curPer === nextPer) return - void router.replace({ query: next }) + if (curPage === next.page && curPer === next.per_page) return + void router.replace({ + name: 'manage-projects', + params: { slug: competitionSlug.value }, + query: next, + }) } const tracks = ref([]) const signupChannels = ref([]) @@ -233,6 +245,7 @@ async function refresh(page = pager.page) { if (!cid) return loading.value = true pager.page = page + persistListState() syncListQueryToRoute() try { const res = await listAdminApplications(cid, buildListParams(page)) @@ -240,6 +253,7 @@ async function refresh(page = pager.page) { pager.total = res.meta.total if (res.meta.current_page && res.meta.current_page !== pager.page) { pager.page = res.meta.current_page + persistListState() syncListQueryToRoute() } } finally { @@ -316,9 +330,10 @@ async function confirmBatch(result: 'passed' | 'rejected') { } function openDetail(row: AdminApplicationRow) { + persistListState() void router.push({ name: 'manage-project-detail', - params: { slug: competitionSlug.value, id: row.id }, + params: { slug: competitionSlug.value, id: String(row.id) }, query: listReturnQuery(), }) } @@ -326,7 +341,7 @@ function openDetail(row: AdminApplicationRow) { watch(competitionId, async (cid) => { if (!cid) return await loadFilters() - applyListQueryFromRoute() + applyListState() await refresh(pager.page) }, { immediate: true }) diff --git a/src/views/reviewer/ReviewerApplicationDetailView.vue b/src/views/reviewer/ReviewerApplicationDetailView.vue index fc0d61c..6a0f16c 100644 --- a/src/views/reviewer/ReviewerApplicationDetailView.vue +++ b/src/views/reviewer/ReviewerApplicationDetailView.vue @@ -10,6 +10,12 @@ import { import { ElMessage } from 'element-plus' import { Modal } from 'bootstrap' import ApplicationSignupDetailFields from '../../components/portal/ApplicationSignupDetailFields.vue' +import { + listPageStateKey, + listPageStateToQuery, + loadListPageState, + parsePositiveInt, +} from '../../utils/listPageState' interface FileItem { id: number @@ -329,10 +335,17 @@ function goBack() { void router.push('/c') return } - const query: Record = {} - if (typeof route.query.page === 'string' && route.query.page) query.page = route.query.page - if (typeof route.query.per_page === 'string' && route.query.per_page) { - query.per_page = route.query.per_page + 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 }) } diff --git a/src/views/reviewer/ReviewerProjectsView.vue b/src/views/reviewer/ReviewerProjectsView.vue index 2081159..8933819 100644 --- a/src/views/reviewer/ReviewerProjectsView.vue +++ b/src/views/reviewer/ReviewerProjectsView.vue @@ -7,6 +7,12 @@ import { REVIEW_APPLICATIONS_INDEX_PATH, } from '../../config/api' import PortalPagination from '../../components/portal/PortalPagination.vue' +import { + listPageStateKey, + listPageStateToQuery, + resolveListPageState, + saveListPageState, +} from '../../utils/listPageState' export interface ReviewApplicationRow { id: number @@ -48,21 +54,25 @@ const meta = ref({ const page = ref(1) const perPage = ref(15) -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 +function listStateStorageKey(): string { + return listPageStateKey('reviewer-projects', slug.value) } function listReturnQuery(): Record { - const q: Record = {} - if (page.value > 1) q.page = String(page.value) - if (perPage.value !== 15) q.per_page = String(perPage.value) - return q + return listPageStateToQuery({ page: page.value, perPage: perPage.value }) +} + +function persistListState(): void { + const key = listStateStorageKey() + if (!key.endsWith('.')) { + saveListPageState(key, { page: page.value, perPage: perPage.value }) + } } -function applyListQueryFromRoute(): void { - page.value = parsePositiveInt(route.query.page, 1) - perPage.value = parsePositiveInt(route.query.per_page, 15) +function applyListState(): void { + const next = resolveListPageState(listStateStorageKey(), route.query) + page.value = next.page + perPage.value = next.perPage } function syncListQueryToRoute(): void { @@ -70,7 +80,12 @@ function syncListQueryToRoute(): void { 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 - void router.replace({ query: next }) + if (!slug.value) return + void router.replace({ + name: 'reviewer-projects', + params: { slug: slug.value }, + query: next, + }) } function buildListParams(): URLSearchParams { @@ -159,9 +174,10 @@ function resetFilters() { refresh(1) } -applyListQueryFromRoute() +applyListState() watch([slug, page, perPage], () => { + persistListState() syncListQueryToRoute() void fetchList() }, { immediate: true }) @@ -266,6 +282,7 @@ function actionLabel(row: ReviewApplicationRow): string { params: { slug, id: String(row.id) }, query: listReturnQuery(), }" + @click="persistListState" > {{ actionLabel(row) }}