PagerNav.vue 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. <!--
  2. 共享分页导航 PagerNav.vue(票 59,规范条文 2/3/4/9 + 跳页节)。
  3. 全仓 pager 控件只允许这一份:上一页|页码直达|下一页 + 输入框跳页 + 计数行。
  4. · 总页数 ≤1 → 整块不渲染(含跳页输入框);
  5. · 到边按钮 disabled 但保留占位(按钮消失会让计数行跳动,读起来像出错——票 46 既有语义);
  6. · 当前页标 aria-current="page" 且不可点;>7 页收缩「1 … c−1 c c+1 … Y」(pageWindow 单源);
  7. · 跳页输入非数字/0/超界一律夹回 1..pageCount(clampPage 单源),不报错、不发请求;
  8. · **翻页出口只有一个**:所有交互(前后页/页码/跳页)都 emit `page` 携带目标页码(1 起),
  9. 组件内不存第二套页码状态,父组件只需把一个 `gotoPage(panel, n)` 接上。
  10. 断言面:data-pager / data-page-prev / data-page-next / data-page-state 保留票 46 语义,
  11. 跳页新增 data-page-jump / data-page-jump-input。
  12. -->
  13. <script setup>
  14. import { computed, ref } from 'vue'
  15. import { clampPage, pageWindow } from '../domain/pagination.js'
  16. const props = defineProps({
  17. /** 当前页(1 起,由父组件的 pages 状态提供——本组件不存页码)。 */
  18. page: { type: Number, required: true },
  19. /** 总页数(pagedRows().pageCount)。 */
  20. pageCount: { type: Number, required: true },
  21. /** 过滤后总条数(pagedRows().total,不由当前页反推)。 */
  22. total: { type: Number, required: true },
  23. /** 列表名(aria-label「X分页」与 data-list 归属用)。 */
  24. listName: { type: String, required: true },
  25. /**
  26. * 计数行开关(票 60 批次二,维护者当次裁定 2)。默认 `true`=票 46/59 既有形态不变;
  27. * 传 `false` 的场合:清单上方**已经**由领域结论给出权威总数(点位摘要的设备合计 KPI、
  28. * 关联设备清单的「总数 X 台(已上图 Y)」)。那里再画一条「共 N 条」就是并排的第二个
  29. * 总数口径——窗口条数与快照总数本来就是两个数(票 42 的「总数 ≠ 清单行数」),
  30. * 摆在一起读必定被当成数据不一致。翻页能力(前后页/页码/跳页)不受它影响。
  31. */
  32. showCount: { type: Boolean, default: true },
  33. })
  34. const emit = defineEmits(['page'])
  35. /** 一页都不到 → 整块不渲染(条文 4)。 */
  36. const visible = computed(() => props.pageCount > 1)
  37. /** 页码直达窗口(pageWindow 单源,>7 页收缩)。 */
  38. const pageItems = computed(() => pageWindow(props.page, props.pageCount))
  39. /** 前后页/页码/跳页共用同一出口:目标页码(越界仍由 domain pagedRows 夹回兜底)。 */
  40. function go(target) {
  41. emit('page', target)
  42. }
  43. /** 跳页:夹回合法范围后走同一出口;与当前页同值则什么都不做(不发请求、不清列表)。 */
  44. const jumpValue = ref('')
  45. function jump() {
  46. const target = clampPage(jumpValue.value, props.pageCount)
  47. jumpValue.value = ''
  48. if (target === props.page) return
  49. go(target)
  50. }
  51. </script>
  52. <template>
  53. <div v-if="visible" class="pager-nav" role="group" :aria-label="`${listName}分页`" data-pager>
  54. <button type="button" class="pager-nav__btn" :disabled="page <= 1" data-page-prev @click="go(page - 1)">上一页</button>
  55. <span class="pager-nav__pages" data-page-numbers>
  56. <template v-for="(item, index) in pageItems" :key="`${item}-${index}`">
  57. <span v-if="item === '…'" class="pager-nav__ellipsis" data-page-ellipsis aria-hidden="true">…</span>
  58. <button
  59. v-else
  60. type="button"
  61. class="pager-nav__btn pager-nav__btn--num"
  62. :class="{ 'is-current': item === page }"
  63. :aria-current="item === page ? 'page' : undefined"
  64. :aria-disabled="item === page ? 'true' : undefined"
  65. :data-page-number="item"
  66. @click="item !== page && go(item)"
  67. >{{ item }}</button>
  68. </template>
  69. </span>
  70. <button type="button" class="pager-nav__btn" :disabled="page >= pageCount" data-page-next @click="go(page + 1)">下一页</button>
  71. <span class="pager-nav__jump" data-page-jump>
  72. <input
  73. v-model="jumpValue"
  74. type="text"
  75. inputmode="numeric"
  76. class="pager-nav__input"
  77. :placeholder="String(page)"
  78. aria-label="跳转到指定页"
  79. data-page-jump-input
  80. @keydown.enter.prevent="jump"
  81. />
  82. <button type="button" class="pager-nav__btn pager-nav__btn--jump" data-page-jump-btn @click="jump">跳转</button>
  83. </span>
  84. <span v-if="showCount" class="pager-nav__state" data-page-state>第 {{ page }}/{{ pageCount }} 页 · 共 {{ total }} 条</span>
  85. </div>
  86. </template>
  87. <style scoped>
  88. /* 与票 46 的 equipment-pager 同一套视觉语言(大屏暗底、胶囊按钮、到边置灰保占位)。 */
  89. .pager-nav {
  90. display: flex;
  91. flex-wrap: wrap;
  92. align-items: center;
  93. gap: var(--space-2, 8px);
  94. margin-top: calc(2 * var(--u, 1px));
  95. }
  96. .pager-nav__pages {
  97. display: flex;
  98. align-items: center;
  99. gap: calc(2 * var(--u, 1px));
  100. }
  101. .pager-nav__btn {
  102. padding: 1px calc(8 * var(--u, 1px));
  103. border: 1px solid rgba(1, 171, 234, 0.42);
  104. background: rgba(0, 20, 72, 0.6);
  105. color: var(--text-2);
  106. font: inherit;
  107. font-size: var(--fs-caption, 10.5px);
  108. cursor: pointer;
  109. }
  110. .pager-nav__btn:hover:not(:disabled):not(.is-current),
  111. .pager-nav__btn:focus-visible:not(:disabled):not(.is-current) {
  112. color: var(--text-strong);
  113. border-color: rgba(1, 171, 234, 0.75);
  114. outline: none;
  115. }
  116. /* 到边置灰但保留占位(计数行不跳)。 */
  117. .pager-nav__btn:disabled {
  118. opacity: 0.4;
  119. cursor: default;
  120. }
  121. .pager-nav__btn--num {
  122. padding: 1px calc(6 * var(--u, 1px));
  123. font-family: var(--num, monospace);
  124. }
  125. .pager-nav__btn--num.is-current {
  126. border-color: rgba(0, 184, 255, 0.85);
  127. background: linear-gradient(180deg, rgba(1, 171, 234, 0.85), rgba(0, 82, 168, 0.85));
  128. color: var(--text-strong);
  129. cursor: default;
  130. }
  131. .pager-nav__ellipsis {
  132. color: var(--muted, #5680b0);
  133. user-select: none;
  134. }
  135. .pager-nav__input {
  136. width: calc(52 * var(--u, 1px));
  137. padding: 1px calc(5 * var(--u, 1px));
  138. border: 1px solid rgba(1, 171, 234, 0.42);
  139. background: rgba(0, 20, 72, 0.6);
  140. color: var(--text, #d7ecff);
  141. font-family: var(--num, monospace);
  142. font-size: var(--fs-caption, 10.5px);
  143. text-align: center;
  144. }
  145. .pager-nav__input::placeholder {
  146. color: var(--muted, #5680b0);
  147. }
  148. .pager-nav__input:hover,
  149. .pager-nav__input:focus-visible {
  150. border-color: rgba(1, 171, 234, 0.75);
  151. outline: none;
  152. }
  153. .pager-nav__state {
  154. color: var(--muted, #5680b0);
  155. font-family: var(--num, monospace);
  156. font-size: var(--fs-caption, 10.5px);
  157. letter-spacing: 0.04em;
  158. }
  159. </style>