/** * 全局分页单源(票 59,规范条文 11):翻页窗口计算只允许有这一份。 * * 自 `domain/equipmentDetailView.js` 的 `pagedRows` 升格而来;equipmentDetailView * 按原名保留导出兼容(`HISTORY_PAGE_SIZE`/`pagedRows`),旧调用与 t46 闸门不受影响。 * 判据全部是纯函数(可脱浏览器断言):页码边界、页码窗口、跳页夹回都在这里收口, * 组件(`PagerNav.vue` 与消费方)只画结论、不再各写一份翻页算术。 */ /** 每页固定 10 条(规范条文 1:PAGE_SIZE = 10)。 */ export const PAGE_SIZE = 10 /** 页码窗口里省略号占位(恒含首末页,中间按当前页收缩,规范条文 2)。 */ export const PAGE_ELLIPSIS = '…' /** * 分页视图(票 46 成例保留):在已过滤的序列上切窗口。 * 刻意**不排序、不改行次序**(票 10/11 立场),越界/脏页码夹回,空表也是 1 页。 * @param {Array} rows 待分页的行(已按分类过滤) * @param {number} page 1 起的页码 * @param {number} [pageSize] 每页条数(默认 10) */ export function pagedRows(rows, page, pageSize = PAGE_SIZE) { const list = Array.isArray(rows) ? rows : [] const size = Number.isFinite(pageSize) && pageSize > 0 ? pageSize : PAGE_SIZE const pageCount = list.length > 0 ? Math.ceil(list.length / size) : 1 const current = Math.min(Math.max(1, Math.trunc(Number(page) || 1)), pageCount) const start = (current - 1) * size return { rows: list.slice(start, start + size), page: current, pageCount, total: list.length, from: list.length ? start + 1 : 0, to: list.length ? Math.min(start + size, list.length) : 0, hasPrev: current > 1, hasNext: current < pageCount, } } /** * 页码直达窗口(规范条文 2): * · 总页数 ≤7 → 全列(1..Y,不带省略号); * · 总页数 >7 → 收缩为「1 … c−1 c c+1 … Y」,恒含首页、末页与至少一个省略号; * · 非法输入不抛错(脏页码/脏总数按最小安全形状回)。 * @param {number} currentPage 当前页(1 起) * @param {number} totalPages 总页数 * @returns {Array} 页码序列(数字或 PAGE_ELLIPSIS) */ export function pageWindow(currentPage, totalPages) { const total = Number.isFinite(totalPages) && totalPages >= 1 ? Math.trunc(totalPages) : 1 const raw = Number(currentPage) const current = Math.min(Math.max(1, Number.isFinite(raw) ? Math.trunc(raw) : 1), total) if (total <= 7) { return Array.from({ length: total }, (_, index) => index + 1) } const windowStart = Math.max(2, current - 1) const windowEnd = Math.min(total - 1, current + 1) const items = [1] if (windowStart > 2) items.push(PAGE_ELLIPSIS) for (let page = windowStart; page <= windowEnd; page += 1) items.push(page) if (windowEnd < total - 1) items.push(PAGE_ELLIPSIS) items.push(total) return items } /** * 跳页输入夹回(规范「跳页」节,2026-10-19 维护者追加): * 非数字、0、超出 pageCount 一律夹回 1..pageCount,不报错。 * @param {number|string} value 用户输入(按字符串或数字都认) * @param {number} pageCount 总页数 */ export function clampPage(value, pageCount) { const limit = Number.isFinite(pageCount) && pageCount >= 1 ? Math.trunc(pageCount) : 1 const num = Number(String(value === undefined || value === null ? '' : value).trim()) if (!Number.isFinite(num)) return 1 return Math.min(Math.max(1, Math.trunc(num)), limit) } /** * 页码状态的**归属键**(票 64):同一面板内按「块」分槽,两块不共用一个页码。 * * 键**只用块归属,不把筛选条件编进去**。把条件编进键(第一版做法)会让「切走再切回」 * 读到上次留下的旧槽位 —— 燃气看到第 3 页 → 切排水 → 切回燃气又回到第 3 页, * 而条文 6 要求「任何条件变化 → 页码回第 1 页」,切回来也是一次条件变化。 * 复位走 `resetListPages` 显式清槽(票 46/59「换设备清页码」的同一条成例): * 到第 1 页只有一条路,读侧的默认值不会被旧槽位顶掉。 * 脏输入收成空串段,不造出 `undefined` 这种会漂移的假键。 * * @param {string} blockKey 归属标识(渲染层给:面板名 + 块标题) */ export function listPageKey(blockKey) { const owner = typeof blockKey === 'string' ? blockKey : '' return `list@${owner}` } /** * 读某一块当前生效的页码(键不存在=这块还没翻过 ⇒ 第 1 页)。 * 脏值收成 1,绝不把 NaN 漏给 `pagedRows`(那会把控件画成「第 NaN 页」)。 * @param {object} store 渲染层持有的页码表(`{ [listPageKey]: 页码 }`) */ export function readListPage(store, blockKey) { const table = store && typeof store === 'object' ? store : {} const value = table[listPageKey(blockKey)] const num = Number(value) return Number.isFinite(num) && num >= 1 ? Math.trunc(num) : 1 } /** * 写页码(返回新表,不改入参——渲染层按 Vue 的替换语义整体赋值)。 * 与 `readListPage` 共用同一个键函数:读写键不一致就会造出「该复位的没复位、 * 不该复位的复位了」这类最难查的漂移,所以寻址也只允许有一份。 * 越界**不在这里夹**(条文 6:夹回是渲染窗口的事,统一由 `pagedRows` 兜底)。 */ export function writeListPage(store, blockKey, target) { const table = store && typeof store === 'object' ? store : {} return { ...table, [listPageKey(blockKey)]: Number(target) || 1 } } /** * 筛选条件变化时清掉这些块的页码槽(条文 5/6 的复位半边),返回新表。 * * 为什么是「清槽」而不是「把页码写成 1」:清槽与「这块还没翻过」是同一个状态, * 读侧只剩一条默认路径;写成 1 就有两条路都能到第 1 页,将来哪条先坏看不出来。 * 只清点名的块,别的面板/块的页码不受牵连。 * @param {object} store 当前页码表 * @param {Array} blockKeys 要复位的块归属键 */ export function resetListPages(store, blockKeys) { const table = store && typeof store === 'object' ? store : {} const keys = new Set((Array.isArray(blockKeys) ? blockKeys : []).map((blockKey) => listPageKey(blockKey))) if (!keys.size) return table const next = {} for (const [key, value] of Object.entries(table)) { if (!keys.has(key)) next[key] = value } return next }