| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183 |
- <!--
- 共享分页导航 PagerNav.vue(票 59,规范条文 2/3/4/9 + 跳页节)。
- 全仓 pager 控件只允许这一份:上一页|页码直达|下一页 + 输入框跳页 + 计数行。
- · 总页数 ≤1 → 整块不渲染(含跳页输入框);
- · 到边按钮 disabled 但保留占位(按钮消失会让计数行跳动,读起来像出错——票 46 既有语义);
- · 当前页标 aria-current="page" 且不可点;>7 页收缩「1 … c−1 c c+1 … Y」(pageWindow 单源);
- · 跳页输入非数字/0/超界一律夹回 1..pageCount(clampPage 单源),不报错、不发请求;
- · **翻页出口只有一个**:所有交互(前后页/页码/跳页)都 emit `page` 携带目标页码(1 起),
- 组件内不存第二套页码状态,父组件只需把一个 `gotoPage(panel, n)` 接上。
- 断言面:data-pager / data-page-prev / data-page-next / data-page-state 保留票 46 语义,
- 跳页新增 data-page-jump / data-page-jump-input。
- -->
- <script setup>
- import { computed, ref } from 'vue'
- import { clampPage, pageWindow } from '../domain/pagination.js'
- const props = defineProps({
- /** 当前页(1 起,由父组件的 pages 状态提供——本组件不存页码)。 */
- page: { type: Number, required: true },
- /** 总页数(pagedRows().pageCount)。 */
- pageCount: { type: Number, required: true },
- /** 过滤后总条数(pagedRows().total,不由当前页反推)。 */
- total: { type: Number, required: true },
- /** 列表名(aria-label「X分页」与 data-list 归属用)。 */
- listName: { type: String, required: true },
- /**
- * 计数行开关(票 60 批次二,维护者当次裁定 2)。默认 `true`=票 46/59 既有形态不变;
- * 传 `false` 的场合:清单上方**已经**由领域结论给出权威总数(点位摘要的设备合计 KPI、
- * 关联设备清单的「总数 X 台(已上图 Y)」)。那里再画一条「共 N 条」就是并排的第二个
- * 总数口径——窗口条数与快照总数本来就是两个数(票 42 的「总数 ≠ 清单行数」),
- * 摆在一起读必定被当成数据不一致。翻页能力(前后页/页码/跳页)不受它影响。
- */
- showCount: { type: Boolean, default: true },
- })
- const emit = defineEmits(['page'])
- /** 一页都不到 → 整块不渲染(条文 4)。 */
- const visible = computed(() => props.pageCount > 1)
- /** 页码直达窗口(pageWindow 单源,>7 页收缩)。 */
- const pageItems = computed(() => pageWindow(props.page, props.pageCount))
- /** 前后页/页码/跳页共用同一出口:目标页码(越界仍由 domain pagedRows 夹回兜底)。 */
- function go(target) {
- emit('page', target)
- }
- /** 跳页:夹回合法范围后走同一出口;与当前页同值则什么都不做(不发请求、不清列表)。 */
- const jumpValue = ref('')
- function jump() {
- const target = clampPage(jumpValue.value, props.pageCount)
- jumpValue.value = ''
- if (target === props.page) return
- go(target)
- }
- </script>
- <template>
- <div v-if="visible" class="pager-nav" role="group" :aria-label="`${listName}分页`" data-pager>
- <button type="button" class="pager-nav__btn" :disabled="page <= 1" data-page-prev @click="go(page - 1)">上一页</button>
- <span class="pager-nav__pages" data-page-numbers>
- <template v-for="(item, index) in pageItems" :key="`${item}-${index}`">
- <span v-if="item === '…'" class="pager-nav__ellipsis" data-page-ellipsis aria-hidden="true">…</span>
- <button
- v-else
- type="button"
- class="pager-nav__btn pager-nav__btn--num"
- :class="{ 'is-current': item === page }"
- :aria-current="item === page ? 'page' : undefined"
- :aria-disabled="item === page ? 'true' : undefined"
- :data-page-number="item"
- @click="item !== page && go(item)"
- >{{ item }}</button>
- </template>
- </span>
- <button type="button" class="pager-nav__btn" :disabled="page >= pageCount" data-page-next @click="go(page + 1)">下一页</button>
- <span class="pager-nav__jump" data-page-jump>
- <input
- v-model="jumpValue"
- type="text"
- inputmode="numeric"
- class="pager-nav__input"
- :placeholder="String(page)"
- aria-label="跳转到指定页"
- data-page-jump-input
- @keydown.enter.prevent="jump"
- />
- <button type="button" class="pager-nav__btn pager-nav__btn--jump" data-page-jump-btn @click="jump">跳转</button>
- </span>
- <span v-if="showCount" class="pager-nav__state" data-page-state>第 {{ page }}/{{ pageCount }} 页 · 共 {{ total }} 条</span>
- </div>
- </template>
- <style scoped>
- /* 与票 46 的 equipment-pager 同一套视觉语言(大屏暗底、胶囊按钮、到边置灰保占位)。 */
- .pager-nav {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: var(--space-2, 8px);
- margin-top: calc(2 * var(--u, 1px));
- }
- .pager-nav__pages {
- display: flex;
- align-items: center;
- gap: calc(2 * var(--u, 1px));
- }
- .pager-nav__btn {
- padding: 1px calc(8 * var(--u, 1px));
- border: 1px solid rgba(1, 171, 234, 0.42);
- background: rgba(0, 20, 72, 0.6);
- color: var(--text-2);
- font: inherit;
- font-size: var(--fs-caption, 10.5px);
- cursor: pointer;
- }
- .pager-nav__btn:hover:not(:disabled):not(.is-current),
- .pager-nav__btn:focus-visible:not(:disabled):not(.is-current) {
- color: var(--text-strong);
- border-color: rgba(1, 171, 234, 0.75);
- outline: none;
- }
- /* 到边置灰但保留占位(计数行不跳)。 */
- .pager-nav__btn:disabled {
- opacity: 0.4;
- cursor: default;
- }
- .pager-nav__btn--num {
- padding: 1px calc(6 * var(--u, 1px));
- font-family: var(--num, monospace);
- }
- .pager-nav__btn--num.is-current {
- border-color: rgba(0, 184, 255, 0.85);
- background: linear-gradient(180deg, rgba(1, 171, 234, 0.85), rgba(0, 82, 168, 0.85));
- color: var(--text-strong);
- cursor: default;
- }
- .pager-nav__ellipsis {
- color: var(--muted, #5680b0);
- user-select: none;
- }
- .pager-nav__input {
- width: calc(52 * var(--u, 1px));
- padding: 1px calc(5 * var(--u, 1px));
- border: 1px solid rgba(1, 171, 234, 0.42);
- background: rgba(0, 20, 72, 0.6);
- color: var(--text, #d7ecff);
- font-family: var(--num, monospace);
- font-size: var(--fs-caption, 10.5px);
- text-align: center;
- }
- .pager-nav__input::placeholder {
- color: var(--muted, #5680b0);
- }
- .pager-nav__input:hover,
- .pager-nav__input:focus-visible {
- border-color: rgba(1, 171, 234, 0.75);
- outline: none;
- }
- .pager-nav__state {
- color: var(--muted, #5680b0);
- font-family: var(--num, monospace);
- font-size: var(--fs-caption, 10.5px);
- letter-spacing: 0.04em;
- }
- </style>
|