Pagination.vue
3.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
<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>