pagination.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. /**
  2. * 全局分页单源(票 59,规范条文 11):翻页窗口计算只允许有这一份。
  3. *
  4. * 自 `domain/equipmentDetailView.js` 的 `pagedRows` 升格而来;equipmentDetailView
  5. * 按原名保留导出兼容(`HISTORY_PAGE_SIZE`/`pagedRows`),旧调用与 t46 闸门不受影响。
  6. * 判据全部是纯函数(可脱浏览器断言):页码边界、页码窗口、跳页夹回都在这里收口,
  7. * 组件(`PagerNav.vue` 与消费方)只画结论、不再各写一份翻页算术。
  8. */
  9. /** 每页固定 10 条(规范条文 1:PAGE_SIZE = 10)。 */
  10. export const PAGE_SIZE = 10
  11. /** 页码窗口里省略号占位(恒含首末页,中间按当前页收缩,规范条文 2)。 */
  12. export const PAGE_ELLIPSIS = '…'
  13. /**
  14. * 分页视图(票 46 成例保留):在已过滤的序列上切窗口。
  15. * 刻意**不排序、不改行次序**(票 10/11 立场),越界/脏页码夹回,空表也是 1 页。
  16. * @param {Array} rows 待分页的行(已按分类过滤)
  17. * @param {number} page 1 起的页码
  18. * @param {number} [pageSize] 每页条数(默认 10)
  19. */
  20. export function pagedRows(rows, page, pageSize = PAGE_SIZE) {
  21. const list = Array.isArray(rows) ? rows : []
  22. const size = Number.isFinite(pageSize) && pageSize > 0 ? pageSize : PAGE_SIZE
  23. const pageCount = list.length > 0 ? Math.ceil(list.length / size) : 1
  24. const current = Math.min(Math.max(1, Math.trunc(Number(page) || 1)), pageCount)
  25. const start = (current - 1) * size
  26. return {
  27. rows: list.slice(start, start + size),
  28. page: current,
  29. pageCount,
  30. total: list.length,
  31. from: list.length ? start + 1 : 0,
  32. to: list.length ? Math.min(start + size, list.length) : 0,
  33. hasPrev: current > 1,
  34. hasNext: current < pageCount,
  35. }
  36. }
  37. /**
  38. * 页码直达窗口(规范条文 2):
  39. * · 总页数 ≤7 → 全列(1..Y,不带省略号);
  40. * · 总页数 >7 → 收缩为「1 … c−1 c c+1 … Y」,恒含首页、末页与至少一个省略号;
  41. * · 非法输入不抛错(脏页码/脏总数按最小安全形状回)。
  42. * @param {number} currentPage 当前页(1 起)
  43. * @param {number} totalPages 总页数
  44. * @returns {Array<number|string>} 页码序列(数字或 PAGE_ELLIPSIS)
  45. */
  46. export function pageWindow(currentPage, totalPages) {
  47. const total = Number.isFinite(totalPages) && totalPages >= 1 ? Math.trunc(totalPages) : 1
  48. const raw = Number(currentPage)
  49. const current = Math.min(Math.max(1, Number.isFinite(raw) ? Math.trunc(raw) : 1), total)
  50. if (total <= 7) {
  51. return Array.from({ length: total }, (_, index) => index + 1)
  52. }
  53. const windowStart = Math.max(2, current - 1)
  54. const windowEnd = Math.min(total - 1, current + 1)
  55. const items = [1]
  56. if (windowStart > 2) items.push(PAGE_ELLIPSIS)
  57. for (let page = windowStart; page <= windowEnd; page += 1) items.push(page)
  58. if (windowEnd < total - 1) items.push(PAGE_ELLIPSIS)
  59. items.push(total)
  60. return items
  61. }
  62. /**
  63. * 跳页输入夹回(规范「跳页」节,2026-10-19 维护者追加):
  64. * 非数字、0、超出 pageCount 一律夹回 1..pageCount,不报错。
  65. * @param {number|string} value 用户输入(按字符串或数字都认)
  66. * @param {number} pageCount 总页数
  67. */
  68. export function clampPage(value, pageCount) {
  69. const limit = Number.isFinite(pageCount) && pageCount >= 1 ? Math.trunc(pageCount) : 1
  70. const num = Number(String(value === undefined || value === null ? '' : value).trim())
  71. if (!Number.isFinite(num)) return 1
  72. return Math.min(Math.max(1, Math.trunc(num)), limit)
  73. }
  74. /**
  75. * 页码状态的**归属键**(票 64):同一面板内按「块」分槽,两块不共用一个页码。
  76. *
  77. * 键**只用块归属,不把筛选条件编进去**。把条件编进键(第一版做法)会让「切走再切回」
  78. * 读到上次留下的旧槽位 —— 燃气看到第 3 页 → 切排水 → 切回燃气又回到第 3 页,
  79. * 而条文 6 要求「任何条件变化 → 页码回第 1 页」,切回来也是一次条件变化。
  80. * 复位走 `resetListPages` 显式清槽(票 46/59「换设备清页码」的同一条成例):
  81. * 到第 1 页只有一条路,读侧的默认值不会被旧槽位顶掉。
  82. * 脏输入收成空串段,不造出 `undefined` 这种会漂移的假键。
  83. *
  84. * @param {string} blockKey 归属标识(渲染层给:面板名 + 块标题)
  85. */
  86. export function listPageKey(blockKey) {
  87. const owner = typeof blockKey === 'string' ? blockKey : ''
  88. return `list@${owner}`
  89. }
  90. /**
  91. * 读某一块当前生效的页码(键不存在=这块还没翻过 ⇒ 第 1 页)。
  92. * 脏值收成 1,绝不把 NaN 漏给 `pagedRows`(那会把控件画成「第 NaN 页」)。
  93. * @param {object} store 渲染层持有的页码表(`{ [listPageKey]: 页码 }`)
  94. */
  95. export function readListPage(store, blockKey) {
  96. const table = store && typeof store === 'object' ? store : {}
  97. const value = table[listPageKey(blockKey)]
  98. const num = Number(value)
  99. return Number.isFinite(num) && num >= 1 ? Math.trunc(num) : 1
  100. }
  101. /**
  102. * 写页码(返回新表,不改入参——渲染层按 Vue 的替换语义整体赋值)。
  103. * 与 `readListPage` 共用同一个键函数:读写键不一致就会造出「该复位的没复位、
  104. * 不该复位的复位了」这类最难查的漂移,所以寻址也只允许有一份。
  105. * 越界**不在这里夹**(条文 6:夹回是渲染窗口的事,统一由 `pagedRows` 兜底)。
  106. */
  107. export function writeListPage(store, blockKey, target) {
  108. const table = store && typeof store === 'object' ? store : {}
  109. return { ...table, [listPageKey(blockKey)]: Number(target) || 1 }
  110. }
  111. /**
  112. * 筛选条件变化时清掉这些块的页码槽(条文 5/6 的复位半边),返回新表。
  113. *
  114. * 为什么是「清槽」而不是「把页码写成 1」:清槽与「这块还没翻过」是同一个状态,
  115. * 读侧只剩一条默认路径;写成 1 就有两条路都能到第 1 页,将来哪条先坏看不出来。
  116. * 只清点名的块,别的面板/块的页码不受牵连。
  117. * @param {object} store 当前页码表
  118. * @param {Array<string>} blockKeys 要复位的块归属键
  119. */
  120. export function resetListPages(store, blockKeys) {
  121. const table = store && typeof store === 'object' ? store : {}
  122. const keys = new Set((Array.isArray(blockKeys) ? blockKeys : []).map((blockKey) => listPageKey(blockKey)))
  123. if (!keys.size) return table
  124. const next = {}
  125. for (const [key, value] of Object.entries(table)) {
  126. if (!keys.has(key)) next[key] = value
  127. }
  128. return next
  129. }