Pagination.vue 3.71 KB
<script setup>
/**
 * Pagination —— keyset 游标分页器:「上一页 / 下一页」两个方向,页面自己维护游标栈。
 *
 * **总条数和页码是可选的**(`summary`)。后端能给总数就给(列表带上时间窗时,`COUNT(*)` 和列表跑在
 * 同一段索引区间上,不贵),给了就显示「共 N 条 · 第 X / Y 页」—— 一个说不出"一共多少条、我在第几页"
 * 的后台没法用。给不了就只留两个方向的按钮,不编一个数出来。
 *
 * `summary` 是**拼好的字符串**,不是 `{ total, page }`:`@drama/ui` 不认识 i18n,措辞与数字格式归页面。
 *
 * 每页条数(`pageSize` / `pageSizeOptions`)同理可选:给了 options 才出现选择器。
 */

import { computed } from 'vue'

import Button from './Button.vue'
import Select from './Select.vue'
import Spinner from './Spinner.vue'

const props = defineProps({
  /** 还有下一页(后端返回了 next_cursor)。 */
  hasNext: { type: Boolean, default: false },
  /** 还有上一页(页面的游标栈非空)。 */
  hasPrev: { type: Boolean, default: false },
  /** 翻页请求进行中:所有控件禁用。 */
  loading: { type: Boolean, default: false },
  /** 上一页按钮文案。 */
  prevLabel: { type: String, default: 'Previous' },
  /** 下一页按钮文案。 */
  nextLabel: { type: String, default: 'Next' },
  /** 拼好的摘要,如「共 1,204 条 · 第 2 / 25 页」。空串 = 后端给不出总数,不显示。 */
  summary: { type: String, default: '' },
  /** 每页条数选择器的标签。 */
  pageSizeLabel: { type: String, default: '' },
  /** 当前每页条数。 */
  pageSize: { type: Number, default: 0 },
  /** 可选的每页条数。空数组 = 不给选。 */
  pageSizeOptions: { type: Array, default: () => [] },
})

defineEmits(['prev', 'next', 'update:pageSize'])

const sizeOptions = computed(() =>
  props.pageSizeOptions.map((size) => ({ value: size, label: String(size) })),
)
</script>

<template>
  <div class="drama-pagination">
    <span
      v-if="summary"
      class="drama-pagination__summary"
    >{{ summary }}</span>

    <div class="drama-pagination__controls">
      <div
        v-if="pageSizeOptions.length > 0"
        class="drama-pagination__size"
      >
        <label>{{ pageSizeLabel }}</label>
        <Select
          :model-value="pageSize"
          :options="sizeOptions"
          :disabled="loading"
          size="sm"
          @update:model-value="$emit('update:pageSize', Number($event))"
        />
      </div>

      <Spinner
        v-if="loading"
        size="sm"
      />
      <Button
        variant="secondary"
        size="sm"
        :disabled="!hasPrev || loading"
        @click="$emit('prev')"
      >
        {{ prevLabel }}
      </Button>
      <Button
        variant="secondary"
        size="sm"
        :disabled="!hasNext || loading"
        @click="$emit('next')"
      >
        {{ nextLabel }}
      </Button>
    </div>
  </div>
</template>

<style scoped>
.drama-pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--drama-space-lg);
  padding: var(--drama-space-md) 0;
}

.drama-pagination__summary {
  font-size: var(--drama-font-size-sm);
  color: var(--drama-color-text-muted);
  font-variant-numeric: tabular-nums;
}

.drama-pagination__controls {
  display: flex;
  align-items: center;
  gap: var(--drama-space-md);
  margin-left: auto;
}

.drama-pagination__size {
  display: flex;
  align-items: center;
  gap: var(--drama-space-sm);
}

.drama-pagination__size label {
  font-size: var(--drama-font-size-sm);
  color: var(--drama-color-text-muted);
  white-space: nowrap;
}

.drama-pagination__size :deep(.drama-select) {
  width: 76px;
}
</style>