|
|
|
|
@ -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,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|